From ecbabb4b283b448518835af714ab8a0b0fef132c Mon Sep 17 00:00:00 2001 From: Eric Crooks Date: Mon, 9 Oct 2023 20:11:09 -0400 Subject: [PATCH] BREAKING!: v3-beta (#698) BREAKING!: v3-beta --- .github/release_drafter_config.yml | 29 - .github/workflows/main.yml | 166 + .github/workflows/master.yml | 74 - .github/workflows/release.yml | 80 +- .gitignore | 32 +- AUTHORS | 13 + COPYING | 674 ++ LICENSE | 21 - README.md | 8 +- babel.config.cjs | 7 + console/benchmark_app.ts | 19 - console/benchmarks_app_dev.ts | 34 - console/deps.ts | 1 - deno.json | 45 + deps.ts | 21 - egg.json | 22 - jest.config.cloudflare.ts | 32 + jest.config.node.ts | 33 + logo.svg | 21 - mod.ts | 38 - package.json | 37 + package.lib.json | 23 + scripts/build_all | 11 + scripts/check_file_headers.ts | 93 + scripts/lib_builder/build_esm_lib.ts | 198 + scripts/lib_builder/build_intermediary | 11 + scripts/lib_builder/deps.ts | 6 + scripts/pre_check_release.ts | 19 + scripts/pre_release/validate_version.ts | 23 + scripts/tsconfig.json | 8 + services.ts | 5 - src/core/Interfaces.ts | 23 + src/core/Types.ts | 26 + src/core/errors/HTTPError.ts | 88 + src/core/http/Header.ts | 40 + src/core/http/Resource.ts | 70 + src/core/http/request/Method.ts | 37 + src/core/http/response/Status.ts | 284 + src/core/http/response/StatusCode.ts | 101 + src/core/http/response/StatusDescription.ts | 87 + src/core/http/response/StatusName.ts | 85 + src/core/interfaces/IBuilder.ts | 30 + src/core/interfaces/IHandler.ts | 40 + src/core/types/Header.ts | 24 + src/core/types/MethodOf.ts | 53 + src/core/types/RequestMethod.ts | 24 + src/core/types/ResponseStatus.ts | 28 + src/core/types/ResponseStatusCode.ts | 24 + src/core/types/ResponseStatusDescription.ts | 25 + src/core/types/ResponseStatusName.ts | 24 + src/dictionaries/mime_db.ts | 8196 ----------------- src/dictionaries/mime_types.ts | 1169 --- src/errors.ts | 29 - src/http/error_handler.ts | 56 - src/http/request.ts | 352 - src/http/resource.ts | 51 - src/http/response.ts | 239 - src/http/server.ts | 456 - src/http/service.ts | 15 - src/interfaces.ts | 212 - src/modules/README.md | 25 + src/modules/RequestChain/mod.native.ts | 47 + src/modules/RequestChain/mod.polyfill.ts | 41 + src/modules/base/Chain.ts | 63 + src/modules/base/RequestChain.ts | 94 + src/modules/builders/RequestBuilder.ts | 70 + src/modules/builders/ResourceBuilder.ts | 183 + src/modules/builders/ResponseBuilder.ts | 149 + src/modules/middleware/AcceptHeader/mod.ts | 150 + src/modules/middleware/CORS/mod.ts | 391 + src/modules/middleware/ETag/ETagResponse.ts | 132 + src/modules/middleware/ETag/mod.ts | 274 + .../RateLimiter/RateLimitResponse.ts | 139 + .../RateLimiter/RateLimitedClient.ts | 129 + .../RateLimiter/RateLimiterErrorResponse.ts | 36 + src/modules/middleware/RateLimiter/mod.ts | 336 + src/services/cors/deps.ts | 1 - src/services/csrf/csrf.ts | 57 - src/services/csrf/deps.ts | 2 - src/services/dexter/deps.ts | 2 - src/services/etag/deps.ts | 1 - src/services/etag/etag.ts | 86 - src/services/graphql/deps.ts | 7 - src/services/graphql/graphql_resource.ts | 21 - src/services/rate_limiter/rate_limiter.ts | 77 - src/services/resource_loader/deps.ts | 2 - src/services/response_time/response_time.ts | 17 - src/services/tengine/tengine.ts | 25 - src/standard/_unstable/Mixin.ts | 153 + .../_unstable/services/deno}/cors/cors.ts | 88 +- .../_unstable/services/deno/cors/deps.ts | 22 + .../_unstable/services/deno/csrf/csrf.ts | 78 + .../_unstable/services/deno/csrf/deps.ts | 23 + .../_unstable/services/deno/dexter/deps.ts | 23 + .../_unstable/services/deno}/dexter/dexter.ts | 29 +- .../_unstable/services/deno/etag/deps.ts | 22 + .../_unstable/services/deno/etag/etag.ts | 111 + .../_unstable/services/deno/graphql/deps.ts | 28 + .../services/deno}/graphql/graphql.ts | 79 +- .../services/deno/graphql/graphql_resource.ts | 47 + .../services/deno/resource_loader/deps.ts | 23 + .../deno}/resource_loader/resource_loader.ts | 34 +- .../native/accept_header/accepter_header.ts | 87 + .../services/native}/paladin/paladin.ts | 93 +- .../native}/rate_limiter/memory_store.ts | 21 + .../native/rate_limiter/rate_limiter.ts | 107 + .../native/response_time/response_time.ts | 43 + .../_unstable/services/native}/tengine/jae.ts | 21 + .../services/native/tengine/tengine.ts | 46 + src/standard/builders/Builder.ts | 34 + src/standard/chains/AbstractChainBuilder.ts | 62 + src/standard/handlers/AbstractSearchIndex.ts | 43 + src/standard/handlers/Handler.ts | 65 + src/standard/handlers/RequestParamsParser.ts | 124 + src/standard/handlers/RequestValidator.ts | 89 + src/standard/handlers/ResourceCaller.ts | 77 + .../handlers/ResourceNotFoundHandler.ts | 89 + src/standard/handlers/ResourcesIndex.ts | 178 + src/standard/http/Middleware.ts | 158 + src/standard/http/ResourceGroup.ts | 351 + src/standard/http/ResourceProxy.ts | 74 + src/standard/log/AbstractLogger.ts | 161 + src/standard/log/ConsoleLogger.ts | 49 + src/standard/log/GroupConsoleLogger.ts | 62 + src/standard/log/Level.ts | 34 + src/standard/log/LogLevel.ts | 27 + src/standard/log/Logger.ts | 56 + src/standard/polyfill/URLPatternPolyfill.ts | 227 + src/tsconfig.json | 14 + src/types.ts | 55 - tests/README.md | 68 + tests/compat/bun/tsconfig.json | 11 + .../URLPattern_not_supported_app_test_.ts | 141 + .../native/default-behavior/app.ts | 79 + .../URLPattern_not_supported_app_test_.ts | 141 + .../native/request-in-context-object/app.ts | 112 + .../polyfill/default-behavior/app.ts | 79 + .../polyfill/default-behavior/app_test.ts | 141 + .../polyfill/request-in-context-object/app.ts | 107 + .../request-in-context-object/app_test.ts | 141 + .../modules/base/Chain/app.native.test.ts | 49 + .../modules/base/Chain/app.native.ts | 93 + .../modules/base/Chain/app.polyfill.test.ts | 49 + .../modules/base/Chain/app.polyfill.ts | 93 + .../modules/base/Chain/app.native.test.ts | 49 + .../modules/base/Chain/app.native.ts | 91 + .../modules/base/Chain/app.polyfill.test.ts | 49 + .../modules/base/Chain/app.polyfill.ts | 93 + .../modules/base/Chain/app.native.test.ts | 49 + .../modules/base/Chain/app.native.ts | 93 + .../modules/base/Chain/app.polyfill.test.ts | 49 + .../modules/base/Chain/app.polyfill.ts | 93 + tests/compat/cloudflare/tsconfig.json | 6 + .../RequestChain/native/concurrency/app.ts | 116 + .../native/concurrency/app_test.ts | 182 + .../native/default-behavior/app.ts | 79 + .../native/default-behavior/app_test.ts | 143 + .../native/request-in-context-object/app.ts | 111 + .../request-in-context-object/app_test.ts | 143 + .../native/resource-groups/app.ts | 268 + .../native/resource-groups/app_test.ts | 332 + .../RequestChain/polyfill/concurrency/app.ts | 116 + .../polyfill/concurrency/app_test.ts | 182 + .../polyfill/default-behavior/app.ts | 78 + .../polyfill/default-behavior/app_test.ts | 143 + .../polyfill/request-in-context-object/app.ts | 106 + .../request-in-context-object/app_test.ts | 143 + .../v1.x/modules/base/Chain/app_native.ts | 90 + .../modules/base/Chain/app_native_test.ts | 143 + .../v1.x/modules/base/Chain/app_native_use.ts | 168 + .../modules/base/Chain/app_native_use_test.ts | 143 + .../v1.x/modules/base/Chain/app_polyfill.ts | 90 + .../modules/base/Chain/app_polyfill_test.ts | 143 + .../node-http-in-context-object/app.test.ts | 154 + .../node-http-in-context-object/app.ts | 105 + .../app.test.ts | 154 + .../node-http-converted-to-web-api/app.ts | 95 + .../node-http-in-context-object/app.test.ts | 154 + .../node-http-in-context-object/app.ts | 105 + .../app.test.ts | 154 + .../node-http-converted-to-web-api/app.ts | 95 + .../node-http-in-context-object/app.test.ts | 154 + .../node-http-in-context-object/app.ts | 105 + tests/compat/node/tsconfig.json | 6 + .../example_code/class_one_p_property_one.ts | 4 - .../example_code/class_one_p_property_two.ts | 1 - tests/data/index.html | 1 - tests/data/resources/test_resource_1.ts | 5 - tests/data/resources/test_resource_2.ts | 5 - tests/data/sample_1.txt | 35 - tests/data/sample_2.txt | 6 - tests/data/sample_3.txt | 6 - tests/data/server.crt | 21 - tests/data/server.key | 28 - tests/data/static_file.html | 1 - tests/data/static_file.json | 1 - tests/data/static_file.txt | 1 - tests/data/views/tengine_index.html | 1 - tests/deps.ts | 33 +- .../browser_request_resource_test.ts | 56 - tests/integration/coffee_resource_test.ts | 190 - tests/integration/cookie_resource_test.ts | 92 - tests/integration/error_handler_test.ts | 205 - tests/integration/etag_test.ts | 124 - tests/integration/files_resource_test.ts | 49 - tests/integration/graphql_test.ts | 96 - tests/integration/home_resource_test.ts | 140 - tests/integration/https_test.ts | 60 - .../optional_path_params_resource_test.ts | 435 - tests/integration/post_no_body_test.ts | 50 - .../integration/posting_invalid_json_test.ts | 64 - tests/integration/rate_limiter_test.ts | 107 - tests/integration/redirect_test.ts | 79 - .../request_accepts_resource_test.ts | 198 - tests/integration/response_time_test.ts | 54 - .../server_with_optionals_request_test.ts | 289 - .../service_ends_lifecycle_test.ts | 471 - tests/integration/service_run_at_startup.ts | 99 - tests/integration/service_test.ts | 103 - .../resource_loader/resource_loader_test.ts | 82 - .../resources/api/users_resource.ts | 31 - .../resources/ssr/home_resource.ts | 9 - tests/integration/tengine_test.ts | 58 - tests/integration/upgrade_websocket_test.ts | 72 - tests/integration/users.json | 14 - tests/integration/users_resource_test.ts | 171 - tests/middleware/deno/README.md | 17 + tests/middleware/deno/utils.ts | 134 + .../middleware/AcceptHeader/mod_test.ts | 390 + .../v1.x/modules/middleware/CORS/mod_test.ts | 1239 +++ .../v1.x/modules/middleware/ETag/mod_test.ts | 495 + .../middleware/RateLimiter/mod_test.ts | 661 ++ tests/test_helpers.ts | 54 - tests/unit/http/error_handler_test.ts | 127 - tests/unit/http/request_test.ts | 804 -- tests/unit/http/response_test.ts | 98 - tests/unit/http/server_test.ts | 127 - tests/unit/services/cors_test.ts | 219 - tests/unit/services/csrf_test.ts | 198 - tests/unit/services/dexter_test.ts | 77 - tests/unit/services/jae_test.ts | 18 - tests/unit/services/paladin_test.ts | 513 -- .../handlers/RequestValidator_test.ts | 114 + tsconfig.build.base.json | 11 + tsconfig.build.cjs.json | 9 + tsconfig.build.esm.json | 8 + 246 files changed, 17081 insertions(+), 17311 deletions(-) create mode 100644 .github/workflows/main.yml delete mode 100644 .github/workflows/master.yml create mode 100644 AUTHORS create mode 100644 COPYING delete mode 100644 LICENSE create mode 100644 babel.config.cjs delete mode 100644 console/benchmark_app.ts delete mode 100644 console/benchmarks_app_dev.ts delete mode 100644 console/deps.ts create mode 100644 deno.json delete mode 100644 deps.ts delete mode 100644 egg.json create mode 100644 jest.config.cloudflare.ts create mode 100644 jest.config.node.ts delete mode 100644 logo.svg delete mode 100644 mod.ts create mode 100644 package.json create mode 100644 package.lib.json create mode 100755 scripts/build_all create mode 100644 scripts/check_file_headers.ts create mode 100644 scripts/lib_builder/build_esm_lib.ts create mode 100755 scripts/lib_builder/build_intermediary create mode 100644 scripts/lib_builder/deps.ts create mode 100644 scripts/pre_check_release.ts create mode 100644 scripts/pre_release/validate_version.ts create mode 100644 scripts/tsconfig.json delete mode 100644 services.ts create mode 100644 src/core/Interfaces.ts create mode 100644 src/core/Types.ts create mode 100644 src/core/errors/HTTPError.ts create mode 100644 src/core/http/Header.ts create mode 100644 src/core/http/Resource.ts create mode 100644 src/core/http/request/Method.ts create mode 100644 src/core/http/response/Status.ts create mode 100644 src/core/http/response/StatusCode.ts create mode 100644 src/core/http/response/StatusDescription.ts create mode 100644 src/core/http/response/StatusName.ts create mode 100644 src/core/interfaces/IBuilder.ts create mode 100644 src/core/interfaces/IHandler.ts create mode 100644 src/core/types/Header.ts create mode 100644 src/core/types/MethodOf.ts create mode 100644 src/core/types/RequestMethod.ts create mode 100644 src/core/types/ResponseStatus.ts create mode 100644 src/core/types/ResponseStatusCode.ts create mode 100644 src/core/types/ResponseStatusDescription.ts create mode 100644 src/core/types/ResponseStatusName.ts delete mode 100644 src/dictionaries/mime_db.ts delete mode 100644 src/dictionaries/mime_types.ts delete mode 100644 src/errors.ts delete mode 100644 src/http/error_handler.ts delete mode 100644 src/http/request.ts delete mode 100644 src/http/resource.ts delete mode 100644 src/http/response.ts delete mode 100644 src/http/server.ts delete mode 100644 src/http/service.ts delete mode 100644 src/interfaces.ts create mode 100644 src/modules/README.md create mode 100644 src/modules/RequestChain/mod.native.ts create mode 100644 src/modules/RequestChain/mod.polyfill.ts create mode 100644 src/modules/base/Chain.ts create mode 100644 src/modules/base/RequestChain.ts create mode 100644 src/modules/builders/RequestBuilder.ts create mode 100644 src/modules/builders/ResourceBuilder.ts create mode 100644 src/modules/builders/ResponseBuilder.ts create mode 100644 src/modules/middleware/AcceptHeader/mod.ts create mode 100644 src/modules/middleware/CORS/mod.ts create mode 100644 src/modules/middleware/ETag/ETagResponse.ts create mode 100644 src/modules/middleware/ETag/mod.ts create mode 100644 src/modules/middleware/RateLimiter/RateLimitResponse.ts create mode 100644 src/modules/middleware/RateLimiter/RateLimitedClient.ts create mode 100644 src/modules/middleware/RateLimiter/RateLimiterErrorResponse.ts create mode 100644 src/modules/middleware/RateLimiter/mod.ts delete mode 100644 src/services/cors/deps.ts delete mode 100644 src/services/csrf/csrf.ts delete mode 100644 src/services/csrf/deps.ts delete mode 100644 src/services/dexter/deps.ts delete mode 100644 src/services/etag/deps.ts delete mode 100644 src/services/etag/etag.ts delete mode 100644 src/services/graphql/deps.ts delete mode 100644 src/services/graphql/graphql_resource.ts delete mode 100644 src/services/rate_limiter/rate_limiter.ts delete mode 100644 src/services/resource_loader/deps.ts delete mode 100644 src/services/response_time/response_time.ts delete mode 100644 src/services/tengine/tengine.ts create mode 100644 src/standard/_unstable/Mixin.ts rename src/{services => standard/_unstable/services/deno}/cors/cors.ts (68%) create mode 100644 src/standard/_unstable/services/deno/cors/deps.ts create mode 100644 src/standard/_unstable/services/deno/csrf/csrf.ts create mode 100644 src/standard/_unstable/services/deno/csrf/deps.ts create mode 100644 src/standard/_unstable/services/deno/dexter/deps.ts rename src/{services => standard/_unstable/services/deno}/dexter/dexter.ts (72%) create mode 100644 src/standard/_unstable/services/deno/etag/deps.ts create mode 100644 src/standard/_unstable/services/deno/etag/etag.ts create mode 100644 src/standard/_unstable/services/deno/graphql/deps.ts rename src/{services => standard/_unstable/services/deno}/graphql/graphql.ts (62%) create mode 100644 src/standard/_unstable/services/deno/graphql/graphql_resource.ts create mode 100644 src/standard/_unstable/services/deno/resource_loader/deps.ts rename src/{services => standard/_unstable/services/deno}/resource_loader/resource_loader.ts (69%) create mode 100644 src/standard/_unstable/services/native/accept_header/accepter_header.ts rename src/{services => standard/_unstable/services/native}/paladin/paladin.ts (66%) rename src/{services => standard/_unstable/services/native}/rate_limiter/memory_store.ts (70%) create mode 100644 src/standard/_unstable/services/native/rate_limiter/rate_limiter.ts create mode 100644 src/standard/_unstable/services/native/response_time/response_time.ts rename src/{services => standard/_unstable/services/native}/tengine/jae.ts (79%) create mode 100644 src/standard/_unstable/services/native/tengine/tengine.ts create mode 100644 src/standard/builders/Builder.ts create mode 100644 src/standard/chains/AbstractChainBuilder.ts create mode 100644 src/standard/handlers/AbstractSearchIndex.ts create mode 100644 src/standard/handlers/Handler.ts create mode 100644 src/standard/handlers/RequestParamsParser.ts create mode 100644 src/standard/handlers/RequestValidator.ts create mode 100644 src/standard/handlers/ResourceCaller.ts create mode 100644 src/standard/handlers/ResourceNotFoundHandler.ts create mode 100644 src/standard/handlers/ResourcesIndex.ts create mode 100644 src/standard/http/Middleware.ts create mode 100644 src/standard/http/ResourceGroup.ts create mode 100644 src/standard/http/ResourceProxy.ts create mode 100644 src/standard/log/AbstractLogger.ts create mode 100644 src/standard/log/ConsoleLogger.ts create mode 100644 src/standard/log/GroupConsoleLogger.ts create mode 100644 src/standard/log/Level.ts create mode 100644 src/standard/log/LogLevel.ts create mode 100644 src/standard/log/Logger.ts create mode 100644 src/standard/polyfill/URLPatternPolyfill.ts create mode 100644 src/tsconfig.json delete mode 100644 src/types.ts create mode 100644 tests/README.md create mode 100644 tests/compat/bun/tsconfig.json create mode 100644 tests/compat/bun/v0.x/modules/RequestChain/native/default-behavior/URLPattern_not_supported_app_test_.ts create mode 100644 tests/compat/bun/v0.x/modules/RequestChain/native/default-behavior/app.ts create mode 100644 tests/compat/bun/v0.x/modules/RequestChain/native/request-in-context-object/URLPattern_not_supported_app_test_.ts create mode 100644 tests/compat/bun/v0.x/modules/RequestChain/native/request-in-context-object/app.ts create mode 100644 tests/compat/bun/v0.x/modules/RequestChain/polyfill/default-behavior/app.ts create mode 100644 tests/compat/bun/v0.x/modules/RequestChain/polyfill/default-behavior/app_test.ts create mode 100644 tests/compat/bun/v0.x/modules/RequestChain/polyfill/request-in-context-object/app.ts create mode 100644 tests/compat/bun/v0.x/modules/RequestChain/polyfill/request-in-context-object/app_test.ts create mode 100644 tests/compat/cloudflare/node-v16.x/modules/base/Chain/app.native.test.ts create mode 100644 tests/compat/cloudflare/node-v16.x/modules/base/Chain/app.native.ts create mode 100644 tests/compat/cloudflare/node-v16.x/modules/base/Chain/app.polyfill.test.ts create mode 100644 tests/compat/cloudflare/node-v16.x/modules/base/Chain/app.polyfill.ts create mode 100644 tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.native.test.ts create mode 100644 tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.native.ts create mode 100644 tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.polyfill.test.ts create mode 100644 tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.polyfill.ts create mode 100644 tests/compat/cloudflare/node-v20.x/modules/base/Chain/app.native.test.ts create mode 100644 tests/compat/cloudflare/node-v20.x/modules/base/Chain/app.native.ts create mode 100644 tests/compat/cloudflare/node-v20.x/modules/base/Chain/app.polyfill.test.ts create mode 100644 tests/compat/cloudflare/node-v20.x/modules/base/Chain/app.polyfill.ts create mode 100644 tests/compat/cloudflare/tsconfig.json create mode 100644 tests/compat/deno/v1.x/modules/RequestChain/native/concurrency/app.ts create mode 100644 tests/compat/deno/v1.x/modules/RequestChain/native/concurrency/app_test.ts create mode 100644 tests/compat/deno/v1.x/modules/RequestChain/native/default-behavior/app.ts create mode 100644 tests/compat/deno/v1.x/modules/RequestChain/native/default-behavior/app_test.ts create mode 100644 tests/compat/deno/v1.x/modules/RequestChain/native/request-in-context-object/app.ts create mode 100644 tests/compat/deno/v1.x/modules/RequestChain/native/request-in-context-object/app_test.ts create mode 100644 tests/compat/deno/v1.x/modules/RequestChain/native/resource-groups/app.ts create mode 100644 tests/compat/deno/v1.x/modules/RequestChain/native/resource-groups/app_test.ts create mode 100644 tests/compat/deno/v1.x/modules/RequestChain/polyfill/concurrency/app.ts create mode 100644 tests/compat/deno/v1.x/modules/RequestChain/polyfill/concurrency/app_test.ts create mode 100644 tests/compat/deno/v1.x/modules/RequestChain/polyfill/default-behavior/app.ts create mode 100644 tests/compat/deno/v1.x/modules/RequestChain/polyfill/default-behavior/app_test.ts create mode 100644 tests/compat/deno/v1.x/modules/RequestChain/polyfill/request-in-context-object/app.ts create mode 100644 tests/compat/deno/v1.x/modules/RequestChain/polyfill/request-in-context-object/app_test.ts create mode 100644 tests/compat/deno/v1.x/modules/base/Chain/app_native.ts create mode 100644 tests/compat/deno/v1.x/modules/base/Chain/app_native_test.ts create mode 100644 tests/compat/deno/v1.x/modules/base/Chain/app_native_use.ts create mode 100644 tests/compat/deno/v1.x/modules/base/Chain/app_native_use_test.ts create mode 100644 tests/compat/deno/v1.x/modules/base/Chain/app_polyfill.ts create mode 100644 tests/compat/deno/v1.x/modules/base/Chain/app_polyfill_test.ts create mode 100644 tests/compat/node/node-v16.x/modules/RequestChain/polyfill/node-http-in-context-object/app.test.ts create mode 100644 tests/compat/node/node-v16.x/modules/RequestChain/polyfill/node-http-in-context-object/app.ts create mode 100644 tests/compat/node/node-v18.x/modules/RequestChain/polyfill/node-http-converted-to-web-api/app.test.ts create mode 100644 tests/compat/node/node-v18.x/modules/RequestChain/polyfill/node-http-converted-to-web-api/app.ts create mode 100644 tests/compat/node/node-v18.x/modules/RequestChain/polyfill/node-http-in-context-object/app.test.ts create mode 100644 tests/compat/node/node-v18.x/modules/RequestChain/polyfill/node-http-in-context-object/app.ts create mode 100644 tests/compat/node/node-v20.x/modules/RequestChain/polyfill/node-http-converted-to-web-api/app.test.ts create mode 100644 tests/compat/node/node-v20.x/modules/RequestChain/polyfill/node-http-converted-to-web-api/app.ts create mode 100644 tests/compat/node/node-v20.x/modules/RequestChain/polyfill/node-http-in-context-object/app.test.ts create mode 100644 tests/compat/node/node-v20.x/modules/RequestChain/polyfill/node-http-in-context-object/app.ts create mode 100644 tests/compat/node/tsconfig.json delete mode 100644 tests/data/example_code/class_one_p_property_one.ts delete mode 100644 tests/data/example_code/class_one_p_property_two.ts delete mode 100644 tests/data/index.html delete mode 100644 tests/data/resources/test_resource_1.ts delete mode 100644 tests/data/resources/test_resource_2.ts delete mode 100644 tests/data/sample_1.txt delete mode 100644 tests/data/sample_2.txt delete mode 100644 tests/data/sample_3.txt delete mode 100644 tests/data/server.crt delete mode 100644 tests/data/server.key delete mode 100644 tests/data/static_file.html delete mode 100644 tests/data/static_file.json delete mode 100644 tests/data/static_file.txt delete mode 100644 tests/data/views/tengine_index.html delete mode 100644 tests/integration/browser_request_resource_test.ts delete mode 100644 tests/integration/coffee_resource_test.ts delete mode 100644 tests/integration/cookie_resource_test.ts delete mode 100644 tests/integration/error_handler_test.ts delete mode 100644 tests/integration/etag_test.ts delete mode 100644 tests/integration/files_resource_test.ts delete mode 100644 tests/integration/graphql_test.ts delete mode 100644 tests/integration/home_resource_test.ts delete mode 100644 tests/integration/https_test.ts delete mode 100644 tests/integration/optional_path_params_resource_test.ts delete mode 100644 tests/integration/post_no_body_test.ts delete mode 100644 tests/integration/posting_invalid_json_test.ts delete mode 100644 tests/integration/rate_limiter_test.ts delete mode 100644 tests/integration/redirect_test.ts delete mode 100644 tests/integration/request_accepts_resource_test.ts delete mode 100644 tests/integration/response_time_test.ts delete mode 100644 tests/integration/server_with_optionals_request_test.ts delete mode 100644 tests/integration/service_ends_lifecycle_test.ts delete mode 100644 tests/integration/service_run_at_startup.ts delete mode 100644 tests/integration/service_test.ts delete mode 100644 tests/integration/services/resource_loader/resource_loader_test.ts delete mode 100644 tests/integration/services/resource_loader/resources/api/users_resource.ts delete mode 100644 tests/integration/services/resource_loader/resources/ssr/home_resource.ts delete mode 100644 tests/integration/tengine_test.ts delete mode 100644 tests/integration/upgrade_websocket_test.ts delete mode 100644 tests/integration/users.json delete mode 100644 tests/integration/users_resource_test.ts create mode 100644 tests/middleware/deno/README.md create mode 100644 tests/middleware/deno/utils.ts create mode 100644 tests/middleware/deno/v1.x/modules/middleware/AcceptHeader/mod_test.ts create mode 100644 tests/middleware/deno/v1.x/modules/middleware/CORS/mod_test.ts create mode 100644 tests/middleware/deno/v1.x/modules/middleware/ETag/mod_test.ts create mode 100644 tests/middleware/deno/v1.x/modules/middleware/RateLimiter/mod_test.ts delete mode 100644 tests/test_helpers.ts delete mode 100644 tests/unit/http/error_handler_test.ts delete mode 100644 tests/unit/http/request_test.ts delete mode 100644 tests/unit/http/response_test.ts delete mode 100644 tests/unit/http/server_test.ts delete mode 100644 tests/unit/services/cors_test.ts delete mode 100644 tests/unit/services/csrf_test.ts delete mode 100644 tests/unit/services/dexter_test.ts delete mode 100644 tests/unit/services/jae_test.ts delete mode 100644 tests/unit/services/paladin_test.ts create mode 100644 tests/unit/standard/handlers/RequestValidator_test.ts create mode 100644 tsconfig.build.base.json create mode 100644 tsconfig.build.cjs.json create mode 100644 tsconfig.build.esm.json diff --git a/.github/release_drafter_config.yml b/.github/release_drafter_config.yml index 361a5f4a1..b571dc56e 100644 --- a/.github/release_drafter_config.yml +++ b/.github/release_drafter_config.yml @@ -26,35 +26,6 @@ version-resolver: # What our release will look like. If no draft has been created, then this will be used, otherwise $CHANGES just gets addedd template: | - ## Compatibility - - Requires Deno v - Uses Deno Standard Modules - - ## Documentation - - * [Full Documentation](https://drash.land/drash/v2.x/getting-started/introduction) - - ## Usage - - 1. Create a `deps.ts` file. - - ```typescript - // deps.ts - - export * as Drash from "https://deno.land/x/drash@v$RESOLVED_VERSION/mod.ts"; - ``` - - 2. Import `Drash` from your `deps.ts` file. - - ```typescript - import { Drash } from "./deps.ts" - - ... your - ... code - ... here - ``` - ## Release Summary $CHANGES diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..6215c6f78 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,166 @@ +name: main + +on: + pull_request: + branches: + - main + push: + branches: + - main + +jobs: + + compat-cloudflare: + name: Compat - Cloudflare + + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + node-version: [16.x, 18.x, 20.x] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v3 + + - name: Install Deno (for `deno task` command) + uses: denoland/setup-deno@v1 + + - name: Install Node ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Install Node dependencies + run: | + yarn install + + - name: Run Tests + run: | + deno task build:all:ci && deno task test:compat:cloudflare + + compat-bun: + name: Compat - Bun + + strategy: + matrix: + # Add Windows when https://github.com/oven-sh/bun/issues/43 is complete + os: [ubuntu-latest, macos-latest] + node-version: [16.x, 18.x, 20.x] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v3 + + - name: Install Deno (for `deno task` command) + uses: denoland/setup-deno@v1 + + - name: Install Node ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Install Node dependencies + run: | + yarn install + + - name: Install Bun + run: | + curl -fsSL https://bun.sh/install | bash + + - name: Run Tests + run: | + deno task build:all:ci && ~/.bun/bin/bun test tests/compat/bun + + + compat-deno: + name: Compat - Deno + + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v3 + + - name: Install Deno + uses: denoland/setup-deno@v1 + + - name: Run Tests + run: | + deno task test:compat:deno + + - name: Run Tests (concurrency) + run: | + deno task test:compat:deno:concurrency + + compat-node: + name: Compat - Node + + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + node-version: [16.x, 18.x, 20.x] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v3 + + - name: Install Deno (for `deno task` command) + uses: denoland/setup-deno@v1 + + - name: Install Node ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Instal Node dependencies + run: | + yarn install + + - name: Run Tests + run: | + deno task build:all:ci && deno task test:compat:node + + # benchmarks: + # strategy: + # matrix: + # os: [ubuntu-latest] + + # runs-on: ${{ matrix.os }} + + # steps: + # - uses: actions/checkout@v3 + + # - name: Install Deno + # uses: denoland/setup-deno@v1 + + # - name: Setup Node 13 + # uses: actions/setup-node@v3 + # with: + # node-version: '13' + + # - name: Install Autocannon + # run: npm install -g autocannon + + # - name: Run Drash application + # run: deno run --allow-net ./console/benchmark_app.ts & + + # - name: Run Autocannon against Drash application + # run: autocannon -c 40 -d 10 -j http://localhost:1447 + + # code_quality: + # # Only one OS is required since fmt is cross platform + # runs-on: ubuntu-latest + + # steps: + # - uses: actions/checkout@v3 + + # - name: Install Deno + # uses: denoland/setup-deno@v1 + + # - name: "Check: deno lint" + # run: deno lint src + + # - name: "Check deno fmt" + # run: deno fmt --check diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml deleted file mode 100644 index 4abab8e2b..000000000 --- a/.github/workflows/master.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Master - -on: - pull_request: - branches: - - main - push: - branches: - - main - -jobs: - - api-test: - - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v2 - - - name: Install Deno - uses: denoland/setup-deno@v1 - - - name: Integration Tests - run: | - deno test -A --unsafely-ignore-certificate-errors tests/integration - - - name: Unit Tests - run: deno test -A tests/unit - - benchmarks: - strategy: - matrix: - os: [ubuntu-latest] - - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v2 - - - name: Install Deno - uses: denoland/setup-deno@v1 - - - name: Setup Node 13 - uses: actions/setup-node@v1 - with: - node-version: '13' - - - name: Install Autocannon - run: npm install -g autocannon - - - name: Run Drash application - run: deno run --allow-net ./console/benchmark_app.ts & - - - name: Run Autocannon against Drash application - run: autocannon -c 40 -d 10 -j http://localhost:1447 - - code_quality: - # Only one OS is required since fmt is cross platform - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - name: Install Deno - uses: denoland/setup-deno@v1 - - - name: Check `deno lint` passes - run: deno lint --ignore=tests/data - - - name: Check `deno fmt` passes - run: deno fmt --check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7a04142e8..a27684295 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,15 +2,79 @@ name: Release on: release: types: [published] + + # In the even this workflow fails, it can be started manually via `workflow_dispatch` + workflow_dispatch: + jobs: - publish-egg: + + npm: runs-on: ubuntu-latest steps: - - name: Notify the castle about this release + - uses: actions/checkout@v3 + with: + token: ${{ secrets.CI_USER_PAT }} + + # We need deno because the "Build CJS and ESM" step runs `deno run` + - name: Install Deno + uses: denoland/setup-deno@v1 + + - name: Pre-check release version run: | - curl -X POST \ - -u "${{ secrets.CI_USER_NAME }}:${{ secrets.CI_USER_PAT }}" \ - -H "Accept: application/vnd.github.everest-preview+json" \ - -H "Content-Type: application/json" \ - --data '{"event_type": "release", "client_payload": { "repo": "deno-drash", "module": "drash", "version": "${{ github.ref }}" }}' \ - https://api.github.com/repos/drashland/castle/dispatches + deno run --allow-read ./scripts/pre_check_release.ts ${{ github.event.release.tag_name }} + + # Setup .npmrc file to publish to npm + - name: Install Node + uses: actions/setup-node@v3 + with: + registry-url: 'https://registry.npmjs.org' + scope: '@drashland' + + - name: Install Node dependencies + run: yarn install + + - name: Build Drash libs + run: | + deno task build:all:ci + cp package.lib.json .drashland/lib/package.json + + # - name: Publish + # run: | + # cd .drashland/lib + # yarn publish --access public + # env: + # NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTOMATION_TOKEN }} + + github: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + token: ${{ secrets.CI_USER_PAT }} + + # We need deno because the "Build CJS and ESM" step runs `deno run` + - name: Install Deno + uses: denoland/setup-deno@v1 + + # Setup .npmrc file to publish to github + - name: Install Node + uses: actions/setup-node@v3 + with: + registry-url: 'https://npm.pkg.github.com' + scope: '@drashland' + + - name: Install deps + run: | + yarn install + + - name: Build Drash libs + run: | + deno task build:all:ci + cp package.lib.json .drashland/lib/package.json + + # - name: Publish + # run: | + # cd .drashland/lib + # yarn publish --access public + # env: + # NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 8cfe4158e..e59a3439d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,24 @@ -# files -*conf.json +# Files + .DS_Store .env -bundle.js -compiled_*.js -compiled_*.json -doc_blocks_output_actual.json +*.log package-lock.json +yarn.lock + +# Directories + +.drashland/ +.idea/ +.vim/ +.vscode/ +benchmarks/ +dist/ +lib/ +node_modules/ +tmp*/ +tmp/ -# directories -.idea -.sass-cache -.vim -node_modules -tmp*/* -tmp/* +# Exceptions -# exceptions !.gitkeep diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 000000000..d6a31668c --- /dev/null +++ b/AUTHORS @@ -0,0 +1,13 @@ +Drash was originally created in 2019 under the MIT license. The MIT license was +used for Drash version 0.X.X, version 1.X.X, and version 2.X.X. Drash's license +was changed in January 2023 to the GNU General Public License version 3. This +AUTHORS file applies to Drash version 3.X.X and later versions (unless otherwise +stated in files or other license is used in later versions) in accordance with +its copypright and license notices. + +The Drash authors are below: + + Breno Salles + Edward Bebbington + Eric Crooks + Sara Gee diff --git a/COPYING b/COPYING new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed 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. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 3b1ced205..000000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019-2022 Drash Land - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md index b2f9aa2b6..b30ed692d 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,8 @@ [![CI](https://img.shields.io/github/actions/workflow/status/drashland/drash/master.yml?branch=main&label=branch:main)](https://github.com/drashland/drash/actions/workflows/master.yml?query=branch%3Amain) [![Drash Land Discord](https://img.shields.io/badge/discord-join-blue?logo=discord)](https://discord.gg/RFsCSaHRWK) -Drash Land - Drash logo - -Drash is a microframework for Deno's HTTP server. +Drash - A microframework for building JavaScript/TypeScript HTTP systems. View the full documentation at https://drash.land/drash. -In the event the documentation pages are not accessible, please view the raw -version of the documentation at -https://github.com/drashland/website-v2/tree/main/docs. +In the event the documentation pages are not accessible, please view the raw version of the documentation at https://github.com/drashland/website-v2/. diff --git a/babel.config.cjs b/babel.config.cjs new file mode 100644 index 000000000..f88d1e89a --- /dev/null +++ b/babel.config.cjs @@ -0,0 +1,7 @@ +// This config is used by Node compat tests through `jest.config.node.ts` + +module.exports = { + presets: [ + ["@babel/preset-env"], + ], +}; diff --git a/console/benchmark_app.ts b/console/benchmark_app.ts deleted file mode 100644 index b5549ccec..000000000 --- a/console/benchmark_app.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as Drash from "../mod.ts"; - -class HomeResource extends Drash.Resource { - paths = ["/"]; - public GET(_request: Drash.Request, response: Drash.Response) { - response.body = "Hello World!"; - } -} - -const server = new Drash.Server({ - port: 1447, - protocol: "http", - hostname: "127.0.0.1", - resources: [HomeResource], -}); - -server.run(); - -console.log(`App running at ${server.address}.`); diff --git a/console/benchmarks_app_dev.ts b/console/benchmarks_app_dev.ts deleted file mode 100644 index 8b565cc71..000000000 --- a/console/benchmarks_app_dev.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as Drash from "../mod.ts"; - -export class Resource extends Drash.Resource { - static paths = ["/", "/:someParam"]; - - public GET(_request: Drash.Request, response: Drash.Response) { - response.body = "test"; - } -} - -export class CoffeeResource extends Resource { - public paths = ["/coffee", "/coffee/:someParam"]; - - public GET(_request: Drash.Request, _response: Drash.Response) { - } -} - -const server = new Drash.Server({ - protocol: "http", - hostname: "127.0.0.1", - port: 1337, - resources: [ - Resource, - CoffeeResource, - ], -}); - -server.run(); - -console.log(`Server running at ${server.address}`); - -// Deno.run({ -// cmd: ["autocannon", "-c", "40", "-d", "5", "http://localhost:1337"], -// }); diff --git a/console/deps.ts b/console/deps.ts deleted file mode 100644 index f2e00c1dd..000000000 --- a/console/deps.ts +++ /dev/null @@ -1 +0,0 @@ -export { green, red } from "https://deno.land/std@0.141.0/fmt/colors.ts"; diff --git a/deno.json b/deno.json new file mode 100644 index 000000000..725fafa22 --- /dev/null +++ b/deno.json @@ -0,0 +1,45 @@ +{ + "lock": false, + "lint": { + "rules": { + "exclude": [ + "no-explicit-any" + ] + }, + "include": ["./src", "./tests"], + "exclude": [".deno", ".drashland", "node_modules", "**/_unstable"] + }, + "fmt": { + "useTabs": false, + "lineWidth": 80, + "indentWidth": 2, + "semiColons": true, + "singleQuote": false, + "proseWrap": "preserve", + "exclude": [".deno", ".drashland", "node_modules"] + }, + "tasks": { + "build:all:ci": "deno task build:intermediary:ci && deno task build:esm && deno task build:cjs", + "build:all": "./scripts/build_all", + "build:check": "deno task check:file-headers && deno task yarn:clean && yarn install && deno task build:all && deno task test:all", + "check:file-headers": "deno run --allow-read ./scripts/check_file_headers.ts", + "build:cjs": "yarn tsc --project tsconfig.build.cjs.json", + "build:esm": "yarn tsc --project tsconfig.build.esm.json", + "build:intermediary:ci": "deno fmt && deno run -A ./scripts/lib_builder/build_esm_lib.ts --debug --workspace=./.drashland/lib/intermediary --copy-files=./src", + "build:intermediary": "./scripts/lib_builder/build_intermediary", + "lint:tests:bun": "deno lint tests/compat/bun", + "lint:tests:node": "deno lint tests/compat/node*/*", + "release": "yarn publish --access public", + "test:all": "deno task test:compat:bun && deno task test:compat:deno && deno task test:compat:node && deno task test:middleware:deno && deno task test:unit", + "test:compat:bun": "bun test tests/compat/bun", + "test:compat:cloudflare": "yarn jest --config jest.config.cloudflare.ts tests/compat/cloudflare", + "test:compat:deno:concurrency": "deno test tests/compat/deno/*/concurrency", + "test:compat:deno": "deno test tests/compat/deno --ignore='**/concurrency/*.ts'", + "test:compat:node": "yarn jest --config jest.config.node.ts tests/compat/node", + "test:middleware:deno": "deno test tests/middleware/deno --allow-net", + "test:unit": "deno test tests/unit", + "validate:file-headers": "deno run --allow-read ./scripts/check_file_headers.ts", + "yarn:clean": "rm -rf node_modules && rm yarn.lock || true", + "yarn:publish": "deno task build:all:ci" + } +} diff --git a/deps.ts b/deps.ts deleted file mode 100644 index cc489b27c..000000000 --- a/deps.ts +++ /dev/null @@ -1,21 +0,0 @@ -const decoder = new TextDecoder(); -const encoder = new TextEncoder(); -export { decoder, encoder }; - -import { STATUS_TEXT as StdStatusText } from "https://deno.land/std@0.175.0/http/http_status.ts"; -export const STATUS_TEXT = new Map( - Object.entries(StdStatusText), -); - -export { - deleteCookie, - getCookies, - setCookie, -} from "https://deno.land/std@0.175.0/http/cookie.ts"; - -export type { Cookie } from "https://deno.land/std@0.175.0/http/cookie.ts"; - -export { - Server as StdServer, -} from "https://deno.land/std@0.175.0/http/server.ts"; -export type { ConnInfo } from "https://deno.land/std@0.175.0/http/server.ts"; diff --git a/egg.json b/egg.json deleted file mode 100644 index 38d402004..000000000 --- a/egg.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "drash", - "description": "A REST microframework for Deno's HTTP server with zero dependencies.", - "version": "2.5.4", - "stable": true, - "repository": "https://github.com/drashland/drash", - "files": [ - "./mod.ts", - "./deps.ts", - "./create_app.ts", - "./src/**/*", - "./README.md", - "./mod.ts" - ], - "entry": "./mod.ts", - "checkAll": false, - "unlisted": false, - "checkTests": false, - "check": true, - "homepage": "https://github.com/drashland/drash", - "ignore": [] -} diff --git a/jest.config.cloudflare.ts b/jest.config.cloudflare.ts new file mode 100644 index 000000000..f6d8de507 --- /dev/null +++ b/jest.config.cloudflare.ts @@ -0,0 +1,32 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +import type { Config } from "@jest/types"; + +const testDirectory = getTestDirectory(); + +console.log(`\nRunning tests in cloudflare/${testDirectory} directory\n`); + +const config: Config.InitialOptions = { + testMatch: [ + `**/cloudflare/${testDirectory}/**/(*.)+(test).+(ts|tsx)`, + ], + transform: { + "^.+\\.(ts|tsx)$": "ts-jest", + }, +}; + +export default config; + +function getTestDirectory() { + console.log(`\nNode version: ${process.version}\n`); + + const matchedVersion = process.version.match(/v[0-9]+/); + + if (!matchedVersion) { + console.log( + `\nFailed to get test directory. \`process.version\` match returned ${matchedVersion}.\n`, + ); + process.exit(1); + } + + return "node-" + matchedVersion[0] + ".x"; +} diff --git a/jest.config.node.ts b/jest.config.node.ts new file mode 100644 index 000000000..fcb9b35e8 --- /dev/null +++ b/jest.config.node.ts @@ -0,0 +1,33 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +import type { Config } from "@jest/types"; + +const testDirectory = getTestDirectory(); + +console.log(`\nRunning tests in node/${testDirectory} directory\n`); + +const config: Config.InitialOptions = { + testMatch: [ + `**/node/${testDirectory}/**/(*.)+(test).+(ts|tsx)`, + ], + transform: { + "^.+\\.(ts|tsx)?$": "ts-jest", + "^.+\\.(js|jsx)$": "babel-jest", + }, +}; + +export default config; + +function getTestDirectory() { + console.log(`\nNode version: ${process.version}\n`); + + const matchedVersion = process.version.match(/v[0-9]+/); + + if (!matchedVersion) { + console.log( + `\nFailed to get test directory. \`process.version\` match returned ${matchedVersion}.\n`, + ); + process.exit(1); + } + + return "node-" + matchedVersion[0] + ".x"; +} diff --git a/logo.svg b/logo.svg deleted file mode 100644 index 8cec7445d..000000000 --- a/logo.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - logo - - - - \ No newline at end of file diff --git a/mod.ts b/mod.ts deleted file mode 100644 index 6ce6da8c0..000000000 --- a/mod.ts +++ /dev/null @@ -1,38 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - IMPORTS /////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -import * as Interfaces from "./src/interfaces.ts"; -import * as Types from "./src/types.ts"; -import { Server } from "./src/http/server.ts"; - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - EXPORTS - CLASSES ///////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -function getVersion() { - const url = import.meta.url; - if (url.match(/v\d/) === null) { - return null; - } - return "v" + url.split("v")[1].split("/")[0]; -} -export const version = getVersion(); - -// Dictionaries -export { mimeDb as MimeDb } from "./src/dictionaries/mime_db.ts"; - -// Errors -export * as Errors from "./src/errors.ts"; - -// Http -export { DrashRequest as Request } from "./src/http/request.ts"; -export { Resource } from "./src/http/resource.ts"; -export { DrashResponse as Response } from "./src/http/response.ts"; -export { Service } from "./src/http/service.ts"; -export { ErrorHandler } from "./src/http/error_handler.ts"; - -// Export members from the IMPORTS section above -export { Interfaces, Server, Types }; - -export type { IErrorHandler, IResource, IService } from "./src/interfaces.ts"; diff --git a/package.json b/package.json new file mode 100644 index 000000000..d06c9c0e8 --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "private": true, + "name": "@drashland/drash", + "version": "3.0.0-preview.202308281922", + "description": "A microframework for building JavaScript HTTP applications", + "license": "GPL-3.0", + "repository": "git@github.com:drashland/drash.git", + "author": "Drash Land", + "contributors": [ + { + "name": "Breno Salles" + }, + { + "name": "Edward Bebbington" + }, + { + "name": "Eric Crooks" + }, + { + "name": "Sara Gee" + } + ], + "devDependencies": { + "@babel/preset-env": "^7.22.10", + "@cloudflare/workers-types": "^4.20221111.1", + "@types/jest": "^29.5.4", + "@types/node": "^20.5.7", + "babel-jest": "^29.6.4", + "bun-types": "^0.8.1", + "jest": "^29.4.3", + "jest-environment-miniflare": "2.7.1", + "ts-jest": "^29.0.1", + "ts-node": "^10.9.1", + "typescript": "5.x", + "wrangler": "^2.12.0" + } +} diff --git a/package.lib.json b/package.lib.json new file mode 100644 index 000000000..86acd991f --- /dev/null +++ b/package.lib.json @@ -0,0 +1,23 @@ +{ + "private": true, + "name": "@drashland/drash", + "version": "3.0.0-preview.202308281922", + "description": "A microframework for building JavaScript HTTP applications", + "license": "GPL-3.0", + "repository": "git@github.com:drashland/drash.git", + "author": "Drash Land", + "contributors": [ + { + "name": "Breno Salles" + }, + { + "name": "Edward Bebbington" + }, + { + "name": "Eric Crooks" + }, + { + "name": "Sara Gee" + } + ] +} diff --git a/scripts/build_all b/scripts/build_all new file mode 100755 index 000000000..27ca17539 --- /dev/null +++ b/scripts/build_all @@ -0,0 +1,11 @@ +#!/bin/bash + +# This script only exists to run Deno in different OSs. This script runs in the +# CI to build the CJS and ESM libs. +( + rm -rf .drashland/lib && \ + deno task build:intermediary && \ + deno task build:esm && \ + deno task build:cjs && \ + rm -rf .drashland/lib/intermediary +) diff --git a/scripts/check_file_headers.ts b/scripts/check_file_headers.ts new file mode 100644 index 000000000..5e28c76a4 --- /dev/null +++ b/scripts/check_file_headers.ts @@ -0,0 +1,93 @@ +import { walk, WalkEntry } from "https://deno.land/std@0.201.0/fs/walk.ts"; + +const write = Deno.args[0] === "--write"; + +const fileHeader = ` +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ +`; + +const filesRequiringHeaders: { path: string; contents: string }[] = []; + +const directoriesToCheck = [ + "./src", + "./tests", +]; + +const pathsToIgnore = [ + ".DS_Store", + "node_modules", + ".md", + ".json", +]; + +for await (const directory of directoriesToCheck) { + for await (const entry of walk(directory)) { + check(entry); + } +} + +async function check(entry: WalkEntry) { + for (const ignore of pathsToIgnore) { + if (entry.path.includes(ignore)) { + return; + } + } + + if (entry.isFile) { + const contents = await Deno.readTextFile(entry.path); + const header = contents.includes(fileHeader.trim()); + if (!header) { + filesRequiringHeaders.push({ + path: entry.path, + contents, + }); + } + } +} + +if (!filesRequiringHeaders.length) { + console.log(`\nDone checking files. All good.`); + Deno.exit(0); +} + +console.log(` +The following files are missing the file header: + +${filesRequiringHeaders.map((f) => f.path).join("\n")} + +`); + +if (!write) { + console.log(`Use --write option to write file headers in these files\n`); + Deno.exit(1); +} + +console.log(`\nOption \`--write\` provided. Writing file headers.\n`); + +for (const file of filesRequiringHeaders) { + await Deno.writeTextFile( + file.path, + fileHeader.trim() + "\n\n" + file.contents, + ); +} + +console.log(`\nDone writing files.\n`); diff --git a/scripts/lib_builder/build_esm_lib.ts b/scripts/lib_builder/build_esm_lib.ts new file mode 100644 index 000000000..f5f375ab1 --- /dev/null +++ b/scripts/lib_builder/build_esm_lib.ts @@ -0,0 +1,198 @@ +/** + * @todo (crookse) This can probably use Line. Line wouldn't necessariliy be a + * dependency of the lib. It would be a dependency of the build process. + * + * This script takes TypeScript files that follow Deno's requirements of (e.g., + * `import` statements require `.ts` extensions) and converts them to a portable + * format that other non-Deno processes can use. + * + * For example, running `deno run -A ./console/build_esm_lib.ts ./src ./mod.ts` + * will do the following: + * + * 1. Create a directory to build the portable TypeScript code. The directory + * in this context is called the "workspace directory" and lives at + * `./tmp/conversion_workspace` directory. + * 2. Copy `./src` into the workspace directory. + * 3. Copy `./mod.ts` into the workspace directory. + * 4. Remove all `.ts` extensions from all `import` and `export` statements in + * all files in the workspace directory. + * + * Now that all `.ts` extensions are removed from the `import` and `export` + * statements, the workspace directory can be used by other processes. For + * example, `tsc` can be used on the workspace directory to compile the code + * down to a specific format like CommonJS for Node applications and a plain + * JavaScript module syntax for browser applications. + * + * @param Deno.args A list of directories and files containing TypeScript files + * that this script should convert. + */ + +import { copySync, emptyDirSync, ensureDirSync, walk } from "./deps.ts"; +import { ConsoleLogger, Level } from "../../src/standard/log/ConsoleLogger.ts"; + +const logger = ConsoleLogger.create("build_esm_lib.ts", Level.Info); + +const decoder = new TextDecoder(); +const encoder = new TextEncoder(); +const args = Deno.args.slice(); + +const debug = optionEnabled("--debug"); +const debugContents = optionEnabled("--debug-contents"); +const workspace = optionValue("--workspace") || ""; +const copyFiles = optionValue("--copy-files"); + +Promise + .resolve((() => logger.debug(`Options:`, { debug, debugContents }))()) + .then(createWorkspace) + .then(copyFilesToWorkspace) + .then(convertCode) + .then(() => Deno.exit(0)) + .catch((error) => { + logger.debug(error); + Deno.exit(1); + }); + +/** + * Convert the code given to this script. + */ +async function convertCode(): Promise { + logger.debug("\nStarting .ts extension removal process.\n"); + + for await (const entry of walk(workspace)) { + if (entry.isDirectory) { + if ( + entry.path.includes("_unstable") || + entry.path.includes("standard/services/") + ) { + emptyDirSync(entry.path); + continue; + } + } + + if (!entry.isFile) { + continue; + } + + logger.debug(`Removing .ts extensions from ${entry.path}.`); + removeTsExtensions(entry.path); + logger.debug("Moving to next file."); + logger.debug(""); + } + + logger.debug("Done removing .ts extensions from source files."); +} + +function copyFilesToWorkspace(): void { + const split = copyFiles?.split(",") || []; + logger.debug(`Copying files into workspace:`, split); + + for (const item of split) { + logger.debug(`Renaming file: ${item}`); + + const srcDirPrefixRemovedFilename = item.replace("./src", "./"); + logger.debug(` -> ${srcDirPrefixRemovedFilename}`); + + let nonDotFilename = srcDirPrefixRemovedFilename.replace("./", "/"); + logger.debug(` -> ${nonDotFilename}`); + + if (nonDotFilename === "package.builds.json") { + nonDotFilename = "package.json"; + } + + logger.debug(`Copying ${item} to ${workspace}${nonDotFilename}`); + copySync(item, workspace + nonDotFilename, { + overwrite: true, + }); + } +} + +/** + * Create the workspace directory. + */ +function createWorkspace() { + logger.debug(`Creating ${workspace}.`); + emptyDirSync(workspace); + ensureDirSync(workspace); +} + +/** + * Remove the .ts extensions for runtimes that do not require it. + */ +function removeTsExtensions(filename: string): void { + // Step 1: Read contents + let contents = decoder.decode(Deno.readFileSync(filename)); + + // // Step 2: Create an array of import/export statements from the contents + // const importStatements = contents.match( + // /(import.+\.ts";)|(import.+((\n|\r)\s.+)+(\n|\r).+\.ts";)/g, + // ); + // const exportStatements = contents.match( + // /(export.+\.ts";)|(export.+((\n|\r)\s.+)+(\n|\r).+\.ts";)/g, + // ); + + // // Step 3: Remove all .ts extensions from the import/export statements + // const newImportStatements = importStatements?.map((statement: string) => { + // return statement.replace(/\.ts";/, `";`); + // }); + + contents = contents.replace(/\.ts";/g, '";'); + + // const newExportStatements = exportStatements?.map((statement: string) => { + // return statement.replace(/\.ts";/, `";`); + // }); + + // // Step 4: Replace the original contents with the new contents + // if (newImportStatements) { + // importStatements?.forEach((statement: string, index: number) => { + // contents = contents.replace(statement, newImportStatements[index]); + // }); + // } + // if (newExportStatements) { + // exportStatements?.forEach((statement: string, index: number) => { + // contents = contents.replace(statement, newExportStatements[index]); + // }); + // } + + if (debugContents) { + logger.debug(`New contents (without .ts extensions):`); + logger.debug(contents); + } + + // Step 5: Rewrite the original file without .ts extensions + logger.debug(`Overwriting ${filename} with new contents.`); + Deno.writeFileSync(filename, encoder.encode(contents)); + logger.debug("File written."); +} + +/** + * Is the given option enabled? + * @param option The name of the option (e.g., `--debug`). + * @returns True if yes, false if no. + */ +function optionEnabled(option: string): boolean { + const optionIndex = args.indexOf(option); + const enabled = optionIndex !== -1; + + if (enabled) { + args.splice(optionIndex, 1); + } + + return enabled; +} + +/** + * What is the given option value? + * @param option The name of the option (e.g., `--workspace`). + * @returns The value of the option if the option exists. + */ +function optionValue(option: string): string | undefined { + const extractedOption = args.filter((arg) => arg.includes(option)); + + if (!extractedOption.length) { + return; + } + + args.splice(args.indexOf(extractedOption[0]), 1); + + return extractedOption[0].replace(option + "=", ""); +} diff --git a/scripts/lib_builder/build_intermediary b/scripts/lib_builder/build_intermediary new file mode 100755 index 000000000..ea2f9aa32 --- /dev/null +++ b/scripts/lib_builder/build_intermediary @@ -0,0 +1,11 @@ +#!/bin/bash + +# This script only exists to run Deno in different OSs. This script runs in the +# CI to build the CJS and ESM libs. +( + deno fmt \ + && deno run -A ./scripts/lib_builder/build_esm_lib.ts \ + --debug \ + --workspace=./.drashland/lib/intermediary \ + --copy-files=./src +) diff --git a/scripts/lib_builder/deps.ts b/scripts/lib_builder/deps.ts new file mode 100644 index 000000000..4fcf5d9e9 --- /dev/null +++ b/scripts/lib_builder/deps.ts @@ -0,0 +1,6 @@ +export { + emptyDirSync, + ensureDirSync, + walk, +} from "https://deno.land/std@0.191.0/fs/mod.ts"; +export { copySync } from "https://deno.land/std@0.191.0/fs/copy.ts"; diff --git a/scripts/pre_check_release.ts b/scripts/pre_check_release.ts new file mode 100644 index 000000000..8c67d81dc --- /dev/null +++ b/scripts/pre_check_release.ts @@ -0,0 +1,19 @@ +const versionToPublish = Deno.args[0]; + +const packageJsonContents = new TextDecoder().decode( + Deno.readFileSync("./package.json"), +); + +const packageJson = JSON.parse(packageJsonContents); +const packageJsonVersion = `v${packageJson.version}`; + +console.log("Checking package.json version with GitHub release tag version.\n"); +console.log(`packge.json version: ${packageJsonVersion}`); +console.log(`GitHub release version: ${versionToPublish}\n`); + +if (packageJsonVersion !== versionToPublish) { + console.log("Version mismatch. Stopping Release workflow."); + Deno.exit(1); +} else { + console.log("Versions match. Proceeding with Release workflow."); +} diff --git a/scripts/pre_release/validate_version.ts b/scripts/pre_release/validate_version.ts new file mode 100644 index 000000000..8902ba78b --- /dev/null +++ b/scripts/pre_release/validate_version.ts @@ -0,0 +1,23 @@ +import { ConsoleLogger } from "../../src/standard/log/ConsoleLogger"; + +const logger = ConsoleLogger.create("validate_version.ts"); + +const versionToPublish = Deno.args[0]; + +const packageJsonContents = new TextDecoder().decode( + Deno.readFileSync("./package.json"), +); + +const packageJson = JSON.parse(packageJsonContents); +const packageJsonVersion = `v${packageJson.version}`; + +logger.info("Checking package.json version with GitHub release tag version.\n"); +logger.info(`packge.json version: ${packageJsonVersion}`); +logger.info(`GitHub release version: ${versionToPublish}\n`); + +if (packageJsonVersion !== versionToPublish) { + logger.info("Version mismatch. Stopping Release workflow."); + Deno.exit(1); +} else { + logger.info("Versions match. Proceeding with Release workflow."); +} diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 000000000..fa91509b7 --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true + }, + "include": [ + "./**/*.ts" + ] +} diff --git a/services.ts b/services.ts deleted file mode 100644 index ada019baf..000000000 --- a/services.ts +++ /dev/null @@ -1,5 +0,0 @@ -// export { CORSService } from "./src/services/cors/cors.ts"; -export { CSRFService } from "./src/services/csrf/csrf.ts"; -// export { DexterService } from "./src/services/dexter/dexter.ts"; -export { PaladinService } from "./src/services/paladin/paladin.ts"; -// export { TengineService } from "./src/services/tengine/tengine.ts"; diff --git a/src/core/Interfaces.ts b/src/core/Interfaces.ts new file mode 100644 index 000000000..ba440e211 --- /dev/null +++ b/src/core/Interfaces.ts @@ -0,0 +1,23 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +export type * from "./interfaces/IBuilder.ts"; +export type * from "./interfaces/IHandler.ts"; diff --git a/src/core/Types.ts b/src/core/Types.ts new file mode 100644 index 000000000..c0803c0c4 --- /dev/null +++ b/src/core/Types.ts @@ -0,0 +1,26 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +export type * from "./types/MethodOf.ts"; +export type * from "./types/RequestMethod.ts"; +export type * from "./types/ResponseStatus.ts"; +export type * from "./types/ResponseStatusDescription.ts"; +export type * from "./types/ResponseStatusName.ts"; diff --git a/src/core/errors/HTTPError.ts b/src/core/errors/HTTPError.ts new file mode 100644 index 000000000..0b9f7a267 --- /dev/null +++ b/src/core/errors/HTTPError.ts @@ -0,0 +1,88 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import type { ResponseStatus } from "../types/ResponseStatus.ts"; +import type { ResponseStatusCode } from "../types/ResponseStatusCode.ts"; + +/** + * Base class for all HTTP errors in Drash. Use this class to throw uniform HTTP + * errors. + * + * @example + * + * ```ts + * // Usage in resource's GET method + * + * class MyResource extends Resource { + * public GET(request: Request) { + * if (!request.header("authorization")) { + * throw new HTTPError(401, "Get out!"); + * } + * + * return new Response("Authed"); + * } + * } + * ``` + */ +class HTTPError extends Error { + /** + * The HTTP status code associated with this error. + */ + public readonly status_code: ResponseStatusCode; + public readonly status_code_description: string; + + /** + * The name of this error to be used with conditionals that do not work with + * `instanceof`. + * + * @example + * ```ts + * // If this is false ... + * console.log(error instanceof HTTPError); + * + * // ... then do this + * console.log(error.name === 'HTTPError'); + * ``` + */ + public readonly name = "HTTPError"; + + /** + * @param status A valid response status tuple. + * @param message (optional) The error message. Defaults to the description + * associated with the provided `statusCode`. + */ + constructor(status: ResponseStatus, message?: string) { + super(message); + + const { code, description } = status; + + this.status_code = code; + this.status_code_description = description; + + if (!this.message) { + this.message = this.status_code_description; + } + } +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { HTTPError }; diff --git a/src/core/http/Header.ts b/src/core/http/Header.ts new file mode 100644 index 000000000..4fc4079b9 --- /dev/null +++ b/src/core/http/Header.ts @@ -0,0 +1,40 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +export const Header = { + AccessControlAllowCredentials: "Access-Control-Allow-Credentials", + AccessControlAllowHeaders: "Access-Control-Allow-Headers", + AccessControlAllowMethods: "Access-Control-Allow-Methods", + AccessControlAllowOrigin: "Access-Control-Allow-Origin", + AccessControlExposeHeaders: "Access-Control-Expose-Headers", + AccessControlMaxAge: "Access-Control-Max-Age", + AccessControlRequestHeaders: "Access-Control-Request-Headers", + AccessControlRequestMethod: "Acces-Control-Request-Method", + ContentLength: "Content-Length", + ContentType: "Content-Type", + Date: "Date", + ETag: "ETag", + IfMatch: "If-Match", + IfNoneMatch: "If-None-Match", + LastModified: "Last-Modified", + RetryAfter: "Retry-After", + Vary: "Vary", +} as const; diff --git a/src/core/http/Resource.ts b/src/core/http/Resource.ts new file mode 100644 index 000000000..45dc1a96c --- /dev/null +++ b/src/core/http/Resource.ts @@ -0,0 +1,70 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { HTTPError } from "../errors/HTTPError.ts"; +import { Status } from "./response/Status.ts"; + +/** + * The base resource class for all resources. + */ +class Resource { + public paths: string[] = []; + + public CONNECT(_request: unknown): unknown { + throw new HTTPError(Status.NotImplemented); + } + + public DELETE(_request: unknown): unknown { + throw new HTTPError(Status.NotImplemented); + } + + public GET(_request: unknown): unknown { + throw new HTTPError(Status.NotImplemented); + } + + public HEAD(_request: unknown): unknown { + throw new HTTPError(Status.NotImplemented); + } + + public OPTIONS(_request: unknown): unknown { + throw new HTTPError(Status.NotImplemented); + } + + public PATCH(_request: unknown): unknown { + throw new HTTPError(Status.NotImplemented); + } + + public POST(_request: unknown): unknown { + throw new HTTPError(Status.NotImplemented); + } + + public PUT(_request: unknown): unknown { + throw new HTTPError(Status.NotImplemented); + } + + public TRACE(_request: unknown): unknown { + throw new HTTPError(Status.NotImplemented); + } +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { Resource }; diff --git a/src/core/http/request/Method.ts b/src/core/http/request/Method.ts new file mode 100644 index 000000000..37646aac1 --- /dev/null +++ b/src/core/http/request/Method.ts @@ -0,0 +1,37 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +/** + * All HTTP methods. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods} for more information on HTTP methods. + */ +export const Method = { + CONNECT: "CONNECT", + DELETE: "DELETE", + GET: "GET", + HEAD: "HEAD", + OPTIONS: "OPTIONS", + PATCH: "PATCH", + POST: "POST", + PUT: "PUT", + TRACE: "TRACE", +} as const; diff --git a/src/core/http/response/Status.ts b/src/core/http/response/Status.ts new file mode 100644 index 000000000..6d3b23e63 --- /dev/null +++ b/src/core/http/response/Status.ts @@ -0,0 +1,284 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +// Imports > Core +import { StatusCode } from "./StatusCode.ts"; +import { StatusDescription } from "./StatusDescription.ts"; +import type { ResponseStatusCode } from "../../types/ResponseStatusCode.ts"; +import type { ResponseStatusDescription } from "../../types/ResponseStatusDescription.ts"; +import type { ResponseStatusName } from "../../types/ResponseStatusName.ts"; + +export const Status: Record< + ResponseStatusName, + { + readonly code: ResponseStatusCode; + readonly description: ResponseStatusDescription; + } +> = { + Continue: { + code: StatusCode.Continue, + description: StatusDescription.Continue, + }, + SwitchingProtocols: { + code: StatusCode.SwitchingProtocols, + description: StatusDescription.SwitchingProtocols, + }, + Processing: { + code: StatusCode.Processing, + description: StatusDescription.Processing, + }, + EarlyHints: { + code: StatusCode.EarlyHints, + description: StatusDescription.EarlyHints, + }, + OK: { + code: StatusCode.OK, + description: StatusDescription.OK, + }, + Created: { + code: StatusCode.Created, + description: StatusDescription.Created, + }, + Accepted: { + code: StatusCode.Accepted, + description: StatusDescription.Accepted, + }, + NonAuthoritativeInformation: { + code: StatusCode.NonAuthoritativeInformation, + description: StatusDescription.NonAuthoritativeInformation, + }, + NoContent: { + code: StatusCode.NoContent, + description: StatusDescription.NoContent, + }, + ResetContent: { + code: StatusCode.ResetContent, + description: StatusDescription.ResetContent, + }, + PartialContent: { + code: StatusCode.PartialContent, + description: StatusDescription.PartialContent, + }, + MultiStatus: { + code: StatusCode.MultiStatus, + description: StatusDescription.MultiStatus, + }, + AlreadyReported: { + code: StatusCode.AlreadyReported, + description: StatusDescription.AlreadyReported, + }, + IMUsed: { + code: StatusCode.IMUsed, + description: StatusDescription.IMUsed, + }, + MultipleChoices: { + code: StatusCode.MultipleChoices, + description: StatusDescription.MultipleChoices, + }, + MovedPermanently: { + code: StatusCode.MovedPermanently, + description: StatusDescription.MovedPermanently, + }, + Found: { + code: StatusCode.Found, + description: StatusDescription.Found, + }, + SeeOther: { + code: StatusCode.SeeOther, + description: StatusDescription.SeeOther, + }, + NotModified: { + code: StatusCode.NotModified, + description: StatusDescription.NotModified, + }, + UseProxy: { + code: StatusCode.UseProxy, + description: StatusDescription.UseProxy, + }, + TemporaryRedirect: { + code: StatusCode.TemporaryRedirect, + description: StatusDescription.TemporaryRedirect, + }, + PermanentRedirect: { + code: StatusCode.PermanentRedirect, + description: StatusDescription.PermanentRedirect, + }, + BadRequest: { + code: StatusCode.BadRequest, + description: StatusDescription.BadRequest, + }, + Unauthorized: { + code: StatusCode.Unauthorized, + description: StatusDescription.Unauthorized, + }, + PaymentRequired: { + code: StatusCode.PaymentRequired, + description: StatusDescription.PaymentRequired, + }, + Forbidden: { + code: StatusCode.Forbidden, + description: StatusDescription.Forbidden, + }, + NotFound: { + code: StatusCode.NotFound, + description: StatusDescription.NotFound, + }, + MethodNotAllowed: { + code: StatusCode.MethodNotAllowed, + description: StatusDescription.MethodNotAllowed, + }, + NotAcceptable: { + code: StatusCode.NotAcceptable, + description: StatusDescription.NotAcceptable, + }, + ProxyAuthenticationRequired: { + code: StatusCode.ProxyAuthenticationRequired, + description: StatusDescription.ProxyAuthenticationRequired, + }, + RequestTimeout: { + code: StatusCode.RequestTimeout, + description: StatusDescription.RequestTimeout, + }, + Conflict: { + code: StatusCode.Conflict, + description: StatusDescription.Conflict, + }, + Gone: { + code: StatusCode.Gone, + description: StatusDescription.Gone, + }, + LengthRequired: { + code: StatusCode.LengthRequired, + description: StatusDescription.LengthRequired, + }, + PreconditionFailed: { + code: StatusCode.PreconditionFailed, + description: StatusDescription.PreconditionFailed, + }, + PayloadTooLarge: { + code: StatusCode.PayloadTooLarge, + description: StatusDescription.PayloadTooLarge, + }, + URITooLong: { + code: StatusCode.URITooLong, + description: StatusDescription.URITooLong, + }, + UnsupportedMediaType: { + code: StatusCode.UnsupportedMediaType, + description: StatusDescription.UnsupportedMediaType, + }, + RangeNotSatisfiable: { + code: StatusCode.RangeNotSatisfiable, + description: StatusDescription.RangeNotSatisfiable, + }, + ExpectationFailed: { + code: StatusCode.ExpectationFailed, + description: StatusDescription.ExpectationFailed, + }, + Imateapot: { + code: StatusCode.Imateapot, + description: StatusDescription.Imateapot, + }, + MisdirectedRequest: { + code: StatusCode.MisdirectedRequest, + description: StatusDescription.MisdirectedRequest, + }, + UnprocessableEntity: { + code: StatusCode.UnprocessableEntity, + description: StatusDescription.UnprocessableEntity, + }, + Locked: { + code: StatusCode.Locked, + description: StatusDescription.Locked, + }, + FailedDependency: { + code: StatusCode.FailedDependency, + description: StatusDescription.FailedDependency, + }, + TooEarly: { + code: StatusCode.TooEarly, + description: StatusDescription.TooEarly, + }, + UpgradeRequired: { + code: StatusCode.UpgradeRequired, + description: StatusDescription.UpgradeRequired, + }, + PreconditionRequired: { + code: StatusCode.PreconditionRequired, + description: StatusDescription.PreconditionRequired, + }, + TooManyRequests: { + code: StatusCode.TooManyRequests, + description: StatusDescription.TooManyRequests, + }, + RequestHeaderFieldsTooLarge: { + code: StatusCode.RequestHeaderFieldsTooLarge, + description: StatusDescription.RequestHeaderFieldsTooLarge, + }, + UnavailableForLegalReasons: { + code: StatusCode.UnavailableForLegalReasons, + description: StatusDescription.UnavailableForLegalReasons, + }, + InternalServerError: { + code: StatusCode.InternalServerError, + description: StatusDescription.InternalServerError, + }, + NotImplemented: { + code: StatusCode.NotImplemented, + description: StatusDescription.NotImplemented, + }, + BadGateway: { + code: StatusCode.BadGateway, + description: StatusDescription.BadGateway, + }, + ServiceUnavailable: { + code: StatusCode.ServiceUnavailable, + description: StatusDescription.ServiceUnavailable, + }, + GatewayTimeout: { + code: StatusCode.GatewayTimeout, + description: StatusDescription.GatewayTimeout, + }, + HTTPVersionNotSupported: { + code: StatusCode.HTTPVersionNotSupported, + description: StatusDescription.HTTPVersionNotSupported, + }, + VariantAlsoNegotiates: { + code: StatusCode.VariantAlsoNegotiates, + description: StatusDescription.VariantAlsoNegotiates, + }, + InsufficientStorage: { + code: StatusCode.InsufficientStorage, + description: StatusDescription.InsufficientStorage, + }, + LoopDetected: { + code: StatusCode.LoopDetected, + description: StatusDescription.LoopDetected, + }, + NotExtended: { + code: StatusCode.NotExtended, + description: StatusDescription.NotExtended, + }, + NetworkAuthenticationRequired: { + code: StatusCode.NetworkAuthenticationRequired, + description: StatusDescription.NetworkAuthenticationRequired, + }, +} as const; diff --git a/src/core/http/response/StatusCode.ts b/src/core/http/response/StatusCode.ts new file mode 100644 index 000000000..25cfdc071 --- /dev/null +++ b/src/core/http/response/StatusCode.ts @@ -0,0 +1,101 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { ResponseStatusName } from "../../types/ResponseStatusName.ts"; + +/** + * All HTTP response status codes from {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status}. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status} from more information on HTTP status codes. + */ +export const StatusCode: Record = { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#information_responses + Continue: 100, + SwitchingProtocols: 101, + Processing: 102, + EarlyHints: 103, + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#successful_responses + OK: 200, + Created: 201, + Accepted: 202, + NonAuthoritativeInformation: 203, + NoContent: 204, + ResetContent: 205, + PartialContent: 206, + MultiStatus: 207, + AlreadyReported: 208, + IMUsed: 226, + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages + MultipleChoices: 300, + MovedPermanently: 301, + Found: 302, + SeeOther: 303, + NotModified: 304, + UseProxy: 305, + TemporaryRedirect: 307, + PermanentRedirect: 308, + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses + BadRequest: 400, + Unauthorized: 401, + PaymentRequired: 402, + Forbidden: 403, + NotFound: 404, + MethodNotAllowed: 405, + NotAcceptable: 406, + ProxyAuthenticationRequired: 407, + RequestTimeout: 408, + Conflict: 409, + Gone: 410, + LengthRequired: 411, + PreconditionFailed: 412, + PayloadTooLarge: 413, + URITooLong: 414, + UnsupportedMediaType: 415, + RangeNotSatisfiable: 416, + ExpectationFailed: 417, + Imateapot: 418, + MisdirectedRequest: 421, + UnprocessableEntity: 422, + Locked: 423, + FailedDependency: 424, + TooEarly: 425, + UpgradeRequired: 426, + PreconditionRequired: 428, + TooManyRequests: 429, + RequestHeaderFieldsTooLarge: 431, + UnavailableForLegalReasons: 451, + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses + InternalServerError: 500, + NotImplemented: 501, + BadGateway: 502, + ServiceUnavailable: 503, + GatewayTimeout: 504, + HTTPVersionNotSupported: 505, + VariantAlsoNegotiates: 506, + InsufficientStorage: 507, + LoopDetected: 508, + NotExtended: 510, + NetworkAuthenticationRequired: 511, +} as const; diff --git a/src/core/http/response/StatusDescription.ts b/src/core/http/response/StatusDescription.ts new file mode 100644 index 000000000..9287e5f6b --- /dev/null +++ b/src/core/http/response/StatusDescription.ts @@ -0,0 +1,87 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { ResponseStatusName } from "../../types/ResponseStatusName.ts"; + +export const StatusDescription: Record = { + Continue: "Continue", + SwitchingProtocols: "Switching Protocols", + Processing: "Processing", + EarlyHints: "Early Hints", + OK: "OK", + Created: "Created", + Accepted: "Accepted", + NonAuthoritativeInformation: "Non-Authoritative Information", + NoContent: "No Content", + ResetContent: "Reset Content", + PartialContent: "Partial Content", + MultiStatus: "Multi Status", + AlreadyReported: "Already Reported", + IMUsed: "IM Used", + MultipleChoices: "Multiple Choices", + MovedPermanently: "Moved Permanently", + Found: "Found", + SeeOther: "See Other", + NotModified: "Not Modified", + UseProxy: "Use Proxy", + TemporaryRedirect: "Temporary Redirect", + PermanentRedirect: "Permanent Redirect", + BadRequest: "Bad Request", + Unauthorized: "Unauthorized", + PaymentRequired: "Payment Required", + Forbidden: "Forbidden", + NotFound: "Not Found", + MethodNotAllowed: "Method Not Allowed", + NotAcceptable: "Not Acceptable", + ProxyAuthenticationRequired: "Proxy Auth Required", + RequestTimeout: "Request Timeout", + Conflict: "Conflict", + Gone: "Gone", + LengthRequired: "Length Required", + PreconditionFailed: "Precondition Failed", + PayloadTooLarge: "Request Entity Too Large", + URITooLong: "Request URI Too Long", + UnsupportedMediaType: "Unsupported Media Type", + RangeNotSatisfiable: "Requested Range Not Satisfiable", + ExpectationFailed: "Expectation Failed", + Imateapot: "I'm a teapot", + MisdirectedRequest: "Misdirected Request", + UnprocessableEntity: "Unprocessable Entity", + Locked: "Locked", + FailedDependency: "Failed Dependency", + TooEarly: "Too Early", + UpgradeRequired: "Upgrade Required", + PreconditionRequired: "Precondition Required", + TooManyRequests: "Too Many Requests", + RequestHeaderFieldsTooLarge: "Request Header Fields Too Large", + UnavailableForLegalReasons: "Unavailable For Legal Reasons", + InternalServerError: "Internal Server Error", + NotImplemented: "Not Implemented", + BadGateway: "Bad Gateway", + ServiceUnavailable: "Service Unavailable", + GatewayTimeout: "Gateway Timeout", + HTTPVersionNotSupported: "HTTP Version Not Supported", + VariantAlsoNegotiates: "Variant Also Negotiates", + InsufficientStorage: "Insufficient Storage", + LoopDetected: "Loop Detected", + NotExtended: "Not Extended", + NetworkAuthenticationRequired: "Network Authentication Required", +} as const; diff --git a/src/core/http/response/StatusName.ts b/src/core/http/response/StatusName.ts new file mode 100644 index 000000000..92ba0310a --- /dev/null +++ b/src/core/http/response/StatusName.ts @@ -0,0 +1,85 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +export const StatusName = { + Continue: "Continue", + SwitchingProtocols: "SwitchingProtocols", + Processing: "Processing", + EarlyHints: "EarlyHints", + OK: "OK", + Created: "Created", + Accepted: "Accepted", + NonAuthoritativeInformation: "NonAuthoritativeInformation", + NoContent: "NoContent", + ResetContent: "ResetContent", + PartialContent: "PartialContent", + MultiStatus: "MultiStatus", + AlreadyReported: "AlreadyReported", + IMUsed: "IMUsed", + MultipleChoices: "MultipleChoices", + MovedPermanently: "MovedPermanently", + Found: "Found", + SeeOther: "SeeOther", + NotModified: "NotModified", + UseProxy: "UseProxy", + TemporaryRedirect: "TemporaryRedirect", + PermanentRedirect: "PermanentRedirect", + BadRequest: "BadRequest", + Unauthorized: "Unauthorized", + PaymentRequired: "PaymentRequired", + Forbidden: "Forbidden", + NotFound: "NotFound", + MethodNotAllowed: "MethodNotAllowed", + NotAcceptable: "NotAcceptable", + ProxyAuthenticationRequired: "ProxyAuthenticationRequired", + RequestTimeout: "RequestTimeout", + Conflict: "Conflict", + Gone: "Gone", + LengthRequired: "LengthRequired", + PreconditionFailed: "PreconditionFailed", + PayloadTooLarge: "PayloadTooLarge", + URITooLong: "URITooLong", + UnsupportedMediaType: "UnsupportedMediaType", + RangeNotSatisfiable: "RangeNotSatisfiable", + ExpectationFailed: "ExpectationFailed", + Imateapot: "Imateapot", + MisdirectedRequest: "MisdirectedRequest", + UnprocessableEntity: "UnprocessableEntity", + Locked: "Locked", + FailedDependency: "FailedDependency", + TooEarly: "TooEarly", + UpgradeRequired: "UpgradeRequired", + PreconditionRequired: "PreconditionRequired", + TooManyRequests: "TooManyRequests", + RequestHeaderFieldsTooLarge: "RequestHeaderFieldsTooLarge", + UnavailableForLegalReasons: "UnavailableForLegalReasons", + InternalServerError: "InternalServerError", + NotImplemented: "NotImplemented", + BadGateway: "BadGateway", + ServiceUnavailable: "ServiceUnavailable", + GatewayTimeout: "GatewayTimeout", + HTTPVersionNotSupported: "HTTPVersionNotSupported", + VariantAlsoNegotiates: "VariantAlsoNegotiates", + InsufficientStorage: "InsufficientStorage", + LoopDetected: "LoopDetected", + NotExtended: "NotExtended", + NetworkAuthenticationRequired: "NetworkAuthenticationRequired", +} as const; diff --git a/src/core/interfaces/IBuilder.ts b/src/core/interfaces/IBuilder.ts new file mode 100644 index 000000000..300079a95 --- /dev/null +++ b/src/core/interfaces/IBuilder.ts @@ -0,0 +1,30 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +/** + * A base interface for builder classes. + */ +export interface IBuilder { + /** + * Build the object. + */ + build(): Product; +} diff --git a/src/core/interfaces/IHandler.ts b/src/core/interfaces/IHandler.ts new file mode 100644 index 000000000..a72a6ca3a --- /dev/null +++ b/src/core/interfaces/IHandler.ts @@ -0,0 +1,40 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +/** + * A base interface for handler classes. + */ +export interface IHandler { + /** + * Handle the given input to produce an output. For example, handle a request + * (the input) to produce a response (the output). + * @param input The input in question. + * @return The output. + */ + handle(req: Input): Promise; + + /** + * Set this handler's next handler. + * @param nextHandler + * @returns The next handler. + */ + setNext(h: IHandler): IHandler; +} diff --git a/src/core/types/Header.ts b/src/core/types/Header.ts new file mode 100644 index 000000000..a57bb607c --- /dev/null +++ b/src/core/types/Header.ts @@ -0,0 +1,24 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Header } from "../http/Header.ts"; + +export type Header = (typeof Header)[keyof typeof Header]; diff --git a/src/core/types/MethodOf.ts b/src/core/types/MethodOf.ts new file mode 100644 index 000000000..194fdc1c9 --- /dev/null +++ b/src/core/types/MethodOf.ts @@ -0,0 +1,53 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +/** + * A utility type that lets the compiler and reader know: the member + * assigned this type is a method of the given generic `Object`. + * + * @example + * ```ts + * class One { + * a() {} + * b() {} + * c() {} + * } + * + * const methods = ["a", "b", "c"]; + * + * const one = new One(); + * + * one[methods[0]](); + * // Element implicitly has an 'any' type because expression of type 'string' + * // can't be used to index type 'One'.(7053) + * + * one[methods[0] as MethodOf](); + * // OK + * ``` + */ +export type MethodOf = { + [K in keyof Object]: Object[K] extends Func ? K + : never; +}[keyof Object]; + +type Func = + | ((...args: unknown[]) => unknown) + | (() => unknown); diff --git a/src/core/types/RequestMethod.ts b/src/core/types/RequestMethod.ts new file mode 100644 index 000000000..1cbf9040f --- /dev/null +++ b/src/core/types/RequestMethod.ts @@ -0,0 +1,24 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Method } from "../http/request/Method.ts"; + +export type RequestMethod = (typeof Method)[keyof typeof Method]; diff --git a/src/core/types/ResponseStatus.ts b/src/core/types/ResponseStatus.ts new file mode 100644 index 000000000..4b3845212 --- /dev/null +++ b/src/core/types/ResponseStatus.ts @@ -0,0 +1,28 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import type { ResponseStatusCode } from "./ResponseStatusCode.ts"; +import type { ResponseStatusDescription } from "./ResponseStatusDescription.ts"; + +export type ResponseStatus = { + readonly code: ResponseStatusCode; + readonly description: ResponseStatusDescription; +}; diff --git a/src/core/types/ResponseStatusCode.ts b/src/core/types/ResponseStatusCode.ts new file mode 100644 index 000000000..6ef456664 --- /dev/null +++ b/src/core/types/ResponseStatusCode.ts @@ -0,0 +1,24 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { StatusCode } from "../http/response/StatusCode.ts"; + +export type ResponseStatusCode = (typeof StatusCode)[keyof typeof StatusCode]; diff --git a/src/core/types/ResponseStatusDescription.ts b/src/core/types/ResponseStatusDescription.ts new file mode 100644 index 000000000..bb41743f8 --- /dev/null +++ b/src/core/types/ResponseStatusDescription.ts @@ -0,0 +1,25 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { StatusDescription } from "../http/response/StatusDescription.ts"; + +export type ResponseStatusDescription = + (typeof StatusDescription)[keyof typeof StatusDescription]; diff --git a/src/core/types/ResponseStatusName.ts b/src/core/types/ResponseStatusName.ts new file mode 100644 index 000000000..3e026a9ca --- /dev/null +++ b/src/core/types/ResponseStatusName.ts @@ -0,0 +1,24 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { StatusName } from "../http/response/StatusName.ts"; + +export type ResponseStatusName = (typeof StatusName)[keyof typeof StatusName]; diff --git a/src/dictionaries/mime_db.ts b/src/dictionaries/mime_db.ts deleted file mode 100644 index f229735ca..000000000 --- a/src/dictionaries/mime_db.ts +++ /dev/null @@ -1,8196 +0,0 @@ -import * as Drash from "../../mod.ts"; - -export const mimeDb: Drash.Interfaces.IMime = { - "application/1d-interleaved-parityfec": { - source: "iana", - }, - "application/3gpdash-qoe-report+xml": { - source: "iana", - compressible: true, - }, - "application/3gpp-ims+xml": { - source: "iana", - compressible: true, - }, - "application/a2l": { - source: "iana", - }, - "application/activemessage": { - source: "iana", - }, - "application/activity+json": { - source: "iana", - compressible: true, - }, - "application/alto-costmap+json": { - source: "iana", - compressible: true, - }, - "application/alto-costmapfilter+json": { - source: "iana", - compressible: true, - }, - "application/alto-directory+json": { - source: "iana", - compressible: true, - }, - "application/alto-endpointcost+json": { - source: "iana", - compressible: true, - }, - "application/alto-endpointcostparams+json": { - source: "iana", - compressible: true, - }, - "application/alto-endpointprop+json": { - source: "iana", - compressible: true, - }, - "application/alto-endpointpropparams+json": { - source: "iana", - compressible: true, - }, - "application/alto-error+json": { - source: "iana", - compressible: true, - }, - "application/alto-networkmap+json": { - source: "iana", - compressible: true, - }, - "application/alto-networkmapfilter+json": { - source: "iana", - compressible: true, - }, - "application/alto-updatestreamcontrol+json": { - source: "iana", - compressible: true, - }, - "application/alto-updatestreamparams+json": { - source: "iana", - compressible: true, - }, - "application/aml": { - source: "iana", - }, - "application/andrew-inset": { - source: "iana", - extensions: ["ez"], - }, - "application/applefile": { - source: "iana", - }, - "application/applixware": { - source: "apache", - extensions: ["aw"], - }, - "application/atf": { - source: "iana", - }, - "application/atfx": { - source: "iana", - }, - "application/atom+xml": { - source: "iana", - compressible: true, - extensions: ["atom"], - }, - "application/atomcat+xml": { - source: "iana", - compressible: true, - extensions: ["atomcat"], - }, - "application/atomdeleted+xml": { - source: "iana", - compressible: true, - extensions: ["atomdeleted"], - }, - "application/atomicmail": { - source: "iana", - }, - "application/atomsvc+xml": { - source: "iana", - compressible: true, - extensions: ["atomsvc"], - }, - "application/atsc-dwd+xml": { - source: "iana", - compressible: true, - extensions: ["dwd"], - }, - "application/atsc-dynamic-event-message": { - source: "iana", - }, - "application/atsc-held+xml": { - source: "iana", - compressible: true, - extensions: ["held"], - }, - "application/atsc-rdt+json": { - source: "iana", - compressible: true, - }, - "application/atsc-rsat+xml": { - source: "iana", - compressible: true, - extensions: ["rsat"], - }, - "application/atxml": { - source: "iana", - }, - "application/auth-policy+xml": { - source: "iana", - compressible: true, - }, - "application/bacnet-xdd+zip": { - source: "iana", - compressible: false, - }, - "application/batch-smtp": { - source: "iana", - }, - "application/bdoc": { - compressible: false, - extensions: ["bdoc"], - }, - "application/beep+xml": { - source: "iana", - compressible: true, - }, - "application/calendar+json": { - source: "iana", - compressible: true, - }, - "application/calendar+xml": { - source: "iana", - compressible: true, - extensions: ["xcs"], - }, - "application/call-completion": { - source: "iana", - }, - "application/cals-1840": { - source: "iana", - }, - "application/cap+xml": { - source: "iana", - compressible: true, - }, - "application/cbor": { - source: "iana", - }, - "application/cbor-seq": { - source: "iana", - }, - "application/cccex": { - source: "iana", - }, - "application/ccmp+xml": { - source: "iana", - compressible: true, - }, - "application/ccxml+xml": { - source: "iana", - compressible: true, - extensions: ["ccxml"], - }, - "application/cdfx+xml": { - source: "iana", - compressible: true, - extensions: ["cdfx"], - }, - "application/cdmi-capability": { - source: "iana", - extensions: ["cdmia"], - }, - "application/cdmi-container": { - source: "iana", - extensions: ["cdmic"], - }, - "application/cdmi-domain": { - source: "iana", - extensions: ["cdmid"], - }, - "application/cdmi-object": { - source: "iana", - extensions: ["cdmio"], - }, - "application/cdmi-queue": { - source: "iana", - extensions: ["cdmiq"], - }, - "application/cdni": { - source: "iana", - }, - "application/cea": { - source: "iana", - }, - "application/cea-2018+xml": { - source: "iana", - compressible: true, - }, - "application/cellml+xml": { - source: "iana", - compressible: true, - }, - "application/cfw": { - source: "iana", - }, - "application/clue+xml": { - source: "iana", - compressible: true, - }, - "application/clue_info+xml": { - source: "iana", - compressible: true, - }, - "application/cms": { - source: "iana", - }, - "application/cnrp+xml": { - source: "iana", - compressible: true, - }, - "application/coap-group+json": { - source: "iana", - compressible: true, - }, - "application/coap-payload": { - source: "iana", - }, - "application/commonground": { - source: "iana", - }, - "application/conference-info+xml": { - source: "iana", - compressible: true, - }, - "application/cose": { - source: "iana", - }, - "application/cose-key": { - source: "iana", - }, - "application/cose-key-set": { - source: "iana", - }, - "application/cpl+xml": { - source: "iana", - compressible: true, - }, - "application/csrattrs": { - source: "iana", - }, - "application/csta+xml": { - source: "iana", - compressible: true, - }, - "application/cstadata+xml": { - source: "iana", - compressible: true, - }, - "application/csvm+json": { - source: "iana", - compressible: true, - }, - "application/cu-seeme": { - source: "apache", - extensions: ["cu"], - }, - "application/cwt": { - source: "iana", - }, - "application/cybercash": { - source: "iana", - }, - "application/dart": { - compressible: true, - }, - "application/dash+xml": { - source: "iana", - compressible: true, - extensions: ["mpd"], - }, - "application/dashdelta": { - source: "iana", - }, - "application/davmount+xml": { - source: "iana", - compressible: true, - extensions: ["davmount"], - }, - "application/dca-rft": { - source: "iana", - }, - "application/dcd": { - source: "iana", - }, - "application/dec-dx": { - source: "iana", - }, - "application/dialog-info+xml": { - source: "iana", - compressible: true, - }, - "application/dicom": { - source: "iana", - }, - "application/dicom+json": { - source: "iana", - compressible: true, - }, - "application/dicom+xml": { - source: "iana", - compressible: true, - }, - "application/dii": { - source: "iana", - }, - "application/dit": { - source: "iana", - }, - "application/dns": { - source: "iana", - }, - "application/dns+json": { - source: "iana", - compressible: true, - }, - "application/dns-message": { - source: "iana", - }, - "application/docbook+xml": { - source: "apache", - compressible: true, - extensions: ["dbk"], - }, - "application/dots+cbor": { - source: "iana", - }, - "application/dskpp+xml": { - source: "iana", - compressible: true, - }, - "application/dssc+der": { - source: "iana", - extensions: ["dssc"], - }, - "application/dssc+xml": { - source: "iana", - compressible: true, - extensions: ["xdssc"], - }, - "application/dvcs": { - source: "iana", - }, - "application/ecmascript": { - source: "iana", - compressible: true, - extensions: ["ecma", "es"], - }, - "application/edi-consent": { - source: "iana", - }, - "application/edi-x12": { - source: "iana", - compressible: false, - }, - "application/edifact": { - source: "iana", - compressible: false, - }, - "application/efi": { - source: "iana", - }, - "application/emergencycalldata.comment+xml": { - source: "iana", - compressible: true, - }, - "application/emergencycalldata.control+xml": { - source: "iana", - compressible: true, - }, - "application/emergencycalldata.deviceinfo+xml": { - source: "iana", - compressible: true, - }, - "application/emergencycalldata.ecall.msd": { - source: "iana", - }, - "application/emergencycalldata.providerinfo+xml": { - source: "iana", - compressible: true, - }, - "application/emergencycalldata.serviceinfo+xml": { - source: "iana", - compressible: true, - }, - "application/emergencycalldata.subscriberinfo+xml": { - source: "iana", - compressible: true, - }, - "application/emergencycalldata.veds+xml": { - source: "iana", - compressible: true, - }, - "application/emma+xml": { - source: "iana", - compressible: true, - extensions: ["emma"], - }, - "application/emotionml+xml": { - source: "iana", - compressible: true, - extensions: ["emotionml"], - }, - "application/encaprtp": { - source: "iana", - }, - "application/epp+xml": { - source: "iana", - compressible: true, - }, - "application/epub+zip": { - source: "iana", - compressible: false, - extensions: ["epub"], - }, - "application/eshop": { - source: "iana", - }, - "application/exi": { - source: "iana", - extensions: ["exi"], - }, - "application/expect-ct-report+json": { - source: "iana", - compressible: true, - }, - "application/fastinfoset": { - source: "iana", - }, - "application/fastsoap": { - source: "iana", - }, - "application/fdt+xml": { - source: "iana", - compressible: true, - extensions: ["fdt"], - }, - "application/fhir+json": { - source: "iana", - compressible: true, - }, - "application/fhir+xml": { - source: "iana", - compressible: true, - }, - "application/fido.trusted-apps+json": { - compressible: true, - }, - "application/fits": { - source: "iana", - }, - "application/flexfec": { - source: "iana", - }, - "application/font-sfnt": { - source: "iana", - }, - "application/font-tdpfr": { - source: "iana", - extensions: ["pfr"], - }, - "application/font-woff": { - source: "iana", - compressible: false, - }, - "application/framework-attributes+xml": { - source: "iana", - compressible: true, - }, - "application/geo+json": { - source: "iana", - compressible: true, - extensions: ["geojson"], - }, - "application/geo+json-seq": { - source: "iana", - }, - "application/geopackage+sqlite3": { - source: "iana", - }, - "application/geoxacml+xml": { - source: "iana", - compressible: true, - }, - "application/gltf-buffer": { - source: "iana", - }, - "application/gml+xml": { - source: "iana", - compressible: true, - extensions: ["gml"], - }, - "application/gpx+xml": { - source: "apache", - compressible: true, - extensions: ["gpx"], - }, - "application/gxf": { - source: "apache", - extensions: ["gxf"], - }, - "application/gzip": { - source: "iana", - compressible: false, - extensions: ["gz"], - }, - "application/h224": { - source: "iana", - }, - "application/held+xml": { - source: "iana", - compressible: true, - }, - "application/hjson": { - extensions: ["hjson"], - }, - "application/http": { - source: "iana", - }, - "application/hyperstudio": { - source: "iana", - extensions: ["stk"], - }, - "application/ibe-key-request+xml": { - source: "iana", - compressible: true, - }, - "application/ibe-pkg-reply+xml": { - source: "iana", - compressible: true, - }, - "application/ibe-pp-data": { - source: "iana", - }, - "application/iges": { - source: "iana", - }, - "application/im-iscomposing+xml": { - source: "iana", - compressible: true, - }, - "application/index": { - source: "iana", - }, - "application/index.cmd": { - source: "iana", - }, - "application/index.obj": { - source: "iana", - }, - "application/index.response": { - source: "iana", - }, - "application/index.vnd": { - source: "iana", - }, - "application/inkml+xml": { - source: "iana", - compressible: true, - extensions: ["ink", "inkml"], - }, - "application/iotp": { - source: "iana", - }, - "application/ipfix": { - source: "iana", - extensions: ["ipfix"], - }, - "application/ipp": { - source: "iana", - }, - "application/isup": { - source: "iana", - }, - "application/its+xml": { - source: "iana", - compressible: true, - extensions: ["its"], - }, - "application/java-archive": { - source: "apache", - compressible: false, - extensions: ["jar", "war", "ear"], - }, - "application/java-serialized-object": { - source: "apache", - compressible: false, - extensions: ["ser"], - }, - "application/java-vm": { - source: "apache", - compressible: false, - extensions: ["class"], - }, - "application/javascript": { - source: "iana", - charset: "UTF-8", - compressible: true, - extensions: ["js", "mjs"], - }, - "application/jf2feed+json": { - source: "iana", - compressible: true, - }, - "application/jose": { - source: "iana", - }, - "application/jose+json": { - source: "iana", - compressible: true, - }, - "application/jrd+json": { - source: "iana", - compressible: true, - }, - "application/json": { - source: "iana", - charset: "UTF-8", - compressible: true, - extensions: ["json", "map"], - }, - "application/json-patch+json": { - source: "iana", - compressible: true, - }, - "application/json-seq": { - source: "iana", - }, - "application/json5": { - extensions: ["json5"], - }, - "application/jsonml+json": { - source: "apache", - compressible: true, - extensions: ["jsonml"], - }, - "application/jwk+json": { - source: "iana", - compressible: true, - }, - "application/jwk-set+json": { - source: "iana", - compressible: true, - }, - "application/jwt": { - source: "iana", - }, - "application/kpml-request+xml": { - source: "iana", - compressible: true, - }, - "application/kpml-response+xml": { - source: "iana", - compressible: true, - }, - "application/ld+json": { - source: "iana", - compressible: true, - extensions: ["jsonld"], - }, - "application/lgr+xml": { - source: "iana", - compressible: true, - extensions: ["lgr"], - }, - "application/link-format": { - source: "iana", - }, - "application/load-control+xml": { - source: "iana", - compressible: true, - }, - "application/lost+xml": { - source: "iana", - compressible: true, - extensions: ["lostxml"], - }, - "application/lostsync+xml": { - source: "iana", - compressible: true, - }, - "application/lpf+zip": { - source: "iana", - compressible: false, - }, - "application/lxf": { - source: "iana", - }, - "application/mac-binhex40": { - source: "iana", - extensions: ["hqx"], - }, - "application/mac-compactpro": { - source: "apache", - extensions: ["cpt"], - }, - "application/macwriteii": { - source: "iana", - }, - "application/mads+xml": { - source: "iana", - compressible: true, - extensions: ["mads"], - }, - "application/manifest+json": { - charset: "UTF-8", - compressible: true, - extensions: ["webmanifest"], - }, - "application/marc": { - source: "iana", - extensions: ["mrc"], - }, - "application/marcxml+xml": { - source: "iana", - compressible: true, - extensions: ["mrcx"], - }, - "application/mathematica": { - source: "iana", - extensions: ["ma", "nb", "mb"], - }, - "application/mathml+xml": { - source: "iana", - compressible: true, - extensions: ["mathml"], - }, - "application/mathml-content+xml": { - source: "iana", - compressible: true, - }, - "application/mathml-presentation+xml": { - source: "iana", - compressible: true, - }, - "application/mbms-associated-procedure-description+xml": { - source: "iana", - compressible: true, - }, - "application/mbms-deregister+xml": { - source: "iana", - compressible: true, - }, - "application/mbms-envelope+xml": { - source: "iana", - compressible: true, - }, - "application/mbms-msk+xml": { - source: "iana", - compressible: true, - }, - "application/mbms-msk-response+xml": { - source: "iana", - compressible: true, - }, - "application/mbms-protection-description+xml": { - source: "iana", - compressible: true, - }, - "application/mbms-reception-report+xml": { - source: "iana", - compressible: true, - }, - "application/mbms-register+xml": { - source: "iana", - compressible: true, - }, - "application/mbms-register-response+xml": { - source: "iana", - compressible: true, - }, - "application/mbms-schedule+xml": { - source: "iana", - compressible: true, - }, - "application/mbms-user-service-description+xml": { - source: "iana", - compressible: true, - }, - "application/mbox": { - source: "iana", - extensions: ["mbox"], - }, - "application/media-policy-dataset+xml": { - source: "iana", - compressible: true, - }, - "application/media_control+xml": { - source: "iana", - compressible: true, - }, - "application/mediaservercontrol+xml": { - source: "iana", - compressible: true, - extensions: ["mscml"], - }, - "application/merge-patch+json": { - source: "iana", - compressible: true, - }, - "application/metalink+xml": { - source: "apache", - compressible: true, - extensions: ["metalink"], - }, - "application/metalink4+xml": { - source: "iana", - compressible: true, - extensions: ["meta4"], - }, - "application/mets+xml": { - source: "iana", - compressible: true, - extensions: ["mets"], - }, - "application/mf4": { - source: "iana", - }, - "application/mikey": { - source: "iana", - }, - "application/mipc": { - source: "iana", - }, - "application/mmt-aei+xml": { - source: "iana", - compressible: true, - extensions: ["maei"], - }, - "application/mmt-usd+xml": { - source: "iana", - compressible: true, - extensions: ["musd"], - }, - "application/mods+xml": { - source: "iana", - compressible: true, - extensions: ["mods"], - }, - "application/moss-keys": { - source: "iana", - }, - "application/moss-signature": { - source: "iana", - }, - "application/mosskey-data": { - source: "iana", - }, - "application/mosskey-request": { - source: "iana", - }, - "application/mp21": { - source: "iana", - extensions: ["m21", "mp21"], - }, - "application/mp4": { - source: "iana", - extensions: ["mp4s", "m4p"], - }, - "application/mpeg4-generic": { - source: "iana", - }, - "application/mpeg4-iod": { - source: "iana", - }, - "application/mpeg4-iod-xmt": { - source: "iana", - }, - "application/mrb-consumer+xml": { - source: "iana", - compressible: true, - extensions: ["xdf"], - }, - "application/mrb-publish+xml": { - source: "iana", - compressible: true, - extensions: ["xdf"], - }, - "application/msc-ivr+xml": { - source: "iana", - compressible: true, - }, - "application/msc-mixer+xml": { - source: "iana", - compressible: true, - }, - "application/msword": { - source: "iana", - compressible: false, - extensions: ["doc", "dot"], - }, - "application/mud+json": { - source: "iana", - compressible: true, - }, - "application/multipart-core": { - source: "iana", - }, - "application/mxf": { - source: "iana", - extensions: ["mxf"], - }, - "application/n-quads": { - source: "iana", - extensions: ["nq"], - }, - "application/n-triples": { - source: "iana", - extensions: ["nt"], - }, - "application/nasdata": { - source: "iana", - }, - "application/news-checkgroups": { - source: "iana", - }, - "application/news-groupinfo": { - source: "iana", - }, - "application/news-transmission": { - source: "iana", - }, - "application/nlsml+xml": { - source: "iana", - compressible: true, - }, - "application/node": { - source: "iana", - extensions: ["cjs"], - }, - "application/nss": { - source: "iana", - }, - "application/ocsp-request": { - source: "iana", - }, - "application/ocsp-response": { - source: "iana", - }, - "application/octet-stream": { - source: "iana", - compressible: false, - extensions: [ - "bin", - "dms", - "lrf", - "mar", - "so", - "dist", - "distz", - "pkg", - "bpk", - "dump", - "elc", - "deploy", - "exe", - "dll", - "deb", - "dmg", - "iso", - "img", - "msi", - "msp", - "msm", - "buffer", - ], - }, - "application/oda": { - source: "iana", - extensions: ["oda"], - }, - "application/odm+xml": { - source: "iana", - compressible: true, - }, - "application/odx": { - source: "iana", - }, - "application/oebps-package+xml": { - source: "iana", - compressible: true, - extensions: ["opf"], - }, - "application/ogg": { - source: "iana", - compressible: false, - extensions: ["ogx"], - }, - "application/omdoc+xml": { - source: "apache", - compressible: true, - extensions: ["omdoc"], - }, - "application/onenote": { - source: "apache", - extensions: ["onetoc", "onetoc2", "onetmp", "onepkg"], - }, - "application/oscore": { - source: "iana", - }, - "application/oxps": { - source: "iana", - extensions: ["oxps"], - }, - "application/p2p-overlay+xml": { - source: "iana", - compressible: true, - extensions: ["relo"], - }, - "application/parityfec": { - source: "iana", - }, - "application/passport": { - source: "iana", - }, - "application/patch-ops-error+xml": { - source: "iana", - compressible: true, - extensions: ["xer"], - }, - "application/pdf": { - source: "iana", - compressible: false, - extensions: ["pdf"], - }, - "application/pdx": { - source: "iana", - }, - "application/pem-certificate-chain": { - source: "iana", - }, - "application/pgp-encrypted": { - source: "iana", - compressible: false, - extensions: ["pgp"], - }, - "application/pgp-keys": { - source: "iana", - }, - "application/pgp-signature": { - source: "iana", - extensions: ["asc", "sig"], - }, - "application/pics-rules": { - source: "apache", - extensions: ["prf"], - }, - "application/pidf+xml": { - source: "iana", - compressible: true, - }, - "application/pidf-diff+xml": { - source: "iana", - compressible: true, - }, - "application/pkcs10": { - source: "iana", - extensions: ["p10"], - }, - "application/pkcs12": { - source: "iana", - }, - "application/pkcs7-mime": { - source: "iana", - extensions: ["p7m", "p7c"], - }, - "application/pkcs7-signature": { - source: "iana", - extensions: ["p7s"], - }, - "application/pkcs8": { - source: "iana", - extensions: ["p8"], - }, - "application/pkcs8-encrypted": { - source: "iana", - }, - "application/pkix-attr-cert": { - source: "iana", - extensions: ["ac"], - }, - "application/pkix-cert": { - source: "iana", - extensions: ["cer"], - }, - "application/pkix-crl": { - source: "iana", - extensions: ["crl"], - }, - "application/pkix-pkipath": { - source: "iana", - extensions: ["pkipath"], - }, - "application/pkixcmp": { - source: "iana", - extensions: ["pki"], - }, - "application/pls+xml": { - source: "iana", - compressible: true, - extensions: ["pls"], - }, - "application/poc-settings+xml": { - source: "iana", - compressible: true, - }, - "application/postscript": { - source: "iana", - compressible: true, - extensions: ["ai", "eps", "ps"], - }, - "application/ppsp-tracker+json": { - source: "iana", - compressible: true, - }, - "application/problem+json": { - source: "iana", - compressible: true, - }, - "application/problem+xml": { - source: "iana", - compressible: true, - }, - "application/provenance+xml": { - source: "iana", - compressible: true, - extensions: ["provx"], - }, - "application/prs.alvestrand.titrax-sheet": { - source: "iana", - }, - "application/prs.cww": { - source: "iana", - extensions: ["cww"], - }, - "application/prs.hpub+zip": { - source: "iana", - compressible: false, - }, - "application/prs.nprend": { - source: "iana", - }, - "application/prs.plucker": { - source: "iana", - }, - "application/prs.rdf-xml-crypt": { - source: "iana", - }, - "application/prs.xsf+xml": { - source: "iana", - compressible: true, - }, - "application/pskc+xml": { - source: "iana", - compressible: true, - extensions: ["pskcxml"], - }, - "application/pvd+json": { - source: "iana", - compressible: true, - }, - "application/qsig": { - source: "iana", - }, - "application/raml+yaml": { - compressible: true, - extensions: ["raml"], - }, - "application/raptorfec": { - source: "iana", - }, - "application/rdap+json": { - source: "iana", - compressible: true, - }, - "application/rdf+xml": { - source: "iana", - compressible: true, - extensions: ["rdf", "owl"], - }, - "application/reginfo+xml": { - source: "iana", - compressible: true, - extensions: ["rif"], - }, - "application/relax-ng-compact-syntax": { - source: "iana", - extensions: ["rnc"], - }, - "application/remote-printing": { - source: "iana", - }, - "application/reputon+json": { - source: "iana", - compressible: true, - }, - "application/resource-lists+xml": { - source: "iana", - compressible: true, - extensions: ["rl"], - }, - "application/resource-lists-diff+xml": { - source: "iana", - compressible: true, - extensions: ["rld"], - }, - "application/rfc+xml": { - source: "iana", - compressible: true, - }, - "application/riscos": { - source: "iana", - }, - "application/rlmi+xml": { - source: "iana", - compressible: true, - }, - "application/rls-services+xml": { - source: "iana", - compressible: true, - extensions: ["rs"], - }, - "application/route-apd+xml": { - source: "iana", - compressible: true, - extensions: ["rapd"], - }, - "application/route-s-tsid+xml": { - source: "iana", - compressible: true, - extensions: ["sls"], - }, - "application/route-usd+xml": { - source: "iana", - compressible: true, - extensions: ["rusd"], - }, - "application/rpki-ghostbusters": { - source: "iana", - extensions: ["gbr"], - }, - "application/rpki-manifest": { - source: "iana", - extensions: ["mft"], - }, - "application/rpki-publication": { - source: "iana", - }, - "application/rpki-roa": { - source: "iana", - extensions: ["roa"], - }, - "application/rpki-updown": { - source: "iana", - }, - "application/rsd+xml": { - source: "apache", - compressible: true, - extensions: ["rsd"], - }, - "application/rss+xml": { - source: "apache", - compressible: true, - extensions: ["rss"], - }, - "application/rtf": { - source: "iana", - compressible: true, - extensions: ["rtf"], - }, - "application/rtploopback": { - source: "iana", - }, - "application/rtx": { - source: "iana", - }, - "application/samlassertion+xml": { - source: "iana", - compressible: true, - }, - "application/samlmetadata+xml": { - source: "iana", - compressible: true, - }, - "application/sbml+xml": { - source: "iana", - compressible: true, - extensions: ["sbml"], - }, - "application/scaip+xml": { - source: "iana", - compressible: true, - }, - "application/scim+json": { - source: "iana", - compressible: true, - }, - "application/scvp-cv-request": { - source: "iana", - extensions: ["scq"], - }, - "application/scvp-cv-response": { - source: "iana", - extensions: ["scs"], - }, - "application/scvp-vp-request": { - source: "iana", - extensions: ["spq"], - }, - "application/scvp-vp-response": { - source: "iana", - extensions: ["spp"], - }, - "application/sdp": { - source: "iana", - extensions: ["sdp"], - }, - "application/secevent+jwt": { - source: "iana", - }, - "application/senml+cbor": { - source: "iana", - }, - "application/senml+json": { - source: "iana", - compressible: true, - }, - "application/senml+xml": { - source: "iana", - compressible: true, - extensions: ["senmlx"], - }, - "application/senml-etch+cbor": { - source: "iana", - }, - "application/senml-etch+json": { - source: "iana", - compressible: true, - }, - "application/senml-exi": { - source: "iana", - }, - "application/sensml+cbor": { - source: "iana", - }, - "application/sensml+json": { - source: "iana", - compressible: true, - }, - "application/sensml+xml": { - source: "iana", - compressible: true, - extensions: ["sensmlx"], - }, - "application/sensml-exi": { - source: "iana", - }, - "application/sep+xml": { - source: "iana", - compressible: true, - }, - "application/sep-exi": { - source: "iana", - }, - "application/session-info": { - source: "iana", - }, - "application/set-payment": { - source: "iana", - }, - "application/set-payment-initiation": { - source: "iana", - extensions: ["setpay"], - }, - "application/set-registration": { - source: "iana", - }, - "application/set-registration-initiation": { - source: "iana", - extensions: ["setreg"], - }, - "application/sgml": { - source: "iana", - }, - "application/sgml-open-catalog": { - source: "iana", - }, - "application/shf+xml": { - source: "iana", - compressible: true, - extensions: ["shf"], - }, - "application/sieve": { - source: "iana", - extensions: ["siv", "sieve"], - }, - "application/simple-filter+xml": { - source: "iana", - compressible: true, - }, - "application/simple-message-summary": { - source: "iana", - }, - "application/simplesymbolcontainer": { - source: "iana", - }, - "application/sipc": { - source: "iana", - }, - "application/slate": { - source: "iana", - }, - "application/smil": { - source: "iana", - }, - "application/smil+xml": { - source: "iana", - compressible: true, - extensions: ["smi", "smil"], - }, - "application/smpte336m": { - source: "iana", - }, - "application/soap+fastinfoset": { - source: "iana", - }, - "application/soap+xml": { - source: "iana", - compressible: true, - }, - "application/sparql-query": { - source: "iana", - extensions: ["rq"], - }, - "application/sparql-results+xml": { - source: "iana", - compressible: true, - extensions: ["srx"], - }, - "application/spirits-event+xml": { - source: "iana", - compressible: true, - }, - "application/sql": { - source: "iana", - }, - "application/srgs": { - source: "iana", - extensions: ["gram"], - }, - "application/srgs+xml": { - source: "iana", - compressible: true, - extensions: ["grxml"], - }, - "application/sru+xml": { - source: "iana", - compressible: true, - extensions: ["sru"], - }, - "application/ssdl+xml": { - source: "apache", - compressible: true, - extensions: ["ssdl"], - }, - "application/ssml+xml": { - source: "iana", - compressible: true, - extensions: ["ssml"], - }, - "application/stix+json": { - source: "iana", - compressible: true, - }, - "application/swid+xml": { - source: "iana", - compressible: true, - extensions: ["swidtag"], - }, - "application/tamp-apex-update": { - source: "iana", - }, - "application/tamp-apex-update-confirm": { - source: "iana", - }, - "application/tamp-community-update": { - source: "iana", - }, - "application/tamp-community-update-confirm": { - source: "iana", - }, - "application/tamp-error": { - source: "iana", - }, - "application/tamp-sequence-adjust": { - source: "iana", - }, - "application/tamp-sequence-adjust-confirm": { - source: "iana", - }, - "application/tamp-status-query": { - source: "iana", - }, - "application/tamp-status-response": { - source: "iana", - }, - "application/tamp-update": { - source: "iana", - }, - "application/tamp-update-confirm": { - source: "iana", - }, - "application/tar": { - compressible: true, - }, - "application/taxii+json": { - source: "iana", - compressible: true, - }, - "application/td+json": { - source: "iana", - compressible: true, - }, - "application/tei+xml": { - source: "iana", - compressible: true, - extensions: ["tei", "teicorpus"], - }, - "application/tetra_isi": { - source: "iana", - }, - "application/thraud+xml": { - source: "iana", - compressible: true, - extensions: ["tfi"], - }, - "application/timestamp-query": { - source: "iana", - }, - "application/timestamp-reply": { - source: "iana", - }, - "application/timestamped-data": { - source: "iana", - extensions: ["tsd"], - }, - "application/tlsrpt+gzip": { - source: "iana", - }, - "application/tlsrpt+json": { - source: "iana", - compressible: true, - }, - "application/tnauthlist": { - source: "iana", - }, - "application/toml": { - compressible: true, - extensions: ["toml"], - }, - "application/trickle-ice-sdpfrag": { - source: "iana", - }, - "application/trig": { - source: "iana", - }, - "application/ttml+xml": { - source: "iana", - compressible: true, - extensions: ["ttml"], - }, - "application/tve-trigger": { - source: "iana", - }, - "application/tzif": { - source: "iana", - }, - "application/tzif-leap": { - source: "iana", - }, - "application/ulpfec": { - source: "iana", - }, - "application/urc-grpsheet+xml": { - source: "iana", - compressible: true, - }, - "application/urc-ressheet+xml": { - source: "iana", - compressible: true, - extensions: ["rsheet"], - }, - "application/urc-targetdesc+xml": { - source: "iana", - compressible: true, - }, - "application/urc-uisocketdesc+xml": { - source: "iana", - compressible: true, - }, - "application/vcard+json": { - source: "iana", - compressible: true, - }, - "application/vcard+xml": { - source: "iana", - compressible: true, - }, - "application/vemmi": { - source: "iana", - }, - "application/vividence.scriptfile": { - source: "apache", - }, - "application/vnd.1000minds.decision-model+xml": { - source: "iana", - compressible: true, - extensions: ["1km"], - }, - "application/vnd.3gpp-prose+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp-prose-pc3ch+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp-v2x-local-service-information": { - source: "iana", - }, - "application/vnd.3gpp.access-transfer-events+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.bsf+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.gmop+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mc-signalling-ear": { - source: "iana", - }, - "application/vnd.3gpp.mcdata-affiliation-command+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcdata-info+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcdata-payload": { - source: "iana", - }, - "application/vnd.3gpp.mcdata-service-config+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcdata-signalling": { - source: "iana", - }, - "application/vnd.3gpp.mcdata-ue-config+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcdata-user-profile+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcptt-affiliation-command+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcptt-floor-request+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcptt-info+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcptt-location-info+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcptt-mbms-usage-info+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcptt-service-config+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcptt-signed+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcptt-ue-config+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcptt-ue-init-config+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcptt-user-profile+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcvideo-affiliation-command+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcvideo-affiliation-info+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcvideo-info+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcvideo-location-info+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcvideo-mbms-usage-info+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcvideo-service-config+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcvideo-transmission-request+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcvideo-ue-config+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mcvideo-user-profile+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.mid-call+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.pic-bw-large": { - source: "iana", - extensions: ["plb"], - }, - "application/vnd.3gpp.pic-bw-small": { - source: "iana", - extensions: ["psb"], - }, - "application/vnd.3gpp.pic-bw-var": { - source: "iana", - extensions: ["pvb"], - }, - "application/vnd.3gpp.sms": { - source: "iana", - }, - "application/vnd.3gpp.sms+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.srvcc-ext+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.srvcc-info+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.state-and-event-info+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp.ussd+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp2.bcmcsinfo+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.3gpp2.sms": { - source: "iana", - }, - "application/vnd.3gpp2.tcap": { - source: "iana", - extensions: ["tcap"], - }, - "application/vnd.3lightssoftware.imagescal": { - source: "iana", - }, - "application/vnd.3m.post-it-notes": { - source: "iana", - extensions: ["pwn"], - }, - "application/vnd.accpac.simply.aso": { - source: "iana", - extensions: ["aso"], - }, - "application/vnd.accpac.simply.imp": { - source: "iana", - extensions: ["imp"], - }, - "application/vnd.acucobol": { - source: "iana", - extensions: ["acu"], - }, - "application/vnd.acucorp": { - source: "iana", - extensions: ["atc", "acutc"], - }, - "application/vnd.adobe.air-application-installer-package+zip": { - source: "apache", - compressible: false, - extensions: ["air"], - }, - "application/vnd.adobe.flash.movie": { - source: "iana", - }, - "application/vnd.adobe.formscentral.fcdt": { - source: "iana", - extensions: ["fcdt"], - }, - "application/vnd.adobe.fxp": { - source: "iana", - extensions: ["fxp", "fxpl"], - }, - "application/vnd.adobe.partial-upload": { - source: "iana", - }, - "application/vnd.adobe.xdp+xml": { - source: "iana", - compressible: true, - extensions: ["xdp"], - }, - "application/vnd.adobe.xfdf": { - source: "iana", - extensions: ["xfdf"], - }, - "application/vnd.aether.imp": { - source: "iana", - }, - "application/vnd.afpc.afplinedata": { - source: "iana", - }, - "application/vnd.afpc.afplinedata-pagedef": { - source: "iana", - }, - "application/vnd.afpc.foca-charset": { - source: "iana", - }, - "application/vnd.afpc.foca-codedfont": { - source: "iana", - }, - "application/vnd.afpc.foca-codepage": { - source: "iana", - }, - "application/vnd.afpc.modca": { - source: "iana", - }, - "application/vnd.afpc.modca-formdef": { - source: "iana", - }, - "application/vnd.afpc.modca-mediummap": { - source: "iana", - }, - "application/vnd.afpc.modca-objectcontainer": { - source: "iana", - }, - "application/vnd.afpc.modca-overlay": { - source: "iana", - }, - "application/vnd.afpc.modca-pagesegment": { - source: "iana", - }, - "application/vnd.ah-barcode": { - source: "iana", - }, - "application/vnd.ahead.space": { - source: "iana", - extensions: ["ahead"], - }, - "application/vnd.airzip.filesecure.azf": { - source: "iana", - extensions: ["azf"], - }, - "application/vnd.airzip.filesecure.azs": { - source: "iana", - extensions: ["azs"], - }, - "application/vnd.amadeus+json": { - source: "iana", - compressible: true, - }, - "application/vnd.amazon.ebook": { - source: "apache", - extensions: ["azw"], - }, - "application/vnd.amazon.mobi8-ebook": { - source: "iana", - }, - "application/vnd.americandynamics.acc": { - source: "iana", - extensions: ["acc"], - }, - "application/vnd.amiga.ami": { - source: "iana", - extensions: ["ami"], - }, - "application/vnd.amundsen.maze+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.android.ota": { - source: "iana", - }, - "application/vnd.android.package-archive": { - source: "apache", - compressible: false, - extensions: ["apk"], - }, - "application/vnd.anki": { - source: "iana", - }, - "application/vnd.anser-web-certificate-issue-initiation": { - source: "iana", - extensions: ["cii"], - }, - "application/vnd.anser-web-funds-transfer-initiation": { - source: "apache", - extensions: ["fti"], - }, - "application/vnd.antix.game-component": { - source: "iana", - extensions: ["atx"], - }, - "application/vnd.apache.thrift.binary": { - source: "iana", - }, - "application/vnd.apache.thrift.compact": { - source: "iana", - }, - "application/vnd.apache.thrift.json": { - source: "iana", - }, - "application/vnd.api+json": { - source: "iana", - compressible: true, - }, - "application/vnd.aplextor.warrp+json": { - source: "iana", - compressible: true, - }, - "application/vnd.apothekende.reservation+json": { - source: "iana", - compressible: true, - }, - "application/vnd.apple.installer+xml": { - source: "iana", - compressible: true, - extensions: ["mpkg"], - }, - "application/vnd.apple.keynote": { - source: "iana", - extensions: ["keynote"], - }, - "application/vnd.apple.mpegurl": { - source: "iana", - extensions: ["m3u8"], - }, - "application/vnd.apple.numbers": { - source: "iana", - extensions: ["numbers"], - }, - "application/vnd.apple.pages": { - source: "iana", - extensions: ["pages"], - }, - "application/vnd.apple.pkpass": { - compressible: false, - extensions: ["pkpass"], - }, - "application/vnd.arastra.swi": { - source: "iana", - }, - "application/vnd.aristanetworks.swi": { - source: "iana", - extensions: ["swi"], - }, - "application/vnd.artisan+json": { - source: "iana", - compressible: true, - }, - "application/vnd.artsquare": { - source: "iana", - }, - "application/vnd.astraea-software.iota": { - source: "iana", - extensions: ["iota"], - }, - "application/vnd.audiograph": { - source: "iana", - extensions: ["aep"], - }, - "application/vnd.autopackage": { - source: "iana", - }, - "application/vnd.avalon+json": { - source: "iana", - compressible: true, - }, - "application/vnd.avistar+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.balsamiq.bmml+xml": { - source: "iana", - compressible: true, - extensions: ["bmml"], - }, - "application/vnd.balsamiq.bmpr": { - source: "iana", - }, - "application/vnd.banana-accounting": { - source: "iana", - }, - "application/vnd.bbf.usp.error": { - source: "iana", - }, - "application/vnd.bbf.usp.msg": { - source: "iana", - }, - "application/vnd.bbf.usp.msg+json": { - source: "iana", - compressible: true, - }, - "application/vnd.bekitzur-stech+json": { - source: "iana", - compressible: true, - }, - "application/vnd.bint.med-content": { - source: "iana", - }, - "application/vnd.biopax.rdf+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.blink-idb-value-wrapper": { - source: "iana", - }, - "application/vnd.blueice.multipass": { - source: "iana", - extensions: ["mpm"], - }, - "application/vnd.bluetooth.ep.oob": { - source: "iana", - }, - "application/vnd.bluetooth.le.oob": { - source: "iana", - }, - "application/vnd.bmi": { - source: "iana", - extensions: ["bmi"], - }, - "application/vnd.bpf": { - source: "iana", - }, - "application/vnd.bpf3": { - source: "iana", - }, - "application/vnd.businessobjects": { - source: "iana", - extensions: ["rep"], - }, - "application/vnd.byu.uapi+json": { - source: "iana", - compressible: true, - }, - "application/vnd.cab-jscript": { - source: "iana", - }, - "application/vnd.canon-cpdl": { - source: "iana", - }, - "application/vnd.canon-lips": { - source: "iana", - }, - "application/vnd.capasystems-pg+json": { - source: "iana", - compressible: true, - }, - "application/vnd.cendio.thinlinc.clientconf": { - source: "iana", - }, - "application/vnd.century-systems.tcp_stream": { - source: "iana", - }, - "application/vnd.chemdraw+xml": { - source: "iana", - compressible: true, - extensions: ["cdxml"], - }, - "application/vnd.chess-pgn": { - source: "iana", - }, - "application/vnd.chipnuts.karaoke-mmd": { - source: "iana", - extensions: ["mmd"], - }, - "application/vnd.ciedi": { - source: "iana", - }, - "application/vnd.cinderella": { - source: "iana", - extensions: ["cdy"], - }, - "application/vnd.cirpack.isdn-ext": { - source: "iana", - }, - "application/vnd.citationstyles.style+xml": { - source: "iana", - compressible: true, - extensions: ["csl"], - }, - "application/vnd.claymore": { - source: "iana", - extensions: ["cla"], - }, - "application/vnd.cloanto.rp9": { - source: "iana", - extensions: ["rp9"], - }, - "application/vnd.clonk.c4group": { - source: "iana", - extensions: ["c4g", "c4d", "c4f", "c4p", "c4u"], - }, - "application/vnd.cluetrust.cartomobile-config": { - source: "iana", - extensions: ["c11amc"], - }, - "application/vnd.cluetrust.cartomobile-config-pkg": { - source: "iana", - extensions: ["c11amz"], - }, - "application/vnd.coffeescript": { - source: "iana", - }, - "application/vnd.collabio.xodocuments.document": { - source: "iana", - }, - "application/vnd.collabio.xodocuments.document-template": { - source: "iana", - }, - "application/vnd.collabio.xodocuments.presentation": { - source: "iana", - }, - "application/vnd.collabio.xodocuments.presentation-template": { - source: "iana", - }, - "application/vnd.collabio.xodocuments.spreadsheet": { - source: "iana", - }, - "application/vnd.collabio.xodocuments.spreadsheet-template": { - source: "iana", - }, - "application/vnd.collection+json": { - source: "iana", - compressible: true, - }, - "application/vnd.collection.doc+json": { - source: "iana", - compressible: true, - }, - "application/vnd.collection.next+json": { - source: "iana", - compressible: true, - }, - "application/vnd.comicbook+zip": { - source: "iana", - compressible: false, - }, - "application/vnd.comicbook-rar": { - source: "iana", - }, - "application/vnd.commerce-battelle": { - source: "iana", - }, - "application/vnd.commonspace": { - source: "iana", - extensions: ["csp"], - }, - "application/vnd.contact.cmsg": { - source: "iana", - extensions: ["cdbcmsg"], - }, - "application/vnd.coreos.ignition+json": { - source: "iana", - compressible: true, - }, - "application/vnd.cosmocaller": { - source: "iana", - extensions: ["cmc"], - }, - "application/vnd.crick.clicker": { - source: "iana", - extensions: ["clkx"], - }, - "application/vnd.crick.clicker.keyboard": { - source: "iana", - extensions: ["clkk"], - }, - "application/vnd.crick.clicker.palette": { - source: "iana", - extensions: ["clkp"], - }, - "application/vnd.crick.clicker.template": { - source: "iana", - extensions: ["clkt"], - }, - "application/vnd.crick.clicker.wordbank": { - source: "iana", - extensions: ["clkw"], - }, - "application/vnd.criticaltools.wbs+xml": { - source: "iana", - compressible: true, - extensions: ["wbs"], - }, - "application/vnd.cryptii.pipe+json": { - source: "iana", - compressible: true, - }, - "application/vnd.crypto-shade-file": { - source: "iana", - }, - "application/vnd.ctc-posml": { - source: "iana", - extensions: ["pml"], - }, - "application/vnd.ctct.ws+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.cups-pdf": { - source: "iana", - }, - "application/vnd.cups-postscript": { - source: "iana", - }, - "application/vnd.cups-ppd": { - source: "iana", - extensions: ["ppd"], - }, - "application/vnd.cups-raster": { - source: "iana", - }, - "application/vnd.cups-raw": { - source: "iana", - }, - "application/vnd.curl": { - source: "iana", - }, - "application/vnd.curl.car": { - source: "apache", - extensions: ["car"], - }, - "application/vnd.curl.pcurl": { - source: "apache", - extensions: ["pcurl"], - }, - "application/vnd.cyan.dean.root+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.cybank": { - source: "iana", - }, - "application/vnd.d2l.coursepackage1p0+zip": { - source: "iana", - compressible: false, - }, - "application/vnd.dart": { - source: "iana", - compressible: true, - extensions: ["dart"], - }, - "application/vnd.data-vision.rdz": { - source: "iana", - extensions: ["rdz"], - }, - "application/vnd.datapackage+json": { - source: "iana", - compressible: true, - }, - "application/vnd.dataresource+json": { - source: "iana", - compressible: true, - }, - "application/vnd.dbf": { - source: "iana", - }, - "application/vnd.debian.binary-package": { - source: "iana", - }, - "application/vnd.dece.data": { - source: "iana", - extensions: ["uvf", "uvvf", "uvd", "uvvd"], - }, - "application/vnd.dece.ttml+xml": { - source: "iana", - compressible: true, - extensions: ["uvt", "uvvt"], - }, - "application/vnd.dece.unspecified": { - source: "iana", - extensions: ["uvx", "uvvx"], - }, - "application/vnd.dece.zip": { - source: "iana", - extensions: ["uvz", "uvvz"], - }, - "application/vnd.denovo.fcselayout-link": { - source: "iana", - extensions: ["fe_launch"], - }, - "application/vnd.desmume.movie": { - source: "iana", - }, - "application/vnd.dir-bi.plate-dl-nosuffix": { - source: "iana", - }, - "application/vnd.dm.delegation+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.dna": { - source: "iana", - extensions: ["dna"], - }, - "application/vnd.document+json": { - source: "iana", - compressible: true, - }, - "application/vnd.dolby.mlp": { - source: "apache", - extensions: ["mlp"], - }, - "application/vnd.dolby.mobile.1": { - source: "iana", - }, - "application/vnd.dolby.mobile.2": { - source: "iana", - }, - "application/vnd.doremir.scorecloud-binary-document": { - source: "iana", - }, - "application/vnd.dpgraph": { - source: "iana", - extensions: ["dpg"], - }, - "application/vnd.dreamfactory": { - source: "iana", - extensions: ["dfac"], - }, - "application/vnd.drive+json": { - source: "iana", - compressible: true, - }, - "application/vnd.ds-keypoint": { - source: "apache", - extensions: ["kpxx"], - }, - "application/vnd.dtg.local": { - source: "iana", - }, - "application/vnd.dtg.local.flash": { - source: "iana", - }, - "application/vnd.dtg.local.html": { - source: "iana", - }, - "application/vnd.dvb.ait": { - source: "iana", - extensions: ["ait"], - }, - "application/vnd.dvb.dvbisl+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.dvb.dvbj": { - source: "iana", - }, - "application/vnd.dvb.esgcontainer": { - source: "iana", - }, - "application/vnd.dvb.ipdcdftnotifaccess": { - source: "iana", - }, - "application/vnd.dvb.ipdcesgaccess": { - source: "iana", - }, - "application/vnd.dvb.ipdcesgaccess2": { - source: "iana", - }, - "application/vnd.dvb.ipdcesgpdd": { - source: "iana", - }, - "application/vnd.dvb.ipdcroaming": { - source: "iana", - }, - "application/vnd.dvb.iptv.alfec-base": { - source: "iana", - }, - "application/vnd.dvb.iptv.alfec-enhancement": { - source: "iana", - }, - "application/vnd.dvb.notif-aggregate-root+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.dvb.notif-container+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.dvb.notif-generic+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.dvb.notif-ia-msglist+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.dvb.notif-ia-registration-request+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.dvb.notif-ia-registration-response+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.dvb.notif-init+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.dvb.pfr": { - source: "iana", - }, - "application/vnd.dvb.service": { - source: "iana", - extensions: ["svc"], - }, - "application/vnd.dxr": { - source: "iana", - }, - "application/vnd.dynageo": { - source: "iana", - extensions: ["geo"], - }, - "application/vnd.dzr": { - source: "iana", - }, - "application/vnd.easykaraoke.cdgdownload": { - source: "iana", - }, - "application/vnd.ecdis-update": { - source: "iana", - }, - "application/vnd.ecip.rlp": { - source: "iana", - }, - "application/vnd.ecowin.chart": { - source: "iana", - extensions: ["mag"], - }, - "application/vnd.ecowin.filerequest": { - source: "iana", - }, - "application/vnd.ecowin.fileupdate": { - source: "iana", - }, - "application/vnd.ecowin.series": { - source: "iana", - }, - "application/vnd.ecowin.seriesrequest": { - source: "iana", - }, - "application/vnd.ecowin.seriesupdate": { - source: "iana", - }, - "application/vnd.efi.img": { - source: "iana", - }, - "application/vnd.efi.iso": { - source: "iana", - }, - "application/vnd.emclient.accessrequest+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.enliven": { - source: "iana", - extensions: ["nml"], - }, - "application/vnd.enphase.envoy": { - source: "iana", - }, - "application/vnd.eprints.data+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.epson.esf": { - source: "iana", - extensions: ["esf"], - }, - "application/vnd.epson.msf": { - source: "iana", - extensions: ["msf"], - }, - "application/vnd.epson.quickanime": { - source: "iana", - extensions: ["qam"], - }, - "application/vnd.epson.salt": { - source: "iana", - extensions: ["slt"], - }, - "application/vnd.epson.ssf": { - source: "iana", - extensions: ["ssf"], - }, - "application/vnd.ericsson.quickcall": { - source: "iana", - }, - "application/vnd.espass-espass+zip": { - source: "iana", - compressible: false, - }, - "application/vnd.eszigno3+xml": { - source: "iana", - compressible: true, - extensions: ["es3", "et3"], - }, - "application/vnd.etsi.aoc+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.etsi.asic-e+zip": { - source: "iana", - compressible: false, - }, - "application/vnd.etsi.asic-s+zip": { - source: "iana", - compressible: false, - }, - "application/vnd.etsi.cug+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.etsi.iptvcommand+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.etsi.iptvdiscovery+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.etsi.iptvprofile+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.etsi.iptvsad-bc+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.etsi.iptvsad-cod+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.etsi.iptvsad-npvr+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.etsi.iptvservice+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.etsi.iptvsync+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.etsi.iptvueprofile+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.etsi.mcid+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.etsi.mheg5": { - source: "iana", - }, - "application/vnd.etsi.overload-control-policy-dataset+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.etsi.pstn+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.etsi.sci+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.etsi.simservs+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.etsi.timestamp-token": { - source: "iana", - }, - "application/vnd.etsi.tsl+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.etsi.tsl.der": { - source: "iana", - }, - "application/vnd.eudora.data": { - source: "iana", - }, - "application/vnd.evolv.ecig.profile": { - source: "iana", - }, - "application/vnd.evolv.ecig.settings": { - source: "iana", - }, - "application/vnd.evolv.ecig.theme": { - source: "iana", - }, - "application/vnd.exstream-empower+zip": { - source: "iana", - compressible: false, - }, - "application/vnd.exstream-package": { - source: "iana", - }, - "application/vnd.ezpix-album": { - source: "iana", - extensions: ["ez2"], - }, - "application/vnd.ezpix-package": { - source: "iana", - extensions: ["ez3"], - }, - "application/vnd.f-secure.mobile": { - source: "iana", - }, - "application/vnd.fastcopy-disk-image": { - source: "iana", - }, - "application/vnd.fdf": { - source: "iana", - extensions: ["fdf"], - }, - "application/vnd.fdsn.mseed": { - source: "iana", - extensions: ["mseed"], - }, - "application/vnd.fdsn.seed": { - source: "iana", - extensions: ["seed", "dataless"], - }, - "application/vnd.ffsns": { - source: "iana", - }, - "application/vnd.ficlab.flb+zip": { - source: "iana", - compressible: false, - }, - "application/vnd.filmit.zfc": { - source: "iana", - }, - "application/vnd.fints": { - source: "iana", - }, - "application/vnd.firemonkeys.cloudcell": { - source: "iana", - }, - "application/vnd.flographit": { - source: "iana", - extensions: ["gph"], - }, - "application/vnd.fluxtime.clip": { - source: "iana", - extensions: ["ftc"], - }, - "application/vnd.font-fontforge-sfd": { - source: "iana", - }, - "application/vnd.framemaker": { - source: "iana", - extensions: ["fm", "frame", "maker", "book"], - }, - "application/vnd.frogans.fnc": { - source: "iana", - extensions: ["fnc"], - }, - "application/vnd.frogans.ltf": { - source: "iana", - extensions: ["ltf"], - }, - "application/vnd.fsc.weblaunch": { - source: "iana", - extensions: ["fsc"], - }, - "application/vnd.fujitsu.oasys": { - source: "iana", - extensions: ["oas"], - }, - "application/vnd.fujitsu.oasys2": { - source: "iana", - extensions: ["oa2"], - }, - "application/vnd.fujitsu.oasys3": { - source: "iana", - extensions: ["oa3"], - }, - "application/vnd.fujitsu.oasysgp": { - source: "iana", - extensions: ["fg5"], - }, - "application/vnd.fujitsu.oasysprs": { - source: "iana", - extensions: ["bh2"], - }, - "application/vnd.fujixerox.art-ex": { - source: "iana", - }, - "application/vnd.fujixerox.art4": { - source: "iana", - }, - "application/vnd.fujixerox.ddd": { - source: "iana", - extensions: ["ddd"], - }, - "application/vnd.fujixerox.docuworks": { - source: "iana", - extensions: ["xdw"], - }, - "application/vnd.fujixerox.docuworks.binder": { - source: "iana", - extensions: ["xbd"], - }, - "application/vnd.fujixerox.docuworks.container": { - source: "iana", - }, - "application/vnd.fujixerox.hbpl": { - source: "iana", - }, - "application/vnd.fut-misnet": { - source: "iana", - }, - "application/vnd.futoin+cbor": { - source: "iana", - }, - "application/vnd.futoin+json": { - source: "iana", - compressible: true, - }, - "application/vnd.fuzzysheet": { - source: "iana", - extensions: ["fzs"], - }, - "application/vnd.genomatix.tuxedo": { - source: "iana", - extensions: ["txd"], - }, - "application/vnd.gentics.grd+json": { - source: "iana", - compressible: true, - }, - "application/vnd.geo+json": { - source: "iana", - compressible: true, - }, - "application/vnd.geocube+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.geogebra.file": { - source: "iana", - extensions: ["ggb"], - }, - "application/vnd.geogebra.tool": { - source: "iana", - extensions: ["ggt"], - }, - "application/vnd.geometry-explorer": { - source: "iana", - extensions: ["gex", "gre"], - }, - "application/vnd.geonext": { - source: "iana", - extensions: ["gxt"], - }, - "application/vnd.geoplan": { - source: "iana", - extensions: ["g2w"], - }, - "application/vnd.geospace": { - source: "iana", - extensions: ["g3w"], - }, - "application/vnd.gerber": { - source: "iana", - }, - "application/vnd.globalplatform.card-content-mgt": { - source: "iana", - }, - "application/vnd.globalplatform.card-content-mgt-response": { - source: "iana", - }, - "application/vnd.gmx": { - source: "iana", - extensions: ["gmx"], - }, - "application/vnd.google-apps.document": { - compressible: false, - extensions: ["gdoc"], - }, - "application/vnd.google-apps.presentation": { - compressible: false, - extensions: ["gslides"], - }, - "application/vnd.google-apps.spreadsheet": { - compressible: false, - extensions: ["gsheet"], - }, - "application/vnd.google-earth.kml+xml": { - source: "iana", - compressible: true, - extensions: ["kml"], - }, - "application/vnd.google-earth.kmz": { - source: "iana", - compressible: false, - extensions: ["kmz"], - }, - "application/vnd.gov.sk.e-form+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.gov.sk.e-form+zip": { - source: "iana", - compressible: false, - }, - "application/vnd.gov.sk.xmldatacontainer+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.grafeq": { - source: "iana", - extensions: ["gqf", "gqs"], - }, - "application/vnd.gridmp": { - source: "iana", - }, - "application/vnd.groove-account": { - source: "iana", - extensions: ["gac"], - }, - "application/vnd.groove-help": { - source: "iana", - extensions: ["ghf"], - }, - "application/vnd.groove-identity-message": { - source: "iana", - extensions: ["gim"], - }, - "application/vnd.groove-injector": { - source: "iana", - extensions: ["grv"], - }, - "application/vnd.groove-tool-message": { - source: "iana", - extensions: ["gtm"], - }, - "application/vnd.groove-tool-template": { - source: "iana", - extensions: ["tpl"], - }, - "application/vnd.groove-vcard": { - source: "iana", - extensions: ["vcg"], - }, - "application/vnd.hal+json": { - source: "iana", - compressible: true, - }, - "application/vnd.hal+xml": { - source: "iana", - compressible: true, - extensions: ["hal"], - }, - "application/vnd.handheld-entertainment+xml": { - source: "iana", - compressible: true, - extensions: ["zmm"], - }, - "application/vnd.hbci": { - source: "iana", - extensions: ["hbci"], - }, - "application/vnd.hc+json": { - source: "iana", - compressible: true, - }, - "application/vnd.hcl-bireports": { - source: "iana", - }, - "application/vnd.hdt": { - source: "iana", - }, - "application/vnd.heroku+json": { - source: "iana", - compressible: true, - }, - "application/vnd.hhe.lesson-player": { - source: "iana", - extensions: ["les"], - }, - "application/vnd.hp-hpgl": { - source: "iana", - extensions: ["hpgl"], - }, - "application/vnd.hp-hpid": { - source: "iana", - extensions: ["hpid"], - }, - "application/vnd.hp-hps": { - source: "iana", - extensions: ["hps"], - }, - "application/vnd.hp-jlyt": { - source: "iana", - extensions: ["jlt"], - }, - "application/vnd.hp-pcl": { - source: "iana", - extensions: ["pcl"], - }, - "application/vnd.hp-pclxl": { - source: "iana", - extensions: ["pclxl"], - }, - "application/vnd.httphone": { - source: "iana", - }, - "application/vnd.hydrostatix.sof-data": { - source: "iana", - extensions: ["sfd-hdstx"], - }, - "application/vnd.hyper+json": { - source: "iana", - compressible: true, - }, - "application/vnd.hyper-item+json": { - source: "iana", - compressible: true, - }, - "application/vnd.hyperdrive+json": { - source: "iana", - compressible: true, - }, - "application/vnd.hzn-3d-crossword": { - source: "iana", - }, - "application/vnd.ibm.afplinedata": { - source: "iana", - }, - "application/vnd.ibm.electronic-media": { - source: "iana", - }, - "application/vnd.ibm.minipay": { - source: "iana", - extensions: ["mpy"], - }, - "application/vnd.ibm.modcap": { - source: "iana", - extensions: ["afp", "listafp", "list3820"], - }, - "application/vnd.ibm.rights-management": { - source: "iana", - extensions: ["irm"], - }, - "application/vnd.ibm.secure-container": { - source: "iana", - extensions: ["sc"], - }, - "application/vnd.iccprofile": { - source: "iana", - extensions: ["icc", "icm"], - }, - "application/vnd.ieee.1905": { - source: "iana", - }, - "application/vnd.igloader": { - source: "iana", - extensions: ["igl"], - }, - "application/vnd.imagemeter.folder+zip": { - source: "iana", - compressible: false, - }, - "application/vnd.imagemeter.image+zip": { - source: "iana", - compressible: false, - }, - "application/vnd.immervision-ivp": { - source: "iana", - extensions: ["ivp"], - }, - "application/vnd.immervision-ivu": { - source: "iana", - extensions: ["ivu"], - }, - "application/vnd.ims.imsccv1p1": { - source: "iana", - }, - "application/vnd.ims.imsccv1p2": { - source: "iana", - }, - "application/vnd.ims.imsccv1p3": { - source: "iana", - }, - "application/vnd.ims.lis.v2.result+json": { - source: "iana", - compressible: true, - }, - "application/vnd.ims.lti.v2.toolconsumerprofile+json": { - source: "iana", - compressible: true, - }, - "application/vnd.ims.lti.v2.toolproxy+json": { - source: "iana", - compressible: true, - }, - "application/vnd.ims.lti.v2.toolproxy.id+json": { - source: "iana", - compressible: true, - }, - "application/vnd.ims.lti.v2.toolsettings+json": { - source: "iana", - compressible: true, - }, - "application/vnd.ims.lti.v2.toolsettings.simple+json": { - source: "iana", - compressible: true, - }, - "application/vnd.informedcontrol.rms+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.informix-visionary": { - source: "iana", - }, - "application/vnd.infotech.project": { - source: "iana", - }, - "application/vnd.infotech.project+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.innopath.wamp.notification": { - source: "iana", - }, - "application/vnd.insors.igm": { - source: "iana", - extensions: ["igm"], - }, - "application/vnd.intercon.formnet": { - source: "iana", - extensions: ["xpw", "xpx"], - }, - "application/vnd.intergeo": { - source: "iana", - extensions: ["i2g"], - }, - "application/vnd.intertrust.digibox": { - source: "iana", - }, - "application/vnd.intertrust.nncp": { - source: "iana", - }, - "application/vnd.intu.qbo": { - source: "iana", - extensions: ["qbo"], - }, - "application/vnd.intu.qfx": { - source: "iana", - extensions: ["qfx"], - }, - "application/vnd.iptc.g2.catalogitem+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.iptc.g2.conceptitem+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.iptc.g2.knowledgeitem+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.iptc.g2.newsitem+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.iptc.g2.newsmessage+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.iptc.g2.packageitem+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.iptc.g2.planningitem+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.ipunplugged.rcprofile": { - source: "iana", - extensions: ["rcprofile"], - }, - "application/vnd.irepository.package+xml": { - source: "iana", - compressible: true, - extensions: ["irp"], - }, - "application/vnd.is-xpr": { - source: "iana", - extensions: ["xpr"], - }, - "application/vnd.isac.fcs": { - source: "iana", - extensions: ["fcs"], - }, - "application/vnd.iso11783-10+zip": { - source: "iana", - compressible: false, - }, - "application/vnd.jam": { - source: "iana", - extensions: ["jam"], - }, - "application/vnd.japannet-directory-service": { - source: "iana", - }, - "application/vnd.japannet-jpnstore-wakeup": { - source: "iana", - }, - "application/vnd.japannet-payment-wakeup": { - source: "iana", - }, - "application/vnd.japannet-registration": { - source: "iana", - }, - "application/vnd.japannet-registration-wakeup": { - source: "iana", - }, - "application/vnd.japannet-setstore-wakeup": { - source: "iana", - }, - "application/vnd.japannet-verification": { - source: "iana", - }, - "application/vnd.japannet-verification-wakeup": { - source: "iana", - }, - "application/vnd.jcp.javame.midlet-rms": { - source: "iana", - extensions: ["rms"], - }, - "application/vnd.jisp": { - source: "iana", - extensions: ["jisp"], - }, - "application/vnd.joost.joda-archive": { - source: "iana", - extensions: ["joda"], - }, - "application/vnd.jsk.isdn-ngn": { - source: "iana", - }, - "application/vnd.kahootz": { - source: "iana", - extensions: ["ktz", "ktr"], - }, - "application/vnd.kde.karbon": { - source: "iana", - extensions: ["karbon"], - }, - "application/vnd.kde.kchart": { - source: "iana", - extensions: ["chrt"], - }, - "application/vnd.kde.kformula": { - source: "iana", - extensions: ["kfo"], - }, - "application/vnd.kde.kivio": { - source: "iana", - extensions: ["flw"], - }, - "application/vnd.kde.kontour": { - source: "iana", - extensions: ["kon"], - }, - "application/vnd.kde.kpresenter": { - source: "iana", - extensions: ["kpr", "kpt"], - }, - "application/vnd.kde.kspread": { - source: "iana", - extensions: ["ksp"], - }, - "application/vnd.kde.kword": { - source: "iana", - extensions: ["kwd", "kwt"], - }, - "application/vnd.kenameaapp": { - source: "iana", - extensions: ["htke"], - }, - "application/vnd.kidspiration": { - source: "iana", - extensions: ["kia"], - }, - "application/vnd.kinar": { - source: "iana", - extensions: ["kne", "knp"], - }, - "application/vnd.koan": { - source: "iana", - extensions: ["skp", "skd", "skt", "skm"], - }, - "application/vnd.kodak-descriptor": { - source: "iana", - extensions: ["sse"], - }, - "application/vnd.las": { - source: "iana", - }, - "application/vnd.las.las+json": { - source: "iana", - compressible: true, - }, - "application/vnd.las.las+xml": { - source: "iana", - compressible: true, - extensions: ["lasxml"], - }, - "application/vnd.laszip": { - source: "iana", - }, - "application/vnd.leap+json": { - source: "iana", - compressible: true, - }, - "application/vnd.liberty-request+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.llamagraphics.life-balance.desktop": { - source: "iana", - extensions: ["lbd"], - }, - "application/vnd.llamagraphics.life-balance.exchange+xml": { - source: "iana", - compressible: true, - extensions: ["lbe"], - }, - "application/vnd.logipipe.circuit+zip": { - source: "iana", - compressible: false, - }, - "application/vnd.loom": { - source: "iana", - }, - "application/vnd.lotus-1-2-3": { - source: "iana", - extensions: ["123"], - }, - "application/vnd.lotus-approach": { - source: "iana", - extensions: ["apr"], - }, - "application/vnd.lotus-freelance": { - source: "iana", - extensions: ["pre"], - }, - "application/vnd.lotus-notes": { - source: "iana", - extensions: ["nsf"], - }, - "application/vnd.lotus-organizer": { - source: "iana", - extensions: ["org"], - }, - "application/vnd.lotus-screencam": { - source: "iana", - extensions: ["scm"], - }, - "application/vnd.lotus-wordpro": { - source: "iana", - extensions: ["lwp"], - }, - "application/vnd.macports.portpkg": { - source: "iana", - extensions: ["portpkg"], - }, - "application/vnd.mapbox-vector-tile": { - source: "iana", - }, - "application/vnd.marlin.drm.actiontoken+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.marlin.drm.conftoken+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.marlin.drm.license+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.marlin.drm.mdcf": { - source: "iana", - }, - "application/vnd.mason+json": { - source: "iana", - compressible: true, - }, - "application/vnd.maxmind.maxmind-db": { - source: "iana", - }, - "application/vnd.mcd": { - source: "iana", - extensions: ["mcd"], - }, - "application/vnd.medcalcdata": { - source: "iana", - extensions: ["mc1"], - }, - "application/vnd.mediastation.cdkey": { - source: "iana", - extensions: ["cdkey"], - }, - "application/vnd.meridian-slingshot": { - source: "iana", - }, - "application/vnd.mfer": { - source: "iana", - extensions: ["mwf"], - }, - "application/vnd.mfmp": { - source: "iana", - extensions: ["mfm"], - }, - "application/vnd.micro+json": { - source: "iana", - compressible: true, - }, - "application/vnd.micrografx.flo": { - source: "iana", - extensions: ["flo"], - }, - "application/vnd.micrografx.igx": { - source: "iana", - extensions: ["igx"], - }, - "application/vnd.microsoft.portable-executable": { - source: "iana", - }, - "application/vnd.microsoft.windows.thumbnail-cache": { - source: "iana", - }, - "application/vnd.miele+json": { - source: "iana", - compressible: true, - }, - "application/vnd.mif": { - source: "iana", - extensions: ["mif"], - }, - "application/vnd.minisoft-hp3000-save": { - source: "iana", - }, - "application/vnd.mitsubishi.misty-guard.trustweb": { - source: "iana", - }, - "application/vnd.mobius.daf": { - source: "iana", - extensions: ["daf"], - }, - "application/vnd.mobius.dis": { - source: "iana", - extensions: ["dis"], - }, - "application/vnd.mobius.mbk": { - source: "iana", - extensions: ["mbk"], - }, - "application/vnd.mobius.mqy": { - source: "iana", - extensions: ["mqy"], - }, - "application/vnd.mobius.msl": { - source: "iana", - extensions: ["msl"], - }, - "application/vnd.mobius.plc": { - source: "iana", - extensions: ["plc"], - }, - "application/vnd.mobius.txf": { - source: "iana", - extensions: ["txf"], - }, - "application/vnd.mophun.application": { - source: "iana", - extensions: ["mpn"], - }, - "application/vnd.mophun.certificate": { - source: "iana", - extensions: ["mpc"], - }, - "application/vnd.motorola.flexsuite": { - source: "iana", - }, - "application/vnd.motorola.flexsuite.adsi": { - source: "iana", - }, - "application/vnd.motorola.flexsuite.fis": { - source: "iana", - }, - "application/vnd.motorola.flexsuite.gotap": { - source: "iana", - }, - "application/vnd.motorola.flexsuite.kmr": { - source: "iana", - }, - "application/vnd.motorola.flexsuite.ttc": { - source: "iana", - }, - "application/vnd.motorola.flexsuite.wem": { - source: "iana", - }, - "application/vnd.motorola.iprm": { - source: "iana", - }, - "application/vnd.mozilla.xul+xml": { - source: "iana", - compressible: true, - extensions: ["xul"], - }, - "application/vnd.ms-3mfdocument": { - source: "iana", - }, - "application/vnd.ms-artgalry": { - source: "iana", - extensions: ["cil"], - }, - "application/vnd.ms-asf": { - source: "iana", - }, - "application/vnd.ms-cab-compressed": { - source: "iana", - extensions: ["cab"], - }, - "application/vnd.ms-color.iccprofile": { - source: "apache", - }, - "application/vnd.ms-excel": { - source: "iana", - compressible: false, - extensions: ["xls", "xlm", "xla", "xlc", "xlt", "xlw"], - }, - "application/vnd.ms-excel.addin.macroenabled.12": { - source: "iana", - extensions: ["xlam"], - }, - "application/vnd.ms-excel.sheet.binary.macroenabled.12": { - source: "iana", - extensions: ["xlsb"], - }, - "application/vnd.ms-excel.sheet.macroenabled.12": { - source: "iana", - extensions: ["xlsm"], - }, - "application/vnd.ms-excel.template.macroenabled.12": { - source: "iana", - extensions: ["xltm"], - }, - "application/vnd.ms-fontobject": { - source: "iana", - compressible: true, - extensions: ["eot"], - }, - "application/vnd.ms-htmlhelp": { - source: "iana", - extensions: ["chm"], - }, - "application/vnd.ms-ims": { - source: "iana", - extensions: ["ims"], - }, - "application/vnd.ms-lrm": { - source: "iana", - extensions: ["lrm"], - }, - "application/vnd.ms-office.activex+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.ms-officetheme": { - source: "iana", - extensions: ["thmx"], - }, - "application/vnd.ms-opentype": { - source: "apache", - compressible: true, - }, - "application/vnd.ms-outlook": { - compressible: false, - extensions: ["msg"], - }, - "application/vnd.ms-package.obfuscated-opentype": { - source: "apache", - }, - "application/vnd.ms-pki.seccat": { - source: "apache", - extensions: ["cat"], - }, - "application/vnd.ms-pki.stl": { - source: "apache", - extensions: ["stl"], - }, - "application/vnd.ms-playready.initiator+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.ms-powerpoint": { - source: "iana", - compressible: false, - extensions: ["ppt", "pps", "pot"], - }, - "application/vnd.ms-powerpoint.addin.macroenabled.12": { - source: "iana", - extensions: ["ppam"], - }, - "application/vnd.ms-powerpoint.presentation.macroenabled.12": { - source: "iana", - extensions: ["pptm"], - }, - "application/vnd.ms-powerpoint.slide.macroenabled.12": { - source: "iana", - extensions: ["sldm"], - }, - "application/vnd.ms-powerpoint.slideshow.macroenabled.12": { - source: "iana", - extensions: ["ppsm"], - }, - "application/vnd.ms-powerpoint.template.macroenabled.12": { - source: "iana", - extensions: ["potm"], - }, - "application/vnd.ms-printdevicecapabilities+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.ms-printing.printticket+xml": { - source: "apache", - compressible: true, - }, - "application/vnd.ms-printschematicket+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.ms-project": { - source: "iana", - extensions: ["mpp", "mpt"], - }, - "application/vnd.ms-tnef": { - source: "iana", - }, - "application/vnd.ms-windows.devicepairing": { - source: "iana", - }, - "application/vnd.ms-windows.nwprinting.oob": { - source: "iana", - }, - "application/vnd.ms-windows.printerpairing": { - source: "iana", - }, - "application/vnd.ms-windows.wsd.oob": { - source: "iana", - }, - "application/vnd.ms-wmdrm.lic-chlg-req": { - source: "iana", - }, - "application/vnd.ms-wmdrm.lic-resp": { - source: "iana", - }, - "application/vnd.ms-wmdrm.meter-chlg-req": { - source: "iana", - }, - "application/vnd.ms-wmdrm.meter-resp": { - source: "iana", - }, - "application/vnd.ms-word.document.macroenabled.12": { - source: "iana", - extensions: ["docm"], - }, - "application/vnd.ms-word.template.macroenabled.12": { - source: "iana", - extensions: ["dotm"], - }, - "application/vnd.ms-works": { - source: "iana", - extensions: ["wps", "wks", "wcm", "wdb"], - }, - "application/vnd.ms-wpl": { - source: "iana", - extensions: ["wpl"], - }, - "application/vnd.ms-xpsdocument": { - source: "iana", - compressible: false, - extensions: ["xps"], - }, - "application/vnd.msa-disk-image": { - source: "iana", - }, - "application/vnd.mseq": { - source: "iana", - extensions: ["mseq"], - }, - "application/vnd.msign": { - source: "iana", - }, - "application/vnd.multiad.creator": { - source: "iana", - }, - "application/vnd.multiad.creator.cif": { - source: "iana", - }, - "application/vnd.music-niff": { - source: "iana", - }, - "application/vnd.musician": { - source: "iana", - extensions: ["mus"], - }, - "application/vnd.muvee.style": { - source: "iana", - extensions: ["msty"], - }, - "application/vnd.mynfc": { - source: "iana", - extensions: ["taglet"], - }, - "application/vnd.ncd.control": { - source: "iana", - }, - "application/vnd.ncd.reference": { - source: "iana", - }, - "application/vnd.nearst.inv+json": { - source: "iana", - compressible: true, - }, - "application/vnd.nervana": { - source: "iana", - }, - "application/vnd.netfpx": { - source: "iana", - }, - "application/vnd.neurolanguage.nlu": { - source: "iana", - extensions: ["nlu"], - }, - "application/vnd.nimn": { - source: "iana", - }, - "application/vnd.nintendo.nitro.rom": { - source: "iana", - }, - "application/vnd.nintendo.snes.rom": { - source: "iana", - }, - "application/vnd.nitf": { - source: "iana", - extensions: ["ntf", "nitf"], - }, - "application/vnd.noblenet-directory": { - source: "iana", - extensions: ["nnd"], - }, - "application/vnd.noblenet-sealer": { - source: "iana", - extensions: ["nns"], - }, - "application/vnd.noblenet-web": { - source: "iana", - extensions: ["nnw"], - }, - "application/vnd.nokia.catalogs": { - source: "iana", - }, - "application/vnd.nokia.conml+wbxml": { - source: "iana", - }, - "application/vnd.nokia.conml+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.nokia.iptv.config+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.nokia.isds-radio-presets": { - source: "iana", - }, - "application/vnd.nokia.landmark+wbxml": { - source: "iana", - }, - "application/vnd.nokia.landmark+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.nokia.landmarkcollection+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.nokia.n-gage.ac+xml": { - source: "iana", - compressible: true, - extensions: ["ac"], - }, - "application/vnd.nokia.n-gage.data": { - source: "iana", - extensions: ["ngdat"], - }, - "application/vnd.nokia.n-gage.symbian.install": { - source: "iana", - extensions: ["n-gage"], - }, - "application/vnd.nokia.ncd": { - source: "iana", - }, - "application/vnd.nokia.pcd+wbxml": { - source: "iana", - }, - "application/vnd.nokia.pcd+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.nokia.radio-preset": { - source: "iana", - extensions: ["rpst"], - }, - "application/vnd.nokia.radio-presets": { - source: "iana", - extensions: ["rpss"], - }, - "application/vnd.novadigm.edm": { - source: "iana", - extensions: ["edm"], - }, - "application/vnd.novadigm.edx": { - source: "iana", - extensions: ["edx"], - }, - "application/vnd.novadigm.ext": { - source: "iana", - extensions: ["ext"], - }, - "application/vnd.ntt-local.content-share": { - source: "iana", - }, - "application/vnd.ntt-local.file-transfer": { - source: "iana", - }, - "application/vnd.ntt-local.ogw_remote-access": { - source: "iana", - }, - "application/vnd.ntt-local.sip-ta_remote": { - source: "iana", - }, - "application/vnd.ntt-local.sip-ta_tcp_stream": { - source: "iana", - }, - "application/vnd.oasis.opendocument.chart": { - source: "iana", - extensions: ["odc"], - }, - "application/vnd.oasis.opendocument.chart-template": { - source: "iana", - extensions: ["otc"], - }, - "application/vnd.oasis.opendocument.database": { - source: "iana", - extensions: ["odb"], - }, - "application/vnd.oasis.opendocument.formula": { - source: "iana", - extensions: ["odf"], - }, - "application/vnd.oasis.opendocument.formula-template": { - source: "iana", - extensions: ["odft"], - }, - "application/vnd.oasis.opendocument.graphics": { - source: "iana", - compressible: false, - extensions: ["odg"], - }, - "application/vnd.oasis.opendocument.graphics-template": { - source: "iana", - extensions: ["otg"], - }, - "application/vnd.oasis.opendocument.image": { - source: "iana", - extensions: ["odi"], - }, - "application/vnd.oasis.opendocument.image-template": { - source: "iana", - extensions: ["oti"], - }, - "application/vnd.oasis.opendocument.presentation": { - source: "iana", - compressible: false, - extensions: ["odp"], - }, - "application/vnd.oasis.opendocument.presentation-template": { - source: "iana", - extensions: ["otp"], - }, - "application/vnd.oasis.opendocument.spreadsheet": { - source: "iana", - compressible: false, - extensions: ["ods"], - }, - "application/vnd.oasis.opendocument.spreadsheet-template": { - source: "iana", - extensions: ["ots"], - }, - "application/vnd.oasis.opendocument.text": { - source: "iana", - compressible: false, - extensions: ["odt"], - }, - "application/vnd.oasis.opendocument.text-master": { - source: "iana", - extensions: ["odm"], - }, - "application/vnd.oasis.opendocument.text-template": { - source: "iana", - extensions: ["ott"], - }, - "application/vnd.oasis.opendocument.text-web": { - source: "iana", - extensions: ["oth"], - }, - "application/vnd.obn": { - source: "iana", - }, - "application/vnd.ocf+cbor": { - source: "iana", - }, - "application/vnd.oftn.l10n+json": { - source: "iana", - compressible: true, - }, - "application/vnd.oipf.contentaccessdownload+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oipf.contentaccessstreaming+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oipf.cspg-hexbinary": { - source: "iana", - }, - "application/vnd.oipf.dae.svg+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oipf.dae.xhtml+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oipf.mippvcontrolmessage+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oipf.pae.gem": { - source: "iana", - }, - "application/vnd.oipf.spdiscovery+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oipf.spdlist+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oipf.ueprofile+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oipf.userprofile+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.olpc-sugar": { - source: "iana", - extensions: ["xo"], - }, - "application/vnd.oma-scws-config": { - source: "iana", - }, - "application/vnd.oma-scws-http-request": { - source: "iana", - }, - "application/vnd.oma-scws-http-response": { - source: "iana", - }, - "application/vnd.oma.bcast.associated-procedure-parameter+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.bcast.drm-trigger+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.bcast.imd+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.bcast.ltkm": { - source: "iana", - }, - "application/vnd.oma.bcast.notification+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.bcast.provisioningtrigger": { - source: "iana", - }, - "application/vnd.oma.bcast.sgboot": { - source: "iana", - }, - "application/vnd.oma.bcast.sgdd+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.bcast.sgdu": { - source: "iana", - }, - "application/vnd.oma.bcast.simple-symbol-container": { - source: "iana", - }, - "application/vnd.oma.bcast.smartcard-trigger+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.bcast.sprov+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.bcast.stkm": { - source: "iana", - }, - "application/vnd.oma.cab-address-book+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.cab-feature-handler+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.cab-pcc+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.cab-subs-invite+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.cab-user-prefs+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.dcd": { - source: "iana", - }, - "application/vnd.oma.dcdc": { - source: "iana", - }, - "application/vnd.oma.dd2+xml": { - source: "iana", - compressible: true, - extensions: ["dd2"], - }, - "application/vnd.oma.drm.risd+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.group-usage-list+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.lwm2m+json": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.lwm2m+tlv": { - source: "iana", - }, - "application/vnd.oma.pal+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.poc.detailed-progress-report+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.poc.final-report+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.poc.groups+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.poc.invocation-descriptor+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.poc.optimized-progress-report+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.push": { - source: "iana", - }, - "application/vnd.oma.scidm.messages+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oma.xcap-directory+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.omads-email+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.omads-file+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.omads-folder+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.omaloc-supl-init": { - source: "iana", - }, - "application/vnd.onepager": { - source: "iana", - }, - "application/vnd.onepagertamp": { - source: "iana", - }, - "application/vnd.onepagertamx": { - source: "iana", - }, - "application/vnd.onepagertat": { - source: "iana", - }, - "application/vnd.onepagertatp": { - source: "iana", - }, - "application/vnd.onepagertatx": { - source: "iana", - }, - "application/vnd.openblox.game+xml": { - source: "iana", - compressible: true, - extensions: ["obgx"], - }, - "application/vnd.openblox.game-binary": { - source: "iana", - }, - "application/vnd.openeye.oeb": { - source: "iana", - }, - "application/vnd.openofficeorg.extension": { - source: "apache", - extensions: ["oxt"], - }, - "application/vnd.openstreetmap.data+xml": { - source: "iana", - compressible: true, - extensions: ["osm"], - }, - "application/vnd.openxmlformats-officedocument.custom-properties+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.customxmlproperties+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.drawing+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.drawingml.chart+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.drawingml.diagramcolors+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.drawingml.diagramdata+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.drawingml.diagramlayout+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.drawingml.diagramstyle+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.extended-properties+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.presentationml.commentauthors+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.presentationml.comments+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.presentationml.handoutmaster+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.presentationml.notesmaster+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.presentationml.notesslide+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.presentationml.presentation": { - source: "iana", - compressible: false, - extensions: ["pptx"], - }, - "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.presentationml.presprops+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.presentationml.slide": { - source: "iana", - extensions: ["sldx"], - }, - "application/vnd.openxmlformats-officedocument.presentationml.slide+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.presentationml.slidelayout+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.presentationml.slidemaster+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.presentationml.slideshow": { - source: "iana", - extensions: ["ppsx"], - }, - "application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.presentationml.slideupdateinfo+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.presentationml.tablestyles+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.presentationml.tags+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.presentationml.template": { - source: "iana", - extensions: ["potx"], - }, - "application/vnd.openxmlformats-officedocument.presentationml.template.main+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.presentationml.viewprops+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.calcchain+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.externallink+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcachedefinition+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcacherecords+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.pivottable+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.querytable+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionheaders+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionlog+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedstrings+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": { - source: "iana", - compressible: false, - extensions: ["xlsx"], - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetmetadata+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.tablesinglecells+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.template": { - source: "iana", - extensions: ["xltx"], - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.usernames+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.volatiledependencies+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.theme+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.themeoverride+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.vmldrawing": { - source: "iana", - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.document": { - source: "iana", - compressible: false, - extensions: ["docx"], - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.fonttable+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.template": { - source: "iana", - extensions: ["dotx"], - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.websettings+xml": - { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-package.core-properties+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.openxmlformats-package.relationships+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oracle.resource+json": { - source: "iana", - compressible: true, - }, - "application/vnd.orange.indata": { - source: "iana", - }, - "application/vnd.osa.netdeploy": { - source: "iana", - }, - "application/vnd.osgeo.mapguide.package": { - source: "iana", - extensions: ["mgp"], - }, - "application/vnd.osgi.bundle": { - source: "iana", - }, - "application/vnd.osgi.dp": { - source: "iana", - extensions: ["dp"], - }, - "application/vnd.osgi.subsystem": { - source: "iana", - extensions: ["esa"], - }, - "application/vnd.otps.ct-kip+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.oxli.countgraph": { - source: "iana", - }, - "application/vnd.pagerduty+json": { - source: "iana", - compressible: true, - }, - "application/vnd.palm": { - source: "iana", - extensions: ["pdb", "pqa", "oprc"], - }, - "application/vnd.panoply": { - source: "iana", - }, - "application/vnd.paos.xml": { - source: "iana", - }, - "application/vnd.patentdive": { - source: "iana", - }, - "application/vnd.patientecommsdoc": { - source: "iana", - }, - "application/vnd.pawaafile": { - source: "iana", - extensions: ["paw"], - }, - "application/vnd.pcos": { - source: "iana", - }, - "application/vnd.pg.format": { - source: "iana", - extensions: ["str"], - }, - "application/vnd.pg.osasli": { - source: "iana", - extensions: ["ei6"], - }, - "application/vnd.piaccess.application-licence": { - source: "iana", - }, - "application/vnd.picsel": { - source: "iana", - extensions: ["efif"], - }, - "application/vnd.pmi.widget": { - source: "iana", - extensions: ["wg"], - }, - "application/vnd.poc.group-advertisement+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.pocketlearn": { - source: "iana", - extensions: ["plf"], - }, - "application/vnd.powerbuilder6": { - source: "iana", - extensions: ["pbd"], - }, - "application/vnd.powerbuilder6-s": { - source: "iana", - }, - "application/vnd.powerbuilder7": { - source: "iana", - }, - "application/vnd.powerbuilder7-s": { - source: "iana", - }, - "application/vnd.powerbuilder75": { - source: "iana", - }, - "application/vnd.powerbuilder75-s": { - source: "iana", - }, - "application/vnd.preminet": { - source: "iana", - }, - "application/vnd.previewsystems.box": { - source: "iana", - extensions: ["box"], - }, - "application/vnd.proteus.magazine": { - source: "iana", - extensions: ["mgz"], - }, - "application/vnd.psfs": { - source: "iana", - }, - "application/vnd.publishare-delta-tree": { - source: "iana", - extensions: ["qps"], - }, - "application/vnd.pvi.ptid1": { - source: "iana", - extensions: ["ptid"], - }, - "application/vnd.pwg-multiplexed": { - source: "iana", - }, - "application/vnd.pwg-xhtml-print+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.qualcomm.brew-app-res": { - source: "iana", - }, - "application/vnd.quarantainenet": { - source: "iana", - }, - "application/vnd.quark.quarkxpress": { - source: "iana", - extensions: ["qxd", "qxt", "qwd", "qwt", "qxl", "qxb"], - }, - "application/vnd.quobject-quoxdocument": { - source: "iana", - }, - "application/vnd.radisys.moml+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.radisys.msml+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.radisys.msml-audit+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.radisys.msml-audit-conf+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.radisys.msml-audit-conn+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.radisys.msml-audit-dialog+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.radisys.msml-audit-stream+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.radisys.msml-conf+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.radisys.msml-dialog+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.radisys.msml-dialog-base+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.radisys.msml-dialog-fax-detect+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.radisys.msml-dialog-fax-sendrecv+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.radisys.msml-dialog-group+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.radisys.msml-dialog-speech+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.radisys.msml-dialog-transform+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.rainstor.data": { - source: "iana", - }, - "application/vnd.rapid": { - source: "iana", - }, - "application/vnd.rar": { - source: "iana", - }, - "application/vnd.realvnc.bed": { - source: "iana", - extensions: ["bed"], - }, - "application/vnd.recordare.musicxml": { - source: "iana", - extensions: ["mxl"], - }, - "application/vnd.recordare.musicxml+xml": { - source: "iana", - compressible: true, - extensions: ["musicxml"], - }, - "application/vnd.renlearn.rlprint": { - source: "iana", - }, - "application/vnd.restful+json": { - source: "iana", - compressible: true, - }, - "application/vnd.rig.cryptonote": { - source: "iana", - extensions: ["cryptonote"], - }, - "application/vnd.rim.cod": { - source: "apache", - extensions: ["cod"], - }, - "application/vnd.rn-realmedia": { - source: "apache", - extensions: ["rm"], - }, - "application/vnd.rn-realmedia-vbr": { - source: "apache", - extensions: ["rmvb"], - }, - "application/vnd.route66.link66+xml": { - source: "iana", - compressible: true, - extensions: ["link66"], - }, - "application/vnd.rs-274x": { - source: "iana", - }, - "application/vnd.ruckus.download": { - source: "iana", - }, - "application/vnd.s3sms": { - source: "iana", - }, - "application/vnd.sailingtracker.track": { - source: "iana", - extensions: ["st"], - }, - "application/vnd.sar": { - source: "iana", - }, - "application/vnd.sbm.cid": { - source: "iana", - }, - "application/vnd.sbm.mid2": { - source: "iana", - }, - "application/vnd.scribus": { - source: "iana", - }, - "application/vnd.sealed.3df": { - source: "iana", - }, - "application/vnd.sealed.csf": { - source: "iana", - }, - "application/vnd.sealed.doc": { - source: "iana", - }, - "application/vnd.sealed.eml": { - source: "iana", - }, - "application/vnd.sealed.mht": { - source: "iana", - }, - "application/vnd.sealed.net": { - source: "iana", - }, - "application/vnd.sealed.ppt": { - source: "iana", - }, - "application/vnd.sealed.tiff": { - source: "iana", - }, - "application/vnd.sealed.xls": { - source: "iana", - }, - "application/vnd.sealedmedia.softseal.html": { - source: "iana", - }, - "application/vnd.sealedmedia.softseal.pdf": { - source: "iana", - }, - "application/vnd.seemail": { - source: "iana", - extensions: ["see"], - }, - "application/vnd.sema": { - source: "iana", - extensions: ["sema"], - }, - "application/vnd.semd": { - source: "iana", - extensions: ["semd"], - }, - "application/vnd.semf": { - source: "iana", - extensions: ["semf"], - }, - "application/vnd.shade-save-file": { - source: "iana", - }, - "application/vnd.shana.informed.formdata": { - source: "iana", - extensions: ["ifm"], - }, - "application/vnd.shana.informed.formtemplate": { - source: "iana", - extensions: ["itp"], - }, - "application/vnd.shana.informed.interchange": { - source: "iana", - extensions: ["iif"], - }, - "application/vnd.shana.informed.package": { - source: "iana", - extensions: ["ipk"], - }, - "application/vnd.shootproof+json": { - source: "iana", - compressible: true, - }, - "application/vnd.shopkick+json": { - source: "iana", - compressible: true, - }, - "application/vnd.shp": { - source: "iana", - }, - "application/vnd.shx": { - source: "iana", - }, - "application/vnd.sigrok.session": { - source: "iana", - }, - "application/vnd.simtech-mindmapper": { - source: "iana", - extensions: ["twd", "twds"], - }, - "application/vnd.siren+json": { - source: "iana", - compressible: true, - }, - "application/vnd.smaf": { - source: "iana", - extensions: ["mmf"], - }, - "application/vnd.smart.notebook": { - source: "iana", - }, - "application/vnd.smart.teacher": { - source: "iana", - extensions: ["teacher"], - }, - "application/vnd.software602.filler.form+xml": { - source: "iana", - compressible: true, - extensions: ["fo"], - }, - "application/vnd.software602.filler.form-xml-zip": { - source: "iana", - }, - "application/vnd.solent.sdkm+xml": { - source: "iana", - compressible: true, - extensions: ["sdkm", "sdkd"], - }, - "application/vnd.spotfire.dxp": { - source: "iana", - extensions: ["dxp"], - }, - "application/vnd.spotfire.sfs": { - source: "iana", - extensions: ["sfs"], - }, - "application/vnd.sqlite3": { - source: "iana", - }, - "application/vnd.sss-cod": { - source: "iana", - }, - "application/vnd.sss-dtf": { - source: "iana", - }, - "application/vnd.sss-ntf": { - source: "iana", - }, - "application/vnd.stardivision.calc": { - source: "apache", - extensions: ["sdc"], - }, - "application/vnd.stardivision.draw": { - source: "apache", - extensions: ["sda"], - }, - "application/vnd.stardivision.impress": { - source: "apache", - extensions: ["sdd"], - }, - "application/vnd.stardivision.math": { - source: "apache", - extensions: ["smf"], - }, - "application/vnd.stardivision.writer": { - source: "apache", - extensions: ["sdw", "vor"], - }, - "application/vnd.stardivision.writer-global": { - source: "apache", - extensions: ["sgl"], - }, - "application/vnd.stepmania.package": { - source: "iana", - extensions: ["smzip"], - }, - "application/vnd.stepmania.stepchart": { - source: "iana", - extensions: ["sm"], - }, - "application/vnd.street-stream": { - source: "iana", - }, - "application/vnd.sun.wadl+xml": { - source: "iana", - compressible: true, - extensions: ["wadl"], - }, - "application/vnd.sun.xml.calc": { - source: "apache", - extensions: ["sxc"], - }, - "application/vnd.sun.xml.calc.template": { - source: "apache", - extensions: ["stc"], - }, - "application/vnd.sun.xml.draw": { - source: "apache", - extensions: ["sxd"], - }, - "application/vnd.sun.xml.draw.template": { - source: "apache", - extensions: ["std"], - }, - "application/vnd.sun.xml.impress": { - source: "apache", - extensions: ["sxi"], - }, - "application/vnd.sun.xml.impress.template": { - source: "apache", - extensions: ["sti"], - }, - "application/vnd.sun.xml.math": { - source: "apache", - extensions: ["sxm"], - }, - "application/vnd.sun.xml.writer": { - source: "apache", - extensions: ["sxw"], - }, - "application/vnd.sun.xml.writer.global": { - source: "apache", - extensions: ["sxg"], - }, - "application/vnd.sun.xml.writer.template": { - source: "apache", - extensions: ["stw"], - }, - "application/vnd.sus-calendar": { - source: "iana", - extensions: ["sus", "susp"], - }, - "application/vnd.svd": { - source: "iana", - extensions: ["svd"], - }, - "application/vnd.swiftview-ics": { - source: "iana", - }, - "application/vnd.symbian.install": { - source: "apache", - extensions: ["sis", "sisx"], - }, - "application/vnd.syncml+xml": { - source: "iana", - compressible: true, - extensions: ["xsm"], - }, - "application/vnd.syncml.dm+wbxml": { - source: "iana", - extensions: ["bdm"], - }, - "application/vnd.syncml.dm+xml": { - source: "iana", - compressible: true, - extensions: ["xdm"], - }, - "application/vnd.syncml.dm.notification": { - source: "iana", - }, - "application/vnd.syncml.dmddf+wbxml": { - source: "iana", - }, - "application/vnd.syncml.dmddf+xml": { - source: "iana", - compressible: true, - extensions: ["ddf"], - }, - "application/vnd.syncml.dmtnds+wbxml": { - source: "iana", - }, - "application/vnd.syncml.dmtnds+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.syncml.ds.notification": { - source: "iana", - }, - "application/vnd.tableschema+json": { - source: "iana", - compressible: true, - }, - "application/vnd.tao.intent-module-archive": { - source: "iana", - extensions: ["tao"], - }, - "application/vnd.tcpdump.pcap": { - source: "iana", - extensions: ["pcap", "cap", "dmp"], - }, - "application/vnd.think-cell.ppttc+json": { - source: "iana", - compressible: true, - }, - "application/vnd.tmd.mediaflex.api+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.tml": { - source: "iana", - }, - "application/vnd.tmobile-livetv": { - source: "iana", - extensions: ["tmo"], - }, - "application/vnd.tri.onesource": { - source: "iana", - }, - "application/vnd.trid.tpt": { - source: "iana", - extensions: ["tpt"], - }, - "application/vnd.triscape.mxs": { - source: "iana", - extensions: ["mxs"], - }, - "application/vnd.trueapp": { - source: "iana", - extensions: ["tra"], - }, - "application/vnd.truedoc": { - source: "iana", - }, - "application/vnd.ubisoft.webplayer": { - source: "iana", - }, - "application/vnd.ufdl": { - source: "iana", - extensions: ["ufd", "ufdl"], - }, - "application/vnd.uiq.theme": { - source: "iana", - extensions: ["utz"], - }, - "application/vnd.umajin": { - source: "iana", - extensions: ["umj"], - }, - "application/vnd.unity": { - source: "iana", - extensions: ["unityweb"], - }, - "application/vnd.uoml+xml": { - source: "iana", - compressible: true, - extensions: ["uoml"], - }, - "application/vnd.uplanet.alert": { - source: "iana", - }, - "application/vnd.uplanet.alert-wbxml": { - source: "iana", - }, - "application/vnd.uplanet.bearer-choice": { - source: "iana", - }, - "application/vnd.uplanet.bearer-choice-wbxml": { - source: "iana", - }, - "application/vnd.uplanet.cacheop": { - source: "iana", - }, - "application/vnd.uplanet.cacheop-wbxml": { - source: "iana", - }, - "application/vnd.uplanet.channel": { - source: "iana", - }, - "application/vnd.uplanet.channel-wbxml": { - source: "iana", - }, - "application/vnd.uplanet.list": { - source: "iana", - }, - "application/vnd.uplanet.list-wbxml": { - source: "iana", - }, - "application/vnd.uplanet.listcmd": { - source: "iana", - }, - "application/vnd.uplanet.listcmd-wbxml": { - source: "iana", - }, - "application/vnd.uplanet.signal": { - source: "iana", - }, - "application/vnd.uri-map": { - source: "iana", - }, - "application/vnd.valve.source.material": { - source: "iana", - }, - "application/vnd.vcx": { - source: "iana", - extensions: ["vcx"], - }, - "application/vnd.vd-study": { - source: "iana", - }, - "application/vnd.vectorworks": { - source: "iana", - }, - "application/vnd.vel+json": { - source: "iana", - compressible: true, - }, - "application/vnd.verimatrix.vcas": { - source: "iana", - }, - "application/vnd.veryant.thin": { - source: "iana", - }, - "application/vnd.ves.encrypted": { - source: "iana", - }, - "application/vnd.vidsoft.vidconference": { - source: "iana", - }, - "application/vnd.visio": { - source: "iana", - extensions: ["vsd", "vst", "vss", "vsw"], - }, - "application/vnd.visionary": { - source: "iana", - extensions: ["vis"], - }, - "application/vnd.vividence.scriptfile": { - source: "iana", - }, - "application/vnd.vsf": { - source: "iana", - extensions: ["vsf"], - }, - "application/vnd.wap.sic": { - source: "iana", - }, - "application/vnd.wap.slc": { - source: "iana", - }, - "application/vnd.wap.wbxml": { - source: "iana", - extensions: ["wbxml"], - }, - "application/vnd.wap.wmlc": { - source: "iana", - extensions: ["wmlc"], - }, - "application/vnd.wap.wmlscriptc": { - source: "iana", - extensions: ["wmlsc"], - }, - "application/vnd.webturbo": { - source: "iana", - extensions: ["wtb"], - }, - "application/vnd.wfa.p2p": { - source: "iana", - }, - "application/vnd.wfa.wsc": { - source: "iana", - }, - "application/vnd.windows.devicepairing": { - source: "iana", - }, - "application/vnd.wmc": { - source: "iana", - }, - "application/vnd.wmf.bootstrap": { - source: "iana", - }, - "application/vnd.wolfram.mathematica": { - source: "iana", - }, - "application/vnd.wolfram.mathematica.package": { - source: "iana", - }, - "application/vnd.wolfram.player": { - source: "iana", - extensions: ["nbp"], - }, - "application/vnd.wordperfect": { - source: "iana", - extensions: ["wpd"], - }, - "application/vnd.wqd": { - source: "iana", - extensions: ["wqd"], - }, - "application/vnd.wrq-hp3000-labelled": { - source: "iana", - }, - "application/vnd.wt.stf": { - source: "iana", - extensions: ["stf"], - }, - "application/vnd.wv.csp+wbxml": { - source: "iana", - }, - "application/vnd.wv.csp+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.wv.ssp+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.xacml+json": { - source: "iana", - compressible: true, - }, - "application/vnd.xara": { - source: "iana", - extensions: ["xar"], - }, - "application/vnd.xfdl": { - source: "iana", - extensions: ["xfdl"], - }, - "application/vnd.xfdl.webform": { - source: "iana", - }, - "application/vnd.xmi+xml": { - source: "iana", - compressible: true, - }, - "application/vnd.xmpie.cpkg": { - source: "iana", - }, - "application/vnd.xmpie.dpkg": { - source: "iana", - }, - "application/vnd.xmpie.plan": { - source: "iana", - }, - "application/vnd.xmpie.ppkg": { - source: "iana", - }, - "application/vnd.xmpie.xlim": { - source: "iana", - }, - "application/vnd.yamaha.hv-dic": { - source: "iana", - extensions: ["hvd"], - }, - "application/vnd.yamaha.hv-script": { - source: "iana", - extensions: ["hvs"], - }, - "application/vnd.yamaha.hv-voice": { - source: "iana", - extensions: ["hvp"], - }, - "application/vnd.yamaha.openscoreformat": { - source: "iana", - extensions: ["osf"], - }, - "application/vnd.yamaha.openscoreformat.osfpvg+xml": { - source: "iana", - compressible: true, - extensions: ["osfpvg"], - }, - "application/vnd.yamaha.remote-setup": { - source: "iana", - }, - "application/vnd.yamaha.smaf-audio": { - source: "iana", - extensions: ["saf"], - }, - "application/vnd.yamaha.smaf-phrase": { - source: "iana", - extensions: ["spf"], - }, - "application/vnd.yamaha.through-ngn": { - source: "iana", - }, - "application/vnd.yamaha.tunnel-udpencap": { - source: "iana", - }, - "application/vnd.yaoweme": { - source: "iana", - }, - "application/vnd.yellowriver-custom-menu": { - source: "iana", - extensions: ["cmp"], - }, - "application/vnd.youtube.yt": { - source: "iana", - }, - "application/vnd.zul": { - source: "iana", - extensions: ["zir", "zirz"], - }, - "application/vnd.zzazz.deck+xml": { - source: "iana", - compressible: true, - extensions: ["zaz"], - }, - "application/voicexml+xml": { - source: "iana", - compressible: true, - extensions: ["vxml"], - }, - "application/voucher-cms+json": { - source: "iana", - compressible: true, - }, - "application/vq-rtcpxr": { - source: "iana", - }, - "application/wasm": { - compressible: true, - extensions: ["wasm"], - }, - "application/watcherinfo+xml": { - source: "iana", - compressible: true, - }, - "application/webpush-options+json": { - source: "iana", - compressible: true, - }, - "application/whoispp-query": { - source: "iana", - }, - "application/whoispp-response": { - source: "iana", - }, - "application/widget": { - source: "iana", - extensions: ["wgt"], - }, - "application/winhlp": { - source: "apache", - extensions: ["hlp"], - }, - "application/wita": { - source: "iana", - }, - "application/wordperfect5.1": { - source: "iana", - }, - "application/wsdl+xml": { - source: "iana", - compressible: true, - extensions: ["wsdl"], - }, - "application/wspolicy+xml": { - source: "iana", - compressible: true, - extensions: ["wspolicy"], - }, - "application/x-7z-compressed": { - source: "apache", - compressible: false, - extensions: ["7z"], - }, - "application/x-abiword": { - source: "apache", - extensions: ["abw"], - }, - "application/x-ace-compressed": { - source: "apache", - extensions: ["ace"], - }, - "application/x-amf": { - source: "apache", - }, - "application/x-apple-diskimage": { - source: "apache", - extensions: ["dmg"], - }, - "application/x-arj": { - compressible: false, - extensions: ["arj"], - }, - "application/x-authorware-bin": { - source: "apache", - extensions: ["aab", "x32", "u32", "vox"], - }, - "application/x-authorware-map": { - source: "apache", - extensions: ["aam"], - }, - "application/x-authorware-seg": { - source: "apache", - extensions: ["aas"], - }, - "application/x-bcpio": { - source: "apache", - extensions: ["bcpio"], - }, - "application/x-bdoc": { - compressible: false, - extensions: ["bdoc"], - }, - "application/x-bittorrent": { - source: "apache", - extensions: ["torrent"], - }, - "application/x-blorb": { - source: "apache", - extensions: ["blb", "blorb"], - }, - "application/x-bzip": { - source: "apache", - compressible: false, - extensions: ["bz"], - }, - "application/x-bzip2": { - source: "apache", - compressible: false, - extensions: ["bz2", "boz"], - }, - "application/x-cbr": { - source: "apache", - extensions: ["cbr", "cba", "cbt", "cbz", "cb7"], - }, - "application/x-cdlink": { - source: "apache", - extensions: ["vcd"], - }, - "application/x-cfs-compressed": { - source: "apache", - extensions: ["cfs"], - }, - "application/x-chat": { - source: "apache", - extensions: ["chat"], - }, - "application/x-chess-pgn": { - source: "apache", - extensions: ["pgn"], - }, - "application/x-chrome-extension": { - extensions: ["crx"], - }, - "application/x-cocoa": { - source: "nginx", - extensions: ["cco"], - }, - "application/x-compress": { - source: "apache", - }, - "application/x-conference": { - source: "apache", - extensions: ["nsc"], - }, - "application/x-cpio": { - source: "apache", - extensions: ["cpio"], - }, - "application/x-csh": { - source: "apache", - extensions: ["csh"], - }, - "application/x-deb": { - compressible: false, - }, - "application/x-debian-package": { - source: "apache", - extensions: ["deb", "udeb"], - }, - "application/x-dgc-compressed": { - source: "apache", - extensions: ["dgc"], - }, - "application/x-director": { - source: "apache", - extensions: ["dir", "dcr", "dxr", "cst", "cct", "cxt", "w3d", "fgd", "swa"], - }, - "application/x-doom": { - source: "apache", - extensions: ["wad"], - }, - "application/x-dtbncx+xml": { - source: "apache", - compressible: true, - extensions: ["ncx"], - }, - "application/x-dtbook+xml": { - source: "apache", - compressible: true, - extensions: ["dtb"], - }, - "application/x-dtbresource+xml": { - source: "apache", - compressible: true, - extensions: ["res"], - }, - "application/x-dvi": { - source: "apache", - compressible: false, - extensions: ["dvi"], - }, - "application/x-envoy": { - source: "apache", - extensions: ["evy"], - }, - "application/x-eva": { - source: "apache", - extensions: ["eva"], - }, - "application/x-font-bdf": { - source: "apache", - extensions: ["bdf"], - }, - "application/x-font-dos": { - source: "apache", - }, - "application/x-font-framemaker": { - source: "apache", - }, - "application/x-font-ghostscript": { - source: "apache", - extensions: ["gsf"], - }, - "application/x-font-libgrx": { - source: "apache", - }, - "application/x-font-linux-psf": { - source: "apache", - extensions: ["psf"], - }, - "application/x-font-pcf": { - source: "apache", - extensions: ["pcf"], - }, - "application/x-font-snf": { - source: "apache", - extensions: ["snf"], - }, - "application/x-font-speedo": { - source: "apache", - }, - "application/x-font-sunos-news": { - source: "apache", - }, - "application/x-font-type1": { - source: "apache", - extensions: ["pfa", "pfb", "pfm", "afm"], - }, - "application/x-font-vfont": { - source: "apache", - }, - "application/x-freearc": { - source: "apache", - extensions: ["arc"], - }, - "application/x-futuresplash": { - source: "apache", - extensions: ["spl"], - }, - "application/x-gca-compressed": { - source: "apache", - extensions: ["gca"], - }, - "application/x-glulx": { - source: "apache", - extensions: ["ulx"], - }, - "application/x-gnumeric": { - source: "apache", - extensions: ["gnumeric"], - }, - "application/x-gramps-xml": { - source: "apache", - extensions: ["gramps"], - }, - "application/x-gtar": { - source: "apache", - extensions: ["gtar"], - }, - "application/x-gzip": { - source: "apache", - }, - "application/x-hdf": { - source: "apache", - extensions: ["hdf"], - }, - "application/x-httpd-php": { - compressible: true, - extensions: ["php"], - }, - "application/x-install-instructions": { - source: "apache", - extensions: ["install"], - }, - "application/x-iso9660-image": { - source: "apache", - extensions: ["iso"], - }, - "application/x-java-archive-diff": { - source: "nginx", - extensions: ["jardiff"], - }, - "application/x-java-jnlp-file": { - source: "apache", - compressible: false, - extensions: ["jnlp"], - }, - "application/x-javascript": { - compressible: true, - }, - "application/x-keepass2": { - extensions: ["kdbx"], - }, - "application/x-latex": { - source: "apache", - compressible: false, - extensions: ["latex"], - }, - "application/x-lua-bytecode": { - extensions: ["luac"], - }, - "application/x-lzh-compressed": { - source: "apache", - extensions: ["lzh", "lha"], - }, - "application/x-makeself": { - source: "nginx", - extensions: ["run"], - }, - "application/x-mie": { - source: "apache", - extensions: ["mie"], - }, - "application/x-mobipocket-ebook": { - source: "apache", - extensions: ["prc", "mobi"], - }, - "application/x-mpegurl": { - compressible: false, - }, - "application/x-ms-application": { - source: "apache", - extensions: ["application"], - }, - "application/x-ms-shortcut": { - source: "apache", - extensions: ["lnk"], - }, - "application/x-ms-wmd": { - source: "apache", - extensions: ["wmd"], - }, - "application/x-ms-wmz": { - source: "apache", - extensions: ["wmz"], - }, - "application/x-ms-xbap": { - source: "apache", - extensions: ["xbap"], - }, - "application/x-msaccess": { - source: "apache", - extensions: ["mdb"], - }, - "application/x-msbinder": { - source: "apache", - extensions: ["obd"], - }, - "application/x-mscardfile": { - source: "apache", - extensions: ["crd"], - }, - "application/x-msclip": { - source: "apache", - extensions: ["clp"], - }, - "application/x-msdos-program": { - extensions: ["exe"], - }, - "application/x-msdownload": { - source: "apache", - extensions: ["exe", "dll", "com", "bat", "msi"], - }, - "application/x-msmediaview": { - source: "apache", - extensions: ["mvb", "m13", "m14"], - }, - "application/x-msmetafile": { - source: "apache", - extensions: ["wmf", "wmz", "emf", "emz"], - }, - "application/x-msmoney": { - source: "apache", - extensions: ["mny"], - }, - "application/x-mspublisher": { - source: "apache", - extensions: ["pub"], - }, - "application/x-msschedule": { - source: "apache", - extensions: ["scd"], - }, - "application/x-msterminal": { - source: "apache", - extensions: ["trm"], - }, - "application/x-mswrite": { - source: "apache", - extensions: ["wri"], - }, - "application/x-netcdf": { - source: "apache", - extensions: ["nc", "cdf"], - }, - "application/x-ns-proxy-autoconfig": { - compressible: true, - extensions: ["pac"], - }, - "application/x-nzb": { - source: "apache", - extensions: ["nzb"], - }, - "application/x-perl": { - source: "nginx", - extensions: ["pl", "pm"], - }, - "application/x-pilot": { - source: "nginx", - extensions: ["prc", "pdb"], - }, - "application/x-pkcs12": { - source: "apache", - compressible: false, - extensions: ["p12", "pfx"], - }, - "application/x-pkcs7-certificates": { - source: "apache", - extensions: ["p7b", "spc"], - }, - "application/x-pkcs7-certreqresp": { - source: "apache", - extensions: ["p7r"], - }, - "application/x-pki-message": { - source: "iana", - }, - "application/x-rar-compressed": { - source: "apache", - compressible: false, - extensions: ["rar"], - }, - "application/x-redhat-package-manager": { - source: "nginx", - extensions: ["rpm"], - }, - "application/x-research-info-systems": { - source: "apache", - extensions: ["ris"], - }, - "application/x-sea": { - source: "nginx", - extensions: ["sea"], - }, - "application/x-sh": { - source: "apache", - compressible: true, - extensions: ["sh"], - }, - "application/x-shar": { - source: "apache", - extensions: ["shar"], - }, - "application/x-shockwave-flash": { - source: "apache", - compressible: false, - extensions: ["swf"], - }, - "application/x-silverlight-app": { - source: "apache", - extensions: ["xap"], - }, - "application/x-sql": { - source: "apache", - extensions: ["sql"], - }, - "application/x-stuffit": { - source: "apache", - compressible: false, - extensions: ["sit"], - }, - "application/x-stuffitx": { - source: "apache", - extensions: ["sitx"], - }, - "application/x-subrip": { - source: "apache", - extensions: ["srt"], - }, - "application/x-sv4cpio": { - source: "apache", - extensions: ["sv4cpio"], - }, - "application/x-sv4crc": { - source: "apache", - extensions: ["sv4crc"], - }, - "application/x-t3vm-image": { - source: "apache", - extensions: ["t3"], - }, - "application/x-tads": { - source: "apache", - extensions: ["gam"], - }, - "application/x-tar": { - source: "apache", - compressible: true, - extensions: ["tar"], - }, - "application/x-tcl": { - source: "apache", - extensions: ["tcl", "tk"], - }, - "application/x-tex": { - source: "apache", - extensions: ["tex"], - }, - "application/x-tex-tfm": { - source: "apache", - extensions: ["tfm"], - }, - "application/x-texinfo": { - source: "apache", - extensions: ["texinfo", "texi"], - }, - "application/x-tgif": { - source: "apache", - extensions: ["obj"], - }, - "application/x-ustar": { - source: "apache", - extensions: ["ustar"], - }, - "application/x-virtualbox-hdd": { - compressible: true, - extensions: ["hdd"], - }, - "application/x-virtualbox-ova": { - compressible: true, - extensions: ["ova"], - }, - "application/x-virtualbox-ovf": { - compressible: true, - extensions: ["ovf"], - }, - "application/x-virtualbox-vbox": { - compressible: true, - extensions: ["vbox"], - }, - "application/x-virtualbox-vbox-extpack": { - compressible: false, - extensions: ["vbox-extpack"], - }, - "application/x-virtualbox-vdi": { - compressible: true, - extensions: ["vdi"], - }, - "application/x-virtualbox-vhd": { - compressible: true, - extensions: ["vhd"], - }, - "application/x-virtualbox-vmdk": { - compressible: true, - extensions: ["vmdk"], - }, - "application/x-wais-source": { - source: "apache", - extensions: ["src"], - }, - "application/x-web-app-manifest+json": { - compressible: true, - extensions: ["webapp"], - }, - "application/x-www-form-urlencoded": { - source: "iana", - compressible: true, - }, - "application/x-x509-ca-cert": { - source: "iana", - extensions: ["der", "crt", "pem"], - }, - "application/x-x509-ca-ra-cert": { - source: "iana", - }, - "application/x-x509-next-ca-cert": { - source: "iana", - }, - "application/x-xfig": { - source: "apache", - extensions: ["fig"], - }, - "application/x-xliff+xml": { - source: "apache", - compressible: true, - extensions: ["xlf"], - }, - "application/x-xpinstall": { - source: "apache", - compressible: false, - extensions: ["xpi"], - }, - "application/x-xz": { - source: "apache", - extensions: ["xz"], - }, - "application/x-zmachine": { - source: "apache", - extensions: ["z1", "z2", "z3", "z4", "z5", "z6", "z7", "z8"], - }, - "application/x400-bp": { - source: "iana", - }, - "application/xacml+xml": { - source: "iana", - compressible: true, - }, - "application/xaml+xml": { - source: "apache", - compressible: true, - extensions: ["xaml"], - }, - "application/xcap-att+xml": { - source: "iana", - compressible: true, - extensions: ["xav"], - }, - "application/xcap-caps+xml": { - source: "iana", - compressible: true, - extensions: ["xca"], - }, - "application/xcap-diff+xml": { - source: "iana", - compressible: true, - extensions: ["xdf"], - }, - "application/xcap-el+xml": { - source: "iana", - compressible: true, - extensions: ["xel"], - }, - "application/xcap-error+xml": { - source: "iana", - compressible: true, - extensions: ["xer"], - }, - "application/xcap-ns+xml": { - source: "iana", - compressible: true, - extensions: ["xns"], - }, - "application/xcon-conference-info+xml": { - source: "iana", - compressible: true, - }, - "application/xcon-conference-info-diff+xml": { - source: "iana", - compressible: true, - }, - "application/xenc+xml": { - source: "iana", - compressible: true, - extensions: ["xenc"], - }, - "application/xhtml+xml": { - source: "iana", - compressible: true, - extensions: ["xhtml", "xht"], - }, - "application/xhtml-voice+xml": { - source: "apache", - compressible: true, - }, - "application/xliff+xml": { - source: "iana", - compressible: true, - extensions: ["xlf"], - }, - "application/xml": { - source: "iana", - compressible: true, - extensions: ["xml", "xsl", "xsd", "rng"], - }, - "application/xml-dtd": { - source: "iana", - compressible: true, - extensions: ["dtd"], - }, - "application/xml-external-parsed-entity": { - source: "iana", - }, - "application/xml-patch+xml": { - source: "iana", - compressible: true, - }, - "application/xmpp+xml": { - source: "iana", - compressible: true, - }, - "application/xop+xml": { - source: "iana", - compressible: true, - extensions: ["xop"], - }, - "application/xproc+xml": { - source: "apache", - compressible: true, - extensions: ["xpl"], - }, - "application/xslt+xml": { - source: "iana", - compressible: true, - extensions: ["xslt"], - }, - "application/xspf+xml": { - source: "apache", - compressible: true, - extensions: ["xspf"], - }, - "application/xv+xml": { - source: "iana", - compressible: true, - extensions: ["mxml", "xhvml", "xvml", "xvm"], - }, - "application/yang": { - source: "iana", - extensions: ["yang"], - }, - "application/yang-data+json": { - source: "iana", - compressible: true, - }, - "application/yang-data+xml": { - source: "iana", - compressible: true, - }, - "application/yang-patch+json": { - source: "iana", - compressible: true, - }, - "application/yang-patch+xml": { - source: "iana", - compressible: true, - }, - "application/yin+xml": { - source: "iana", - compressible: true, - extensions: ["yin"], - }, - "application/zip": { - source: "iana", - compressible: false, - extensions: ["zip"], - }, - "application/zlib": { - source: "iana", - }, - "application/zstd": { - source: "iana", - }, - "audio/1d-interleaved-parityfec": { - source: "iana", - }, - "audio/32kadpcm": { - source: "iana", - }, - "audio/3gpp": { - source: "iana", - compressible: false, - extensions: ["3gpp"], - }, - "audio/3gpp2": { - source: "iana", - }, - "audio/aac": { - source: "iana", - }, - "audio/ac3": { - source: "iana", - }, - "audio/adpcm": { - source: "apache", - extensions: ["adp"], - }, - "audio/amr": { - source: "iana", - }, - "audio/amr-wb": { - source: "iana", - }, - "audio/amr-wb+": { - source: "iana", - }, - "audio/aptx": { - source: "iana", - }, - "audio/asc": { - source: "iana", - }, - "audio/atrac-advanced-lossless": { - source: "iana", - }, - "audio/atrac-x": { - source: "iana", - }, - "audio/atrac3": { - source: "iana", - }, - "audio/basic": { - source: "iana", - compressible: false, - extensions: ["au", "snd"], - }, - "audio/bv16": { - source: "iana", - }, - "audio/bv32": { - source: "iana", - }, - "audio/clearmode": { - source: "iana", - }, - "audio/cn": { - source: "iana", - }, - "audio/dat12": { - source: "iana", - }, - "audio/dls": { - source: "iana", - }, - "audio/dsr-es201108": { - source: "iana", - }, - "audio/dsr-es202050": { - source: "iana", - }, - "audio/dsr-es202211": { - source: "iana", - }, - "audio/dsr-es202212": { - source: "iana", - }, - "audio/dv": { - source: "iana", - }, - "audio/dvi4": { - source: "iana", - }, - "audio/eac3": { - source: "iana", - }, - "audio/encaprtp": { - source: "iana", - }, - "audio/evrc": { - source: "iana", - }, - "audio/evrc-qcp": { - source: "iana", - }, - "audio/evrc0": { - source: "iana", - }, - "audio/evrc1": { - source: "iana", - }, - "audio/evrcb": { - source: "iana", - }, - "audio/evrcb0": { - source: "iana", - }, - "audio/evrcb1": { - source: "iana", - }, - "audio/evrcnw": { - source: "iana", - }, - "audio/evrcnw0": { - source: "iana", - }, - "audio/evrcnw1": { - source: "iana", - }, - "audio/evrcwb": { - source: "iana", - }, - "audio/evrcwb0": { - source: "iana", - }, - "audio/evrcwb1": { - source: "iana", - }, - "audio/evs": { - source: "iana", - }, - "audio/flexfec": { - source: "iana", - }, - "audio/fwdred": { - source: "iana", - }, - "audio/g711-0": { - source: "iana", - }, - "audio/g719": { - source: "iana", - }, - "audio/g722": { - source: "iana", - }, - "audio/g7221": { - source: "iana", - }, - "audio/g723": { - source: "iana", - }, - "audio/g726-16": { - source: "iana", - }, - "audio/g726-24": { - source: "iana", - }, - "audio/g726-32": { - source: "iana", - }, - "audio/g726-40": { - source: "iana", - }, - "audio/g728": { - source: "iana", - }, - "audio/g729": { - source: "iana", - }, - "audio/g7291": { - source: "iana", - }, - "audio/g729d": { - source: "iana", - }, - "audio/g729e": { - source: "iana", - }, - "audio/gsm": { - source: "iana", - }, - "audio/gsm-efr": { - source: "iana", - }, - "audio/gsm-hr-08": { - source: "iana", - }, - "audio/ilbc": { - source: "iana", - }, - "audio/ip-mr_v2.5": { - source: "iana", - }, - "audio/isac": { - source: "apache", - }, - "audio/l16": { - source: "iana", - }, - "audio/l20": { - source: "iana", - }, - "audio/l24": { - source: "iana", - compressible: false, - }, - "audio/l8": { - source: "iana", - }, - "audio/lpc": { - source: "iana", - }, - "audio/melp": { - source: "iana", - }, - "audio/melp1200": { - source: "iana", - }, - "audio/melp2400": { - source: "iana", - }, - "audio/melp600": { - source: "iana", - }, - "audio/midi": { - source: "apache", - extensions: ["mid", "midi", "kar", "rmi"], - }, - "audio/mobile-xmf": { - source: "iana", - extensions: ["mxmf"], - }, - "audio/mp3": { - compressible: false, - extensions: ["mp3"], - }, - "audio/mp4": { - source: "iana", - compressible: false, - extensions: ["m4a", "mp4a"], - }, - "audio/mp4a-latm": { - source: "iana", - }, - "audio/mpa": { - source: "iana", - }, - "audio/mpa-robust": { - source: "iana", - }, - "audio/mpeg": { - source: "iana", - compressible: false, - extensions: ["mpga", "mp2", "mp2a", "mp3", "m2a", "m3a"], - }, - "audio/mpeg4-generic": { - source: "iana", - }, - "audio/musepack": { - source: "apache", - }, - "audio/ogg": { - source: "iana", - compressible: false, - extensions: ["oga", "ogg", "spx"], - }, - "audio/opus": { - source: "iana", - }, - "audio/parityfec": { - source: "iana", - }, - "audio/pcma": { - source: "iana", - }, - "audio/pcma-wb": { - source: "iana", - }, - "audio/pcmu": { - source: "iana", - }, - "audio/pcmu-wb": { - source: "iana", - }, - "audio/prs.sid": { - source: "iana", - }, - "audio/qcelp": { - source: "iana", - }, - "audio/raptorfec": { - source: "iana", - }, - "audio/red": { - source: "iana", - }, - "audio/rtp-enc-aescm128": { - source: "iana", - }, - "audio/rtp-midi": { - source: "iana", - }, - "audio/rtploopback": { - source: "iana", - }, - "audio/rtx": { - source: "iana", - }, - "audio/s3m": { - source: "apache", - extensions: ["s3m"], - }, - "audio/silk": { - source: "apache", - extensions: ["sil"], - }, - "audio/smv": { - source: "iana", - }, - "audio/smv-qcp": { - source: "iana", - }, - "audio/smv0": { - source: "iana", - }, - "audio/sp-midi": { - source: "iana", - }, - "audio/speex": { - source: "iana", - }, - "audio/t140c": { - source: "iana", - }, - "audio/t38": { - source: "iana", - }, - "audio/telephone-event": { - source: "iana", - }, - "audio/tetra_acelp": { - source: "iana", - }, - "audio/tetra_acelp_bb": { - source: "iana", - }, - "audio/tone": { - source: "iana", - }, - "audio/uemclip": { - source: "iana", - }, - "audio/ulpfec": { - source: "iana", - }, - "audio/usac": { - source: "iana", - }, - "audio/vdvi": { - source: "iana", - }, - "audio/vmr-wb": { - source: "iana", - }, - "audio/vnd.3gpp.iufp": { - source: "iana", - }, - "audio/vnd.4sb": { - source: "iana", - }, - "audio/vnd.audiokoz": { - source: "iana", - }, - "audio/vnd.celp": { - source: "iana", - }, - "audio/vnd.cisco.nse": { - source: "iana", - }, - "audio/vnd.cmles.radio-events": { - source: "iana", - }, - "audio/vnd.cns.anp1": { - source: "iana", - }, - "audio/vnd.cns.inf1": { - source: "iana", - }, - "audio/vnd.dece.audio": { - source: "iana", - extensions: ["uva", "uvva"], - }, - "audio/vnd.digital-winds": { - source: "iana", - extensions: ["eol"], - }, - "audio/vnd.dlna.adts": { - source: "iana", - }, - "audio/vnd.dolby.heaac.1": { - source: "iana", - }, - "audio/vnd.dolby.heaac.2": { - source: "iana", - }, - "audio/vnd.dolby.mlp": { - source: "iana", - }, - "audio/vnd.dolby.mps": { - source: "iana", - }, - "audio/vnd.dolby.pl2": { - source: "iana", - }, - "audio/vnd.dolby.pl2x": { - source: "iana", - }, - "audio/vnd.dolby.pl2z": { - source: "iana", - }, - "audio/vnd.dolby.pulse.1": { - source: "iana", - }, - "audio/vnd.dra": { - source: "iana", - extensions: ["dra"], - }, - "audio/vnd.dts": { - source: "iana", - extensions: ["dts"], - }, - "audio/vnd.dts.hd": { - source: "iana", - extensions: ["dtshd"], - }, - "audio/vnd.dts.uhd": { - source: "iana", - }, - "audio/vnd.dvb.file": { - source: "iana", - }, - "audio/vnd.everad.plj": { - source: "iana", - }, - "audio/vnd.hns.audio": { - source: "iana", - }, - "audio/vnd.lucent.voice": { - source: "iana", - extensions: ["lvp"], - }, - "audio/vnd.ms-playready.media.pya": { - source: "iana", - extensions: ["pya"], - }, - "audio/vnd.nokia.mobile-xmf": { - source: "iana", - }, - "audio/vnd.nortel.vbk": { - source: "iana", - }, - "audio/vnd.nuera.ecelp4800": { - source: "iana", - extensions: ["ecelp4800"], - }, - "audio/vnd.nuera.ecelp7470": { - source: "iana", - extensions: ["ecelp7470"], - }, - "audio/vnd.nuera.ecelp9600": { - source: "iana", - extensions: ["ecelp9600"], - }, - "audio/vnd.octel.sbc": { - source: "iana", - }, - "audio/vnd.presonus.multitrack": { - source: "iana", - }, - "audio/vnd.qcelp": { - source: "iana", - }, - "audio/vnd.rhetorex.32kadpcm": { - source: "iana", - }, - "audio/vnd.rip": { - source: "iana", - extensions: ["rip"], - }, - "audio/vnd.rn-realaudio": { - compressible: false, - }, - "audio/vnd.sealedmedia.softseal.mpeg": { - source: "iana", - }, - "audio/vnd.vmx.cvsd": { - source: "iana", - }, - "audio/vnd.wave": { - compressible: false, - }, - "audio/vorbis": { - source: "iana", - compressible: false, - }, - "audio/vorbis-config": { - source: "iana", - }, - "audio/wav": { - compressible: false, - extensions: ["wav"], - }, - "audio/wave": { - compressible: false, - extensions: ["wav"], - }, - "audio/webm": { - source: "apache", - compressible: false, - extensions: ["weba"], - }, - "audio/x-aac": { - source: "apache", - compressible: false, - extensions: ["aac"], - }, - "audio/x-aiff": { - source: "apache", - extensions: ["aif", "aiff", "aifc"], - }, - "audio/x-caf": { - source: "apache", - compressible: false, - extensions: ["caf"], - }, - "audio/x-flac": { - source: "apache", - extensions: ["flac"], - }, - "audio/x-m4a": { - source: "nginx", - extensions: ["m4a"], - }, - "audio/x-matroska": { - source: "apache", - extensions: ["mka"], - }, - "audio/x-mpegurl": { - source: "apache", - extensions: ["m3u"], - }, - "audio/x-ms-wax": { - source: "apache", - extensions: ["wax"], - }, - "audio/x-ms-wma": { - source: "apache", - extensions: ["wma"], - }, - "audio/x-pn-realaudio": { - source: "apache", - extensions: ["ram", "ra"], - }, - "audio/x-pn-realaudio-plugin": { - source: "apache", - extensions: ["rmp"], - }, - "audio/x-realaudio": { - source: "nginx", - extensions: ["ra"], - }, - "audio/x-tta": { - source: "apache", - }, - "audio/x-wav": { - source: "apache", - extensions: ["wav"], - }, - "audio/xm": { - source: "apache", - extensions: ["xm"], - }, - "chemical/x-cdx": { - source: "apache", - extensions: ["cdx"], - }, - "chemical/x-cif": { - source: "apache", - extensions: ["cif"], - }, - "chemical/x-cmdf": { - source: "apache", - extensions: ["cmdf"], - }, - "chemical/x-cml": { - source: "apache", - extensions: ["cml"], - }, - "chemical/x-csml": { - source: "apache", - extensions: ["csml"], - }, - "chemical/x-pdb": { - source: "apache", - }, - "chemical/x-xyz": { - source: "apache", - extensions: ["xyz"], - }, - "font/collection": { - source: "iana", - extensions: ["ttc"], - }, - "font/otf": { - source: "iana", - compressible: true, - extensions: ["otf"], - }, - "font/sfnt": { - source: "iana", - }, - "font/ttf": { - source: "iana", - compressible: true, - extensions: ["ttf"], - }, - "font/woff": { - source: "iana", - extensions: ["woff"], - }, - "font/woff2": { - source: "iana", - extensions: ["woff2"], - }, - "image/aces": { - source: "iana", - extensions: ["exr"], - }, - "image/apng": { - compressible: false, - extensions: ["apng"], - }, - "image/avci": { - source: "iana", - }, - "image/avcs": { - source: "iana", - }, - "image/bmp": { - source: "iana", - compressible: true, - extensions: ["bmp"], - }, - "image/cgm": { - source: "iana", - extensions: ["cgm"], - }, - "image/dicom-rle": { - source: "iana", - extensions: ["drle"], - }, - "image/emf": { - source: "iana", - extensions: ["emf"], - }, - "image/fits": { - source: "iana", - extensions: ["fits"], - }, - "image/g3fax": { - source: "iana", - extensions: ["g3"], - }, - "image/gif": { - source: "iana", - compressible: false, - extensions: ["gif"], - }, - "image/heic": { - source: "iana", - extensions: ["heic"], - }, - "image/heic-sequence": { - source: "iana", - extensions: ["heics"], - }, - "image/heif": { - source: "iana", - extensions: ["heif"], - }, - "image/heif-sequence": { - source: "iana", - extensions: ["heifs"], - }, - "image/hej2k": { - source: "iana", - extensions: ["hej2"], - }, - "image/hsj2": { - source: "iana", - extensions: ["hsj2"], - }, - "image/ief": { - source: "iana", - extensions: ["ief"], - }, - "image/jls": { - source: "iana", - extensions: ["jls"], - }, - "image/jp2": { - source: "iana", - compressible: false, - extensions: ["jp2", "jpg2"], - }, - "image/jpeg": { - source: "iana", - compressible: false, - extensions: ["jpeg", "jpg", "jpe"], - }, - "image/jph": { - source: "iana", - extensions: ["jph"], - }, - "image/jphc": { - source: "iana", - extensions: ["jhc"], - }, - "image/jpm": { - source: "iana", - compressible: false, - extensions: ["jpm"], - }, - "image/jpx": { - source: "iana", - compressible: false, - extensions: ["jpx", "jpf"], - }, - "image/jxr": { - source: "iana", - extensions: ["jxr"], - }, - "image/jxra": { - source: "iana", - extensions: ["jxra"], - }, - "image/jxrs": { - source: "iana", - extensions: ["jxrs"], - }, - "image/jxs": { - source: "iana", - extensions: ["jxs"], - }, - "image/jxsc": { - source: "iana", - extensions: ["jxsc"], - }, - "image/jxsi": { - source: "iana", - extensions: ["jxsi"], - }, - "image/jxss": { - source: "iana", - extensions: ["jxss"], - }, - "image/ktx": { - source: "iana", - extensions: ["ktx"], - }, - "image/naplps": { - source: "iana", - }, - "image/pjpeg": { - compressible: false, - }, - "image/png": { - source: "iana", - compressible: false, - extensions: ["png"], - }, - "image/prs.btif": { - source: "iana", - extensions: ["btif"], - }, - "image/prs.pti": { - source: "iana", - extensions: ["pti"], - }, - "image/pwg-raster": { - source: "iana", - }, - "image/sgi": { - source: "apache", - extensions: ["sgi"], - }, - "image/svg+xml": { - source: "iana", - compressible: true, - extensions: ["svg", "svgz"], - }, - "image/t38": { - source: "iana", - extensions: ["t38"], - }, - "image/tiff": { - source: "iana", - compressible: false, - extensions: ["tif", "tiff"], - }, - "image/tiff-fx": { - source: "iana", - extensions: ["tfx"], - }, - "image/vnd.adobe.photoshop": { - source: "iana", - compressible: true, - extensions: ["psd"], - }, - "image/vnd.airzip.accelerator.azv": { - source: "iana", - extensions: ["azv"], - }, - "image/vnd.cns.inf2": { - source: "iana", - }, - "image/vnd.dece.graphic": { - source: "iana", - extensions: ["uvi", "uvvi", "uvg", "uvvg"], - }, - "image/vnd.djvu": { - source: "iana", - extensions: ["djvu", "djv"], - }, - "image/vnd.dvb.subtitle": { - source: "iana", - extensions: ["sub"], - }, - "image/vnd.dwg": { - source: "iana", - extensions: ["dwg"], - }, - "image/vnd.dxf": { - source: "iana", - extensions: ["dxf"], - }, - "image/vnd.fastbidsheet": { - source: "iana", - extensions: ["fbs"], - }, - "image/vnd.fpx": { - source: "iana", - extensions: ["fpx"], - }, - "image/vnd.fst": { - source: "iana", - extensions: ["fst"], - }, - "image/vnd.fujixerox.edmics-mmr": { - source: "iana", - extensions: ["mmr"], - }, - "image/vnd.fujixerox.edmics-rlc": { - source: "iana", - extensions: ["rlc"], - }, - "image/vnd.globalgraphics.pgb": { - source: "iana", - }, - "image/vnd.microsoft.icon": { - source: "iana", - extensions: ["ico"], - }, - "image/vnd.mix": { - source: "iana", - }, - "image/vnd.mozilla.apng": { - source: "iana", - }, - "image/vnd.ms-dds": { - extensions: ["dds"], - }, - "image/vnd.ms-modi": { - source: "iana", - extensions: ["mdi"], - }, - "image/vnd.ms-photo": { - source: "apache", - extensions: ["wdp"], - }, - "image/vnd.net-fpx": { - source: "iana", - extensions: ["npx"], - }, - "image/vnd.radiance": { - source: "iana", - }, - "image/vnd.sealed.png": { - source: "iana", - }, - "image/vnd.sealedmedia.softseal.gif": { - source: "iana", - }, - "image/vnd.sealedmedia.softseal.jpg": { - source: "iana", - }, - "image/vnd.svf": { - source: "iana", - }, - "image/vnd.tencent.tap": { - source: "iana", - extensions: ["tap"], - }, - "image/vnd.valve.source.texture": { - source: "iana", - extensions: ["vtf"], - }, - "image/vnd.wap.wbmp": { - source: "iana", - extensions: ["wbmp"], - }, - "image/vnd.xiff": { - source: "iana", - extensions: ["xif"], - }, - "image/vnd.zbrush.pcx": { - source: "iana", - extensions: ["pcx"], - }, - "image/webp": { - source: "apache", - extensions: ["webp"], - }, - "image/wmf": { - source: "iana", - extensions: ["wmf"], - }, - "image/x-3ds": { - source: "apache", - extensions: ["3ds"], - }, - "image/x-cmu-raster": { - source: "apache", - extensions: ["ras"], - }, - "image/x-cmx": { - source: "apache", - extensions: ["cmx"], - }, - "image/x-freehand": { - source: "apache", - extensions: ["fh", "fhc", "fh4", "fh5", "fh7"], - }, - "image/x-icon": { - source: "apache", - compressible: true, - extensions: ["ico"], - }, - "image/x-jng": { - source: "nginx", - extensions: ["jng"], - }, - "image/x-mrsid-image": { - source: "apache", - extensions: ["sid"], - }, - "image/x-ms-bmp": { - source: "nginx", - compressible: true, - extensions: ["bmp"], - }, - "image/x-pcx": { - source: "apache", - extensions: ["pcx"], - }, - "image/x-pict": { - source: "apache", - extensions: ["pic", "pct"], - }, - "image/x-portable-anymap": { - source: "apache", - extensions: ["pnm"], - }, - "image/x-portable-bitmap": { - source: "apache", - extensions: ["pbm"], - }, - "image/x-portable-graymap": { - source: "apache", - extensions: ["pgm"], - }, - "image/x-portable-pixmap": { - source: "apache", - extensions: ["ppm"], - }, - "image/x-rgb": { - source: "apache", - extensions: ["rgb"], - }, - "image/x-tga": { - source: "apache", - extensions: ["tga"], - }, - "image/x-xbitmap": { - source: "apache", - extensions: ["xbm"], - }, - "image/x-xcf": { - compressible: false, - }, - "image/x-xpixmap": { - source: "apache", - extensions: ["xpm"], - }, - "image/x-xwindowdump": { - source: "apache", - extensions: ["xwd"], - }, - "message/cpim": { - source: "iana", - }, - "message/delivery-status": { - source: "iana", - }, - "message/disposition-notification": { - source: "iana", - extensions: ["disposition-notification"], - }, - "message/external-body": { - source: "iana", - }, - "message/feedback-report": { - source: "iana", - }, - "message/global": { - source: "iana", - extensions: ["u8msg"], - }, - "message/global-delivery-status": { - source: "iana", - extensions: ["u8dsn"], - }, - "message/global-disposition-notification": { - source: "iana", - extensions: ["u8mdn"], - }, - "message/global-headers": { - source: "iana", - extensions: ["u8hdr"], - }, - "message/http": { - source: "iana", - compressible: false, - }, - "message/imdn+xml": { - source: "iana", - compressible: true, - }, - "message/news": { - source: "iana", - }, - "message/partial": { - source: "iana", - compressible: false, - }, - "message/rfc822": { - source: "iana", - compressible: true, - extensions: ["eml", "mime"], - }, - "message/s-http": { - source: "iana", - }, - "message/sip": { - source: "iana", - }, - "message/sipfrag": { - source: "iana", - }, - "message/tracking-status": { - source: "iana", - }, - "message/vnd.si.simp": { - source: "iana", - }, - "message/vnd.wfa.wsc": { - source: "iana", - extensions: ["wsc"], - }, - "model/3mf": { - source: "iana", - extensions: ["3mf"], - }, - "model/gltf+json": { - source: "iana", - compressible: true, - extensions: ["gltf"], - }, - "model/gltf-binary": { - source: "iana", - compressible: true, - extensions: ["glb"], - }, - "model/iges": { - source: "iana", - compressible: false, - extensions: ["igs", "iges"], - }, - "model/mesh": { - source: "iana", - compressible: false, - extensions: ["msh", "mesh", "silo"], - }, - "model/mtl": { - source: "iana", - extensions: ["mtl"], - }, - "model/obj": { - source: "iana", - extensions: ["obj"], - }, - "model/stl": { - source: "iana", - extensions: ["stl"], - }, - "model/vnd.collada+xml": { - source: "iana", - compressible: true, - extensions: ["dae"], - }, - "model/vnd.dwf": { - source: "iana", - extensions: ["dwf"], - }, - "model/vnd.flatland.3dml": { - source: "iana", - }, - "model/vnd.gdl": { - source: "iana", - extensions: ["gdl"], - }, - "model/vnd.gs-gdl": { - source: "apache", - }, - "model/vnd.gs.gdl": { - source: "iana", - }, - "model/vnd.gtw": { - source: "iana", - extensions: ["gtw"], - }, - "model/vnd.moml+xml": { - source: "iana", - compressible: true, - }, - "model/vnd.mts": { - source: "iana", - extensions: ["mts"], - }, - "model/vnd.opengex": { - source: "iana", - extensions: ["ogex"], - }, - "model/vnd.parasolid.transmit.binary": { - source: "iana", - extensions: ["x_b"], - }, - "model/vnd.parasolid.transmit.text": { - source: "iana", - extensions: ["x_t"], - }, - "model/vnd.rosette.annotated-data-model": { - source: "iana", - }, - "model/vnd.usdz+zip": { - source: "iana", - compressible: false, - extensions: ["usdz"], - }, - "model/vnd.valve.source.compiled-map": { - source: "iana", - extensions: ["bsp"], - }, - "model/vnd.vtu": { - source: "iana", - extensions: ["vtu"], - }, - "model/vrml": { - source: "iana", - compressible: false, - extensions: ["wrl", "vrml"], - }, - "model/x3d+binary": { - source: "apache", - compressible: false, - extensions: ["x3db", "x3dbz"], - }, - "model/x3d+fastinfoset": { - source: "iana", - extensions: ["x3db"], - }, - "model/x3d+vrml": { - source: "apache", - compressible: false, - extensions: ["x3dv", "x3dvz"], - }, - "model/x3d+xml": { - source: "iana", - compressible: true, - extensions: ["x3d", "x3dz"], - }, - "model/x3d-vrml": { - source: "iana", - extensions: ["x3dv"], - }, - "multipart/alternative": { - source: "iana", - compressible: false, - }, - "multipart/appledouble": { - source: "iana", - }, - "multipart/byteranges": { - source: "iana", - }, - "multipart/digest": { - source: "iana", - }, - "multipart/encrypted": { - source: "iana", - compressible: false, - }, - "multipart/form-data": { - source: "iana", - compressible: false, - }, - "multipart/header-set": { - source: "iana", - }, - "multipart/mixed": { - source: "iana", - }, - "multipart/multilingual": { - source: "iana", - }, - "multipart/parallel": { - source: "iana", - }, - "multipart/related": { - source: "iana", - compressible: false, - }, - "multipart/report": { - source: "iana", - }, - "multipart/signed": { - source: "iana", - compressible: false, - }, - "multipart/vnd.bint.med-plus": { - source: "iana", - }, - "multipart/voice-message": { - source: "iana", - }, - "multipart/x-mixed-replace": { - source: "iana", - }, - "text/1d-interleaved-parityfec": { - source: "iana", - }, - "text/cache-manifest": { - source: "iana", - compressible: true, - extensions: ["appcache", "manifest"], - }, - "text/calendar": { - source: "iana", - extensions: ["ics", "ifb"], - }, - "text/calender": { - compressible: true, - }, - "text/cmd": { - compressible: true, - }, - "text/coffeescript": { - extensions: ["coffee", "litcoffee"], - }, - "text/css": { - source: "iana", - charset: "UTF-8", - compressible: true, - extensions: ["css"], - }, - "text/csv": { - source: "iana", - compressible: true, - extensions: ["csv"], - }, - "text/csv-schema": { - source: "iana", - }, - "text/directory": { - source: "iana", - }, - "text/dns": { - source: "iana", - }, - "text/ecmascript": { - source: "iana", - }, - "text/encaprtp": { - source: "iana", - }, - "text/enriched": { - source: "iana", - }, - "text/flexfec": { - source: "iana", - }, - "text/fwdred": { - source: "iana", - }, - "text/grammar-ref-list": { - source: "iana", - }, - "text/html": { - source: "iana", - compressible: true, - extensions: ["html", "htm", "shtml"], - }, - "text/jade": { - extensions: ["jade"], - }, - "text/javascript": { - source: "iana", - compressible: true, - }, - "text/jcr-cnd": { - source: "iana", - }, - "text/jsx": { - compressible: true, - extensions: ["jsx"], - }, - "text/less": { - compressible: true, - extensions: ["less"], - }, - "text/markdown": { - source: "iana", - compressible: true, - extensions: ["markdown", "md"], - }, - "text/mathml": { - source: "nginx", - extensions: ["mml"], - }, - "text/mdx": { - compressible: true, - extensions: ["mdx"], - }, - "text/mizar": { - source: "iana", - }, - "text/n3": { - source: "iana", - compressible: true, - extensions: ["n3"], - }, - "text/parameters": { - source: "iana", - }, - "text/parityfec": { - source: "iana", - }, - "text/plain": { - source: "iana", - compressible: true, - extensions: ["txt", "text", "conf", "def", "list", "log", "in", "ini"], - }, - "text/provenance-notation": { - source: "iana", - }, - "text/prs.fallenstein.rst": { - source: "iana", - }, - "text/prs.lines.tag": { - source: "iana", - extensions: ["dsc"], - }, - "text/prs.prop.logic": { - source: "iana", - }, - "text/raptorfec": { - source: "iana", - }, - "text/red": { - source: "iana", - }, - "text/rfc822-headers": { - source: "iana", - }, - "text/richtext": { - source: "iana", - compressible: true, - extensions: ["rtx"], - }, - "text/rtf": { - source: "iana", - compressible: true, - extensions: ["rtf"], - }, - "text/rtp-enc-aescm128": { - source: "iana", - }, - "text/rtploopback": { - source: "iana", - }, - "text/rtx": { - source: "iana", - }, - "text/sgml": { - source: "iana", - extensions: ["sgml", "sgm"], - }, - "text/shex": { - extensions: ["shex"], - }, - "text/slim": { - extensions: ["slim", "slm"], - }, - "text/strings": { - source: "iana", - }, - "text/stylus": { - extensions: ["stylus", "styl"], - }, - "text/t140": { - source: "iana", - }, - "text/tab-separated-values": { - source: "iana", - compressible: true, - extensions: ["tsv"], - }, - "text/troff": { - source: "iana", - extensions: ["t", "tr", "roff", "man", "me", "ms"], - }, - "text/turtle": { - source: "iana", - charset: "UTF-8", - extensions: ["ttl"], - }, - "text/ulpfec": { - source: "iana", - }, - "text/uri-list": { - source: "iana", - compressible: true, - extensions: ["uri", "uris", "urls"], - }, - "text/vcard": { - source: "iana", - compressible: true, - extensions: ["vcard"], - }, - "text/vnd.a": { - source: "iana", - }, - "text/vnd.abc": { - source: "iana", - }, - "text/vnd.ascii-art": { - source: "iana", - }, - "text/vnd.curl": { - source: "iana", - extensions: ["curl"], - }, - "text/vnd.curl.dcurl": { - source: "apache", - extensions: ["dcurl"], - }, - "text/vnd.curl.mcurl": { - source: "apache", - extensions: ["mcurl"], - }, - "text/vnd.curl.scurl": { - source: "apache", - extensions: ["scurl"], - }, - "text/vnd.debian.copyright": { - source: "iana", - }, - "text/vnd.dmclientscript": { - source: "iana", - }, - "text/vnd.dvb.subtitle": { - source: "iana", - extensions: ["sub"], - }, - "text/vnd.esmertec.theme-descriptor": { - source: "iana", - }, - "text/vnd.ficlab.flt": { - source: "iana", - }, - "text/vnd.fly": { - source: "iana", - extensions: ["fly"], - }, - "text/vnd.fmi.flexstor": { - source: "iana", - extensions: ["flx"], - }, - "text/vnd.gml": { - source: "iana", - }, - "text/vnd.graphviz": { - source: "iana", - extensions: ["gv"], - }, - "text/vnd.hgl": { - source: "iana", - }, - "text/vnd.in3d.3dml": { - source: "iana", - extensions: ["3dml"], - }, - "text/vnd.in3d.spot": { - source: "iana", - extensions: ["spot"], - }, - "text/vnd.iptc.newsml": { - source: "iana", - }, - "text/vnd.iptc.nitf": { - source: "iana", - }, - "text/vnd.latex-z": { - source: "iana", - }, - "text/vnd.motorola.reflex": { - source: "iana", - }, - "text/vnd.ms-mediapackage": { - source: "iana", - }, - "text/vnd.net2phone.commcenter.command": { - source: "iana", - }, - "text/vnd.radisys.msml-basic-layout": { - source: "iana", - }, - "text/vnd.senx.warpscript": { - source: "iana", - }, - "text/vnd.si.uricatalogue": { - source: "iana", - }, - "text/vnd.sosi": { - source: "iana", - }, - "text/vnd.sun.j2me.app-descriptor": { - source: "iana", - extensions: ["jad"], - }, - "text/vnd.trolltech.linguist": { - source: "iana", - }, - "text/vnd.wap.si": { - source: "iana", - }, - "text/vnd.wap.sl": { - source: "iana", - }, - "text/vnd.wap.wml": { - source: "iana", - extensions: ["wml"], - }, - "text/vnd.wap.wmlscript": { - source: "iana", - extensions: ["wmls"], - }, - "text/vtt": { - source: "iana", - charset: "UTF-8", - compressible: true, - extensions: ["vtt"], - }, - "text/x-asm": { - source: "apache", - extensions: ["s", "asm"], - }, - "text/x-c": { - source: "apache", - extensions: ["c", "cc", "cxx", "cpp", "h", "hh", "dic"], - }, - "text/x-component": { - source: "nginx", - extensions: ["htc"], - }, - "text/x-fortran": { - source: "apache", - extensions: ["f", "for", "f77", "f90"], - }, - "text/x-gwt-rpc": { - compressible: true, - }, - "text/x-handlebars-template": { - extensions: ["hbs"], - }, - "text/x-java-source": { - source: "apache", - extensions: ["java"], - }, - "text/x-jquery-tmpl": { - compressible: true, - }, - "text/x-lua": { - extensions: ["lua"], - }, - "text/x-markdown": { - compressible: true, - extensions: ["mkd"], - }, - "text/x-nfo": { - source: "apache", - extensions: ["nfo"], - }, - "text/x-opml": { - source: "apache", - extensions: ["opml"], - }, - "text/x-org": { - compressible: true, - extensions: ["org"], - }, - "text/x-pascal": { - source: "apache", - extensions: ["p", "pas"], - }, - "text/x-processing": { - compressible: true, - extensions: ["pde"], - }, - "text/x-sass": { - extensions: ["sass"], - }, - "text/x-scss": { - extensions: ["scss"], - }, - "text/x-setext": { - source: "apache", - extensions: ["etx"], - }, - "text/x-sfv": { - source: "apache", - extensions: ["sfv"], - }, - "text/x-suse-ymp": { - compressible: true, - extensions: ["ymp"], - }, - "text/x-uuencode": { - source: "apache", - extensions: ["uu"], - }, - "text/x-vcalendar": { - source: "apache", - extensions: ["vcs"], - }, - "text/x-vcard": { - source: "apache", - extensions: ["vcf"], - }, - "text/xml": { - source: "iana", - compressible: true, - extensions: ["xml"], - }, - "text/xml-external-parsed-entity": { - source: "iana", - }, - "text/yaml": { - extensions: ["yaml", "yml"], - }, - "video/1d-interleaved-parityfec": { - source: "iana", - }, - "video/3gpp": { - source: "iana", - extensions: ["3gp", "3gpp"], - }, - "video/3gpp-tt": { - source: "iana", - }, - "video/3gpp2": { - source: "iana", - extensions: ["3g2"], - }, - "video/bmpeg": { - source: "iana", - }, - "video/bt656": { - source: "iana", - }, - "video/celb": { - source: "iana", - }, - "video/dv": { - source: "iana", - }, - "video/encaprtp": { - source: "iana", - }, - "video/flexfec": { - source: "iana", - }, - "video/h261": { - source: "iana", - extensions: ["h261"], - }, - "video/h263": { - source: "iana", - extensions: ["h263"], - }, - "video/h263-1998": { - source: "iana", - }, - "video/h263-2000": { - source: "iana", - }, - "video/h264": { - source: "iana", - extensions: ["h264"], - }, - "video/h264-rcdo": { - source: "iana", - }, - "video/h264-svc": { - source: "iana", - }, - "video/h265": { - source: "iana", - }, - "video/iso.segment": { - source: "iana", - }, - "video/jpeg": { - source: "iana", - extensions: ["jpgv"], - }, - "video/jpeg2000": { - source: "iana", - }, - "video/jpm": { - source: "apache", - extensions: ["jpm", "jpgm"], - }, - "video/mj2": { - source: "iana", - extensions: ["mj2", "mjp2"], - }, - "video/mp1s": { - source: "iana", - }, - "video/mp2p": { - source: "iana", - }, - "video/mp2t": { - source: "iana", - extensions: ["ts"], - }, - "video/mp4": { - source: "iana", - compressible: false, - extensions: ["mp4", "mp4v", "mpg4"], - }, - "video/mp4v-es": { - source: "iana", - }, - "video/mpeg": { - source: "iana", - compressible: false, - extensions: ["mpeg", "mpg", "mpe", "m1v", "m2v"], - }, - "video/mpeg4-generic": { - source: "iana", - }, - "video/mpv": { - source: "iana", - }, - "video/nv": { - source: "iana", - }, - "video/ogg": { - source: "iana", - compressible: false, - extensions: ["ogv"], - }, - "video/parityfec": { - source: "iana", - }, - "video/pointer": { - source: "iana", - }, - "video/quicktime": { - source: "iana", - compressible: false, - extensions: ["qt", "mov"], - }, - "video/raptorfec": { - source: "iana", - }, - "video/raw": { - source: "iana", - }, - "video/rtp-enc-aescm128": { - source: "iana", - }, - "video/rtploopback": { - source: "iana", - }, - "video/rtx": { - source: "iana", - }, - "video/smpte291": { - source: "iana", - }, - "video/smpte292m": { - source: "iana", - }, - "video/ulpfec": { - source: "iana", - }, - "video/vc1": { - source: "iana", - }, - "video/vc2": { - source: "iana", - }, - "video/vnd.cctv": { - source: "iana", - }, - "video/vnd.dece.hd": { - source: "iana", - extensions: ["uvh", "uvvh"], - }, - "video/vnd.dece.mobile": { - source: "iana", - extensions: ["uvm", "uvvm"], - }, - "video/vnd.dece.mp4": { - source: "iana", - }, - "video/vnd.dece.pd": { - source: "iana", - extensions: ["uvp", "uvvp"], - }, - "video/vnd.dece.sd": { - source: "iana", - extensions: ["uvs", "uvvs"], - }, - "video/vnd.dece.video": { - source: "iana", - extensions: ["uvv", "uvvv"], - }, - "video/vnd.directv.mpeg": { - source: "iana", - }, - "video/vnd.directv.mpeg-tts": { - source: "iana", - }, - "video/vnd.dlna.mpeg-tts": { - source: "iana", - }, - "video/vnd.dvb.file": { - source: "iana", - extensions: ["dvb"], - }, - "video/vnd.fvt": { - source: "iana", - extensions: ["fvt"], - }, - "video/vnd.hns.video": { - source: "iana", - }, - "video/vnd.iptvforum.1dparityfec-1010": { - source: "iana", - }, - "video/vnd.iptvforum.1dparityfec-2005": { - source: "iana", - }, - "video/vnd.iptvforum.2dparityfec-1010": { - source: "iana", - }, - "video/vnd.iptvforum.2dparityfec-2005": { - source: "iana", - }, - "video/vnd.iptvforum.ttsavc": { - source: "iana", - }, - "video/vnd.iptvforum.ttsmpeg2": { - source: "iana", - }, - "video/vnd.motorola.video": { - source: "iana", - }, - "video/vnd.motorola.videop": { - source: "iana", - }, - "video/vnd.mpegurl": { - source: "iana", - extensions: ["mxu", "m4u"], - }, - "video/vnd.ms-playready.media.pyv": { - source: "iana", - extensions: ["pyv"], - }, - "video/vnd.nokia.interleaved-multimedia": { - source: "iana", - }, - "video/vnd.nokia.mp4vr": { - source: "iana", - }, - "video/vnd.nokia.videovoip": { - source: "iana", - }, - "video/vnd.objectvideo": { - source: "iana", - }, - "video/vnd.radgamettools.bink": { - source: "iana", - }, - "video/vnd.radgamettools.smacker": { - source: "iana", - }, - "video/vnd.sealed.mpeg1": { - source: "iana", - }, - "video/vnd.sealed.mpeg4": { - source: "iana", - }, - "video/vnd.sealed.swf": { - source: "iana", - }, - "video/vnd.sealedmedia.softseal.mov": { - source: "iana", - }, - "video/vnd.uvvu.mp4": { - source: "iana", - extensions: ["uvu", "uvvu"], - }, - "video/vnd.vivo": { - source: "iana", - extensions: ["viv"], - }, - "video/vnd.youtube.yt": { - source: "iana", - }, - "video/vp8": { - source: "iana", - }, - "video/webm": { - source: "apache", - compressible: false, - extensions: ["webm"], - }, - "video/x-f4v": { - source: "apache", - extensions: ["f4v"], - }, - "video/x-fli": { - source: "apache", - extensions: ["fli"], - }, - "video/x-flv": { - source: "apache", - compressible: false, - extensions: ["flv"], - }, - "video/x-m4v": { - source: "apache", - extensions: ["m4v"], - }, - "video/x-matroska": { - source: "apache", - compressible: false, - extensions: ["mkv", "mk3d", "mks"], - }, - "video/x-mng": { - source: "apache", - extensions: ["mng"], - }, - "video/x-ms-asf": { - source: "apache", - extensions: ["asf", "asx"], - }, - "video/x-ms-vob": { - source: "apache", - extensions: ["vob"], - }, - "video/x-ms-wm": { - source: "apache", - extensions: ["wm"], - }, - "video/x-ms-wmv": { - source: "apache", - compressible: false, - extensions: ["wmv"], - }, - "video/x-ms-wmx": { - source: "apache", - extensions: ["wmx"], - }, - "video/x-ms-wvx": { - source: "apache", - extensions: ["wvx"], - }, - "video/x-msvideo": { - source: "apache", - extensions: ["avi"], - }, - "video/x-sgi-movie": { - source: "apache", - extensions: ["movie"], - }, - "video/x-smv": { - source: "apache", - extensions: ["smv"], - }, - "x-conference/x-cooltalk": { - source: "apache", - extensions: ["ice"], - }, - "x-shader/x-fragment": { - compressible: true, - }, - "x-shader/x-vertex": { - compressible: true, - }, -}; diff --git a/src/dictionaries/mime_types.ts b/src/dictionaries/mime_types.ts deleted file mode 100644 index 206b3c47e..000000000 --- a/src/dictionaries/mime_types.ts +++ /dev/null @@ -1,1169 +0,0 @@ -// This file was generated at 2021-03-12T20:11:08.055Z -export const mimeTypes = new Map([ - ["ez", "application/andrew-inset"], - ["aw", "application/applixware"], - ["atom", "application/atom+xml"], - ["atomcat", "application/atomcat+xml"], - ["atomdeleted", "application/atomdeleted+xml"], - ["atomsvc", "application/atomsvc+xml"], - ["dwd", "application/atsc-dwd+xml"], - ["held", "application/atsc-held+xml"], - ["rsat", "application/atsc-rsat+xml"], - ["xcs", "application/calendar+xml"], - ["ccxml", "application/ccxml+xml"], - ["cdfx", "application/cdfx+xml"], - ["cdmia", "application/cdmi-capability"], - ["cdmic", "application/cdmi-container"], - ["cdmid", "application/cdmi-domain"], - ["cdmio", "application/cdmi-object"], - ["cdmiq", "application/cdmi-queue"], - ["cu", "application/cu-seeme"], - ["mpd", "application/dash+xml"], - ["davmount", "application/davmount+xml"], - ["dbk", "application/docbook+xml"], - ["dssc", "application/dssc+der"], - ["xdssc", "application/dssc+xml"], - ["es", "application/ecmascript"], - ["ecma", "application/ecmascript"], - ["emma", "application/emma+xml"], - ["emotionml", "application/emotionml+xml"], - ["epub", "application/epub+zip"], - ["exi", "application/exi"], - ["fdt", "application/fdt+xml"], - ["pfr", "application/font-tdpfr"], - ["geojson", "application/geo+json"], - ["gml", "application/gml+xml"], - ["gpx", "application/gpx+xml"], - ["gxf", "application/gxf"], - ["gz", "application/gzip"], - ["stk", "application/hyperstudio"], - ["ink", "application/inkml+xml"], - ["inkml", "application/inkml+xml"], - ["ipfix", "application/ipfix"], - ["its", "application/its+xml"], - ["jar", "application/java-archive"], - ["war", "application/java-archive"], - ["ear", "application/java-archive"], - ["ser", "application/java-serialized-object"], - ["class", "application/java-vm"], - ["js", "application/javascript"], - ["mjs", "application/javascript"], - ["json", "application/json"], - ["map", "application/json"], - ["jsonml", "application/jsonml+json"], - ["jsonld", "application/ld+json"], - ["lgr", "application/lgr+xml"], - ["lostxml", "application/lost+xml"], - ["hqx", "application/mac-binhex40"], - ["cpt", "application/mac-compactpro"], - ["mads", "application/mads+xml"], - ["mrc", "application/marc"], - ["mrcx", "application/marcxml+xml"], - ["ma", "application/mathematica"], - ["nb", "application/mathematica"], - ["mb", "application/mathematica"], - ["mathml", "application/mathml+xml"], - ["mbox", "application/mbox"], - ["mscml", "application/mediaservercontrol+xml"], - ["metalink", "application/metalink+xml"], - ["meta4", "application/metalink4+xml"], - ["mets", "application/mets+xml"], - ["maei", "application/mmt-aei+xml"], - ["musd", "application/mmt-usd+xml"], - ["mods", "application/mods+xml"], - ["m21", "application/mp21"], - ["mp21", "application/mp21"], - ["mp4s", "application/mp4"], - ["m4p", "application/mp4"], - ["doc", "application/msword"], - ["dot", "application/msword"], - ["mxf", "application/mxf"], - ["nq", "application/n-quads"], - ["nt", "application/n-triples"], - ["cjs", "application/node"], - ["bin", "application/octet-stream"], - ["dms", "application/octet-stream"], - ["lrf", "application/octet-stream"], - ["mar", "application/octet-stream"], - ["so", "application/octet-stream"], - ["dist", "application/octet-stream"], - ["distz", "application/octet-stream"], - ["pkg", "application/octet-stream"], - ["bpk", "application/octet-stream"], - ["dump", "application/octet-stream"], - ["elc", "application/octet-stream"], - ["deploy", "application/octet-stream"], - ["exe", "application/octet-stream"], - ["dll", "application/octet-stream"], - ["deb", "application/octet-stream"], - ["dmg", "application/octet-stream"], - ["iso", "application/octet-stream"], - ["img", "application/octet-stream"], - ["msi", "application/octet-stream"], - ["msp", "application/octet-stream"], - ["msm", "application/octet-stream"], - ["buffer", "application/octet-stream"], - ["oda", "application/oda"], - ["opf", "application/oebps-package+xml"], - ["ogx", "application/ogg"], - ["omdoc", "application/omdoc+xml"], - ["onetoc", "application/onenote"], - ["onetoc2", "application/onenote"], - ["onetmp", "application/onenote"], - ["onepkg", "application/onenote"], - ["oxps", "application/oxps"], - ["relo", "application/p2p-overlay+xml"], - ["xer", "application/patch-ops-error+xml"], - ["pdf", "application/pdf"], - ["pgp", "application/pgp-encrypted"], - ["asc", "application/pgp-signature"], - ["sig", "application/pgp-signature"], - ["prf", "application/pics-rules"], - ["p10", "application/pkcs10"], - ["p7m", "application/pkcs7-mime"], - ["p7c", "application/pkcs7-mime"], - ["p7s", "application/pkcs7-signature"], - ["p8", "application/pkcs8"], - ["ac", "application/pkix-attr-cert"], - ["cer", "application/pkix-cert"], - ["crl", "application/pkix-crl"], - ["pkipath", "application/pkix-pkipath"], - ["pki", "application/pkixcmp"], - ["pls", "application/pls+xml"], - ["ai", "application/postscript"], - ["eps", "application/postscript"], - ["ps", "application/postscript"], - ["provx", "application/provenance+xml"], - ["cww", "application/prs.cww"], - ["pskcxml", "application/pskc+xml"], - ["rdf", "application/rdf+xml"], - ["owl", "application/rdf+xml"], - ["rif", "application/reginfo+xml"], - ["rnc", "application/relax-ng-compact-syntax"], - ["rl", "application/resource-lists+xml"], - ["rld", "application/resource-lists-diff+xml"], - ["rs", "application/rls-services+xml"], - ["rapd", "application/route-apd+xml"], - ["sls", "application/route-s-tsid+xml"], - ["rusd", "application/route-usd+xml"], - ["gbr", "application/rpki-ghostbusters"], - ["mft", "application/rpki-manifest"], - ["roa", "application/rpki-roa"], - ["rsd", "application/rsd+xml"], - ["rss", "application/rss+xml"], - ["rtf", "application/rtf"], - ["sbml", "application/sbml+xml"], - ["scq", "application/scvp-cv-request"], - ["scs", "application/scvp-cv-response"], - ["spq", "application/scvp-vp-request"], - ["spp", "application/scvp-vp-response"], - ["sdp", "application/sdp"], - ["senmlx", "application/senml+xml"], - ["sensmlx", "application/sensml+xml"], - ["setpay", "application/set-payment-initiation"], - ["setreg", "application/set-registration-initiation"], - ["shf", "application/shf+xml"], - ["siv", "application/sieve"], - ["sieve", "application/sieve"], - ["smi", "application/smil+xml"], - ["smil", "application/smil+xml"], - ["rq", "application/sparql-query"], - ["srx", "application/sparql-results+xml"], - ["gram", "application/srgs"], - ["grxml", "application/srgs+xml"], - ["sru", "application/sru+xml"], - ["ssdl", "application/ssdl+xml"], - ["ssml", "application/ssml+xml"], - ["swidtag", "application/swid+xml"], - ["tei", "application/tei+xml"], - ["teicorpus", "application/tei+xml"], - ["tfi", "application/thraud+xml"], - ["tsd", "application/timestamped-data"], - ["ttml", "application/ttml+xml"], - ["rsheet", "application/urc-ressheet+xml"], - ["td", "application/urc-targetdesc+xml"], - ["1km", "application/vnd.1000minds.decision-model+xml"], - ["plb", "application/vnd.3gpp.pic-bw-large"], - ["psb", "application/vnd.3gpp.pic-bw-small"], - ["pvb", "application/vnd.3gpp.pic-bw-var"], - ["tcap", "application/vnd.3gpp2.tcap"], - ["pwn", "application/vnd.3m.post-it-notes"], - ["aso", "application/vnd.accpac.simply.aso"], - ["imp", "application/vnd.accpac.simply.imp"], - ["acu", "application/vnd.acucobol"], - ["atc", "application/vnd.acucorp"], - ["acutc", "application/vnd.acucorp"], - ["air", "application/vnd.adobe.air-application-installer-package+zip"], - ["fcdt", "application/vnd.adobe.formscentral.fcdt"], - ["fxp", "application/vnd.adobe.fxp"], - ["fxpl", "application/vnd.adobe.fxp"], - ["xdp", "application/vnd.adobe.xdp+xml"], - ["xfdf", "application/vnd.adobe.xfdf"], - ["ahead", "application/vnd.ahead.space"], - ["azf", "application/vnd.airzip.filesecure.azf"], - ["azs", "application/vnd.airzip.filesecure.azs"], - ["azw", "application/vnd.amazon.ebook"], - ["acc", "application/vnd.americandynamics.acc"], - ["ami", "application/vnd.amiga.ami"], - ["apk", "application/vnd.android.package-archive"], - ["cii", "application/vnd.anser-web-certificate-issue-initiation"], - ["fti", "application/vnd.anser-web-funds-transfer-initiation"], - ["atx", "application/vnd.antix.game-component"], - ["mpkg", "application/vnd.apple.installer+xml"], - ["key", "application/vnd.apple.keynote"], - ["m3u8", "application/vnd.apple.mpegurl"], - ["numbers", "application/vnd.apple.numbers"], - ["pages", "application/vnd.apple.pages"], - ["swi", "application/vnd.aristanetworks.swi"], - ["iota", "application/vnd.astraea-software.iota"], - ["aep", "application/vnd.audiograph"], - ["bmml", "application/vnd.balsamiq.bmml+xml"], - ["mpm", "application/vnd.blueice.multipass"], - ["bmi", "application/vnd.bmi"], - ["rep", "application/vnd.businessobjects"], - ["cdxml", "application/vnd.chemdraw+xml"], - ["mmd", "application/vnd.chipnuts.karaoke-mmd"], - ["cdy", "application/vnd.cinderella"], - ["csl", "application/vnd.citationstyles.style+xml"], - ["cla", "application/vnd.claymore"], - ["rp9", "application/vnd.cloanto.rp9"], - ["c4g", "application/vnd.clonk.c4group"], - ["c4d", "application/vnd.clonk.c4group"], - ["c4f", "application/vnd.clonk.c4group"], - ["c4p", "application/vnd.clonk.c4group"], - ["c4u", "application/vnd.clonk.c4group"], - ["c11amc", "application/vnd.cluetrust.cartomobile-config"], - ["c11amz", "application/vnd.cluetrust.cartomobile-config-pkg"], - ["csp", "application/vnd.commonspace"], - ["cdbcmsg", "application/vnd.contact.cmsg"], - ["cmc", "application/vnd.cosmocaller"], - ["clkx", "application/vnd.crick.clicker"], - ["clkk", "application/vnd.crick.clicker.keyboard"], - ["clkp", "application/vnd.crick.clicker.palette"], - ["clkt", "application/vnd.crick.clicker.template"], - ["clkw", "application/vnd.crick.clicker.wordbank"], - ["wbs", "application/vnd.criticaltools.wbs+xml"], - ["pml", "application/vnd.ctc-posml"], - ["ppd", "application/vnd.cups-ppd"], - ["car", "application/vnd.curl.car"], - ["pcurl", "application/vnd.curl.pcurl"], - ["dart", "application/vnd.dart"], - ["rdz", "application/vnd.data-vision.rdz"], - ["dbf", "application/vnd.dbf"], - ["uvf", "application/vnd.dece.data"], - ["uvvf", "application/vnd.dece.data"], - ["uvd", "application/vnd.dece.data"], - ["uvvd", "application/vnd.dece.data"], - ["uvt", "application/vnd.dece.ttml+xml"], - ["uvvt", "application/vnd.dece.ttml+xml"], - ["uvx", "application/vnd.dece.unspecified"], - ["uvvx", "application/vnd.dece.unspecified"], - ["uvz", "application/vnd.dece.zip"], - ["uvvz", "application/vnd.dece.zip"], - ["fe_launch", "application/vnd.denovo.fcselayout-link"], - ["dna", "application/vnd.dna"], - ["mlp", "application/vnd.dolby.mlp"], - ["dpg", "application/vnd.dpgraph"], - ["dfac", "application/vnd.dreamfactory"], - ["kpxx", "application/vnd.ds-keypoint"], - ["ait", "application/vnd.dvb.ait"], - ["svc", "application/vnd.dvb.service"], - ["geo", "application/vnd.dynageo"], - ["mag", "application/vnd.ecowin.chart"], - ["nml", "application/vnd.enliven"], - ["esf", "application/vnd.epson.esf"], - ["msf", "application/vnd.epson.msf"], - ["qam", "application/vnd.epson.quickanime"], - ["slt", "application/vnd.epson.salt"], - ["ssf", "application/vnd.epson.ssf"], - ["es3", "application/vnd.eszigno3+xml"], - ["et3", "application/vnd.eszigno3+xml"], - ["ez2", "application/vnd.ezpix-album"], - ["ez3", "application/vnd.ezpix-package"], - ["fdf", "application/vnd.fdf"], - ["mseed", "application/vnd.fdsn.mseed"], - ["seed", "application/vnd.fdsn.seed"], - ["dataless", "application/vnd.fdsn.seed"], - ["gph", "application/vnd.flographit"], - ["ftc", "application/vnd.fluxtime.clip"], - ["fm", "application/vnd.framemaker"], - ["frame", "application/vnd.framemaker"], - ["maker", "application/vnd.framemaker"], - ["book", "application/vnd.framemaker"], - ["fnc", "application/vnd.frogans.fnc"], - ["ltf", "application/vnd.frogans.ltf"], - ["fsc", "application/vnd.fsc.weblaunch"], - ["oas", "application/vnd.fujitsu.oasys"], - ["oa2", "application/vnd.fujitsu.oasys2"], - ["oa3", "application/vnd.fujitsu.oasys3"], - ["fg5", "application/vnd.fujitsu.oasysgp"], - ["bh2", "application/vnd.fujitsu.oasysprs"], - ["ddd", "application/vnd.fujixerox.ddd"], - ["xdw", "application/vnd.fujixerox.docuworks"], - ["xbd", "application/vnd.fujixerox.docuworks.binder"], - ["fzs", "application/vnd.fuzzysheet"], - ["txd", "application/vnd.genomatix.tuxedo"], - ["ggb", "application/vnd.geogebra.file"], - ["ggt", "application/vnd.geogebra.tool"], - ["gex", "application/vnd.geometry-explorer"], - ["gre", "application/vnd.geometry-explorer"], - ["gxt", "application/vnd.geonext"], - ["g2w", "application/vnd.geoplan"], - ["g3w", "application/vnd.geospace"], - ["gmx", "application/vnd.gmx"], - ["kml", "application/vnd.google-earth.kml+xml"], - ["kmz", "application/vnd.google-earth.kmz"], - ["gqf", "application/vnd.grafeq"], - ["gqs", "application/vnd.grafeq"], - ["gac", "application/vnd.groove-account"], - ["ghf", "application/vnd.groove-help"], - ["gim", "application/vnd.groove-identity-message"], - ["grv", "application/vnd.groove-injector"], - ["gtm", "application/vnd.groove-tool-message"], - ["tpl", "application/vnd.groove-tool-template"], - ["vcg", "application/vnd.groove-vcard"], - ["hal", "application/vnd.hal+xml"], - ["zmm", "application/vnd.handheld-entertainment+xml"], - ["hbci", "application/vnd.hbci"], - ["les", "application/vnd.hhe.lesson-player"], - ["hpgl", "application/vnd.hp-hpgl"], - ["hpid", "application/vnd.hp-hpid"], - ["hps", "application/vnd.hp-hps"], - ["jlt", "application/vnd.hp-jlyt"], - ["pcl", "application/vnd.hp-pcl"], - ["pclxl", "application/vnd.hp-pclxl"], - ["sfd-hdstx", "application/vnd.hydrostatix.sof-data"], - ["mpy", "application/vnd.ibm.minipay"], - ["afp", "application/vnd.ibm.modcap"], - ["listafp", "application/vnd.ibm.modcap"], - ["list3820", "application/vnd.ibm.modcap"], - ["irm", "application/vnd.ibm.rights-management"], - ["sc", "application/vnd.ibm.secure-container"], - ["icc", "application/vnd.iccprofile"], - ["icm", "application/vnd.iccprofile"], - ["igl", "application/vnd.igloader"], - ["ivp", "application/vnd.immervision-ivp"], - ["ivu", "application/vnd.immervision-ivu"], - ["igm", "application/vnd.insors.igm"], - ["xpw", "application/vnd.intercon.formnet"], - ["xpx", "application/vnd.intercon.formnet"], - ["i2g", "application/vnd.intergeo"], - ["qbo", "application/vnd.intu.qbo"], - ["qfx", "application/vnd.intu.qfx"], - ["rcprofile", "application/vnd.ipunplugged.rcprofile"], - ["irp", "application/vnd.irepository.package+xml"], - ["xpr", "application/vnd.is-xpr"], - ["fcs", "application/vnd.isac.fcs"], - ["jam", "application/vnd.jam"], - ["rms", "application/vnd.jcp.javame.midlet-rms"], - ["jisp", "application/vnd.jisp"], - ["joda", "application/vnd.joost.joda-archive"], - ["ktz", "application/vnd.kahootz"], - ["ktr", "application/vnd.kahootz"], - ["karbon", "application/vnd.kde.karbon"], - ["chrt", "application/vnd.kde.kchart"], - ["kfo", "application/vnd.kde.kformula"], - ["flw", "application/vnd.kde.kivio"], - ["kon", "application/vnd.kde.kontour"], - ["kpr", "application/vnd.kde.kpresenter"], - ["kpt", "application/vnd.kde.kpresenter"], - ["ksp", "application/vnd.kde.kspread"], - ["kwd", "application/vnd.kde.kword"], - ["kwt", "application/vnd.kde.kword"], - ["htke", "application/vnd.kenameaapp"], - ["kia", "application/vnd.kidspiration"], - ["kne", "application/vnd.kinar"], - ["knp", "application/vnd.kinar"], - ["skp", "application/vnd.koan"], - ["skd", "application/vnd.koan"], - ["skt", "application/vnd.koan"], - ["skm", "application/vnd.koan"], - ["sse", "application/vnd.kodak-descriptor"], - ["lasxml", "application/vnd.las.las+xml"], - ["lbd", "application/vnd.llamagraphics.life-balance.desktop"], - ["lbe", "application/vnd.llamagraphics.life-balance.exchange+xml"], - ["123", "application/vnd.lotus-1-2-3"], - ["apr", "application/vnd.lotus-approach"], - ["pre", "application/vnd.lotus-freelance"], - ["nsf", "application/vnd.lotus-notes"], - ["org", "application/vnd.lotus-organizer"], - ["scm", "application/vnd.lotus-screencam"], - ["lwp", "application/vnd.lotus-wordpro"], - ["portpkg", "application/vnd.macports.portpkg"], - ["mcd", "application/vnd.mcd"], - ["mc1", "application/vnd.medcalcdata"], - ["cdkey", "application/vnd.mediastation.cdkey"], - ["mwf", "application/vnd.mfer"], - ["mfm", "application/vnd.mfmp"], - ["flo", "application/vnd.micrografx.flo"], - ["igx", "application/vnd.micrografx.igx"], - ["mif", "application/vnd.mif"], - ["daf", "application/vnd.mobius.daf"], - ["dis", "application/vnd.mobius.dis"], - ["mbk", "application/vnd.mobius.mbk"], - ["mqy", "application/vnd.mobius.mqy"], - ["msl", "application/vnd.mobius.msl"], - ["plc", "application/vnd.mobius.plc"], - ["txf", "application/vnd.mobius.txf"], - ["mpn", "application/vnd.mophun.application"], - ["mpc", "application/vnd.mophun.certificate"], - ["xul", "application/vnd.mozilla.xul+xml"], - ["cil", "application/vnd.ms-artgalry"], - ["cab", "application/vnd.ms-cab-compressed"], - ["xls", "application/vnd.ms-excel"], - ["xlm", "application/vnd.ms-excel"], - ["xla", "application/vnd.ms-excel"], - ["xlc", "application/vnd.ms-excel"], - ["xlt", "application/vnd.ms-excel"], - ["xlw", "application/vnd.ms-excel"], - ["xlam", "application/vnd.ms-excel.addin.macroenabled.12"], - ["xlsb", "application/vnd.ms-excel.sheet.binary.macroenabled.12"], - ["xlsm", "application/vnd.ms-excel.sheet.macroenabled.12"], - ["xltm", "application/vnd.ms-excel.template.macroenabled.12"], - ["eot", "application/vnd.ms-fontobject"], - ["chm", "application/vnd.ms-htmlhelp"], - ["ims", "application/vnd.ms-ims"], - ["lrm", "application/vnd.ms-lrm"], - ["thmx", "application/vnd.ms-officetheme"], - ["cat", "application/vnd.ms-pki.seccat"], - ["stl", "application/vnd.ms-pki.stl"], - ["ppt", "application/vnd.ms-powerpoint"], - ["pps", "application/vnd.ms-powerpoint"], - ["pot", "application/vnd.ms-powerpoint"], - ["ppam", "application/vnd.ms-powerpoint.addin.macroenabled.12"], - ["pptm", "application/vnd.ms-powerpoint.presentation.macroenabled.12"], - ["sldm", "application/vnd.ms-powerpoint.slide.macroenabled.12"], - ["ppsm", "application/vnd.ms-powerpoint.slideshow.macroenabled.12"], - ["potm", "application/vnd.ms-powerpoint.template.macroenabled.12"], - ["mpp", "application/vnd.ms-project"], - ["mpt", "application/vnd.ms-project"], - ["docm", "application/vnd.ms-word.document.macroenabled.12"], - ["dotm", "application/vnd.ms-word.template.macroenabled.12"], - ["wps", "application/vnd.ms-works"], - ["wks", "application/vnd.ms-works"], - ["wcm", "application/vnd.ms-works"], - ["wdb", "application/vnd.ms-works"], - ["wpl", "application/vnd.ms-wpl"], - ["xps", "application/vnd.ms-xpsdocument"], - ["mseq", "application/vnd.mseq"], - ["mus", "application/vnd.musician"], - ["msty", "application/vnd.muvee.style"], - ["taglet", "application/vnd.mynfc"], - ["nlu", "application/vnd.neurolanguage.nlu"], - ["ntf", "application/vnd.nitf"], - ["nitf", "application/vnd.nitf"], - ["nnd", "application/vnd.noblenet-directory"], - ["nns", "application/vnd.noblenet-sealer"], - ["nnw", "application/vnd.noblenet-web"], - ["ac", "application/vnd.nokia.n-gage.ac+xml"], - ["ngdat", "application/vnd.nokia.n-gage.data"], - ["n-gage", "application/vnd.nokia.n-gage.symbian.install"], - ["rpst", "application/vnd.nokia.radio-preset"], - ["rpss", "application/vnd.nokia.radio-presets"], - ["edm", "application/vnd.novadigm.edm"], - ["edx", "application/vnd.novadigm.edx"], - ["ext", "application/vnd.novadigm.ext"], - ["odc", "application/vnd.oasis.opendocument.chart"], - ["otc", "application/vnd.oasis.opendocument.chart-template"], - ["odb", "application/vnd.oasis.opendocument.database"], - ["odf", "application/vnd.oasis.opendocument.formula"], - ["odft", "application/vnd.oasis.opendocument.formula-template"], - ["odg", "application/vnd.oasis.opendocument.graphics"], - ["otg", "application/vnd.oasis.opendocument.graphics-template"], - ["odi", "application/vnd.oasis.opendocument.image"], - ["oti", "application/vnd.oasis.opendocument.image-template"], - ["odp", "application/vnd.oasis.opendocument.presentation"], - ["otp", "application/vnd.oasis.opendocument.presentation-template"], - ["ods", "application/vnd.oasis.opendocument.spreadsheet"], - ["ots", "application/vnd.oasis.opendocument.spreadsheet-template"], - ["odt", "application/vnd.oasis.opendocument.text"], - ["odm", "application/vnd.oasis.opendocument.text-master"], - ["ott", "application/vnd.oasis.opendocument.text-template"], - ["oth", "application/vnd.oasis.opendocument.text-web"], - ["xo", "application/vnd.olpc-sugar"], - ["dd2", "application/vnd.oma.dd2+xml"], - ["obgx", "application/vnd.openblox.game+xml"], - ["oxt", "application/vnd.openofficeorg.extension"], - ["osm", "application/vnd.openstreetmap.data+xml"], - [ - "pptx", - "application/vnd.openxmlformats-officedocument.presentationml.presentation", - ], - [ - "sldx", - "application/vnd.openxmlformats-officedocument.presentationml.slide", - ], - [ - "ppsx", - "application/vnd.openxmlformats-officedocument.presentationml.slideshow", - ], - [ - "potx", - "application/vnd.openxmlformats-officedocument.presentationml.template", - ], - ["xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"], - [ - "xltx", - "application/vnd.openxmlformats-officedocument.spreadsheetml.template", - ], - [ - "docx", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - ], - [ - "dotx", - "application/vnd.openxmlformats-officedocument.wordprocessingml.template", - ], - ["mgp", "application/vnd.osgeo.mapguide.package"], - ["dp", "application/vnd.osgi.dp"], - ["esa", "application/vnd.osgi.subsystem"], - ["pdb", "application/vnd.palm"], - ["pqa", "application/vnd.palm"], - ["oprc", "application/vnd.palm"], - ["paw", "application/vnd.pawaafile"], - ["str", "application/vnd.pg.format"], - ["ei6", "application/vnd.pg.osasli"], - ["efif", "application/vnd.picsel"], - ["wg", "application/vnd.pmi.widget"], - ["plf", "application/vnd.pocketlearn"], - ["pbd", "application/vnd.powerbuilder6"], - ["box", "application/vnd.previewsystems.box"], - ["mgz", "application/vnd.proteus.magazine"], - ["qps", "application/vnd.publishare-delta-tree"], - ["ptid", "application/vnd.pvi.ptid1"], - ["qxd", "application/vnd.quark.quarkxpress"], - ["qxt", "application/vnd.quark.quarkxpress"], - ["qwd", "application/vnd.quark.quarkxpress"], - ["qwt", "application/vnd.quark.quarkxpress"], - ["qxl", "application/vnd.quark.quarkxpress"], - ["qxb", "application/vnd.quark.quarkxpress"], - ["rar", "application/vnd.rar"], - ["bed", "application/vnd.realvnc.bed"], - ["mxl", "application/vnd.recordare.musicxml"], - ["musicxml", "application/vnd.recordare.musicxml+xml"], - ["cryptonote", "application/vnd.rig.cryptonote"], - ["cod", "application/vnd.rim.cod"], - ["rm", "application/vnd.rn-realmedia"], - ["rmvb", "application/vnd.rn-realmedia-vbr"], - ["link66", "application/vnd.route66.link66+xml"], - ["st", "application/vnd.sailingtracker.track"], - ["see", "application/vnd.seemail"], - ["sema", "application/vnd.sema"], - ["semd", "application/vnd.semd"], - ["semf", "application/vnd.semf"], - ["ifm", "application/vnd.shana.informed.formdata"], - ["itp", "application/vnd.shana.informed.formtemplate"], - ["iif", "application/vnd.shana.informed.interchange"], - ["ipk", "application/vnd.shana.informed.package"], - ["twd", "application/vnd.simtech-mindmapper"], - ["twds", "application/vnd.simtech-mindmapper"], - ["mmf", "application/vnd.smaf"], - ["teacher", "application/vnd.smart.teacher"], - ["fo", "application/vnd.software602.filler.form+xml"], - ["sdkm", "application/vnd.solent.sdkm+xml"], - ["sdkd", "application/vnd.solent.sdkm+xml"], - ["dxp", "application/vnd.spotfire.dxp"], - ["sfs", "application/vnd.spotfire.sfs"], - ["sdc", "application/vnd.stardivision.calc"], - ["sda", "application/vnd.stardivision.draw"], - ["sdd", "application/vnd.stardivision.impress"], - ["smf", "application/vnd.stardivision.math"], - ["sdw", "application/vnd.stardivision.writer"], - ["vor", "application/vnd.stardivision.writer"], - ["sgl", "application/vnd.stardivision.writer-global"], - ["smzip", "application/vnd.stepmania.package"], - ["sm", "application/vnd.stepmania.stepchart"], - ["wadl", "application/vnd.sun.wadl+xml"], - ["sxc", "application/vnd.sun.xml.calc"], - ["stc", "application/vnd.sun.xml.calc.template"], - ["sxd", "application/vnd.sun.xml.draw"], - ["std", "application/vnd.sun.xml.draw.template"], - ["sxi", "application/vnd.sun.xml.impress"], - ["sti", "application/vnd.sun.xml.impress.template"], - ["sxm", "application/vnd.sun.xml.math"], - ["sxw", "application/vnd.sun.xml.writer"], - ["sxg", "application/vnd.sun.xml.writer.global"], - ["stw", "application/vnd.sun.xml.writer.template"], - ["sus", "application/vnd.sus-calendar"], - ["susp", "application/vnd.sus-calendar"], - ["svd", "application/vnd.svd"], - ["sis", "application/vnd.symbian.install"], - ["sisx", "application/vnd.symbian.install"], - ["xsm", "application/vnd.syncml+xml"], - ["bdm", "application/vnd.syncml.dm+wbxml"], - ["xdm", "application/vnd.syncml.dm+xml"], - ["ddf", "application/vnd.syncml.dmddf+xml"], - ["tao", "application/vnd.tao.intent-module-archive"], - ["pcap", "application/vnd.tcpdump.pcap"], - ["cap", "application/vnd.tcpdump.pcap"], - ["dmp", "application/vnd.tcpdump.pcap"], - ["tmo", "application/vnd.tmobile-livetv"], - ["tpt", "application/vnd.trid.tpt"], - ["mxs", "application/vnd.triscape.mxs"], - ["tra", "application/vnd.trueapp"], - ["ufd", "application/vnd.ufdl"], - ["ufdl", "application/vnd.ufdl"], - ["utz", "application/vnd.uiq.theme"], - ["umj", "application/vnd.umajin"], - ["unityweb", "application/vnd.unity"], - ["uoml", "application/vnd.uoml+xml"], - ["vcx", "application/vnd.vcx"], - ["vsd", "application/vnd.visio"], - ["vst", "application/vnd.visio"], - ["vss", "application/vnd.visio"], - ["vsw", "application/vnd.visio"], - ["vis", "application/vnd.visionary"], - ["vsf", "application/vnd.vsf"], - ["wbxml", "application/vnd.wap.wbxml"], - ["wmlc", "application/vnd.wap.wmlc"], - ["wmlsc", "application/vnd.wap.wmlscriptc"], - ["wtb", "application/vnd.webturbo"], - ["nbp", "application/vnd.wolfram.player"], - ["wpd", "application/vnd.wordperfect"], - ["wqd", "application/vnd.wqd"], - ["stf", "application/vnd.wt.stf"], - ["xar", "application/vnd.xara"], - ["xfdl", "application/vnd.xfdl"], - ["hvd", "application/vnd.yamaha.hv-dic"], - ["hvs", "application/vnd.yamaha.hv-script"], - ["hvp", "application/vnd.yamaha.hv-voice"], - ["osf", "application/vnd.yamaha.openscoreformat"], - ["osfpvg", "application/vnd.yamaha.openscoreformat.osfpvg+xml"], - ["saf", "application/vnd.yamaha.smaf-audio"], - ["spf", "application/vnd.yamaha.smaf-phrase"], - ["cmp", "application/vnd.yellowriver-custom-menu"], - ["zir", "application/vnd.zul"], - ["zirz", "application/vnd.zul"], - ["zaz", "application/vnd.zzazz.deck+xml"], - ["vxml", "application/voicexml+xml"], - ["wgt", "application/widget"], - ["hlp", "application/winhlp"], - ["wsdl", "application/wsdl+xml"], - ["wspolicy", "application/wspolicy+xml"], - ["7z", "application/x-7z-compressed"], - ["abw", "application/x-abiword"], - ["ace", "application/x-ace-compressed"], - ["dmg", "application/x-apple-diskimage"], - ["aab", "application/x-authorware-bin"], - ["x32", "application/x-authorware-bin"], - ["u32", "application/x-authorware-bin"], - ["vox", "application/x-authorware-bin"], - ["aam", "application/x-authorware-map"], - ["aas", "application/x-authorware-seg"], - ["bcpio", "application/x-bcpio"], - ["torrent", "application/x-bittorrent"], - ["blb", "application/x-blorb"], - ["blorb", "application/x-blorb"], - ["bz", "application/x-bzip"], - ["bz2", "application/x-bzip2"], - ["boz", "application/x-bzip2"], - ["cbr", "application/x-cbr"], - ["cba", "application/x-cbr"], - ["cbt", "application/x-cbr"], - ["cbz", "application/x-cbr"], - ["cb7", "application/x-cbr"], - ["vcd", "application/x-cdlink"], - ["cfs", "application/x-cfs-compressed"], - ["chat", "application/x-chat"], - ["pgn", "application/x-chess-pgn"], - ["cco", "application/x-cocoa"], - ["nsc", "application/x-conference"], - ["cpio", "application/x-cpio"], - ["csh", "application/x-csh"], - ["deb", "application/x-debian-package"], - ["udeb", "application/x-debian-package"], - ["dgc", "application/x-dgc-compressed"], - ["dir", "application/x-director"], - ["dcr", "application/x-director"], - ["dxr", "application/x-director"], - ["cst", "application/x-director"], - ["cct", "application/x-director"], - ["cxt", "application/x-director"], - ["w3d", "application/x-director"], - ["fgd", "application/x-director"], - ["swa", "application/x-director"], - ["wad", "application/x-doom"], - ["ncx", "application/x-dtbncx+xml"], - ["dtb", "application/x-dtbook+xml"], - ["res", "application/x-dtbresource+xml"], - ["dvi", "application/x-dvi"], - ["evy", "application/x-envoy"], - ["eva", "application/x-eva"], - ["bdf", "application/x-font-bdf"], - ["gsf", "application/x-font-ghostscript"], - ["psf", "application/x-font-linux-psf"], - ["pcf", "application/x-font-pcf"], - ["snf", "application/x-font-snf"], - ["pfa", "application/x-font-type1"], - ["pfb", "application/x-font-type1"], - ["pfm", "application/x-font-type1"], - ["afm", "application/x-font-type1"], - ["arc", "application/x-freearc"], - ["spl", "application/x-futuresplash"], - ["gca", "application/x-gca-compressed"], - ["ulx", "application/x-glulx"], - ["gnumeric", "application/x-gnumeric"], - ["gramps", "application/x-gramps-xml"], - ["gtar", "application/x-gtar"], - ["hdf", "application/x-hdf"], - ["install", "application/x-install-instructions"], - ["iso", "application/x-iso9660-image"], - ["jardiff", "application/x-java-archive-diff"], - ["jnlp", "application/x-java-jnlp-file"], - ["latex", "application/x-latex"], - ["lzh", "application/x-lzh-compressed"], - ["lha", "application/x-lzh-compressed"], - ["run", "application/x-makeself"], - ["mie", "application/x-mie"], - ["prc", "application/x-mobipocket-ebook"], - ["mobi", "application/x-mobipocket-ebook"], - ["application", "application/x-ms-application"], - ["lnk", "application/x-ms-shortcut"], - ["wmd", "application/x-ms-wmd"], - ["wmz", "application/x-ms-wmz"], - ["xbap", "application/x-ms-xbap"], - ["mdb", "application/x-msaccess"], - ["obd", "application/x-msbinder"], - ["crd", "application/x-mscardfile"], - ["clp", "application/x-msclip"], - ["exe", "application/x-msdownload"], - ["dll", "application/x-msdownload"], - ["com", "application/x-msdownload"], - ["bat", "application/x-msdownload"], - ["msi", "application/x-msdownload"], - ["mvb", "application/x-msmediaview"], - ["m13", "application/x-msmediaview"], - ["m14", "application/x-msmediaview"], - ["wmf", "application/x-msmetafile"], - ["wmz", "application/x-msmetafile"], - ["emf", "application/x-msmetafile"], - ["emz", "application/x-msmetafile"], - ["mny", "application/x-msmoney"], - ["pub", "application/x-mspublisher"], - ["scd", "application/x-msschedule"], - ["trm", "application/x-msterminal"], - ["wri", "application/x-mswrite"], - ["nc", "application/x-netcdf"], - ["cdf", "application/x-netcdf"], - ["nzb", "application/x-nzb"], - ["pl", "application/x-perl"], - ["pm", "application/x-perl"], - ["prc", "application/x-pilot"], - ["pdb", "application/x-pilot"], - ["p12", "application/x-pkcs12"], - ["pfx", "application/x-pkcs12"], - ["p7b", "application/x-pkcs7-certificates"], - ["spc", "application/x-pkcs7-certificates"], - ["p7r", "application/x-pkcs7-certreqresp"], - ["rar", "application/x-rar-compressed"], - ["rpm", "application/x-redhat-package-manager"], - ["ris", "application/x-research-info-systems"], - ["sea", "application/x-sea"], - ["sh", "application/x-sh"], - ["shar", "application/x-shar"], - ["swf", "application/x-shockwave-flash"], - ["xap", "application/x-silverlight-app"], - ["sql", "application/x-sql"], - ["sit", "application/x-stuffit"], - ["sitx", "application/x-stuffitx"], - ["srt", "application/x-subrip"], - ["sv4cpio", "application/x-sv4cpio"], - ["sv4crc", "application/x-sv4crc"], - ["t3", "application/x-t3vm-image"], - ["gam", "application/x-tads"], - ["tar", "application/x-tar"], - ["tcl", "application/x-tcl"], - ["tk", "application/x-tcl"], - ["tex", "application/x-tex"], - ["tfm", "application/x-tex-tfm"], - ["texinfo", "application/x-texinfo"], - ["texi", "application/x-texinfo"], - ["obj", "application/x-tgif"], - ["ustar", "application/x-ustar"], - ["src", "application/x-wais-source"], - ["der", "application/x-x509-ca-cert"], - ["crt", "application/x-x509-ca-cert"], - ["pem", "application/x-x509-ca-cert"], - ["fig", "application/x-xfig"], - ["xlf", "application/x-xliff+xml"], - ["xpi", "application/x-xpinstall"], - ["xz", "application/x-xz"], - ["z1", "application/x-zmachine"], - ["z2", "application/x-zmachine"], - ["z3", "application/x-zmachine"], - ["z4", "application/x-zmachine"], - ["z5", "application/x-zmachine"], - ["z6", "application/x-zmachine"], - ["z7", "application/x-zmachine"], - ["z8", "application/x-zmachine"], - ["xaml", "application/xaml+xml"], - ["xav", "application/xcap-att+xml"], - ["xca", "application/xcap-caps+xml"], - ["xdf", "application/xcap-diff+xml"], - ["xel", "application/xcap-el+xml"], - ["xns", "application/xcap-ns+xml"], - ["xenc", "application/xenc+xml"], - ["xhtml", "application/xhtml+xml"], - ["xht", "application/xhtml+xml"], - ["xlf", "application/xliff+xml"], - ["xml", "application/xml"], - ["xsl", "application/xml"], - ["xsd", "application/xml"], - ["rng", "application/xml"], - ["dtd", "application/xml-dtd"], - ["xop", "application/xop+xml"], - ["xpl", "application/xproc+xml"], - ["xsl", "application/xslt+xml"], - ["xslt", "application/xslt+xml"], - ["xspf", "application/xspf+xml"], - ["mxml", "application/xv+xml"], - ["xhvml", "application/xv+xml"], - ["xvml", "application/xv+xml"], - ["xvm", "application/xv+xml"], - ["yang", "application/yang"], - ["yin", "application/yin+xml"], - ["zip", "application/zip"], - ["3gpp", "audio/3gpp"], - ["adp", "audio/adpcm"], - ["amr", "audio/amr"], - ["au", "audio/basic"], - ["snd", "audio/basic"], - ["mid", "audio/midi"], - ["midi", "audio/midi"], - ["kar", "audio/midi"], - ["rmi", "audio/midi"], - ["mxmf", "audio/mobile-xmf"], - ["m4a", "audio/mp4"], - ["mp4a", "audio/mp4"], - ["mpga", "audio/mpeg"], - ["mp2", "audio/mpeg"], - ["mp2a", "audio/mpeg"], - ["mp3", "audio/mpeg"], - ["m2a", "audio/mpeg"], - ["m3a", "audio/mpeg"], - ["oga", "audio/ogg"], - ["ogg", "audio/ogg"], - ["spx", "audio/ogg"], - ["opus", "audio/ogg"], - ["s3m", "audio/s3m"], - ["sil", "audio/silk"], - ["uva", "audio/vnd.dece.audio"], - ["uvva", "audio/vnd.dece.audio"], - ["eol", "audio/vnd.digital-winds"], - ["dra", "audio/vnd.dra"], - ["dts", "audio/vnd.dts"], - ["dtshd", "audio/vnd.dts.hd"], - ["lvp", "audio/vnd.lucent.voice"], - ["pya", "audio/vnd.ms-playready.media.pya"], - ["ecelp4800", "audio/vnd.nuera.ecelp4800"], - ["ecelp7470", "audio/vnd.nuera.ecelp7470"], - ["ecelp9600", "audio/vnd.nuera.ecelp9600"], - ["rip", "audio/vnd.rip"], - ["weba", "audio/webm"], - ["aac", "audio/x-aac"], - ["aif", "audio/x-aiff"], - ["aiff", "audio/x-aiff"], - ["aifc", "audio/x-aiff"], - ["caf", "audio/x-caf"], - ["flac", "audio/x-flac"], - ["m4a", "audio/x-m4a"], - ["mka", "audio/x-matroska"], - ["m3u", "audio/x-mpegurl"], - ["wax", "audio/x-ms-wax"], - ["wma", "audio/x-ms-wma"], - ["ram", "audio/x-pn-realaudio"], - ["ra", "audio/x-pn-realaudio"], - ["rmp", "audio/x-pn-realaudio-plugin"], - ["ra", "audio/x-realaudio"], - ["wav", "audio/x-wav"], - ["xm", "audio/xm"], - ["cdx", "chemical/x-cdx"], - ["cif", "chemical/x-cif"], - ["cmdf", "chemical/x-cmdf"], - ["cml", "chemical/x-cml"], - ["csml", "chemical/x-csml"], - ["xyz", "chemical/x-xyz"], - ["ttc", "font/collection"], - ["otf", "font/otf"], - ["ttf", "font/ttf"], - ["woff", "font/woff"], - ["woff2", "font/woff2"], - ["exr", "image/aces"], - ["avif", "image/avif"], - ["bmp", "image/bmp"], - ["cgm", "image/cgm"], - ["drle", "image/dicom-rle"], - ["emf", "image/emf"], - ["fits", "image/fits"], - ["g3", "image/g3fax"], - ["gif", "image/gif"], - ["heic", "image/heic"], - ["heics", "image/heic-sequence"], - ["heif", "image/heif"], - ["heifs", "image/heif-sequence"], - ["hej2", "image/hej2k"], - ["hsj2", "image/hsj2"], - ["ief", "image/ief"], - ["jls", "image/jls"], - ["jp2", "image/jp2"], - ["jpg2", "image/jp2"], - ["jpeg", "image/jpeg"], - ["jpg", "image/jpeg"], - ["jpe", "image/jpeg"], - ["jph", "image/jph"], - ["jhc", "image/jphc"], - ["jpm", "image/jpm"], - ["jpx", "image/jpx"], - ["jpf", "image/jpx"], - ["jxr", "image/jxr"], - ["jxra", "image/jxra"], - ["jxrs", "image/jxrs"], - ["jxs", "image/jxs"], - ["jxsc", "image/jxsc"], - ["jxsi", "image/jxsi"], - ["jxss", "image/jxss"], - ["ktx", "image/ktx"], - ["ktx2", "image/ktx2"], - ["png", "image/png"], - ["btif", "image/prs.btif"], - ["pti", "image/prs.pti"], - ["sgi", "image/sgi"], - ["svg", "image/svg+xml"], - ["svgz", "image/svg+xml"], - ["t38", "image/t38"], - ["tif", "image/tiff"], - ["tiff", "image/tiff"], - ["tfx", "image/tiff-fx"], - ["psd", "image/vnd.adobe.photoshop"], - ["azv", "image/vnd.airzip.accelerator.azv"], - ["uvi", "image/vnd.dece.graphic"], - ["uvvi", "image/vnd.dece.graphic"], - ["uvg", "image/vnd.dece.graphic"], - ["uvvg", "image/vnd.dece.graphic"], - ["djvu", "image/vnd.djvu"], - ["djv", "image/vnd.djvu"], - ["sub", "image/vnd.dvb.subtitle"], - ["dwg", "image/vnd.dwg"], - ["dxf", "image/vnd.dxf"], - ["fbs", "image/vnd.fastbidsheet"], - ["fpx", "image/vnd.fpx"], - ["fst", "image/vnd.fst"], - ["mmr", "image/vnd.fujixerox.edmics-mmr"], - ["rlc", "image/vnd.fujixerox.edmics-rlc"], - ["ico", "image/vnd.microsoft.icon"], - ["mdi", "image/vnd.ms-modi"], - ["wdp", "image/vnd.ms-photo"], - ["npx", "image/vnd.net-fpx"], - ["b16", "image/vnd.pco.b16"], - ["tap", "image/vnd.tencent.tap"], - ["vtf", "image/vnd.valve.source.texture"], - ["wbmp", "image/vnd.wap.wbmp"], - ["xif", "image/vnd.xiff"], - ["pcx", "image/vnd.zbrush.pcx"], - ["webp", "image/webp"], - ["wmf", "image/wmf"], - ["3ds", "image/x-3ds"], - ["ras", "image/x-cmu-raster"], - ["cmx", "image/x-cmx"], - ["fh", "image/x-freehand"], - ["fhc", "image/x-freehand"], - ["fh4", "image/x-freehand"], - ["fh5", "image/x-freehand"], - ["fh7", "image/x-freehand"], - ["ico", "image/x-icon"], - ["jng", "image/x-jng"], - ["sid", "image/x-mrsid-image"], - ["bmp", "image/x-ms-bmp"], - ["pcx", "image/x-pcx"], - ["pic", "image/x-pict"], - ["pct", "image/x-pict"], - ["pnm", "image/x-portable-anymap"], - ["pbm", "image/x-portable-bitmap"], - ["pgm", "image/x-portable-graymap"], - ["ppm", "image/x-portable-pixmap"], - ["rgb", "image/x-rgb"], - ["tga", "image/x-tga"], - ["xbm", "image/x-xbitmap"], - ["xpm", "image/x-xpixmap"], - ["xwd", "image/x-xwindowdump"], - ["disposition-notification", "message/disposition-notification"], - ["u8msg", "message/global"], - ["u8dsn", "message/global-delivery-status"], - ["u8mdn", "message/global-disposition-notification"], - ["u8hdr", "message/global-headers"], - ["eml", "message/rfc822"], - ["mime", "message/rfc822"], - ["wsc", "message/vnd.wfa.wsc"], - ["3mf", "model/3mf"], - ["gltf", "model/gltf+json"], - ["glb", "model/gltf-binary"], - ["igs", "model/iges"], - ["iges", "model/iges"], - ["msh", "model/mesh"], - ["mesh", "model/mesh"], - ["silo", "model/mesh"], - ["mtl", "model/mtl"], - ["obj", "model/obj"], - ["stl", "model/stl"], - ["dae", "model/vnd.collada+xml"], - ["dwf", "model/vnd.dwf"], - ["gdl", "model/vnd.gdl"], - ["gtw", "model/vnd.gtw"], - ["mts", "model/vnd.mts"], - ["ogex", "model/vnd.opengex"], - ["x_b", "model/vnd.parasolid.transmit.binary"], - ["x_t", "model/vnd.parasolid.transmit.text"], - ["usdz", "model/vnd.usdz+zip"], - ["bsp", "model/vnd.valve.source.compiled-map"], - ["vtu", "model/vnd.vtu"], - ["wrl", "model/vrml"], - ["vrml", "model/vrml"], - ["x3db", "model/x3d+binary"], - ["x3dbz", "model/x3d+binary"], - ["x3db", "model/x3d+fastinfoset"], - ["x3dv", "model/x3d+vrml"], - ["x3dvz", "model/x3d+vrml"], - ["x3d", "model/x3d+xml"], - ["x3dz", "model/x3d+xml"], - ["x3dv", "model/x3d-vrml"], - ["appcache", "text/cache-manifest"], - ["manifest", "text/cache-manifest"], - ["ics", "text/calendar"], - ["ifb", "text/calendar"], - ["css", "text/css"], - ["csv", "text/csv"], - ["html", "text/html"], - ["htm", "text/html"], - ["shtml", "text/html"], - ["markdown", "text/markdown"], - ["md", "text/markdown"], - ["mml", "text/mathml"], - ["n3", "text/n3"], - ["txt", "text/plain"], - ["text", "text/plain"], - ["conf", "text/plain"], - ["def", "text/plain"], - ["list", "text/plain"], - ["log", "text/plain"], - ["in", "text/plain"], - ["ini", "text/plain"], - ["dsc", "text/prs.lines.tag"], - ["rtx", "text/richtext"], - ["rtf", "text/rtf"], - ["sgml", "text/sgml"], - ["sgm", "text/sgml"], - ["spdx", "text/spdx"], - ["tsv", "text/tab-separated-values"], - ["t", "text/troff"], - ["tr", "text/troff"], - ["roff", "text/troff"], - ["man", "text/troff"], - ["me", "text/troff"], - ["ms", "text/troff"], - ["ttl", "text/turtle"], - ["uri", "text/uri-list"], - ["uris", "text/uri-list"], - ["urls", "text/uri-list"], - ["vcard", "text/vcard"], - ["curl", "text/vnd.curl"], - ["dcurl", "text/vnd.curl.dcurl"], - ["mcurl", "text/vnd.curl.mcurl"], - ["scurl", "text/vnd.curl.scurl"], - ["sub", "text/vnd.dvb.subtitle"], - ["fly", "text/vnd.fly"], - ["flx", "text/vnd.fmi.flexstor"], - ["gv", "text/vnd.graphviz"], - ["3dml", "text/vnd.in3d.3dml"], - ["spot", "text/vnd.in3d.spot"], - ["jad", "text/vnd.sun.j2me.app-descriptor"], - ["wml", "text/vnd.wap.wml"], - ["wmls", "text/vnd.wap.wmlscript"], - ["vtt", "text/vtt"], - ["s", "text/x-asm"], - ["asm", "text/x-asm"], - ["c", "text/x-c"], - ["cc", "text/x-c"], - ["cxx", "text/x-c"], - ["cpp", "text/x-c"], - ["h", "text/x-c"], - ["hh", "text/x-c"], - ["dic", "text/x-c"], - ["htc", "text/x-component"], - ["f", "text/x-fortran"], - ["for", "text/x-fortran"], - ["f77", "text/x-fortran"], - ["f90", "text/x-fortran"], - ["java", "text/x-java-source"], - ["nfo", "text/x-nfo"], - ["opml", "text/x-opml"], - ["p", "text/x-pascal"], - ["pas", "text/x-pascal"], - ["etx", "text/x-setext"], - ["sfv", "text/x-sfv"], - ["uu", "text/x-uuencode"], - ["vcs", "text/x-vcalendar"], - ["vcf", "text/x-vcard"], - ["xml", "text/xml"], - ["3gp", "video/3gpp"], - ["3gpp", "video/3gpp"], - ["3g2", "video/3gpp2"], - ["h261", "video/h261"], - ["h263", "video/h263"], - ["h264", "video/h264"], - ["m4s", "video/iso.segment"], - ["jpgv", "video/jpeg"], - ["jpm", "video/jpm"], - ["jpgm", "video/jpm"], - ["mj2", "video/mj2"], - ["mjp2", "video/mj2"], - ["ts", "video/mp2t"], - ["mp4", "video/mp4"], - ["mp4v", "video/mp4"], - ["mpg4", "video/mp4"], - ["mpeg", "video/mpeg"], - ["mpg", "video/mpeg"], - ["mpe", "video/mpeg"], - ["m1v", "video/mpeg"], - ["m2v", "video/mpeg"], - ["ogv", "video/ogg"], - ["qt", "video/quicktime"], - ["mov", "video/quicktime"], - ["uvh", "video/vnd.dece.hd"], - ["uvvh", "video/vnd.dece.hd"], - ["uvm", "video/vnd.dece.mobile"], - ["uvvm", "video/vnd.dece.mobile"], - ["uvp", "video/vnd.dece.pd"], - ["uvvp", "video/vnd.dece.pd"], - ["uvs", "video/vnd.dece.sd"], - ["uvvs", "video/vnd.dece.sd"], - ["uvv", "video/vnd.dece.video"], - ["uvvv", "video/vnd.dece.video"], - ["dvb", "video/vnd.dvb.file"], - ["fvt", "video/vnd.fvt"], - ["mxu", "video/vnd.mpegurl"], - ["m4u", "video/vnd.mpegurl"], - ["pyv", "video/vnd.ms-playready.media.pyv"], - ["uvu", "video/vnd.uvvu.mp4"], - ["uvvu", "video/vnd.uvvu.mp4"], - ["viv", "video/vnd.vivo"], - ["webm", "video/webm"], - ["f4v", "video/x-f4v"], - ["fli", "video/x-fli"], - ["flv", "video/x-flv"], - ["m4v", "video/x-m4v"], - ["mkv", "video/x-matroska"], - ["mk3d", "video/x-matroska"], - ["mks", "video/x-matroska"], - ["mng", "video/x-mng"], - ["asf", "video/x-ms-asf"], - ["asx", "video/x-ms-asf"], - ["vob", "video/x-ms-vob"], - ["wm", "video/x-ms-wm"], - ["wmv", "video/x-ms-wmv"], - ["wmx", "video/x-ms-wmx"], - ["wvx", "video/x-ms-wvx"], - ["avi", "video/x-msvideo"], - ["movie", "video/x-sgi-movie"], - ["smv", "video/x-smv"], - ["ice", "x-conference/x-cooltalk"], -]); diff --git a/src/errors.ts b/src/errors.ts deleted file mode 100644 index 7e1212d7a..000000000 --- a/src/errors.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { STATUS_TEXT } from "../deps.ts"; - -/** - * This class is for throwing errors in the request-resource-response lifecycle. - */ -export class HttpError extends Error { - /** - * A property to hold the HTTP response code associated with this - * exception. - */ - public code: number; - - /** - * Construct an object of this class. - * - * @param code - See HttpError.code. - * @param message - (optional) The exception message. - */ - constructor(code: number, message?: string) { - super(message); - if (!message) { - const statusText = STATUS_TEXT.get(code.toString()); - if (statusText) { - this.message = statusText; - } - } - this.code = code; - } -} diff --git a/src/http/error_handler.ts b/src/http/error_handler.ts deleted file mode 100644 index 325872199..000000000 --- a/src/http/error_handler.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Errors, IErrorHandler, Response } from "../../mod.ts"; -import { STATUS_TEXT } from "../../deps.ts"; -import type { ConnInfo } from "../../deps.ts"; - -export class ErrorHandler implements IErrorHandler { - /** - * Catch and handle the given error. - * - * @param error - The Error object that is thrown during runtime. - * @param request - The original request that threw the error. Note this is - * not that `Drash.Request` object. Reason being, the `Drash.Request` object - * can also throw an error and would not be available as a parameter. - * @param response - The `Drash.Response` object. - */ - public catch( - error: Error, - _request: Request, - response: Response, - _connInfo: ConnInfo, - ): void { - const errorMessage = error.stack ?? "Error: Unknown Error"; - - // Built-in Drash HTTP error object? Use the error code as the HTTP status - // code. The error code is always in the range of HTTP status codes. - if (error instanceof Errors.HttpError) { - return response.text(errorMessage, error.code); - } - - // No code? Default to 500. - if (!("code" in error)) { - return response.text(errorMessage, 500); - } - - // If the error has a code, then we need to make sure it is within the range - // of HTTP status codes. Otherwise, we cannot convert this to a response. - if ("code" in error) { - const errorWithCode = error as unknown as { code: unknown }; - - // Start off with 500 as the default - let code = 500; - - // Status codes should be a number. Not a string, not a boolean, etc. If - // it is a number AND it is within the range of HTTP status codes, then we - // replace the default 500 with it. - if ( - typeof errorWithCode.code === "number" && - STATUS_TEXT.get(errorWithCode.code.toString()) - ) { - code = errorWithCode.code; - } - return response.text(errorMessage, code); - } - - return response.text(errorMessage); - } -} diff --git a/src/http/request.ts b/src/http/request.ts deleted file mode 100644 index 9d5cc799c..000000000 --- a/src/http/request.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { getCookies } from "../../deps.ts"; -import { Errors } from "../../mod.ts"; -import type { ConnInfo } from "../../deps.ts"; -import { BodyFile, RequestOptions } from "../types.ts"; - -export type ParsedBody = - | Record - | undefined - | string; - -/** - * A class that holds the representation of an incoming request - */ -export class DrashRequest extends Request { - public conn_info: ConnInfo; - - #end_lifecycle = false; - #original: Request; - #parsed_body?: ParsedBody; - readonly #path_params: Map; - #search_params!: URLSearchParams; - - ////////////////////////////////////////////////////////////////////////////// - // FILE MARKER - CONSTRUCTOR ///////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// - - /** - * Construct an object of this class. - * - * This class is just a wrapper around the native Request object. The only - * reason we wrap around the native Request object is so we can add more - * methods to interact with the native Request object (e.g., req.bodyParam()). - * - * @param originalRequest - The original request coming. - * @param pathParams - The path params to match the request's URL to. The path - * params come from a resource's path(s). - * @param connInfo - The connection information for the current request - */ - constructor( - originalRequest: Request, - pathParams: Map, - connInfo: ConnInfo, - ) { - super(originalRequest.clone()); - this.#path_params = pathParams; - this.conn_info = connInfo; - this.#original = originalRequest.clone(); - } - - ////////////////////////////////////////////////////////////////////////////// - // GETTERS / SETTERS ///////////////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// - - get end_lifecycle(): boolean { - return this.#end_lifecycle; - } - - get original(): Request { - return this.#original; - } - - ////////////////////////////////////////////////////////////////////////////// - // FILE MARKER - METHODS - STATIC //////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// - - /** - * Create a Drash request object. We use this method to create request objects - * because we need to use `async-await` and cannot use them in constructor - * methods. This is the only reason for this abstraction. - * - * @param request - The original request. - * @param pathParams - The path params to match the request's URL to. The path - * params come from a resource's path(s). - * @param connInfo - The connection info Deno provides on a new request - * @param options - Any options to control the way requests behave. - * - * @returns A Drash request object. - */ - static async create( - request: Request, - pathParams: Map, - connInfo: ConnInfo, - options?: RequestOptions, - ) { - const req = new DrashRequest(request, pathParams, connInfo); - - if (options) { - if (options.read_body === false) { - return req; - } - } - - // This is here because `parseBody` is async. We can't parse the request - // body on the fly as we dont want users to have to use await when getting a - // body param. - const contentLength = req.headers.get("content-length") ?? "0"; - if (req.body && req.bodyUsed === false && contentLength !== "0") { - req.#parsed_body = await req.#parseBody(); - } - return req; - } - - ////////////////////////////////////////////////////////////////////////////// - // FILE MARKER - METHODS - PUBLIC //////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// - - /** - * Check if the content type in question are accepted by the request. - * - * @param contentType - A proper MIME type. See /src/dictionaries/mime_db.ts for proper MIME types - * that Drash can handle. - * - * @returns True if yes, false if no. - */ - public accepts(contentType: string): boolean { - const acceptHeader = this.headers.get("Accept"); - if (!acceptHeader) { - return false; - } - return acceptHeader.includes(contentType); - } - - /** - * Set this request to end early? Calling this will tell the request to stop - * where it is at in the request-resource-response lifecycle and immediately - * return a response. - */ - public end(): void { - this.#end_lifecycle = true; - } - - /** - * Get a cookie value by the name that is sent in with the request. - * - * @param name - The name of the cookie to retrieve - * - * @returns The cookie value associated with the cookie name or `undefined` if - * a cookie with that name doesn't exist - */ - public getCookie(name: string): string { - const cookies = getCookies( - this.headers, - ); - return cookies[name]; - } - - /** - * Get a body parameter of the request by the name - * - * @example - * ```js - * // Assume you've sent a fetch request with: - * // { - * // user: { - * // name: "Drash" - * // } - * // } - * const user = request.bodyParam<{ name: string }>("user") // { name: "Drash" } - * // More examples: - * request.bodyParam('uploads') - * ``` - * - * @param name The body parameter name - * - * @returns The value of the parameter if found, or `undefined` if not found. - */ - public bodyParam(name: string): T | undefined { - if (this.#parsed_body === undefined) { - return undefined; - } - - if (typeof this.#parsed_body !== "object") { - return undefined; - } - - if (this.#parsed_body[name] === undefined) { - return undefined; - } - - return this.#parsed_body[name] as unknown as T; - } - - /** - * Get all body params. - * - * @returns All params contained in the body or an empty body if no params - * exist. - */ - public bodyAll(): ParsedBody | T { - if (this.#parsed_body === undefined) { - return {}; - } - - if (typeof this.#parsed_body !== "object") { - return {}; - } - - return this.#parsed_body; - } - - /** - * Parse the request body. - * - * @returns A parsed body based on the content type of the request body. - */ - async #parseBody(): Promise { - const contentLength = this.headers.get("content-length"); - - // The Content-Length header indicates that the client is sending a body. - // Some clients send a Content-Length header of "0", which indicates that - // there is in fact no body present with the request. We need to check for - // this or else we try to parse a body for no reason. - if (!contentLength || contentLength === "0") { - return undefined; - } - - // If we get to this point, then that means there is a body (since the - // Content-Length header is greater than 0). In order for us to parse the - // body, we need to know the Content-Type of the body. Otherwise, we have - // no way of knowing how to parse it. We can assume, but let's just tell the - // client to modify their request to include the Content-Type header. - const contentType = this.headers.get("content-type"); - - if (!contentType) { - throw new Errors.HttpError( - 400, - "Bad Request. The request body cannot be parsed due to the Content-Type header missing.", - ); - } - - try { - if (contentType.includes("multipart/form-data")) { - return await this.#constructFormDataUsingBody(); - } - if (contentType.includes("application/json")) { - return await this.json(); - } - if (contentType.includes("application/x-www-form-urlencoded")) { - return await this.#constructFormDataUsingBody(); - } - if (contentType.includes("text/plain")) { - return await this.text(); - } - - // If all else fails, then try to parse the body using FormData - return await this.#constructFormDataUsingBody(); - } catch (_e) { - throw new Errors.HttpError( - 422, - "Unprocessable Entity. The request body seems to be invalid as there was an error parsing it.", - ); - } - } - - /** - * Get a path parameter from the request based on the request's URL and the - * resource path it matched to. - * - * @example - * ```js - * // Assume a path for your resource is "/users/:id/:city?", and the request - * // is "/users/2/". - * const id = this.paramParam("id") // Returns 2 - * const city = this.queryParam("city") // Returns undefined - * ``` - * - * @param name - The parameter name in the resource path. - * - * @returns The value for the parameter if found, or undefined if not set. - */ - public pathParam(name: string): string | undefined { - const param = this.#path_params.get(name); - if (!param) { - return undefined; - } - return this.#decodeValue(param); - } - - /** - * Find a query string parameter. - * - * @example - * ```js - * // Assume url is "http://localhost:1336/users?city=London&country=England" - * const city = this.queryParam("city") // Returns "London" - * const country = this.queryParam("country") // Returns "England" - * ``` - * - * @param name - The name of the query string. - * - * @returns The value of the param if found, or undefined if not. - */ - public queryParam(name: string): string | undefined { - if (!this.#search_params) { - this.#search_params = new URL(this.url).searchParams; - } - const param = this.#search_params.get(name); - if (!param) { - return undefined; - } - return this.#decodeValue(param); - } - - ////////////////////////////////////////////////////////////////////////////// - // FILE MARKER - METHODS - PRIVATE /////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// - - /** - * Construct the form data of a request body. - * - * @returns The form data body as a key-value pair object. - */ - async #constructFormDataUsingBody(): Promise { - const formData = await this.formData(); - const formDataJSON: ParsedBody = {}; - for (const [key, value] of formData.entries()) { - if (value instanceof File) { - const content = new Uint8Array(await value.arrayBuffer()); - const file = { - size: value.size, - type: value.type, - content, - filename: value.name, - }; - if (key.endsWith("[]")) { - const name = key.slice(0, -2); - if (!formDataJSON[name]) { - formDataJSON[name] = []; - } - (formDataJSON[name] as BodyFile[]).push(file); - } else { - formDataJSON[key] = file; - } - continue; - } - formDataJSON[key] = value as string; - } - return formDataJSON; - } - - /** - * Decode a URI component -- netrualizing the string by replacing characters - * not required. - * - * @param value - The string to decode. - * - * @returns The neutralized string. - */ - #decodeValue(value: string): string { - return decodeURIComponent(value.replace(/\+/g, " ")); - } -} diff --git a/src/http/resource.ts b/src/http/resource.ts deleted file mode 100644 index e5f50495e..000000000 --- a/src/http/resource.ts +++ /dev/null @@ -1,51 +0,0 @@ -import * as Drash from "../../mod.ts"; - -/** - * This is the base resource class for all resources. All resource classes must - * extend this base resource class. - * - * Drash defines a resource according to the MDN at the following page: - * - * https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Identifying_resources_on_the_Web - */ -export class Resource implements Drash.Interfaces.IResource { - /** - * Internal property used to identify this as a Drash resource. - */ - protected drash_resource = true; - - public services: Drash.Interfaces.IResourceServices = {}; - public paths: string[] = []; - - // TODO(crookse) Deprecate this method and introduce `response.redirect()` - /** - * Redirect the incoming request to another resource - * - * @example - * ```js - * this.redirect("http://localhost/login", response); - * this.redirect("http://localhost/login", response, 301); - * this.redirect("http://localhost/login", response, 301, {"some-header": "some value"}); - * ``` - * - * @param location - The location or resource uri of where you want to - * redirect the request to - * @param response - The response object, to set the related headers and - * status code on - * @param status - (optional) The response status. Defaults to 302. - * @param headers - (optional) Any extra headers to specify with the response. - * Defaults to an empty object. - */ - public redirect( - location: string, - response: Drash.Response, - status = 302, - headers: Drash.Types.HttpHeadersKeyValuePairs = {}, - ): void { - response.headers.set("Location", location); - response.status = status; - Object.keys(headers).forEach((key) => { - response.headers.set(key, headers[key]); - }); - } -} diff --git a/src/http/response.ts b/src/http/response.ts deleted file mode 100644 index 714edfe5a..000000000 --- a/src/http/response.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { Cookie, deleteCookie, setCookie } from "../../deps.ts"; -import { mimeTypes } from "../dictionaries/mime_types.ts"; -import * as Drash from "../../mod.ts"; - -export class DrashResponse { - body: BodyInit | null = null; - public headers: Headers = new Headers(); - public status = 200; - public statusText = "OK"; - public upgraded = false; - public upgraded_response: Response | null = null; - - ////////////////////////////////////////////////////////////////////////////// - // FILE MARKER - PUBLIC METHODS ////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// - - /** - * Delete a cookie for the response. - * - * @param name - The name of the cookie. - * @param attributes - Path and domain, can be used to pass the exact same - * attributes that were used to set the cookie. - */ - public deleteCookie( - name: string, - attributes?: { - domain: string; - path?: string; - }, - ): void { - deleteCookie(this.headers, name, attributes); - } - - /** - * Set the body of this response as a downloaded type given the filepath, - * filename, and content type of the downloadable type. - * - * @param filepath - The filepath of the file to download, relative to the CWD - * that executed the entrypoint script. - * @param contentType - The content type of the associated file. - * @param headers - Any extra headers you wish to specify apart of the content-type header - * - * @example - * ```js - * response.download( - * "./images/user_1_profile_pic.png", - * "image/png" - * ); - * ``` - */ - public download( - filepath: string, - contentType: string, - headers: Record = {}, - ): void { - const filepathSplit = filepath.split("/"); - const filename = filepathSplit[filepathSplit.length - 1]; - this.headers.set( - "Content-Disposition", - `attachment; filename="${filename}"`, - ); - this.headers.set("Content-Type", contentType); - Object.keys(headers).forEach((key) => { - this.headers.set(key, headers[key]); - }); - this.body = Deno.readFileSync(filepath); - } - - /** - * Set the body of this response as the contents of the given filepath. The - * Content-Type header will be set automatically based on the extension of the - * filepath. - * - * @param filepath - The filepath of the file to download, relative to the CWD - * that executed the entrypoint script. - * @param status - The status to respond with. Default on the response is 200. - * @param headers - Any extra headers you wish to specify apart of the content-type header - */ - public file( - filepath: string, - status?: number, - headers: Record = {}, - ): void { - // Get the extension of the file - const extension = filepath.split(".").at(-1); - if (!extension) { - throw new Drash.Errors.HttpError( - 415, - "`filepath` passed into response.file()` must contain a valid extension.", - ); - } - - // Get the MIME type of the file - const type = mimeTypes.get(extension); - if (!type) { - throw new Drash.Errors.HttpError( - 500, - "Unable to retrieve content type for " + filepath + - ", please submit an issue.", - ); - } - - this.body = Deno.readFileSync(filepath); - this.headers.set("Content-Type", type); - Object.keys(headers).forEach((key) => { - this.headers.set(key, headers[key]); - }); - if (status) { - this.status = status; - } - } - - /** - * Set the body of this response as HTML. - * - * @param html - The HTML string to assign to the body. - * @param status - Status to respond with. - * @param headers - Any extra headers you wish to specify apart of the content-type header. - */ - public html( - html: string, - status?: number, - headers: Record = {}, - ): void { - this.body = html; - this.headers.set("Content-Type", "text/html"); - if (status) { - this.status = status; - } - Object.keys(headers).forEach((key) => { - this.headers.set(key, headers[key]); - }); - } - - /** - * Set the body of this response as JSON. - * - * @param json - The object to assign to the body. - * @param status - The status to respond with. - * @param headers - Any extra headers you wish to specify apart of the content-type header - */ - public json( - // We ignore the following because this means a user can do - // `const user: IUSer = ...; response.json(user)`, which isn't possible with - // Record - // deno-lint-ignore ban-types - json: object, - status?: number, - headers: Record = {}, - ) { - this.body = JSON.stringify(json); - this.headers.set("Content-Type", "application/json"); - Object.keys(headers).forEach((key) => { - this.headers.set(key, headers[key]); - }); - if (status) this.status = status; - } - - /** - * Set the body of this response as XML. - * - * @param xml - The XML string to assign to the body. - * @param status - The status to respond with. - * @param headers - Any extra headers you wish to specify apart of the content-type header - */ - public xml( - xml: string, - status?: number, - headers: Record = {}, - ) { - this.body = xml; - this.headers.set("Content-Type", "text/xml"); - Object.keys(headers).forEach((key) => { - this.headers.set(key, headers[key]); - }); - if (status) this.status = status; - } - - /** - * This method allows users to make `this.response.render()` calls in - * resources. This method is also used by Tengine: - * - * https://github.com/drashland/deno-drash-middleware/tree/master/tengine - */ - public render(_filepath: string, _data: unknown): boolean | string { - return false; - } - - /** - * Set a cookie on the response to be handled by the client. - * - * @param cookie - The cookie data. - */ - public setCookie(cookie: Cookie): void { - setCookie(this.headers, cookie); - } - - /** - * Set thie body of this response. - * - * @param contentType - The content type to use in the Content-Type header. - * @param body - The body of the response. - */ - public send(contentType: string, body: T): void { - this.body = body; - this.headers.set("Content-Type", contentType); - } - - /** - * Set the body of this response as text. - * - * @param text - The text to assign to the body. - * @param status - The status to respond with. - * @param headers - Any extra headers you wish to specify apart of the content-type header - */ - public text( - text: string, - status?: number, - headers: Record = {}, - ) { - this.body = text; - this.headers.set("Content-Type", "text/plain"); - Object.keys(headers).forEach((key) => { - this.headers.set(key, headers[key]); - }); - if (status) this.status = status; - } - - /** - * Upgrade the response. - * - * @param response - The upgraded response (e.g. a WebSocket connection - * response via Deno.upgradeWebSocket()). - */ - public upgrade(response: Response): void { - this.upgraded = true; - this.upgraded_response = response; - } -} diff --git a/src/http/server.ts b/src/http/server.ts deleted file mode 100644 index 5df802cf2..000000000 --- a/src/http/server.ts +++ /dev/null @@ -1,456 +0,0 @@ -import * as Drash from "../../mod.ts"; -import { ConnInfo, StdServer } from "../../deps.ts"; - -async function runServices( - Services: Drash.Interfaces.IService[], - request: Drash.Request, - response: Drash.Response, - serviceMethod: "runBeforeResource" | "runAfterResource", -): Promise { - // There are two ways a service can short-circuit the - // request-resource-response lifecycle: - // - // 1. The service throws an error. - // 2. The service calls `request.end()`. - // - // If the service throws an error, then the request handler we pass in to `new - // StdServer()` will catch it and return a response. - // - // If the service calls `request.end()`, then the request handler we pass in - // to `new StdServer()` will return `new Response()`. - for (const Service of Services) { - if (serviceMethod in Service) { - await Service[serviceMethod]!(request, response); - if (request.end_lifecycle) { - break; - } - } - } -} - -/** - * This class handles the entire request-resource-response lifecycle. It is in - * charge of handling incoming requests, matching them to resources for further - * processing, and sending responses based on the processes set in the resource. - * It is also in charge of sending error responses that "bubble up" during the - * request-resource-response lifecycle. - */ -export class Server { - /** - * See Drash.Interfaces.IServerOptions. - */ - readonly #options: Drash.Interfaces.IServerOptions; - - /** - * A list of all instanced resources the user specified, and - * a url pattern for every path specified. This means when a request - * comes in, the paths are already converted to patterns, saving us time - */ - readonly #resources: Drash.Types.ResourcesAndPatternsMap = new Map(); - - /** - * Our server instance that is serving the app - */ - #server!: StdServer; - - /** - * All services that provide extra functionality to the server and the overall - * application. - */ - #services: Drash.Interfaces.IService[] = []; - - /** - * A promise we need to await after calling close() on #server - */ - #server_promise!: Promise; - - /** - * A custom Error object handler. - */ - #error_handler!: Drash.Interfaces.IErrorHandler; - - /** - * The error handler to use in the event `this.#error_handler` cannot handle - * errors. - */ - #default_error_handler = new Drash.ErrorHandler(); - - /** - * Property to track request URLs to resources. This is used so that the - * server does not have to find a resource if it was already matched to a - * previous request's URL. - */ - #request_to_resource_map = new Map< - string, - Drash.Interfaces.IResourceAndParams - >(); - - ////////////////////////////////////////////////////////////////////////////// - // FILE MARKER - CONSTRUCTOR ///////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// - - /** - * @param options - See the interface for the options' schema. - */ - constructor(options: Drash.Interfaces.IServerOptions) { - this.#options = options; - this.#error_handler = new (options.error_handler || Drash.ErrorHandler)(); - - // Compile the application - this.#addServices(); - this.#addResources(); - } - - ////////////////////////////////////////////////////////////////////////////// - // FILE MARKER - GETTERS / SETTERS /////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// - - /** - * Get the full address that this server is running on. - */ - get address(): string { - return `${this.#options.protocol}://${this.#options.hostname}:${this.#options.port}`; - } - - ////////////////////////////////////////////////////////////////////////////// - // FILE MARKER - PUBLIC METHODS ////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// - - /** - * Add the given resource to this server's list of resources. - * - * @param resourceClass - The resource class to instantiate and store in the - * resources map. - */ - public addResource(resourceClass: typeof Drash.Resource): void { - const resource = new resourceClass(); - const patterns: URLPattern[] = []; - resource.paths.forEach((path: string) => { - // Add "{/}?" to match possible trailing slashes too - patterns.push(new URLPattern({ pathname: path + "{/}?" })); - }); - this.#resources.set(this.#resources.size, { - resource, - patterns, - }); - } - - /** - * Close the server. - */ - public async close(): Promise { - try { - this.#server.close(); - await this.#server_promise; - } catch (_error) { - // Do nothing. The server was probably already closed. - } - } - - /** - * Run the server. - */ - public run() { - this.#server = new StdServer({ - hostname: this.#options.hostname, - port: this.#options.port, - handler: async (originalRequest: Request, connInfo: ConnInfo) => { - return await this.#handleRequest(originalRequest, connInfo); - }, - }); - - if (this.#options.protocol === "http") { - this.#server_promise = this.#server.listenAndServe(); - } - - if (this.#options.protocol === "https") { - this.#server_promise = this.#server.listenAndServeTls( - this.#options.cert_file as string, - this.#options.key_file as string, - ); - } - } - - ////////////////////////////////////////////////////////////////////////////// - // FILE MARKER - METHODS - PRIVATE /////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// - - /** - * Add all resources to this server -- instantiating them so that they - * are ready to handle requests at runtime. - */ - #addResources(): void { - this.#options.resources?.forEach((resourceClass: typeof Drash.Resource) => { - this.addResource(resourceClass); - }); - } - - /** - * Add all given services in the options. - */ - #addServices(): void { - if (this.#options.services) { - this.#services = this.#options.services; - } - - this.#services.forEach(async (service: Drash.Interfaces.IService) => { - if (service.runAtStartup) { - await service.runAtStartup({ - server: this, - resources: this.#resources, - }); - } - }); - } - - /** - * Get the resource associated with the given URL and its path params - * associated with it. - * - * @param url - The URL to match to a resource. - * @param resources - The resources map to use to find the resource. - */ - #getResourceAndParams( - url: string, - resources: Drash.Types.ResourcesAndPatternsMap, - ): Drash.Interfaces.IResourceAndParams | undefined { - let resourceAndParams: Drash.Interfaces.IResourceAndParams | undefined = - undefined; - - if (this.#request_to_resource_map.has(url)) { - return this.#request_to_resource_map.get(url)!; - } - - for (const { resource, patterns } of resources.values()) { - for (const pattern of patterns) { - const result = pattern.exec(url); - - // No resource? Check the next one. - if (result === null) { - continue; - } - - // this is the resource we need, and below are the params - const params = new Map(); - for (const key in result.pathname.groups) { - params.set(key, result.pathname.groups[key]); - } - - resourceAndParams = { - resource, - pathParams: params, - }; - - this.#request_to_resource_map.set(url, resourceAndParams); - break; - } - } - - return resourceAndParams; - } - - /** - * Handle the given native request. This request gets wrapped around by a - * `Drash.Request` object. Reason being we want to make sure methods like - * `request.bodyAll()` is available in resources. - * - * @param originalRequest The native request from Deno's internals. - * @param connInfo The connection info from Deno's internals. - * - * @returns A native response. - */ - async #handleRequest( - originalRequest: Request, - connInfo: ConnInfo, - ): Promise { - // Grab resource and path params - const resourceAndParams = this.#getResourceAndParams( - originalRequest.url, - this.#resources, - ) ?? { - resource: null, - pathParams: new Map(), - }; - - const { resource, pathParams } = resourceAndParams; - - // Construct request and response objects to pass to services and resource - // Keep response top level so we can reuse the headers should an error be thrown - // in the try - const response = new Drash.Response(); - - try { - const request = await Drash.Request.create( - originalRequest, - pathParams, - connInfo, - this.#options.request ?? {}, - ); - - // Run server-level services (before we get to the resource) - await runServices( - this.#services, - request, - response, - "runBeforeResource", - ); - - if (request.end_lifecycle) { - return this.#respond(response); - } - - // If no resource found, throw 404. Unable to call class/resource services - // when the class doesn't exist! - if (!resource) { - throw new Drash.Errors.HttpError(404); - } - - // Run resource-level services (before their HTTP method is called) - await runServices( - resource.services.ALL ?? [], - request, - response, - "runBeforeResource", - ); - - if (request.end_lifecycle) { - return this.#respond(response); - } - - // If the method does not exist on the resource, then the method is not - // allowed. So, throw that 405 and GTFO. Unable to call resource method - // services if the method doesn't exist! - const method = request.method - .toUpperCase() as Drash.Types.HttpMethodName; - if (!(method in resource)) { - throw new Drash.Errors.HttpError(405); - } - - // Run resource HTTP method level services (before the HTTP method is - // called) - await runServices( - resource.services[method] ?? [], - request, - response, - "runBeforeResource", - ); - - if (request.end_lifecycle) { - return this.#respond(response); - } - - // Execute the HTTP method on the resource - // Ignoring because we know by now the method exists due to the above check - // deno-lint-ignore ban-ts-comment - // @ts-ignore - await resource[method](request, response); - - // Run resource HTTP method level services (after the HTTP method is - // called) - await runServices( - resource.services[method] ?? [], - request, - response, - "runAfterResource", - ); - - if (request.end_lifecycle) { - return this.#respond(response); - } - - // Run resource-level services (after the HTTP method is called) - await runServices( - resource.services.ALL ?? [], - request, - response, - "runAfterResource", - ); - - if (request.end_lifecycle) { - return this.#respond(response); - } - - // Run server-level services as a last step before returning a response - // that the resource has formed - await runServices( - this.#services, - request, - response, - "runAfterResource", - ); - - if (request.end_lifecycle) { - return this.#respond(response); - } - - const requestAcceptHeader = request.headers.get("accept"); - const responseContentTypeHeader = response.headers.get("content-type"); - - if (requestAcceptHeader && responseContentTypeHeader) { - this.#verifyAcceptHeader( - requestAcceptHeader, - responseContentTypeHeader, - ); - } - - return this.#respond(response); - } catch (e) { - try { - await this.#error_handler.catch(e, originalRequest, response, connInfo); - } catch (e) { - await this.#default_error_handler.catch( - e, - originalRequest, - response, - connInfo, - ); - } - - return this.#respond(response); - } - } - - /** - * Respond to the client making the request. - * - * @param response The response details to use in the `Response` object. - * - * @returns A native Response. - */ - #respond(response: Drash.Response): Response { - if (response.upgraded && response.upgraded_response) { - return response.upgraded_response; - } - - return new Response(response.body, { - headers: response.headers, - statusText: response.statusText, - status: response.status, - }); - } - - /** - * If the request Accept header is present, then make sure the response - * Content-Type header is accepted. - * - * @param requestAcceptHeader - * @param responseContentTypeHeader - */ - #verifyAcceptHeader( - requestAcceptHeader: string, - responseContentTypeHeader: string, - ): void { - if (requestAcceptHeader.includes("*/*")) { - return; - } - - if (requestAcceptHeader.includes(responseContentTypeHeader)) { - return; - } - - throw new Drash.Errors.HttpError( - 406, - "The requested resource is only capable of returning content that is not acceptable according to the request's Accept headers.", - ); - } -} diff --git a/src/http/service.ts b/src/http/service.ts deleted file mode 100644 index 1ee3c6a22..000000000 --- a/src/http/service.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { IService, Request, Response } from "../../mod.ts"; - -export class Service implements IService { - public runAfterResource( - _request: Request, - _response: Response, - ) { - } - - public runBeforeResource( - _request: Request, - _response: Response, - ) { - } -} diff --git a/src/interfaces.ts b/src/interfaces.ts deleted file mode 100644 index da1e408cf..000000000 --- a/src/interfaces.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { - Errors, - Request as DrashRequest, - Resource, - Response, - Server, - Types, -} from "../mod.ts"; -import type { ConnInfo } from "../deps.ts"; -import { RequestOptions } from "./types.ts"; - -// This file contains ALL interfaces used by Drash. As a result, it is a very -// large file. -// -// To make it easier to search this file, it is recommended you search for -// "interface" to "jump" to each interface quickly. -// -// This interfaces in this file are sorted in alphabetical order. -// -// Interfaces: -// - IKeyValuePairs -// - IMime -// - IRequest -// - IRequestOptions -// - IRequestUrl -// - IResource -// - IServer -// - IServerOptions -// - IService - -/** - * An interface to help type key-value pair objects with different values. - * - * Examples: - * - * const myKvpObject: IKeyValuePairs = { - * some_key: "some string", - * }; - * - * const myKvpObject: IKeyValuePairs = { - * some_key: 1, - * }; - * - * interface SomeOtherInterface { - * [key: string]: number; - * } - * const myKvpObject: IKeyValuePairs = { - * some_key: { - * some_other_key: 1 - * }, - * }; - */ -export interface IKeyValuePairs { - [key: string]: T; -} - -/** - * This is used to type a MIME type object. Below are more details on the - * members in this interface. - * - * [key: string] - * The MIME type (e.g., application/json). - * - * [key: string].charset?: string; - * The character encoding of the MIME type. - * - * [key: string].compressible?: boolean; - * Is this MIME type compressible? - * - * [key: string].extensions?: string[] - * An array of extensions that match this MIME type. - * - * [key: string].source?: string; - * where the mime type is defined. If not set, it's probably a custom media type. - * - apache - Apache common media types - * - iana - IANA-defined media types - * - nginx - nginx media types - */ -export interface IMime { - [key: string]: { - charset?: string; - compressible?: boolean; - extensions?: string[]; - source?: string; - }; -} - -/** - * path_parameters - * A key-value string defining the path parameters that were passed in by - * the request. This value is set in resource_handler.ts#getResource(). - * - * uri_paths_parsed - * See IResourcePathsParsed. - * - * services - * The services that will be run before or after one of this resource's HTTP - * methods. - * - * CONNECT() - * DELETE() - * GET() - * HEAD() - * OPTIONS() - * PATCH() - * POST() - * PUT() - * TRACE() - * If a request performs one of the above HTTP methods and the request is - * matched to this resource, then this method will be executed. - */ -export interface IResource { - paths: string[]; - services?: IResourceServices; - // Methods - CONNECT?: (request: DrashRequest, response: Response) => Promise | void; - DELETE?: (request: DrashRequest, response: Response) => Promise | void; - GET?: (request: DrashRequest, response: Response) => Promise | void; - HEAD?: (request: DrashRequest, response: Response) => Promise | void; - OPTIONS?: (request: DrashRequest, response: Response) => Promise | void; - PATCH?: (request: DrashRequest, response: Response) => Promise | void; - POST?: (request: DrashRequest, response: Response) => Promise | void; - PUT?: (request: DrashRequest, response: Response) => Promise | void; - TRACE?: (request: DrashRequest, response: Response) => Promise | void; -} - -export interface IResourceServices { - CONNECT?: IService[]; - DELETE?: IService[]; - GET?: IService[]; - HEAD?: IService[]; - OPTIONS?: IService[]; - PATCH?: IService[]; - POST?: IService[]; - PUT?: IService[]; - TRACE?: IService[]; - ALL?: IService[]; -} - -/** - * Options to help create the server object. - */ -export interface IServerOptions { - // deno-lint-ignore camelcase - cert_file?: string; - hostname: string; - // deno-lint-ignore camelcase - key_file?: string; - request?: RequestOptions; - port: number; - protocol: "http" | "https"; - resources?: typeof Resource[]; - services?: IService[]; - // deno-lint-ignore no-explicit-any camelcase - error_handler?: new (...args: any[]) => IErrorHandler; -} - -export interface IService { - /** - * Method that is ran before a resource is handled - */ - runBeforeResource?: ( - request: DrashRequest, - response: Response, - ) => void | Promise; - - /** - * Method that is ran after a reosurce is handled - */ - runAfterResource?: ( - request: DrashRequest, - response: Response, - ) => void | Promise; - - /** - * Method that runs during server build time. - */ - runAtStartup?: (options: IServiceStartupOptions) => void | Promise; -} - -export interface IServiceStartupOptions { - server: Server; - resources: Types.ResourcesAndPatternsMap; -} - -type Catch = - | (( - error: Errors.HttpError, - request: Request, - response: Response, - ) => void | Promise) - | (( - error: Errors.HttpError, - request: Request, - response: Response, - connInfo: ConnInfo, - ) => void | Promise); - -export interface IErrorHandler { - /** - * Method that gets executed during the request-resource-response lifecycle in - * the event an error is thrown. - */ - catch: Catch; -} - -export interface IResourceAndParams { - /** The instantiated resource class. */ - resource: Resource; - /** The instantiated resource class' path params (if any). */ - pathParams: Map; -} diff --git a/src/modules/README.md b/src/modules/README.md new file mode 100644 index 000000000..d1d1cb086 --- /dev/null +++ b/src/modules/README.md @@ -0,0 +1,25 @@ +# Drash Modules + +All modules in this directory are composed of Drash Core and Drash Standard code.functionality to other modules, are complete frameworks that implement functionality for processing HTTP requests. + +## Module Types + +## Base + +Base modules contain code that provide default functionality to other modules. These can be classes, builders, functions, etc. + +### Native + +Native modules are directories with a `mod.ts` file and any classes related to their functionality. They support all JavaScript native code in their runtime environment. + +For example, `URLPattern` is used in the native `RequestChain` module (located at [`./RequestChain/native.ts`](./RequestChain/native.ts)). If your runtime supports `URLPattern`, then you can use the native version of the `RequestChain` module. If not, you will have to use the polyfill version of the `RequestChain` module (located at [`./RequestChain/polyfill.ts`](./RequestChain/polyfill.ts)) which does not use `URLPattern`. + +### Polyfill + +Polyfill are copies of native modules, but they polyfill any JavaScript native code that is not supported in **all runtimes**. They exist to allow higher cross-compatibility between runtime environments. + +For example, if you want to build a `RequestChain` in Node and `URLPattern` is not supported, then you can: + +- use the polyfill version of the `RequestChain`; +- build your application just like you would if you were using the native version of `RequestChain`; and +- switch to Deno (which supports `URLPattern`) at a later time without having to migrate your code before switching to Deno. diff --git a/src/modules/RequestChain/mod.native.ts b/src/modules/RequestChain/mod.native.ts new file mode 100644 index 000000000..172c19ded --- /dev/null +++ b/src/modules/RequestChain/mod.native.ts @@ -0,0 +1,47 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +// Imports > Standard +import { WithParams } from "../../standard/handlers/RequestParamsParser.ts"; + +// Imports > Modules +import { RequestChain } from "../base/RequestChain.ts"; + +type HttpRequest = WithParams; + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { HTTPError } from "../../core/errors/HTTPError.ts"; +export { Resource } from "../../core/http/Resource.ts"; +export { Middleware } from "../../standard/http/Middleware.ts"; +export type { HttpRequest as Request }; + +/** + * Get the builder that builds an HTTP request chain. + */ +export function builder() { + return RequestChain + .builder() + // @ts-ignore URLPattern is available when using the Deno extension, but we + // should not force using a the Deno extension just to accomodate the build + // process having this API. Therefore, it is ignored. + .urlPatternClass(URLPattern); +} diff --git a/src/modules/RequestChain/mod.polyfill.ts b/src/modules/RequestChain/mod.polyfill.ts new file mode 100644 index 000000000..407f87960 --- /dev/null +++ b/src/modules/RequestChain/mod.polyfill.ts @@ -0,0 +1,41 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +// Imports > Standard +import { URLPatternPolyfill } from "../../standard/polyfill/URLPatternPolyfill.ts"; + +// Imports > Modules +import { RequestChain } from "../base/RequestChain.ts"; + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { HTTPError } from "../../core/errors/HTTPError.ts"; +export { Middleware } from "../../standard/http/Middleware.ts"; +export { Resource } from "../../core/http/Resource.ts"; + +/** + * Get the builder that builds an HTTP request chain. + */ +export function builder() { + return RequestChain + .builder() + .urlPatternClass(URLPatternPolyfill); +} diff --git a/src/modules/base/Chain.ts b/src/modules/base/Chain.ts new file mode 100644 index 000000000..5344c57fd --- /dev/null +++ b/src/modules/base/Chain.ts @@ -0,0 +1,63 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +// Imports > Standard +import { AbstractChainBuilder } from "../../standard/chains/AbstractChainBuilder.ts"; + +/** + * Builder for building a chain of handlers. + */ +class Builder extends AbstractChainBuilder { + /** + * Chain all handlers together. + * @returns The first handler. + */ + public build() { + const firstHandler = this.link(); + + if (!firstHandler) { + throw new Error( + "Chain.Builder: No handlers set. Did you forget to call `this.handlers()`?", + ); + } + + return firstHandler; + } +} + +class Chain { + /** + * @see {@link Builder} for implementation. + */ + static Builder = Builder; + + /** + * Get the builder for building a chain of handlers. + * @returns An instance of the builder. + */ + static builder(): Builder { + return new Builder(); + } +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { type Builder, Chain }; diff --git a/src/modules/base/RequestChain.ts b/src/modules/base/RequestChain.ts new file mode 100644 index 000000000..c1cd09240 --- /dev/null +++ b/src/modules/base/RequestChain.ts @@ -0,0 +1,94 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +// Imports > Core +import { Resource } from "../../core/http/Resource.ts"; + +// Imports > Standard +import { AbstractChainBuilder } from "../../standard/chains/AbstractChainBuilder.ts"; +import { RequestParamsParser } from "../../standard/handlers/RequestParamsParser.ts"; +import { RequestValidator } from "../../standard/handlers/RequestValidator.ts"; +import { ResourceCaller } from "../../standard/handlers/ResourceCaller.ts"; +import { ResourceNotFoundHandler } from "../../standard/handlers/ResourceNotFoundHandler.ts"; +import { + ResourcesIndex, + type URLPatternClass, +} from "../../standard/handlers/ResourcesIndex.ts"; + +type ResourceClasses = typeof Resource | typeof Resource[]; + +/** + * Builder for building a chain of handlers. + */ +class Builder extends AbstractChainBuilder { + #resources: ResourceClasses[] = []; + #URLPatternClass?: URLPatternClass; + + public build() { + if (!this.#URLPatternClass) { + throw new Error( + `\`this.urlPatternClass(Resource)\` not called. Cannot create RequestChain without a \`URLPattern\`-like class.`, + ); + } + + const firstHandler = new RequestValidator(); + + this + .handler(firstHandler) + .handler(new ResourcesIndex(this.#URLPatternClass, ...this.#resources)) + .handler(new ResourceNotFoundHandler()) + .handler(new RequestParamsParser()) + .handler(new ResourceCaller()) + .link(); + + return firstHandler; + } + + /** + * Add resources to this chain. + * @param resources + * @returns This instance for method chaining. + */ + public resources(...resources: ResourceClasses[]) { + this.#resources = resources; + return this; + } + + /** + * Set the handler that matches requests to resources. + * @param handler + * @returns + */ + public urlPatternClass(urlPatternClass: URLPatternClass): this { + this.#URLPatternClass = urlPatternClass; + return this; + } +} + +class RequestChain { + static builder(): Builder { + return new Builder(); + } +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { type Builder, RequestChain }; diff --git a/src/modules/builders/RequestBuilder.ts b/src/modules/builders/RequestBuilder.ts new file mode 100644 index 000000000..d0b063fa2 --- /dev/null +++ b/src/modules/builders/RequestBuilder.ts @@ -0,0 +1,70 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +class RequestBuilder { + #request: RequestInit = {}; + #path = ""; + + path(path: string) { + this.#path = path; + return this; + } + + get() { + this.#request.method = "get"; + return this; + } + + post() { + this.#request.method = "post"; + return this; + } + + put() { + this.#request.method = "put"; + return this; + } + + patch() { + this.#request.method = "patch"; + return this; + } + + delete() { + this.#request.method = "delete"; + return this; + } + + build() { + return new Request(this.#path, this.#request); + } +} + +/** + * Get a {@link Request} builder. + * + * @returns A response builder. + * + * @see {@link RequestBuilder} for implementation details. + */ +export function request() { + return new RequestBuilder(); +} diff --git a/src/modules/builders/ResourceBuilder.ts b/src/modules/builders/ResourceBuilder.ts new file mode 100644 index 000000000..43cc0fbfb --- /dev/null +++ b/src/modules/builders/ResourceBuilder.ts @@ -0,0 +1,183 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { RequestMethod } from "../../core/types/RequestMethod.ts"; +import { Resource } from "../../core/http/Resource.ts"; +import { Builder } from "../../standard/builders/Builder.ts"; + +type ResourceHTTPMethod = (input: I) => O; + +export class ResourceBuilder implements Builder { + #name?: string; + #paths: string[] = []; + #http_methods: Partial> = {}; + + constructor(name?: string) { + this.#name = name; + } + + public paths(paths: string[]) { + this.#paths = paths; + return this; + } + + public CONNET(cb: ResourceHTTPMethod) { + this.#http_methods.CONNECT = cb; + return this; + } + + public DELETE(cb: ResourceHTTPMethod) { + this.#http_methods.DELETE = cb; + return this; + } + + public GET(cb: ResourceHTTPMethod) { + this.#http_methods.GET = cb; + return this; + } + + public HEAD(cb: ResourceHTTPMethod) { + this.#http_methods.HEAD = cb; + return this; + } + + public OPTIONS(cb: ResourceHTTPMethod) { + this.#http_methods.OPTIONS = cb; + return this; + } + + public PATCH(cb: ResourceHTTPMethod) { + this.#http_methods.PATCH = cb; + return this; + } + + public POST(cb: ResourceHTTPMethod) { + this.#http_methods.POST = cb; + return this; + } + + public PUT(cb: ResourceHTTPMethod) { + this.#http_methods.PUT = cb; + return this; + } + + public TRACE(cb: ResourceHTTPMethod) { + this.#http_methods.TRACE = cb; + return this; + } + + build(): typeof Resource { + const paths = this.#paths; + const httpMethods = this.#http_methods; + + const resource = class extends Resource { + public paths = paths; + + public CONNECT(input: unknown) { + if (!httpMethods.CONNECT) { + return super.CONNECT(input); + } + + return httpMethods.CONNECT(input); + } + + public DELETE(input: unknown) { + if (!httpMethods.DELETE) { + return super.DELETE(input); + } + + return httpMethods.DELETE(input); + } + + public GET(input: unknown) { + if (!httpMethods.GET) { + return super.GET(input); + } + + return httpMethods.GET(input); + } + + public HEAD(input: unknown) { + if (!httpMethods.HEAD) { + return super.HEAD(input); + } + + return httpMethods.HEAD(input); + } + + public OPTIONS(input: unknown) { + if (!httpMethods.OPTIONS) { + return super.OPTIONS(input); + } + + return httpMethods.OPTIONS(input); + } + + public PATCH(input: unknown) { + if (!httpMethods.PATCH) { + return super.PATCH(input); + } + + return httpMethods.PATCH(input); + } + + public POST(input: unknown) { + if (!httpMethods.POST) { + return super.POST(input); + } + + return httpMethods.POST(input); + } + + public PUT(input: unknown) { + if (!httpMethods.PUT) { + return super.PUT(input); + } + + return httpMethods.PUT(input); + } + + public TRACE(input: unknown) { + if (!httpMethods.TRACE) { + return super.TRACE(input); + } + + return httpMethods.TRACE(input); + } + }; + + Object.defineProperty(resource, "name", { + value: this.#name ?? "BuiltResource", + }); + + return resource; + } +} + +/** + * Get a {@link Resource} builder. + * @param name The name of the resource. + * @returns A `Resource` builder. + * @see {@link ResourceBuilder} for implementation details. + */ +export function resource(name?: string) { + return new ResourceBuilder(name); +} diff --git a/src/modules/builders/ResponseBuilder.ts b/src/modules/builders/ResponseBuilder.ts new file mode 100644 index 000000000..16bd7b9bc --- /dev/null +++ b/src/modules/builders/ResponseBuilder.ts @@ -0,0 +1,149 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Builder } from "../../standard/builders/Builder.ts"; + +/** + * A builder to help build a `Response` object. This is useful if the response's + * data needs to be modified throughout a lifecycle, but not instantiated into a + * `Response` object until required. + * + * @example + * ```ts + * const builder = new ResponseBuilder(); + * + * // This call ... + * const resA = builder + * .headers({ + * "x-some-header": "Some Value" + * "x-some-other-header": "Some Other Value", + * }) + * .body("Nope.") + * .status(400) + * .statusText("Bad Request") + * .build(); + * + * // ... results in the same response as this call ... + * const resB = new Response("Nope.", { + * status: 400, + * statusText: "Bad Request", + * headers: { + * "x-some-header": "Some Value" + * "x-some-other-header": "Some Other Value", + * } + * }) + * ``` + */ +class ResponseBuilder implements Builder { + protected response_body: BodyInit | null = null; + protected response_headers = new Headers(); + protected response_init: ResponseInit = { + status: 200, + statusText: "OK", + }; + + /** + * Set the body the built response will use as its {@link BodyInit}. + * + * @param body See {@link BodyInit}. + * + * @returns This instance. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Response/body} + */ + body(body: BodyInit | null) { + this.response_body = body; + return this; + } + + /** + * Set the headers (using key-value pairs) the built response will use as its + * {@link ResponseInit.headers}. + * + * @param headers A key-value pair of headers where the key is the header name + * and the value is the header value. + * + * @returns This instance. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Response/headers} + */ + headers(headers: Record) { + for (const [k, v] of Object.entries(headers)) { + this.response_headers.set(k, v); + } + + return this; + } + + /** + * Set the status the built response will use as its + * {@link ResponseInit.status}. + * + * @param status See {@link ResponseInit.status}. + * + * @returns This instance. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Response/status} + */ + status(status: number) { + this.response_init.status = status; + return this; + } + + /** + * Set the {@link ResponseInit.statuText} property. + * + * @param statusText See {@link ResponseInit.statuText}. + * + * @returns This instance. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Response/statusText} + */ + statusText(statusText: string) { + this.response_init.statusText = statusText; + return this; + } + + /** + * @returns A `Response` object using the values set in this builder. + */ + build() { + return new Response(this.response_body, { + ...this.response_init, + headers: this.response_headers, + }); + } +} + +/** + * Get a {@link Response} builder. + * + * @returns A response builder. + * + * @see {@link ResponseBuilder} for implementation details. + */ +function response() { + return new ResponseBuilder(); +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { response, ResponseBuilder }; diff --git a/src/modules/middleware/AcceptHeader/mod.ts b/src/modules/middleware/AcceptHeader/mod.ts new file mode 100644 index 000000000..c98069b27 --- /dev/null +++ b/src/modules/middleware/AcceptHeader/mod.ts @@ -0,0 +1,150 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Status } from "../../../core/http/response/Status.ts"; +import { HTTPError } from "../../../core/errors/HTTPError.ts"; +import { Middleware } from "../../../standard/http/Middleware.ts"; + +type Options = { + /** Throw if the response's Content-Type header does not match the request's Accept header? */ + throw_if_accept_header_mismatched?: boolean; + /** Throw if the request's Accept header is missing? */ + throw_if_accept_header_missing?: boolean; +}; + +type Context = { + request: Request; + response: Response; + /** A flag each handler function can use to see if it should or should not process the context further. */ + done?: boolean; +}; + +const defaultOptions: Options = { + throw_if_accept_header_mismatched: true, + throw_if_accept_header_missing: true, +}; + +class AcceptHeaderMiddleware extends Middleware { + #options: Options; + + /** + * Construct the middleware that handles Accept headers. + * + * @param options (Optional) Options to control the middleware's behavior. See + * {@link Options} for details. + */ + constructor(options: Options = defaultOptions) { + super(); + + // TODO(crookse) Check if the options are correct before setting them + this.#options = { + ...defaultOptions, + ...options, + }; + } + + public ALL(request: Request) { + return Promise + .resolve() + .then(() => this.handleIfAcceptHeaderMissing(request)) + .then(() => super.next(request)) + .then((response) => ({ request, response })) + .then((context) => this.handleHeaders(context)) + .then((context) => this.sendResponse(context)); + } + + protected handleHeaders(context: Context) { + if (context.done) { + return context; + } + + const reqHeader = context.request.headers.get("accept")?.toLowerCase(); + + // Request accepts anything so send it + if (reqHeader && reqHeader.includes("*/*")) { + context.done = true; + return context; + } + + const resHeader = context.response.headers.get("content-type") + ?.toLowerCase(); + + if (!resHeader) { + throw new HTTPError( + Status.InternalServerError, + "The server did not generate a response with a Content-Type header", + ); + } + + const [contentType, _charset] = resHeader.split(";"); + + if ( + (reqHeader !== resHeader) || + !reqHeader.includes(contentType) + ) { + // Only throw if the option is enabled + if (this.#options.throw_if_accept_header_mismatched) { + throw new HTTPError( + Status.UnprocessableEntity, + "The server did not generate a response matching the request's Accept header", + ); + } + } + + context.done = true; + + return context; + } + + protected handleIfAcceptHeaderMissing(request: Request) { + if ( + this.#options.throw_if_accept_header_missing && + !request.headers.get("accept") + ) { + throw new HTTPError( + Status.BadRequest, + `Accept header is required`, + ); + } + } + + protected sendResponse(context: Context) { + return context.response; + } +} + +/** + * Get the middleware class that handles Accept headers. + * + * @param options (Optional) Options to control the middleware's behavior. See + * {@link Options} for details. + * + * @returns The middleware class that can be instantiated. When it is + * instantiated, it instantiates with the provided `options`. If no options are + * provided, it uses its default options. + */ +function AcceptHeader(options: Options = defaultOptions) { + return new AcceptHeaderMiddleware(options); +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { AcceptHeader, AcceptHeaderMiddleware, defaultOptions, type Options }; diff --git a/src/modules/middleware/CORS/mod.ts b/src/modules/middleware/CORS/mod.ts new file mode 100644 index 000000000..b6b8c542f --- /dev/null +++ b/src/modules/middleware/CORS/mod.ts @@ -0,0 +1,391 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +// Imports > Core +import { + RequestMethod, + ResponseStatus, + ResponseStatusName, +} from "../../../core/Types.ts"; +import { Method } from "../../../core/http/request/Method.ts"; +import { StatusCode } from "../../../core/http/response/StatusCode.ts"; +import { StatusDescription } from "../../../core/http/response/StatusDescription.ts"; +import { Header } from "../../../core/http/Header.ts"; + +// Imports > Standard +import { Middleware } from "../../../standard/http/Middleware.ts"; +import { Status } from "../../../core/http/response/Status.ts"; + +// TODO(crookse) +// - [ ] Alphabetize +// - [ ] Doc blocks +// - [ ] Parse the Acces-Control-Request-Method header according to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method +// - [ ] Handle responses that affect the Vary header +// - [ ] Compare to https://fetch.spec.whatwg.org/#concept-cors-check + +type Options = { + access_control_allow_headers?: string[]; + access_control_allow_methods?: RequestMethod[]; + access_control_allow_origin?: (string | RegExp)[]; + access_control_allow_credentials?: boolean; + access_control_expose_headers?: string[]; + access_control_max_age?: number; + options_success_status?: ResponseStatus; +}; + +const defaultOptions: Options = { + access_control_allow_headers: [], + access_control_allow_methods: [ + "GET", + "HEAD", + "PUT", + "PATCH", + "POST", + "DELETE", + ], + access_control_allow_origin: ["*"], + access_control_allow_credentials: false, + access_control_expose_headers: [], + // access_control_max_age: 5, // MDN says default 5, so should this be 5? + options_success_status: Status.NoContent, +}; + +class CORSMiddleware extends Middleware { + #options: Options; + + /** + * Construct the middleware that handles CORS requests. + */ + constructor(options: Options = defaultOptions) { + super(); + + // TODO(crookse) Ensure options are correct before setting them + this.#options = { + ...defaultOptions, + ...options, + }; + } + + public ALL(request: Request): Response { + const method = request.method.toUpperCase(); + + if (method === Method.OPTIONS) { + return this.OPTIONS(request); + } + + const headers = this.getCorsResponseHeaders(request); + + // Send the request to the requested resource + const resourceResponse = super.next(request); + + // Merge the resource's response headers with the CORs response headers + if (resourceResponse.headers) { + for (const [key, value] of resourceResponse.headers.entries()) { + this.appendHeaderValue({ key, value }, headers); + } + } + + return new Response(resourceResponse.body, { + status: resourceResponse.status || StatusCode.OK, + statusText: resourceResponse.statusText || StatusDescription.OK, + headers, + }); + } + + public OPTIONS(request: Request): Response { + const headers = this.getCorsResponseHeaders(request); + this.setPreflightHeaders(request, headers); + + let status: ResponseStatus = Status.NoContent; + + if (this.#options.options_success_status) { + for (const [name, statusCode] of Object.entries(StatusCode)) { + if (this.#options.options_success_status?.code === statusCode) { + status = Status[name as ResponseStatusName]; + } + } + } + + return new Response(null, { + status: status.code, + statusText: status.description, + headers, + }); + } + + /** + * @param request + * @param headers The headers that will receive the preflight headers. + */ + protected setPreflightHeaders(request: Request, headers: Headers): void { + this.setHeaderAllowHeaders(request, headers); + this.setHeaderAllowMethods(headers); + this.setHeaderMaxAge(headers); + + // Body is always empty, so the Content-Length header should denote that. + // This setting helps align with the following RFC: + // + // https://www.rfc-editor.org/rfc/rfc9112#section-6.2 + // + // In summary, this header must be set to "0" to help clients (e.g., other + // servers, CDNs, browsers) know where this message ends when they receive + // it. + headers.set("content-length", "0"); + } + + /** + * @param header The header (in key-value pair format) to add to the current + * headers. + * @param headers The current headers. + */ + protected appendHeaderValue( + header: { key: string; value: string }, + headers: Headers, + ): void { + const currentValue = headers.get(header.key); + + // If the header does not exist yet, then add it + if (!currentValue) { + headers.set(header.key, header.value); + return; + } + + // For vary headers, we need to check if values were already supplied. If so, + // then we skip adding them. + if ( + header.value.toLowerCase() === "vary" && + !currentValue.toLowerCase().includes(header.value) + ) { + headers.set(header.key, `${headers.get(header.key)}, ${header.value}`); + } + } + + /** + * @param request + * @returns The value that should be set in the + * `Access-Control-Allow-Origin` header. + */ + protected getAllowOriginHeaderValue(request: Request): string | null { + if ( + !this.#options.access_control_allow_origin || + !this.#options.access_control_allow_origin.length + ) { + return "*"; + } + + const origin = request.headers.get("origin"); + + if (this.#options.access_control_allow_origin.includes("*")) { + return "*"; + } + + if (!origin) { + return null; + } + + if (this.#options.access_control_allow_origin.includes(origin)) { + return origin; + } + + for (const allowedOrigin of this.#options.access_control_allow_origin) { + if (allowedOrigin instanceof RegExp) { + if (allowedOrigin.test(origin)) { + return origin; + } + } + } + + return null; + } + + /** + * All CORS-related response headers start out with the values defined in + * this method. + * @param request + * @returns The initial set of headers. + */ + protected getCorsResponseHeaders(request: Request): Headers { + const headers = new Headers(); + + this.setHeaderExposeHeaders(headers); + this.setHeaderAllowOrigin(request, headers); + this.setHeaderAllowCredentials(headers); + + return headers; + } + + /** + * @param headers + * @returns + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials} + */ + protected setHeaderAllowCredentials(headers: Headers): void { + if (!this.#options.access_control_allow_credentials) { + return; + } + + headers.set(Header.AccessControlAllowCredentials, "true"); + } + + /** + * @param headers + * @returns + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers} + */ + protected setHeaderAllowHeaders(req: Request, headers: Headers): void { + let allowedHeaders = req.headers.get( + Header.AccessControlRequestHeaders, + ); + + if (allowedHeaders) { + this.appendHeaderValue( + { + key: Header.Vary, + value: Header.AccessControlRequestHeaders, + }, + headers, + ); + } + + if ( + this.#options.access_control_allow_headers && + this.#options.access_control_allow_headers.length + ) { + if (allowedHeaders) { + allowedHeaders = this.#options.access_control_allow_headers.concat( + allowedHeaders.split(","), + ).join(","); + } else { + allowedHeaders = this.#options.access_control_allow_headers.join(","); + } + + headers.set(Header.AccessControlAllowHeaders, allowedHeaders); + } + } + + /** + * @param headers + * @returns + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods} + */ + protected setHeaderAllowMethods(headers: Headers) { + if ( + !this.#options.access_control_allow_methods || + !this.#options.access_control_allow_methods.length + ) { + return; + } + + headers.set( + Header.AccessControlAllowMethods, + this.#options.access_control_allow_methods.join(","), + ); + } + + /** + * @param req + * @param headers + * @returns + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin} + */ + protected setHeaderAllowOrigin(request: Request, headers: Headers): void { + if ( + !this.#options.access_control_allow_origin || + !this.#options.access_control_allow_origin.length + ) { + return; + } + + if (this.#options.access_control_allow_origin.includes("*")) { + headers.set(Header.AccessControlAllowOrigin, "*"); + return; + } + + const origin = this.getAllowOriginHeaderValue(request); + headers.set( + Header.AccessControlAllowOrigin, + origin ? origin : "false", + ); + this.appendHeaderValue( + { key: Header.Vary, value: "Origin" }, + headers, + ); + } + + /** + * @param headers + * @returns + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers} + */ + protected setHeaderExposeHeaders(headers: Headers): void { + if ( + !this.#options.access_control_expose_headers || + !this.#options.access_control_expose_headers.length + ) { + return; + } + + headers.set( + Header.AccessControlExposeHeaders, + this.#options.access_control_expose_headers.join(","), + ); + } + + /** + * @param headers + * @returns + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age} + */ + protected setHeaderMaxAge(headers: Headers) { + if (typeof this.#options.access_control_max_age !== "number") { + return; + } + + headers.set( + Header.AccessControlMaxAge, + `${this.#options.access_control_max_age}`, + ); + } +} + +/** + * Get the middleware class that handles CORS requests. + * + * @param options (Optional) Options to control the middleware's behavior. See + * {@link Options} for details. + * + * @returns The middleware class that can be instantiated. When it is + * instantiated, it instantiates with the provided `options`. If no options are + * provided, it uses its default options. + */ +function CORS(options: Options = defaultOptions) { + return new CORSMiddleware(options); +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { CORS, CORSMiddleware, defaultOptions, type Options }; diff --git a/src/modules/middleware/ETag/ETagResponse.ts b/src/modules/middleware/ETag/ETagResponse.ts new file mode 100644 index 000000000..f4e56069e --- /dev/null +++ b/src/modules/middleware/ETag/ETagResponse.ts @@ -0,0 +1,132 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { ResponseBuilder } from "../../builders/ResponseBuilder.ts"; + +type Options = { + /** Should the ETag contain the weak "W/" directive? See "W/" under {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#directives}. */ + weak?: boolean; + /** */ + max_hash_length?: number; +}; + +/** + * A `Response` decorator that attaches the following behaviors: + * + * - Response building + * - ETag and ETag-related header setting + * - Response body hashing + */ +class ETagResponseBuilder extends ResponseBuilder { + /** The response to decorate. */ + protected decoratee: Response; + + /** The options this decorator will use when processing the response. */ + #default_options: Options = { + weak: false, + max_hash_length: 27, + }; + + /** + * Decorate a `Response` with X-RateLimit-* header capabilities. + * + * @param response The response to decorate. + */ + constructor(response: Response) { + super(); + this.decoratee = response; + } + + /** + * Add the ETag header to the response. + * + * @param options See {@link Options}. + * + * @returns A `Promise` with `this` instance as the resulting value. + */ + public addETagHeader(options: Options = this.#default_options) { + return this + .etagHeader(options) + .then((etag) => { + this.headers({ + etag, + }); + + return this; + }); + } + + /** + * Create the ETag header from the response's body. + * + * @param options See {@link Options}. + * + * @returns A `Promise` with the ETag header as the resulting value. + */ + public etagHeader(options: Options = this.#default_options) { + return this + .hash(options.max_hash_length) + .then((hash) => { + return this.decoratee + .clone() + .text() + .then((text) => text.length.toString(16)) + .then((text) => `"${text}-${hash}"`); + }) + .then((header) => { + if (options.weak) { + return "W/" + header; + } + + return header; + }); + } + + /** + * Create a base-64 ASCII encoded string from text representation of the + * response's body with a max length of the given `maxLength`. + * + * @param maxLength (Optional) The max length of the hash. + * + * @returns A `Promise` with the hash as he resulting value. + */ + public hash(maxLength = 27) { + return this.decoratee + .clone() + .text() + .then((text) => btoa(text.substring(0, maxLength))); + } +} + +/** + * Decorate the provided `response` with this module's decorator. + * + * @param response The response to decorate. + * + * @returns The decorated response. + */ +function response(response: Response) { + return new ETagResponseBuilder(response); +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { ETagResponseBuilder, type Options, response }; diff --git a/src/modules/middleware/ETag/mod.ts b/src/modules/middleware/ETag/mod.ts new file mode 100644 index 000000000..4b340355d --- /dev/null +++ b/src/modules/middleware/ETag/mod.ts @@ -0,0 +1,274 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Header } from "../../../core/http/Header.ts"; +import { Middleware } from "../../../standard/http/Middleware.ts"; +import { response } from "./ETagResponse.ts"; +import { ResponseStatus, ResponseStatusName } from "../../../core/Types.ts"; +import { Status } from "../../../core/http/response/Status.ts"; +import { StatusCode } from "../../../core/http/response/StatusCode.ts"; +import { StatusDescription } from "../../../core/http/response/StatusDescription.ts"; +import { HTTPError } from "../../../core/errors/HTTPError.ts"; + +type Options = { + /** The maximum length of the ETag header. */ + etag_max_length?: number; + /** Add the "W/" directive to all generated ETag headers? */ + weak?: boolean; +}; + +type Context = { + request: Request; + response: Response; + /** The Etag header for this context's response. */ + etag?: string; + /** A flag each handler function can use to see if it should or should not process the context further. */ + done?: boolean; +}; + +type CachedResource = { + [Header.ETag]: string; + [Header.LastModified]: string; +}; + +const defaultOptions: Options = { + etag_max_length: 27, + weak: false, +}; + +class ETagMiddleware extends Middleware { + #cache: Record = {}; + #default_etag = '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"'; + #options: Options; + + /** + * Construct the middleware that handles ETag and ETag-related headers. + * + * @param options (Optional) See {@link Options}. + */ + constructor(options: Options = defaultOptions) { + super(); + + // TODO(crookse) Check if the options are correct before setting them. + this.#options = { + ...defaultOptions, + ...options, + }; + } + + public ALL(request: Request): Promise { + return Promise + .resolve() + .then(() => this.handleEtagMatchesRequestIfMatchHeader(request)) + .then(() => super.next(request)) + .then((response) => ({ request, response })) + .then((context) => this.handleIfResponseEmpty(context)) + .then((context) => this.createEtagHeader(context)) + .then((context) => + this.handleEtagMatchesRequestIfNoneMatchHeader(context) + ) + .then((context) => this.sendResponse(context)); + } + + /** + * Create the ETag header from the response's body. + * @param context The context containing all data this middleware requires. + * @returns The context + */ + protected createEtagHeader(context: Context) { + if (context.done) { + return context; + } + + return response(context.response) + .etagHeader(this.#options) + .then((etag) => { + context.etag = etag; + return context; + }); + } + + protected createLastModifiedHeader() { + return new Date().toUTCString(); + } + + protected getCacheKey(request: Request) { + const { method, url } = request; + return method + ";" + url; + } + + protected handleEtagMatchesRequestIfNoneMatchHeader(context: Context) { + if (context.done) { + return context; + } + + if (!context.etag) { + return context; + } + + if (context.request.headers.get(Header.IfNoneMatch) === context.etag) { + // Edge case: We need to check if the etag was already cached. If we do + // not do this, then we could end up sending a 304 for a response that + // this middleware has not processed yet. This can happen if a client + // sends a request with an etag (for shits and giggles) and the response + // to that request's etag matches. In this case, we need to send the + // repsonse as if it was being requested for the first time. After that, + // we cache the etag so subsequent requests result in a 304 response. + if (this.requestIsCached(context.request)) { + context.response = new Response(null, { + status: StatusCode.NotModified, + statusText: StatusDescription.NotModified, + headers: { + [Header.ETag]: context.etag, + [Header.LastModified]: this + .#cache[this.getCacheKey(context.request)][Header.LastModified], + }, + }); + + context.done = true; + } + } + + return context; + } + + protected handleEtagMatchesRequestIfMatchHeader(request: Request) { + if (!this.requestIsCached(request)) { + return; + } + + if (!request.headers.get(Header.IfMatch)) { + return; + } + + const cacheKey = this.getCacheKey(request); + const ifMatchHeader = request.headers.get(Header.IfMatch); + + // If the headers do not match, then a mid-air collision will happen if + // we do not error out + if (ifMatchHeader !== this.#cache[cacheKey][Header.ETag]) { + throw new HTTPError(Status.PreconditionFailed); + } + } + + protected handleIfResponseEmpty(context: Context) { + if (context.done) { + return context; + } + + const contentLength = context.response.headers.get(Header.ContentLength); + + // This method should only handle empty responses. That is, a response with + // no body. So gtfo if you got one. + if ( + context.response.body || + (context.response.body !== null) || + (contentLength && contentLength !== "0") + ) { + return context; + } + + let lastModified; + + // If etag is already present, then use the previous last-modified value + if (context.request.headers.get(Header.IfNoneMatch)) { + lastModified = this.#cache[this.#default_etag][Header.LastModified]; + } else { + // Otherwise, create a new "Last-Modified" value + lastModified = this.createLastModifiedHeader(); + this.#cache[this.getCacheKey(context.request)][Header.LastModified] = + lastModified; + } + + context.response = new Response(null, { + status: StatusCode.NotModified, + statusText: StatusDescription.NotModified, + headers: { + [Header.ETag]: this.#default_etag, + [Header.LastModified]: lastModified, + }, + }); + + context.done = true; + + return context; + } + + protected requestIsCached(request: Request) { + if (this.getCacheKey(request) in this.#cache) { + return true; + } + + return false; + } + + protected sendResponse(context: Context) { + if (context.done) { + return context.response; + } + + if (!context.etag) { + throw new Error("Error generating ETag"); + } + + const newLastModifiedDate = this.createLastModifiedHeader(); + this.#cache[this.getCacheKey(context.request)] = { + [Header.ETag]: context.etag, + [Header.LastModified]: newLastModifiedDate, + }; + + const responseStatusCode = context.response.status; + let status: ResponseStatus = Status.OK; + + for (const [name, statusCode] of Object.entries(StatusCode)) { + if (responseStatusCode === statusCode) { + status = Status[name as ResponseStatusName]; + } + } + + return new Response(context.response.body, { + status: status.code, + statusText: status.description, + headers: { + [Header.ETag]: context.etag, + [Header.LastModified]: newLastModifiedDate, + }, + }); + } +} + +/** + * Get the middleware class that handles ETag and ETag-related headers. + * + * @param options (Optional) Options to control the middleware's behavior. See + * {@link Options} for details. + * + * @returns The middleware class that can be instantiated. When it is + * instantiated, it instantiates with the provided `options`. If no options are + * provided, it uses its default options. + */ +function ETag(options: Options = defaultOptions) { + return new ETagMiddleware(options); +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { defaultOptions, ETag, ETagMiddleware, type Options }; diff --git a/src/modules/middleware/RateLimiter/RateLimitResponse.ts b/src/modules/middleware/RateLimiter/RateLimitResponse.ts new file mode 100644 index 000000000..217094a78 --- /dev/null +++ b/src/modules/middleware/RateLimiter/RateLimitResponse.ts @@ -0,0 +1,139 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Status } from "../../../core/http/response/Status.ts"; +import { ResponseBuilder } from "../../builders/ResponseBuilder.ts"; +import { Header as CoreHeader } from "../../../core/http/Header.ts"; + +const Header = { + XRateLimitLimit: "X-RateLimit-Limit", + XRateLimitRemaining: "X-RateLimit-Remaining", + XRateLimitReset: "X-RateLimit-Reset", + XRetryAfter: "X-Retry-After", +} as const; + +/** + * A `Response` decorator that attaches the following behaviors: + * + * - Response building + * - X-RateLimit-* header setting + */ +class RateLimitResponseBuilder extends ResponseBuilder { + /** The response to decorate. */ + protected decoratee: Response; + + /** + * Decorate a `Response` with X-RateLimit-* header capabilities. + * + * @param response The response to decorate. + */ + constructor(response: Response) { + super(); + + this.decoratee = response; + } + + /** + * Add the `x-ratelimit-*` headers to the response. + * + * @param values The values for the `x-ratelimit-*` headers. + * + * @returns This for further method chaining. + */ + public addRateLimitHeaders(values: { + limit: number; + remaining: number; + reset: number; + retry_after: Date; + }) { + const headers = { + [CoreHeader.RetryAfter]: values.retry_after.toUTCString(), + [Header.XRateLimitLimit]: values.limit.toString(), + [Header.XRateLimitRemaining]: values.remaining.toString(), + [Header.XRateLimitReset]: values.reset.toString(), + }; + + this.headers(headers); + + return this; + } + + /** + * @returns A `Response` object using the values set in this class. Notes: + * - The values set in this class take precedence + * - Any value not set in this class will be replaced with the wrapped + * response's value + */ + public build(): Response { + let status = this.response_init.status; + if (!status) { + status = this.decoratee.status; + } + + let statusText; + for (const [_key, { code, description }] of Object.entries(Status)) { + if (status === code) { + statusText = description; + break; + } + } + if (!statusText) { + statusText = this.decoratee.statusText; + } + + let body = this.response_body; + if (!body) { + body = this.decoratee.body; + } + + // Keep the decoratee's headers intact and append any headers that were set + // in this class. Headers in this class that have the same name as headers + // in the decoratee's headers will be overwritten. + const headers = this.decoratee.headers ?? new Headers(); + + for (const [key, value] of this.response_headers) { + headers.set(key, value); + } + + const r = new Response(body, { + status, + statusText, + headers, + }); + + return r; + } +} + +/** + * Decorate the provided `response` with this module's decorator. + * + * @param response The response to decorate. + * + * @returns The decorated response. + */ +function response(response: Response) { + return new RateLimitResponseBuilder(response); +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { RateLimitResponseBuilder, response }; diff --git a/src/modules/middleware/RateLimiter/RateLimitedClient.ts b/src/modules/middleware/RateLimiter/RateLimitedClient.ts new file mode 100644 index 000000000..0ea4514f3 --- /dev/null +++ b/src/modules/middleware/RateLimiter/RateLimitedClient.ts @@ -0,0 +1,129 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +type Options = { + /** + * TODO(crookse) Description + */ + max_requests_allowed_in_time_window: number; + + /** + * TODO(crookse) Description + */ + rate_limit_time_window_length: number; +}; + +const defaultOptions: Options = { + max_requests_allowed_in_time_window: 3, // Client can make 3 requests + rate_limit_time_window_length: 60000, // 1 minute +}; + +class RateLimitedClient { + #num_requests_made = 0; // Client's request count starts at 0 + + /** + * Used to calculate the client's current request's time against the client's + * rate limit window to see if rate limiting should occur. + * + * Initially set to -1 to denote a "note set" value. + */ + #current_request_time = -1; + + /** + * @see {@link Options} + */ + #options: Options; + + #rate_limit_window_end_time: number; + + /** + * @param options See {@link Options}. + */ + constructor(options: Options = defaultOptions) { + // TODO(crookse) Ensure options are valid before setting + this.#options = options; + + this.#rate_limit_window_end_time = this.#getRateLimitEndTimeFromNow(); + } + + get num_requests_made() { + return this.#num_requests_made; + } + + get current_request_time() { + return this.#current_request_time; + } + + get hit_request_limit() { + return this.#num_requests_made > + this.#options.max_requests_allowed_in_time_window; + } + + get rate_limit_window_end_time() { + return this.#rate_limit_window_end_time; + } + + get rate_limit_window_time_elapsed() { + return this.#current_request_time >= this.#rate_limit_window_end_time; + } + + get requests_remaining() { + const remaining = this.#options.max_requests_allowed_in_time_window - + this.#num_requests_made; + return remaining <= 0 ? 0 : remaining; + } + + get max_requests_allowed_in_time_window() { + return this.#options.max_requests_allowed_in_time_window; + } + + incrementRequestCount() { + this.#num_requests_made += 1; + + return this; + } + + setCurrentRequestTimeToNow() { + this.#current_request_time = Date.now(); + + return this; + } + + resetTimeWindow() { + this.#rate_limit_window_end_time = this.#getRateLimitEndTimeFromNow(); + + return this; + } + + resetRequestCount(): this { + this.#num_requests_made = 0; + + return this; + } + + #getRateLimitEndTimeFromNow() { + return Date.now() + this.#options.rate_limit_time_window_length; + } +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { RateLimitedClient }; diff --git a/src/modules/middleware/RateLimiter/RateLimiterErrorResponse.ts b/src/modules/middleware/RateLimiter/RateLimiterErrorResponse.ts new file mode 100644 index 000000000..87773fc91 --- /dev/null +++ b/src/modules/middleware/RateLimiter/RateLimiterErrorResponse.ts @@ -0,0 +1,36 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { ResponseStatus } from "../../../core/Types.ts"; +import { HTTPError } from "../../RequestChain/mod.native.ts"; + +class RateLimiterErrorResponse extends HTTPError { + readonly response: Response; + + constructor(status: ResponseStatus, response: Response) { + super(status); + this.response = response; + } +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { RateLimiterErrorResponse }; diff --git a/src/modules/middleware/RateLimiter/mod.ts b/src/modules/middleware/RateLimiter/mod.ts new file mode 100644 index 000000000..348fc8f24 --- /dev/null +++ b/src/modules/middleware/RateLimiter/mod.ts @@ -0,0 +1,336 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +// Imports > Core +import { HTTPError } from "../../../core/errors/HTTPError.ts"; +import { Header } from "../../../core/http/Header.ts"; +import { Status } from "../../../core/http/response/Status.ts"; +import { StatusCode } from "../../../core/http/response/StatusCode.ts"; +import { StatusDescription } from "../../../core/http/response/StatusDescription.ts"; + +// Imports > Standard +import { Middleware } from "../../../standard/http/Middleware.ts"; +import { response } from "./RateLimitResponse.ts"; + +// Imports > Modules +import { RateLimitedClient } from "./RateLimitedClient.ts"; + +// Imports > Local +import { RateLimiterErrorResponse } from "./RateLimiterErrorResponse.ts"; + +type PreNextContext = { + /** + * The current client being processed. + * + * If `null`, an attempt was tried to identify the client, but no identifer + * (e.g., the `client_id_header_name` option) was provided to help identify + * and track the client. + */ + client: RateLimitedClient | null; + + request: Request; +}; + +type Context = PreNextContext & { + response: Response; +}; + +type Options = { + /** + * How much time (in milliseconds) a client is allocated the `max_requests`. + */ + rate_limit_time_window_length: number; + + /** + * Number of requests a client is allowed within the `rate_limit_window`. + */ + max_requests: number; + + /** + * The header containing the connection information (e.g., a client's address) + * that is subject to rate limiting. + */ + client_id_header_name: string; + + /** + * Should the middleware throw an HTTP 400 error if the connection information + * header is missing? + */ + throw_if_connection_header_name_missing: boolean; +}; + +const defaultOptions: Options = { + client_id_header_name: "x-drash-ratelimit-client-id", + max_requests: 3, + rate_limit_time_window_length: 60000, // 1 minute + throw_if_connection_header_name_missing: true, +}; + +class RateLimiterMiddleware extends Middleware { + #options: Options; + #request_store: Record = {}; + + /** + * Construct the middleware that handles rate limiting requests. + * + * @param options See {@link Options}. + */ + constructor(options: Options = defaultOptions) { + super(); + this.#options = options; + } + + public ALL(request: Request): Promise { + return Promise + .resolve() + .then(() => this.setClientInContext(request)) + .then((context) => this.throwIfRateLimited(context)) + .then((context) => this.sendToNext(context)) + .then((context) => this.sendResponse(context)); + } + + /** + * Send the response in the context object to the caller. + * + * @param context See {@link Context}. + * + * @returns The response in the context object. + * + * @throws An {@link HTTPError} with a 500 status code if no response is set + * in the context object. + */ + protected sendResponse(context: Context) { + const client = context.client; + + // No client, just send the repsonse that was set in the context object + if (!client) { + return context.response; + } + + if (!context.response) { + throw new HTTPError( + Status.InternalServerError, + `The server failed to generate a response`, + ); + } + + const ret = this + .#getDecoratedResponse(context.response, client) + .build(); + + return ret; + } + + /** + * Send the request to the next handler for a response. + * + * @param context See {@link PreNextContext}. + * + * @returns The context object (with the `response` field set) to be handled + * by any further handlers. + */ + protected sendToNext(context: PreNextContext) { + return Promise + .resolve() + .then(() => this.next(context.request)) + .then((response) => { + return { + ...context, + response, + }; + }); + } + + /** + * Set the client in the context object. + * + * @param request The request containing the header that holds the unique + * identifier of the client. The header should be the value that was provided + * in `options.client_id_header_name`. + * + * @returns The context object (with the `request` and `client` fields set). + * + * @throws An {@link HTTPError} if the `option.client_id_header_name` value + * is falsy and `option.throw_if_connection_header_name_missing` is `true`. + */ + protected setClientInContext(request: Request) { + const clientId = request.headers.get(this.#options.client_id_header_name); + + if (!clientId) { + // The value of `client` is `null` to let further processes know this + // method tried to identify the client, but could not due to the missing + // header. + const context = { + client: null, + request, + }; + + if (this.#options.throw_if_connection_header_name_missing) { + throw new HTTPError( + Status.BadRequest, + `Request header '${this.#options.client_id_header_name}' is required`, + ); + } + + return context; + } + + let client = this.#request_store[clientId]; + + // No client yet? This must be a new client, so start tracking it. + if (!client) { + this.#request_store[clientId] = new RateLimitedClient({ + max_requests_allowed_in_time_window: this.#options.max_requests, + rate_limit_time_window_length: + this.#options.rate_limit_time_window_length, + }); + + client = this.#request_store[clientId]; + } + + // We consider the client making a request when it gets to this point in + // this middleware + client.incrementRequestCount(); + + // Track this client's request time as of right now so that rate limit times + // can be calculated against this time + client.setCurrentRequestTimeToNow(); + + return { + client, + request, + }; + } + + /** + * Check the client to see if it should be rate limited. + * + * @param context See {@link PreNextContext}. + * + * @returns The context to be handled by any further handlers. + * + * @throws An {@link RateLimiterErrorResponse} if the client is rate limited. + */ + protected throwIfRateLimited(context: PreNextContext) { + // No rate limit? Move along... clients are allowed to make as many requests + // as they want. + if (!this.#options.max_requests) { + return context; + } + + const client = context.client; + + // No client was identified? Guess the rate limit is a no go. + if (client === null) { + return context; + } + + // If the client is making this request past their rate limit window time, + // then their request should be allowed through. Also, their counters should + // be reset so their subsequent requests in this current rate limit window + // are handled properly. + if (client.rate_limit_window_time_elapsed) { + client + .resetRequestCount() + .resetTimeWindow(); + + return context; + } + + // If the client has not exceeded their max number of requests, then allow + // the client through. Since we already checked if the client was past their + // rate limit window above and reset their counters, we do not reset + // anything here. Otherwise, the client could end up making more requests + // than allowed. + if (!client.hit_request_limit) { + return context; + } + + // If we get here, that means the client hit their request limit and their + // rate limit window has not elapsed. In other words, the have made too + // many requests at this point, so we build and throw the error below. + + // Create a basic response ... + const body = + `Too many requests. Next request can be made after the time set in the Retry-After header.`; + const basicResponse = new Response(body); + + // ... and decorate it + const errorResponse = this + .#getDecoratedResponse(basicResponse, client) + .status(StatusCode.TooManyRequests) + .statusText(StatusDescription.TooManyRequests) + .build(); + + // Chains are expected to be set up to catch this error and handle it + // accordingly -- either using the `errorResponse` as it is built abve or + // doing something else that meets their requirements. + // + // We do not return `new Response("Too many requests", { status: 429 })` + // because that can: + // + // - cause the chain to receive it in a `.then()` block; or + // - cause the chain (depending on how it is set up) to be filtered by other + // processes (e.g., other middleware). + // + // The above process could result in the `errorResponse` built here to be + // modified unexpectedly (e.g., by other middleware). To prevent this side + // effect, we throw an error to "terminate this request early and now" and + // let it be handled by error handling processes only. + throw new RateLimiterErrorResponse( + Status.TooManyRequests, + errorResponse, + ); + } + + #getDecoratedResponse(decoratee: Response, client: RateLimitedClient) { + const retryAfter = client.rate_limit_window_end_time; + + return response(decoratee) + .addRateLimitHeaders({ + limit: client.max_requests_allowed_in_time_window, + remaining: client.requests_remaining, + reset: client.rate_limit_window_end_time, + retry_after: new Date(retryAfter), + }) + .headers({ + [Header.Date]: (new Date()).toUTCString(), + }); + } +} + +/** + * Get the middleware class that handles rate limiting requests. + * + * @param options (Optional) Options to control the middleware's behavior. See + * {@link Options} for details. + * + * @returns The middleware class that can be instantiated. When it is + * instantiated, it instantiates with the provided `options`. If no options are + * provided, it uses its default options. + */ +function RateLimiter(options: Options) { + return new RateLimiterMiddleware(options); +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { type Options, RateLimiter, RateLimiterMiddleware }; diff --git a/src/services/cors/deps.ts b/src/services/cors/deps.ts deleted file mode 100644 index 36dc1ae23..000000000 --- a/src/services/cors/deps.ts +++ /dev/null @@ -1 +0,0 @@ -export { vary } from "https://raw.githubusercontent.com/dmpjs/vary/v1.1.0/mod.ts"; diff --git a/src/services/csrf/csrf.ts b/src/services/csrf/csrf.ts deleted file mode 100644 index b16b72e92..000000000 --- a/src/services/csrf/csrf.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Errors, IService, Request, Response, Service } from "../../../mod.ts"; -import { createHash, v4 } from "./deps.ts"; - -/** - * This allows us to pass the TS compiler, so we can add properties to a method that uses it. See `csrf` method below - */ -interface F { - (): void; - token: string; -} - -const primaryToken = createHash("sha512"); -primaryToken.update(v4.generate()); -const primaryTokenString = primaryToken.toString(); - -type Options = { - cookie?: boolean; -}; - -const defaultOptions: Options = { - cookie: false, -}; - -export class CSRFService extends Service implements IService { - readonly #options: Options; - - public token: string = primaryTokenString; // or const csrf = Object.assign(oldCsrf, { token: primaryToken.toString() }) - - constructor(options: Options = defaultOptions) { - super(); - this.#options = options; - } - - runBeforeResource(request: Request, _response: Response) { - let requestToken: string | null = ""; - - if (this.#options.cookie === true) { - requestToken = request.getCookie("X-CSRF-TOKEN"); - } else { - requestToken = request.headers.get("X-CSRF-TOKEN"); - } - - if (!requestToken) { - throw new Errors.HttpError( - 400, - "No CSRF token was passed in", - ); - } - - if (requestToken !== primaryTokenString) { - throw new Errors.HttpError( - 403, - "The CSRF tokens do not match", - ); - } - } -} diff --git a/src/services/csrf/deps.ts b/src/services/csrf/deps.ts deleted file mode 100644 index a50dd9e38..000000000 --- a/src/services/csrf/deps.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { createHash } from "https://deno.land/std@0.158.0/hash/mod.ts"; -export { v4 } from "https://deno.land/std@0.158.0/uuid/mod.ts"; diff --git a/src/services/dexter/deps.ts b/src/services/dexter/deps.ts deleted file mode 100644 index e5ac816be..000000000 --- a/src/services/dexter/deps.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ConsoleLogger } from "https://deno.land/x/unilogger@v1.1.0/mod.ts"; -export type { LoggerConfigs } from "https://deno.land/x/unilogger@v1.1.0/src/logger.ts"; diff --git a/src/services/etag/deps.ts b/src/services/etag/deps.ts deleted file mode 100644 index a89121cdd..000000000 --- a/src/services/etag/deps.ts +++ /dev/null @@ -1 +0,0 @@ -export { createHash } from "https://deno.land/std@0.175.0/node/crypto.ts"; diff --git a/src/services/etag/etag.ts b/src/services/etag/etag.ts deleted file mode 100644 index 19e15ec0f..000000000 --- a/src/services/etag/etag.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { IService, Request, Response, Service } from "../../../mod.ts"; -import { createHash } from "./deps.ts"; - -export class ETagService extends Service implements IService { - #options: { weak: boolean }; - - #etags: Map = new Map(); - - constructor(options: { weak: boolean } = { weak: false }) { - super(); - this.#options = options; - } - - runAfterResource(request: Request, response: Response) { - // if response body is empty, send a default etag - if ( - response.body === null || - (typeof response.body === "string" && response.body.length === 0) - ) { - // when it's empty, we want to set a default etag - - // but if etag is already present on request, send a 304 - const header = '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"'; - if (request.headers.get("if-none-match")) { - response.status = 304; - response.body = null; - const existingModifiedDate = this.#etags.get(header) as string; // it will always be set due to this conditional - response.headers.set("last-modified", existingModifiedDate); - } else { // set the NEW default etag - const date = new Date().toUTCString(); - response.headers.set("last-modified", date); - this.#etags.set(header, date); - } - response.headers.set("etag", header); - return; - } - - // generate the hash - const body = typeof response.body === "string" - ? new TextEncoder().encode(response.body) - : response.body as Uint8Array; - const hash = createHash("sha1").update(body, "utf8").digest("base64") - .toString().substring(0, 27); - const len = body.byteLength; - - // create the etag value to use - let header = `"${len.toString(16)}-${hash}"`; - if (this.#options.weak === true) { - header = "W/" + header; - } - - response.headers.set("etag", header); - - // check if request already has an etag, if so, - // if its the same as the generated etag from the response body - const incomingRequestIfNoneMatchValue = request.headers.get( - "if-none-match", - ); - if (incomingRequestIfNoneMatchValue) { // request inc already has an etag set - // so check if body hash matches - if (header === incomingRequestIfNoneMatchValue) { - // no need to send body, send not modified - response.status = 304; - response.body = null; - response.headers.set( - "last-modified", - this.#etags.get(header) as string, - ); - return; - } else { - // res body is new - response.status = 200; - const date = new Date().toUTCString(); - this.#etags.set(header, date); - response.headers.set("last-modified", date); - return; - } - } - - // else request doesnt have a new one so generate everything from scratch - response.status = 200; - const date = new Date().toUTCString(); - this.#etags.set(header, date); - response.headers.set("last-modified", date); - } -} diff --git a/src/services/graphql/deps.ts b/src/services/graphql/deps.ts deleted file mode 100644 index b94512fa6..000000000 --- a/src/services/graphql/deps.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * as Drash from "../../../mod.ts"; -export * as GraphQL from "https://cdn.skypack.dev/graphql@15.5.0?dts"; -// TODO ENSURE DMM UPDATES THIS -export { renderPlaygroundPage } from "https://deno.land/x/gql@1.1.2/graphiql/render.ts"; -export type { - ExecutionResult, -} from "https://cdn.skypack.dev/graphql@15.5.0?dts"; // TODO ENSURE DMM UPDATES THIS diff --git a/src/services/graphql/graphql_resource.ts b/src/services/graphql/graphql_resource.ts deleted file mode 100644 index 2b9cccd85..000000000 --- a/src/services/graphql/graphql_resource.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Drash } from "./deps.ts"; - -export class GraphQLResource extends Drash.Resource { - public paths = ["/graphql"]; - - public GET(_request: Drash.Request, _response: Drash.Response): void { - // This is intentionally left blank. - // - // This is only defined to allow GET requests to the front-end playground. - // Without this, Drash will throw a 405 Method Not Allowed error when - // requesting to view the playground at /graphql. - } - - public POST(_request: Drash.Request, _response: Drash.Response): void { - // This is intentionally left blank. - // - // This is only defined so that POST requests to this resource can be - // processed. Without this, Drash will throw a 405 Method Not Allowed error - // when clients try to make GraphQL queries. - } -} diff --git a/src/services/rate_limiter/rate_limiter.ts b/src/services/rate_limiter/rate_limiter.ts deleted file mode 100644 index cbce70f79..000000000 --- a/src/services/rate_limiter/rate_limiter.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Errors, Request, Response, Service } from "../../../mod.ts"; -import { MemoryStore } from "./memory_store.ts"; - -interface IOptions { - /** - * How long (in milliseconds) an IP is allocated the `max_requests`. - */ - timeframe: number; - - /** - * Number of requests an IP is allowed within the `timeframe`. - */ - // deno-lint-ignore camelcase - max_requests: number; -} - -export class RateLimiterService extends Service { - readonly #options: IOptions; - - #memory_store: MemoryStore; - - ////////////////////////////////////////////////////////////////////////////// - // FILE MARKER - CONSTRUCTOR ///////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// - - constructor(options: IOptions) { - super(); - this.#options = options; - this.#memory_store = new MemoryStore(this.#options.timeframe); - } - - ////////////////////////////////////////////////////////////////////////////// - // FILE MARKER - PUBLIC METHODS ////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// - - public cleanup(): void { - this.#memory_store.cleanup(); - } - - public runBeforeResource(request: Request, response: Response): void { - const key = (request.conn_info.remoteAddr as Deno.NetAddr).hostname; - const { current, reset_time: resetTime } = this.#memory_store.increment( - key, - ); - const requestsRemaining = Math.max(this.#options.max_requests - current, 0); - - response.headers.set( - "X-RateLimit-Limit", - this.#options.max_requests.toString(), - ); - - response.headers.set( - "X-RateLimit-Remaining", - requestsRemaining.toString(), - ); - - response.headers.set("Date", new Date().toUTCString()); - - response.headers.set( - "X-RateLimit-Reset", - Math.ceil(resetTime.getTime() / 1000).toString(), - ); - - if (this.#options.max_requests && current > this.#options.max_requests) { - const retryAfter = Math.ceil(this.#options.timeframe / 1000).toString() + - "s"; - response.headers.set( - "X-Retry-After", - retryAfter, - ); - throw new Errors.HttpError( - 429, - `Too Many Requests. Please try again after ${retryAfter}.`, - ); - } - } -} diff --git a/src/services/resource_loader/deps.ts b/src/services/resource_loader/deps.ts deleted file mode 100644 index acf25bb64..000000000 --- a/src/services/resource_loader/deps.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { walkSync } from "https://deno.land/std@0.175.0/fs/mod.ts"; -export { join } from "https://deno.land/std@0.175.0/path/mod.ts"; diff --git a/src/services/response_time/response_time.ts b/src/services/response_time/response_time.ts deleted file mode 100644 index b152459b9..000000000 --- a/src/services/response_time/response_time.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { IService, Request, Response, Service } from "../../../mod.ts"; - -export class ResponseTimeService extends Service implements IService { - #startTime = 0; - - #endTime = 0; - - runBeforeResource() { - this.#startTime = new Date().getTime(); - } - - runAfterResource(_request: Request, response: Response) { - this.#endTime = new Date().getTime(); - const time = (this.#endTime - this.#startTime) + "ms"; - response.headers.set("X-Response-Time", time.toString()); - } -} diff --git a/src/services/tengine/tengine.ts b/src/services/tengine/tengine.ts deleted file mode 100644 index 0575b310f..000000000 --- a/src/services/tengine/tengine.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Request, Response, Service } from "../../../mod.ts"; -import { Jae } from "./jae.ts"; - -interface IOptions { - // deno-lint-ignore camelcase - views_path: string; -} - -export class TengineService extends Service { - readonly #options: IOptions; - #template_engine: Jae; - - constructor(options: IOptions) { - super(); - this.#options = options; - this.#template_engine = new Jae(this.#options.views_path); - } - - runBeforeResource(_request: Request, response: Response) { - response.headers.set("Content-Type", "text/html"); - response.render = (filepath: string, data: unknown) => { - return this.#template_engine.render(filepath, data); - }; - } -} diff --git a/src/standard/_unstable/Mixin.ts b/src/standard/_unstable/Mixin.ts new file mode 100644 index 000000000..7f78d7d9d --- /dev/null +++ b/src/standard/_unstable/Mixin.ts @@ -0,0 +1,153 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +// Types + +// Imports > Core + +// Imports > Standard + +/** + * An instance of the given `T` generic. + */ +export type Constructor = new ( + ...args: A +) => T; + +/** + * Inspiration taken from: + * + * https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type + */ +type Intersect = IntersectEval extends (k: infer I) => void ? I : never; + +/** + * Inspiration taken from: + * + * https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type + */ +type IntersectEval = U extends unknown ? (k: U) => void : never; + +/** + * Build a {@link Mixin} + */ +class MixinBuilder< + B extends Constructor, + C extends Constructor[], +> { + #base_class?: B; + #classes?: C; + + baseClass(BaseClass: B): this { + this.#base_class = BaseClass; + return this; + } + + classes(classes: C): this { + this.#classes = classes; + return this; + } + + build(): Constructor< + Intersect & InstanceType<[...C][number]>> + > { + if (!this.#base_class) { + throw new Error(`Cannot create mixin without the base class.`); + } + + if (!this.#classes) { + throw new Error(`Cannot create mixin without classes to mix.`); + } + + this.#classes.forEach((baseCtor) => { + Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => { + Object.defineProperty( + this.#base_class!.prototype, + name, + Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || + Object.create(null), + ); + }); + }); + + // console.log(this.#classes[0].prototype); + + return this.#base_class as Constructor< + Intersect & InstanceType<[...C][number]>> + >; + } +} + +/** + * Build a mixin class by merging `...classes` into `BasClass`. The result is a mixin as shown in the TypeScript documentation pages at: + * + * {@link https://www.typescriptlang.org/docs/handbook/mixins.html}. + * + * @param BaseClass The class to extend. + * @param classes ({@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters Rest Parameter}) All classes to merge into the `BaseClass`. + * @returns The `BaseClass` with all classes merged into it. + * + * @example + * ```ts + * class SomeBaseClass { + * public run() { + * return "We are running."; + * } + * } + * + * class Hello { + * public hello() { + * return "hello"; + * } + * } + * + * class World { + * public world() { + * return "world"; + * } + * } + * + * class HelloWorld extends BaseClass(SomeBaseClass).With( + * Hello, + * World, + * ) { + * public helloWorld() { + * return this.hello() + this.world(); + * } + * } + * ``` + */ +export function Mixin< + BaseClass extends Constructor, + Constructors extends Constructor[], +>( + BaseClass: BaseClass, + ...constructors: Constructors +): Constructor< + Intersect & InstanceType<[...Constructors][number]>> +> { + const builder = new MixinBuilder(); + + return builder + .baseClass(BaseClass) + .classes(constructors) + .build(); +} diff --git a/src/services/cors/cors.ts b/src/standard/_unstable/services/deno/cors/cors.ts similarity index 68% rename from src/services/cors/cors.ts rename to src/standard/_unstable/services/deno/cors/cors.ts index 8a964683f..ee85c7201 100644 --- a/src/services/cors/cors.ts +++ b/src/standard/_unstable/services/deno/cors/cors.ts @@ -1,4 +1,25 @@ -import { IService, Request, Response, Service } from "../../../mod.ts"; +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Interfaces, Request, Response } from "../../../mod.deno.ts"; import { vary } from "./deps.ts"; type ValueOrArray = T | Array>; @@ -40,7 +61,7 @@ const isOriginAllowed = (reqOrigin: string, origin: OriginOption): boolean => { } }; -export class CORSService extends Service implements IService { +export class CORSService implements Interfaces.Service { #config: CORSMiddlewareConfig; constructor({ @@ -52,7 +73,6 @@ export class CORSService extends Service implements IService { maxAge, preflight = true, }: CORSMiddlewareConfig = {}) { - super(); this.#config = { origin, credentials, @@ -81,9 +101,9 @@ export class CORSService extends Service implements IService { // Always set Vary header // https://github.com/rs/cors/issues/10 vary( - (header: string) => response.headers.get(header) || "", + (header: string) => response.headers_init?.get(header) || "", (header: string, value: string) => { - response.headers.set(header, value); + response.headers({ [header]: value }); }, "origin", ); @@ -114,59 +134,52 @@ export class CORSService extends Service implements IService { ) { // Simple Cross-Origin Request, Actual Request, and Redirects if (this.#config.origin && typeof this.#config.origin === "string") { - response.headers.set( - "Access-Control-Allow-Origin", - this.#config.origin, - ); + response.headers({ + "Access-Control-Allow-Origin": this.#config.origin, + }); } if (this.#config.credentials && this.#config.credentials === true) { - response.headers.set( - "Access-Control-Allow-Credentials", - "true", - ); + response.headers({ + "Access-Control-Allow-Credentials": "true", + }); } if ( this.#config.exposeHeaders && typeof this.#config.exposeHeaders === "string" ) { - response.headers.set( - "Access-Control-Expose-Headers", - this.#config.exposeHeaders, - ); + response.headers({ + "Access-Control-Expose-Headers": this.#config.exposeHeaders, + }); } } else if (this.#config.preflight) { // Preflight Request if (this.#config.origin && typeof this.#config.origin === "string") { - response.headers.set( - "Access-Control-Allow-Origin", - this.#config.origin, - ); + response.headers({ + "Access-Control-Allow-Origin": this.#config.origin, + }); } if (this.#config.credentials && this.#config.credentials === true) { - response.headers.set( - "Access-Control-Allow-Credentials", - "true", - ); + response.headers({ + "Access-Control-Allow-Credentials": "true", + }); } if (this.#config.maxAge) { - response.headers.set( - "Access-Control-Max-Age", - this.#config.maxAge.toString(), - ); + response.headers({ + "Access-Control-Max-Age": this.#config.maxAge.toString(), + }); } if ( this.#config.allowMethods && typeof this.#config.allowMethods === "string" ) { - response.headers.set( - "Access-Control-Allow-Methods", - this.#config.allowMethods, - ); + response.headers({ + "Access-Control-Allow-Methods": this.#config.allowMethods, + }); } if (!this.#config.allowHeaders) { @@ -176,13 +189,12 @@ export class CORSService extends Service implements IService { } if (this.#config.allowHeaders) { - response.headers.set( - "Access-Control-Allow-Headers", - this.#config.allowHeaders, - ); + response.headers({ + "Access-Control-Allow-Headers": this.#config.allowHeaders, + }); } - response.status = 204; + response.status(204); } } } diff --git a/src/standard/_unstable/services/deno/cors/deps.ts b/src/standard/_unstable/services/deno/cors/deps.ts new file mode 100644 index 000000000..d19b41667 --- /dev/null +++ b/src/standard/_unstable/services/deno/cors/deps.ts @@ -0,0 +1,22 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +export { vary } from "https://raw.githubusercontent.com/dmpjs/vary/v1.1.0/mod.ts"; diff --git a/src/standard/_unstable/services/deno/csrf/csrf.ts b/src/standard/_unstable/services/deno/csrf/csrf.ts new file mode 100644 index 000000000..304e57fb2 --- /dev/null +++ b/src/standard/_unstable/services/deno/csrf/csrf.ts @@ -0,0 +1,78 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Errors } from "../../../src/core/http/errors.ts"; +import * as Interfaces from "../../../src/core/Interfaces.ts"; +import { createHash, v4 } from "./deps.ts"; + +/** + * This allows us to pass the TS compiler, so we can add properties to a method that uses it. See `csrf` method below + */ +interface F { + (): void; + token: string; +} + +const primaryToken = createHash("sha512"); +primaryToken.update(v4.generate()); +const primaryTokenString = primaryToken.toString(); + +type Options = { + cookie?: boolean; +}; + +const defaultOptions: Options = { + cookie: false, +}; + +export class CSRFService implements Interfaces.Service { + readonly #options: Options; + + public token: string = primaryTokenString; // or const csrf = Object.assign(oldCsrf, { token: primaryToken.toString() }) + + constructor(options: Options = defaultOptions) { + this.#options = options; + } + + runBeforeResource(request: Interfaces.NativeRequest) { + let requestToken: string | null | undefined = ""; + + if (this.#options.cookie === true) { + requestToken = request.cookie("X-CSRF-TOKEN"); + } else { + requestToken = request.headers.get("X-CSRF-TOKEN"); + } + + if (!requestToken) { + throw new HTTPError( + 400, + "No CSRF token was passed in", + ); + } + + if (requestToken !== primaryTokenString) { + throw new HTTPError( + 403, + "The CSRF tokens do not match", + ); + } + } +} diff --git a/src/standard/_unstable/services/deno/csrf/deps.ts b/src/standard/_unstable/services/deno/csrf/deps.ts new file mode 100644 index 000000000..65c39c4e7 --- /dev/null +++ b/src/standard/_unstable/services/deno/csrf/deps.ts @@ -0,0 +1,23 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +export { createHash } from "https://deno.land/std@0.158.0/hash/mod.ts"; +export { v4 } from "https://deno.land/std@0.158.0/uuid/mod.ts"; diff --git a/src/standard/_unstable/services/deno/dexter/deps.ts b/src/standard/_unstable/services/deno/dexter/deps.ts new file mode 100644 index 000000000..d08f4d946 --- /dev/null +++ b/src/standard/_unstable/services/deno/dexter/deps.ts @@ -0,0 +1,23 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +export { ConsoleLogger } from "https://deno.land/x/unLogger@v1.1.0/mod.ts"; +export type { LoggerConfigs } from "https://deno.land/x/unLogger@v1.1.0/src/logger.ts"; diff --git a/src/services/dexter/dexter.ts b/src/standard/_unstable/services/deno/dexter/dexter.ts similarity index 72% rename from src/services/dexter/dexter.ts rename to src/standard/_unstable/services/deno/dexter/dexter.ts index 8ee061043..04a514c40 100644 --- a/src/services/dexter/dexter.ts +++ b/src/standard/_unstable/services/deno/dexter/dexter.ts @@ -1,5 +1,26 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + import { ConsoleLogger, LoggerConfigs } from "./deps.ts"; -import { IService, Request, Response, Service } from "../../../mod.ts"; +import { Request, Response } from "../../../mod.deno.ts"; /** * See @@ -33,7 +54,7 @@ interface IDexterConfigs extends LoggerConfigs { * }) * ``` */ -export class DexterService extends Service implements IService { +export class DexterService { public configs: IDexterConfigs; #timeEnd = 0; @@ -43,8 +64,6 @@ export class DexterService extends Service implements IService { public logger: ConsoleLogger; constructor(configs: IDexterConfigs = {}) { - super(); - configs = { level: configs.level ?? "all", datetime: configs.datetime || true, @@ -59,7 +78,7 @@ export class DexterService extends Service implements IService { this.configs = configs; // If a user has defined specific strings we allow, ensure they are set - // before we hand it off to unilogger to process into a log statement + // before we hand it off to unLogger to process into a log statement if (configs?.datetime !== false) { this.configs.tag_string += "{datetime} |"; this.configs.tag_string_fns!.datetime = () => diff --git a/src/standard/_unstable/services/deno/etag/deps.ts b/src/standard/_unstable/services/deno/etag/deps.ts new file mode 100644 index 000000000..bb7bab898 --- /dev/null +++ b/src/standard/_unstable/services/deno/etag/deps.ts @@ -0,0 +1,22 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +export { createHash } from "https://deno.land/std@0.175.0/node/crypto.ts"; diff --git a/src/standard/_unstable/services/deno/etag/etag.ts b/src/standard/_unstable/services/deno/etag/etag.ts new file mode 100644 index 000000000..8c5d9c30d --- /dev/null +++ b/src/standard/_unstable/services/deno/etag/etag.ts @@ -0,0 +1,111 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { NativeRequest } from "../../../src/core/Interfaces.ts"; +import { createHash } from "./deps.ts"; +import { NativeResponseBuilder } from "../../../src/builders/native_response_builder.ts"; + +export class ETagService { + #options: { weak: boolean }; + + #etags: Map = new Map(); + + constructor(options: { weak: boolean } = { weak: false }) { + this.#options = options; + } + + runAfterResource(request: NativeRequest, response: Response) { + const responseBuilder = new NativeResponseBuilder(); + + // if response body is empty, send a default etag + if ( + response.body === null || + (typeof response.body === "string" && response.body.length === 0) + ) { + // when it's empty, we want to set a default etag + + // but if etag is already present on request, send a 304 + const header = '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"'; + if (request.headers.get("if-none-match")) { + responseBuilder.status(304).body(null); + const existingModifiedDate = this.#etags.get(header) as string; // it will always be set due to this conditional + responseBuilder.headers({ + "last-modified": existingModifiedDate, + }); + } else { // set the NEW default etag + const date = new Date().toUTCString(); + responseBuilder.headers({ "last-modified": date }); + this.#etags.set(header, date); + } + responseBuilder.headers({ "etag": header }); + return; + } + + // generate the hash + const body = typeof responseBuilder.body_init === "string" + ? new TextEncoder().encode(responseBuilder.body_init) + : responseBuilder.body_init as Uint8Array; + const hash = createHash("sha1").update(body, "utf8").digest("base64") + .toString().substring(0, 27); + const len = body.byteLength; + + // create the etag value to use + let header = `"${len.toString(16)}-${hash}"`; + if (this.#options.weak === true) { + header = "W/" + header; + } + + responseBuilder.headers({ "etag": header }); + + // check if request already has an etag, if so, + // if its the same as the generated etag from the response body + const incomingRequestIfNoneMatchValue = request.headers.get( + "if-none-match", + ); + if (incomingRequestIfNoneMatchValue) { // request inc already has an etag set + // so check if body hash matches + if (header === incomingRequestIfNoneMatchValue) { + // no need to send body, send not modified + responseBuilder.status(304).body(null); + responseBuilder.headers({ + "last-modified": this.#etags.get(header) as string, + }); + return; + } else { + // res body is new + responseBuilder.status(200); + const date = new Date().toUTCString(); + this.#etags.set(header, date); + responseBuilder.headers({ "last-modified": date }); + responseBuilder.body(responseBuilder.body_init ?? response.body); + return responseBuilder.build(); + } + } + + // else request doesnt have a new one so generate everything from scratch + responseBuilder.status(200); + const date = new Date().toUTCString(); + this.#etags.set(header, date); + responseBuilder.headers({ "last-modified": date }); + responseBuilder.body(responseBuilder.body_init ?? response.body); + return responseBuilder.build(); + } +} diff --git a/src/standard/_unstable/services/deno/graphql/deps.ts b/src/standard/_unstable/services/deno/graphql/deps.ts new file mode 100644 index 000000000..34ba49339 --- /dev/null +++ b/src/standard/_unstable/services/deno/graphql/deps.ts @@ -0,0 +1,28 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +export * as Drash from "../../../mod.deno.ts"; +export * as GraphQL from "https://cdn.skypack.dev/graphql@15.5.0?dts"; +// TODO ENSURE DMM UPDATES THIS +export { renderPlaygroundPage } from "https://deno.land/x/gql@1.1.2/graphiql/render.ts"; +export type { + ExecutionResult, +} from "https://cdn.skypack.dev/graphql@15.5.0?dts"; // TODO ENSURE DMM UPDATES THIS diff --git a/src/services/graphql/graphql.ts b/src/standard/_unstable/services/deno/graphql/graphql.ts similarity index 62% rename from src/services/graphql/graphql.ts rename to src/standard/_unstable/services/deno/graphql/graphql.ts index 015b1c55c..db924feec 100644 --- a/src/services/graphql/graphql.ts +++ b/src/standard/_unstable/services/deno/graphql/graphql.ts @@ -1,3 +1,25 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Errors, Interfaces as I, Request, Types as T } from "@mod.ts"; import { Drash, ExecutionResult, @@ -25,7 +47,7 @@ interface GraphQLOptions { * It has been modified to suit Drash's needs in order to utilise as much of * GraphQLs's own code instead. */ -export class GraphQLService extends Drash.Service { +export class GraphQLService { #options: GraphQLOptions; #playground_enabled = false; #playground_endpoint = "/graphql"; @@ -35,7 +57,6 @@ export class GraphQLService extends Drash.Service { ////////////////////////////////////////////////////////////////////////////// constructor(options: GraphQLOptions) { - super(); this.#options = options; } @@ -43,7 +64,9 @@ export class GraphQLService extends Drash.Service { // FILE MARKER - PUBLIC METHODS ////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// - public runAtStartup(options: Drash.Interfaces.IServiceStartupOptions): void { + public runAtStartup( + context: T.ContextForServicesAtStartup, + ): T.Promisable { const serviceOptions = this.#options; // Not enabled, so skip setting up the playground @@ -58,19 +81,19 @@ export class GraphQLService extends Drash.Service { if (typeof this.#options.graphiql === "string") { this.#playground_endpoint = this.#options.graphiql; - return options.server.addResource( + return context.request_handler.addResources([ this.#createUserDefinedGraphQLResource(this.#options.graphiql), - ); + ]); } // Default to the /graphql resource - options.server.addResource(GraphQLResource); + context.request_handler.addResources([GraphQLResource]); } async runBeforeResource( - request: Drash.Request, - response: Drash.Response, - ): Promise { + request: Request, + response: Response, + ): Promise { // Handle GET requests. The expectation should be that on a GET request, the // configs allow a playground. if (request.method.toUpperCase() === "GET") { @@ -91,12 +114,11 @@ export class GraphQLService extends Drash.Service { * * @returns The GraphQL playground resource. */ - #createUserDefinedGraphQLResource(endpoint: string): typeof Drash.Resource { + #createUserDefinedGraphQLResource( + endpoint: string, + ): T.Constructor { return class UserDefinedGraphQLResource extends GraphQLResource { public paths = [endpoint]; - public services = { - ALL: [this as unknown as Drash.Service], - }; }; } @@ -106,9 +128,12 @@ export class GraphQLService extends Drash.Service { * @param request * @param response */ - #handleGetRequests(_request: Drash.Request, response: Drash.Response): void { + #handleGetRequests( + _request: Request, + response: Response, + ): T.Promisable { if (!this.#playground_enabled) { - throw new Drash.Errors.HttpError( + throw new HTTPError( 500, "The GraphQL playground is not enabled.", ); @@ -126,21 +151,27 @@ export class GraphQLService extends Drash.Service { * @param response */ async #handleAllOtherRequests( - request: Drash.Request, - response: Drash.Response, - ): Promise { - const query = request.bodyParam("query"); + request: Request, + response: Response, + ): Promise { + const body = await request.readBody<{ + operationName: string; + variables: Record; + query: string; + }>("json"); + + const query = body.query; if (typeof query !== "string") { - throw new Drash.Errors.HttpError( + throw new HTTPError( 422, "The query is not of the expected type, it should be a string.", ); } - const operationName = request.bodyParam("operationName") ?? null; + const operationName = body.operationName ?? null; - const variables = request.bodyParam>("variables") ?? + const variables = body.variables ?? null; const result = await GraphQL.graphql( @@ -153,9 +184,9 @@ export class GraphQLService extends Drash.Service { ) as ExecutionResult; if (result.errors) { - return response.json(result.errors, 400); + return response.json(result.errors as unknown[]).status(400); } - response.json(result); + return response.json(result as Record); } } diff --git a/src/standard/_unstable/services/deno/graphql/graphql_resource.ts b/src/standard/_unstable/services/deno/graphql/graphql_resource.ts new file mode 100644 index 000000000..2d6de944c --- /dev/null +++ b/src/standard/_unstable/services/deno/graphql/graphql_resource.ts @@ -0,0 +1,47 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Types } from "../../../mod.deno.ts"; +import { Drash } from "./deps.ts"; + +export class GraphQLResource extends Drash.Resource { + public paths = ["/graphql"]; + + public GET( + _request: Drash.Request, + response: Drash.Response, + ): Types.Promisable { + // This is only defined to allow GET requests to the front-end playground. + // Without this, Drash will throw a 501 Not Implemented error when + // requesting to view the playground at /graphql. + return response; + } + + public POST( + _request: Drash.Request, + response: Drash.Response, + ): Types.Promisable { + // This is only defined so that POST requests to this resource can be + // processed. Without this, Drash will throw a 501 Not Implemented error + // when clients try to make GraphQL queries. + return response; + } +} diff --git a/src/standard/_unstable/services/deno/resource_loader/deps.ts b/src/standard/_unstable/services/deno/resource_loader/deps.ts new file mode 100644 index 000000000..576ff131e --- /dev/null +++ b/src/standard/_unstable/services/deno/resource_loader/deps.ts @@ -0,0 +1,23 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +export { walkSync } from "https://deno.land/std@0.175.0/fs/mod.ts"; +export { join } from "https://deno.land/std@0.175.0/path/mod.ts"; diff --git a/src/services/resource_loader/resource_loader.ts b/src/standard/_unstable/services/deno/resource_loader/resource_loader.ts similarity index 69% rename from src/services/resource_loader/resource_loader.ts rename to src/standard/_unstable/services/deno/resource_loader/resource_loader.ts index 30bd94bc5..d3df5375d 100644 --- a/src/services/resource_loader/resource_loader.ts +++ b/src/standard/_unstable/services/deno/resource_loader/resource_loader.ts @@ -1,4 +1,25 @@ -import { Interfaces, Resource, Service } from "../../../mod.ts"; +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Interfaces, Resource, Types } from "../../../mod.deno.ts"; import { join, walkSync } from "./deps.ts"; interface IOptions { @@ -13,7 +34,7 @@ interface IOptions { paths_to_resources: string[]; } -export class ResourceLoaderService extends Service { +export class ResourceLoaderService implements Interfaces.Service { #options: IOptions; /** @@ -40,12 +61,11 @@ export class ResourceLoaderService extends Service { * ``` */ constructor(options: IOptions) { - super(); this.#options = options; } public async runAtStartup( - options: Interfaces.IServiceStartupOptions, + context: Types.ContextForServicesAtStartup, ): Promise { for (const basePath of this.#options.paths_to_resources) { for (const entry of walkSync(basePath)) { @@ -82,11 +102,13 @@ export class ResourceLoaderService extends Service { try { const obj = new typeSafeExportedMember(); const propertyNames = Object.getOwnPropertyNames(obj); - if (!propertyNames.includes("drash_resource")) { + + // The paths must always be present + if (!propertyNames.includes("paths")) { continue; } - options.server.addResource(typeSafeExportedMember); + context.request_handler.addResources([typeSafeExportedMember]); } catch (_error) { // If `obj` cannot be instantiated, then skip it } diff --git a/src/standard/_unstable/services/native/accept_header/accepter_header.ts b/src/standard/_unstable/services/native/accept_header/accepter_header.ts new file mode 100644 index 000000000..8eeeeba48 --- /dev/null +++ b/src/standard/_unstable/services/native/accept_header/accepter_header.ts @@ -0,0 +1,87 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Enums, Errors, Request, Response } from "../../../mod.deno.ts"; + +type Options = { + /** + * (Optional. Defaults to `false`) Should requests fail when a request's + * `Accept` header does not accept a response's `Content-Type` header? + */ + fail_on_accept_header_mismatch: boolean; +}; + +/** + * Class that handles checking a request's `Accept` header and a response's + * `Content-Type` header to further build a response that makes sense for the + * client. + */ +export class AcceptHeaderService { + #options: Options = { + fail_on_accept_header_mismatch: false, + }; + + constructor(options: Options) { + this.#options = Object.assign(this.#options, options ?? {}); + } + + runAfterResource(request: Request, response: Response): void { + const result = this.#requestAcceptsResponse(request, response); + + if (!result && this.#options.fail_on_accept_header_mismatch) { + throw new HTTPError( + Enums.StatusCode.UnprocessableEntity, + "The server cannot produce a response matching the list of acceptable values defined in the request's proactive content negotiation headers and the server is unwilling to supply a default representation.", + ); + } + } + + /** + * If the request Accept header is present, then make sure the response + * Content-Type header is accepted. + * + * @param requestAcceptHeader + * @param responseContentTypeHeader + */ + #requestAcceptsResponse(request: Request, response: Response): boolean { + const requestAcceptHeader = request.headers.get("accept"); + + if (!requestAcceptHeader) { + return false; + } + + const responseContentTypeHeader = response.headers?.get("content-type"); + + if (!responseContentTypeHeader) { + return false; + } + + if (requestAcceptHeader.includes("*/*")) { + return true; + } + + if (requestAcceptHeader.includes(responseContentTypeHeader)) { + return true; + } + + return false; + } +} diff --git a/src/services/paladin/paladin.ts b/src/standard/_unstable/services/native/paladin/paladin.ts similarity index 66% rename from src/services/paladin/paladin.ts rename to src/standard/_unstable/services/native/paladin/paladin.ts index d0328c706..3702b0a00 100644 --- a/src/services/paladin/paladin.ts +++ b/src/standard/_unstable/services/native/paladin/paladin.ts @@ -1,4 +1,25 @@ -import { IService, Request, Response, Service } from "../../../mod.ts"; +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Request, Response } from "../../../mod.deno.ts"; type ReferrerPolicy = | "" @@ -44,10 +65,9 @@ interface Configs { * * @param configs - See Configs */ -export class PaladinService extends Service implements IService { +export class PaladinService { readonly #configs: Configs; constructor(configs: Configs = {}) { - super(); if (!configs.hsts) { configs.hsts = {}; } @@ -64,18 +84,16 @@ export class PaladinService extends Service implements IService { this.#configs["X-XSS-Protection"] === true || this.#configs["X-XSS-Protection"] !== false ) { - response.headers.set( - "X-XSS-Protection", - "1; mode=block", - ); + response.headers({ + "X-XSS-Protection": "1; mode=block", + }); } // Set "Referrer-Policy" header if passed in. See https://helmetjs.github.io/docs/referrer-policy/ if (this.#configs["Referrer-Policy"]) { - response.headers.set( - "Referrer-Policy", - this.#configs["Referrer-Policy"], - ); + response.headers({ + "Referrer-Policy": this.#configs["Referrer-Policy"], + }); } // Set the "X-Content-Type-Options" header. See https://helmetjs.github.io/docs/dont-sniff-mimetype/ @@ -83,10 +101,9 @@ export class PaladinService extends Service implements IService { this.#configs["X-Content-Type-Options"] === true || this.#configs["X-Content-Type-Options"] !== false ) { - response.headers.set( - "X-Content-Type-Options", - "nosniff", - ); + response.headers({ + "X-Content-Type-Options": "nosniff", + }); } // Set the "Strict-Transport-Security" header. See https://helmetjs.github.io/docs/hsts/ @@ -107,30 +124,29 @@ export class PaladinService extends Service implements IService { hstsHeader += "; preload"; } if (hstsHeader) { - response.headers.set("Strict-Transport-Security", hstsHeader); + response.headers({ + "Strict-Transport-Security": hstsHeader, + }); } // Delete or modify the "X-Powered-By" header. See https://helmetjs.github.io/docs/hide-powered-by/ if (typeof this.#configs["X-Powered-By"] === "string") { // user wants to modify the header - response.headers.set( - "X-Powered-By", - this.#configs["X-Powered-By"] as string, - ); + response.headers({ + "X-Powered-By": this.#configs["X-Powered-By"] as string, + }); } else if (this.#configs["X-Powered-By"] !== true) { - response.headers.delete("X-Powered-By"); + response.headers_init?.delete("X-Powered-By"); } // Set the "X-Frame-Options" header. See https://helmetjs.github.io/docs/frameguard/ if (typeof this.#configs["X-Frame-Options"] === "string") { - response.headers.set( - "X-Frame-Options", - this.#configs["X-Frame-Options"] as string, - ); + response.headers({ + "X-Frame-Options": this.#configs["X-Frame-Options"] as string, + }); } else if (this.#configs["X-Frame-Options"] !== false) { - response.headers.set( - "X-Frame-Options", - "SAMEORIGIN", - ); + response.headers({ + "X-Frame-Options": "SAMEORIGIN", + }); } // Set the "Expect-CT" header. See https://helmetjs.github.io/docs/expect-ct/ @@ -145,22 +161,27 @@ export class PaladinService extends Service implements IService { expectCTHeader += "; " + this.#configs.expect_ct!.report_uri; } if (expectCTHeader) { - response.headers.set("Expect-CT", expectCTHeader); + response.headers({ + "Expect-CT": expectCTHeader, + }); } // Set the "X-DNS-Prefetch-Control" header. See https://helmetjs.github.io/docs/dns-prefetch-control/ if (this.#configs["X-DNS-Prefetch-Control"] === true) { - response.headers.set("X-DNS-Prefetch-Control", "on"); + response.headers({ + "X-DNS-Prefetch-Control": "on", + }); } else { - response.headers.set("X-DNS-Prefetch-Control", "off"); + response.headers({ + "X-DNS-Prefetch-Control": "off", + }); } // Set the "Content-Security-Policy" header. See https://helmetjs.github.io/docs/csp/ if (this.#configs["Content-Security-Policy"]) { - response.headers.set( - "Content-Security-Policy", - this.#configs["Content-Security-Policy"], - ); + response.headers({ + "Content-Security-Policy": this.#configs["Content-Security-Policy"], + }); } } } diff --git a/src/services/rate_limiter/memory_store.ts b/src/standard/_unstable/services/native/rate_limiter/memory_store.ts similarity index 70% rename from src/services/rate_limiter/memory_store.ts rename to src/standard/_unstable/services/native/rate_limiter/memory_store.ts index 004d0da7e..f5f4c712e 100644 --- a/src/services/rate_limiter/memory_store.ts +++ b/src/standard/_unstable/services/native/rate_limiter/memory_store.ts @@ -1,3 +1,24 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + export class MemoryStore { #hits: Record = {}; #interval_id: number | null = null; diff --git a/src/standard/_unstable/services/native/rate_limiter/rate_limiter.ts b/src/standard/_unstable/services/native/rate_limiter/rate_limiter.ts new file mode 100644 index 000000000..fc39a1c47 --- /dev/null +++ b/src/standard/_unstable/services/native/rate_limiter/rate_limiter.ts @@ -0,0 +1,107 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Errors, Interfaces, Request } from "../../../mod.deno.ts"; +import { MemoryStore } from "./memory_store.ts"; + +interface IOptions { + /** + * How long (in milliseconds) an IP is allocated the `max_requests`. + */ + timeframe: number; + + /** + * Number of requests an IP is allowed within the `timeframe`. + */ + // deno-lint-ignore camelcase + max_requests: number; +} + +export class RateLimiterService implements Interfaces.Service { + readonly #options: IOptions; + + #memory_store: MemoryStore; + + ////////////////////////////////////////////////////////////////////////////// + // FILE MARKER - CONSTRUCTOR ///////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + + constructor(options: IOptions) { + this.#options = options; + this.#memory_store = new MemoryStore(this.#options.timeframe); + } + + ////////////////////////////////////////////////////////////////////////////// + // FILE MARKER - PUBLIC METHODS ////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + + public cleanup(): void { + this.#memory_store.cleanup(); + } + + public runBeforeResource(request: Request): void { + const connectionInfo = request.headers.get("x-drash-connection-info"); + + if (!connectionInfo) { + return; + } + + const remoteAddressJson = JSON.parse(connectionInfo); + const key = (remoteAddressJson.remoteAddr as Deno.NetAddr).hostname; + const { current, reset_time: resetTime } = this.#memory_store.increment( + key, + ); + const requestsRemaining = Math.max(this.#options.max_requests - current, 0); + + response.headers({ + "X-RateLimit-Limit": this.#options.max_requests.toString(), + }); + + response.headers({ + "X-RateLimit-Remaining": requestsRemaining.toString(), + }); + + response.headers({ + "Date": new Date().toUTCString(), + }); + + response.headers({ + "X-RateLimit-Reset": Math.ceil(resetTime.getTime() / 1000).toString(), + }); + + if (this.#options.max_requests && current > this.#options.max_requests) { + const retryAfter = Math.ceil(this.#options.timeframe / 1000).toString() + + "s"; + response.headers({ + "X-Retry-After": retryAfter, + }); + throw new HTTPError( + 429, + `Too Many Requests. Please try again after ${retryAfter}.`, + ); + } + } + + public runOnError( + request: Interfaces.NativeRequest, + ): Promisable { + } +} diff --git a/src/standard/_unstable/services/native/response_time/response_time.ts b/src/standard/_unstable/services/native/response_time/response_time.ts new file mode 100644 index 000000000..4c45fe775 --- /dev/null +++ b/src/standard/_unstable/services/native/response_time/response_time.ts @@ -0,0 +1,43 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import * as Interfaces from "../../../src/core/Interfaces.ts"; + +export class ResponseTimeService implements Interfaces.Service { + #startTime = 0; + + #endTime = 0; + + runBeforeResource() { + this.#startTime = new Date().getTime(); + } + + runAfterResource(_request: Interfaces.NativeRequest, response: Response) { + this.#endTime = new Date().getTime(); + const time = (this.#endTime - this.#startTime) + "ms"; + + return new Response(response.body, { + headers: { + "X-RESPONSE-TIME": time.toString(), + }, + }); + } +} diff --git a/src/services/tengine/jae.ts b/src/standard/_unstable/services/native/tengine/jae.ts similarity index 79% rename from src/services/tengine/jae.ts rename to src/standard/_unstable/services/native/tengine/jae.ts index 0ce2752f2..97fe90b1b 100644 --- a/src/services/tengine/jae.ts +++ b/src/standard/_unstable/services/native/tengine/jae.ts @@ -1,3 +1,24 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + const decoder = new TextDecoder("utf-8"); export class Jae { diff --git a/src/standard/_unstable/services/native/tengine/tengine.ts b/src/standard/_unstable/services/native/tengine/tengine.ts new file mode 100644 index 000000000..d23711c5b --- /dev/null +++ b/src/standard/_unstable/services/native/tengine/tengine.ts @@ -0,0 +1,46 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import * as Interfaces from "../../../src/core/Interfaces.ts"; +import { Jae } from "./jae.ts"; + +interface IOptions { + // deno-lint-ignore camelcase + views_path: string; +} + +export class TengineService implements Interfaces.Service { + readonly #options: IOptions; + #template_engine: Jae; + + constructor(options: IOptions) { + this.#options = options; + this.#template_engine = new Jae(this.#options.views_path); + } + + runAfterResource(_request: Request, response: Response) { + response.headers.set("Content-Type", "text/html"); + } + + render(filepath: string, data: T): string { + return this.#template_engine.render(filepath, data); + } +} diff --git a/src/standard/builders/Builder.ts b/src/standard/builders/Builder.ts new file mode 100644 index 000000000..2343b193c --- /dev/null +++ b/src/standard/builders/Builder.ts @@ -0,0 +1,34 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +/** + * @template C The class that will be instantiated and returned when calling the + * `build()` method. + */ +interface Builder { + /** + * Instantiate the class and return it. + */ + build(): C; +} +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export type { Builder }; diff --git a/src/standard/chains/AbstractChainBuilder.ts b/src/standard/chains/AbstractChainBuilder.ts new file mode 100644 index 000000000..e5c9525d1 --- /dev/null +++ b/src/standard/chains/AbstractChainBuilder.ts @@ -0,0 +1,62 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +// Imports > Core +import { IBuilder } from "../../core/interfaces/IBuilder.ts"; + +// Imports > Standard +import { Handler } from "../handlers/Handler.ts"; + +abstract class AbstractChainBuilder implements IBuilder { + readonly handlers: Handler[] = []; + + abstract build(): unknown; + + public handler(handler: Handler): this { + this.handlers.push(handler); + + return this; + } + + /** + * @param handlers The handlers that will be chained together. + * @returns This instance so you can chain more methods. + */ + protected link() { + if (!this.handlers) { + throw new Error("Chain.Builder: `this.handlers` should be an array"); + } + + if (!this.handlers.length) { + throw new Error("Chain.Builder: `this.handlers` is empty"); + } + + this.handlers.reduce((previous, current) => { + return previous.setNext(current); + }); + + return this.handlers[0]; + } +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { AbstractChainBuilder }; diff --git a/src/standard/handlers/AbstractSearchIndex.ts b/src/standard/handlers/AbstractSearchIndex.ts new file mode 100644 index 000000000..f5ee0ca01 --- /dev/null +++ b/src/standard/handlers/AbstractSearchIndex.ts @@ -0,0 +1,43 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +// Imports > Standard +import { Handler } from "./Handler.ts"; + +abstract class AbstractSearchIndex extends Handler { + /** + * Build the index that can be searched via `this.search(...)`. + * @param items The items to go into the index. + */ + protected abstract buildIndex(items?: unknown): void; + + /** + * Search the index. + * @param input The data containing the location information for items in the + * index. + * @retuns The results of the search. + */ + protected abstract search(input: unknown): Promise; +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { AbstractSearchIndex }; diff --git a/src/standard/handlers/Handler.ts b/src/standard/handlers/Handler.ts new file mode 100644 index 000000000..2366956b8 --- /dev/null +++ b/src/standard/handlers/Handler.ts @@ -0,0 +1,65 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +// Imports > Core +import { IHandler } from "../../core/interfaces/IHandler.ts"; + +/** + * A class to be extended by handlers so they can share the same interface. + */ +class Handler implements IHandler { + /** + * Handlers can be chained together using this property. See example. + * + * @example + * ``` + * // Instantiate some handlers + * const handlerA = new HandlerA(); + * const handlerB = new HandlerB(); + * const handlerC = new HandlerC(); + * + * // Link them together + * handlerA.setNext(handlerB).setNext(handlerC); + * ``` + */ + protected next: Handler | null = null; + + public handle(input: any): Promise { + return this.sendToNextHandler(input); + } + + public sendToNextHandler(input: any): Promise { + if (this.next !== null) { + return this.next.handle(input); + } + + throw new Error(`Handler ${this.constructor.name} has no next handler`); + } + + public setNext(handler: Handler): Handler { + this.next = handler; + return handler; + } +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { Handler }; diff --git a/src/standard/handlers/RequestParamsParser.ts b/src/standard/handlers/RequestParamsParser.ts new file mode 100644 index 000000000..93edd51ee --- /dev/null +++ b/src/standard/handlers/RequestParamsParser.ts @@ -0,0 +1,124 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +// Imports > Core +import { Resource } from "../../core/http/Resource.ts"; + +// Imports > Standard +import { Handler } from "../handlers/Handler.ts"; + +type Input = { + request: { url: string }; + resource: Resource; + request_params: { + path_params: Record; + }; +}; + +type Output = { + request: Input["request"] & { params: Params }; + resource: Resource; +}; + +type WithParams = Request & { params: Params }; + +class RequestParamsParser extends Handler { + handle(input: Input): Promise { + return Promise + .resolve() + .then(() => this.#validateInput(input)) + .then(() => this.#addParams(input.request, input.request_params)) + .then(() => { + const { request, resource } = input; + + const nextHandlerInput = { request, resource }; + + return super.sendToNextHandler(nextHandlerInput); + }); + } + + /** + * Add the given `requestParams` to the given `request`. + * @param request + * @param params + */ + #addParams( + request: Input["request"], + requestParams: Input["request_params"], + ): void { + Object.defineProperty(request, "params", { + value: new Params( + request, + requestParams, + ), + }); + } + + /** + * Validate the input is the expected type. + * @param input The input passed to `this.handle()`. + */ + #validateInput(input: unknown): void { + if (!input || typeof input !== "object") { + throw new Error("Input received is not an object"); + } + + if ( + !("request" in input) || !input.request || + typeof input.request !== "object" + ) { + throw new Error("Input request received is not an object"); + } + + if (!("url" in input.request) || typeof input.request.url !== "string") { + throw new Error("Input request URL could not be read"); + } + + if (!("resource" in input) || typeof input.resource !== "object") { + throw new Error("Input resource received is not an object"); + } + } +} + +class Params { + #query: URLSearchParams; + #path_params: Record; + + constructor( + request: Input["request"], + params: Input["request_params"], + ) { + this.#query = new URLSearchParams(request.url); + this.#path_params = params.path_params; + } + + public queryParam(param: string): string | undefined { + return this.#query.get(param) ?? undefined; + } + + public pathParam(param: string): string | undefined { + return this.#path_params[param]; + } +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { type Input, type Output, RequestParamsParser, type WithParams }; diff --git a/src/standard/handlers/RequestValidator.ts b/src/standard/handlers/RequestValidator.ts new file mode 100644 index 000000000..c102a8323 --- /dev/null +++ b/src/standard/handlers/RequestValidator.ts @@ -0,0 +1,89 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +// Imports > Core +import { HTTPError } from "../../core/errors/HTTPError.ts"; +import { Status } from "../../core/http/response/Status.ts"; + +// Imports > Standard +import { Handler } from "./Handler.ts"; + +type Input = { + method?: string; + url?: string; +}; + +class RequestValidator extends Handler { + /** + * Validate the given `request`. If valid, the request is sent further down + * the chain. + * + * @param request The request to validate. + * + * @returns The request if validated. + */ + handle(req: Input) { + return Promise + .resolve() + .then(() => this.#validate(req)) + .then(() => { + if (this.next !== null) { + return super.sendToNextHandler(req); + } + + return req as Output; // Intentional cast for now + }); + } + + #validate(request: unknown): void { + if (!request) { + throw new HTTPError( + Status.UnprocessableEntity, + `Request could not be read`, + ); + } + + if (typeof request !== "object") { + throw new HTTPError( + Status.UnprocessableEntity, + `Request could not be read`, + ); + } + + if (!("method" in request) || typeof request.method !== "string") { + throw new HTTPError( + Status.UnprocessableEntity, + `Request HTTP method could not be read`, + ); + } + + if (!("url" in request) || typeof request.url !== "string") { + throw new HTTPError( + Status.UnprocessableEntity, + `Request URL could not be read`, + ); + } + } +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { type Input, RequestValidator }; diff --git a/src/standard/handlers/ResourceCaller.ts b/src/standard/handlers/ResourceCaller.ts new file mode 100644 index 000000000..6cf647da4 --- /dev/null +++ b/src/standard/handlers/ResourceCaller.ts @@ -0,0 +1,77 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +// Imports > Core +import { MethodOf } from "../../core/Types.ts"; +import { Resource } from "../../core/http/Resource.ts"; + +// Imports > Standard +import { Handler } from "./Handler.ts"; + +type Input = { request: { method: string }; resource: Resource }; + +class ResourceCaller extends Handler { + handle(input: Input): Promise { + return Promise + .resolve() + .then(() => this.#validate(input)) + .then(() => this.#sendRequestToResource(input)); + } + + #sendRequestToResource(input: Input): Promise { + const httpMethod = input.request.method.toUpperCase(); + + return Promise + .resolve() + .then(() => { + return input.resource[httpMethod as MethodOf]( + input.request, + ); + }) as Promise; + } + + #validate(input: unknown): void { + if (!input || typeof input !== "object") { + throw new Error("Input received is not an object"); + } + + if ( + !("request" in input) || !input.request || + typeof input.request !== "object" + ) { + throw new Error("Input request received is not an object"); + } + + if ( + !("method" in input.request) || typeof input.request.method !== "string" + ) { + throw new Error("Input request method could not be read"); + } + + if (!("resource" in input) || typeof input.resource !== "object") { + throw new Error("Input resource received is not an object"); + } + } +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { type Input, ResourceCaller }; diff --git a/src/standard/handlers/ResourceNotFoundHandler.ts b/src/standard/handlers/ResourceNotFoundHandler.ts new file mode 100644 index 000000000..ae2d3964c --- /dev/null +++ b/src/standard/handlers/ResourceNotFoundHandler.ts @@ -0,0 +1,89 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +// Imports > Core +import { HTTPError } from "../../core/errors/HTTPError.ts"; +import { Resource } from "../../core/http/Resource.ts"; +import { Status } from "../../core/http/response/Status.ts"; + +// Imports > Standard +import { Handler } from "./Handler.ts"; + +type Input = { + request: { + url: string; + }; + + result: { + resource: Resource; + path_params: Record; + }; +}; + +class ResourceNotFoundHandler extends Handler { + handle(input: Input): Promise { + return Promise + .resolve() + .then(() => this.#validate(input)) + .then(() => + super.sendToNextHandler({ + request: input.request, + resource: input.result.resource, + request_params: { + path_params: input.result.path_params, + }, + }) + ); + } + + #validate(input: unknown): void { + if (!input || typeof input !== "object") { + throw new HTTPError( + Status.InternalServerError, + "Request could not be read", + ); + } + + if ( + !("request" in input) || !input.request || + typeof input.request !== "object" + ) { + throw new HTTPError( + Status.InternalServerError, + "Request could not be read", + ); + } + + if ( + !("result" in input) || !input.result || typeof input.result !== "object" + ) { + throw new HTTPError(Status.NotFound); + } + + if (!("resource" in input.result) || !input.result.resource) { + throw new HTTPError(Status.NotFound); + } + } +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { type Input, ResourceNotFoundHandler }; diff --git a/src/standard/handlers/ResourcesIndex.ts b/src/standard/handlers/ResourcesIndex.ts new file mode 100644 index 000000000..66d1fc20c --- /dev/null +++ b/src/standard/handlers/ResourcesIndex.ts @@ -0,0 +1,178 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +// Imports > Core +import { HTTPError } from "../../core/errors/HTTPError.ts"; +import { Resource } from "../../core/http/Resource.ts"; +import { Status } from "../../core/http/response/Status.ts"; + +// Imports > Standard +import { AbstractSearchIndex } from "../handlers/AbstractSearchIndex.ts"; + +type Input = { url: string }; + +interface IURLPattern { + pathname: string; + exec(input: string): URLPatternExecResult | null; +} + +type ResourceClasses = typeof Resource | typeof Resource[]; + +type SearchResult = { + resource: Resource; + path_params: Record; +}; + +type URLPatternExecResult = { + pathname?: { + groups: Record; + }; +}; + +interface URLPatternClass { + new (options: { pathname: string }): IURLPattern; +} + +class ResourcesIndex extends AbstractSearchIndex { + #cached_search_results: Record = {}; + protected index: { + resource: Resource; + path_patterns: IURLPattern[]; + }[] = []; + protected resources: ResourceClasses[] = []; + protected URLPatternClass: URLPatternClass; + + constructor( + URLPatternClass: URLPatternClass, + ...resources: ResourceClasses[] + ) { + super(); + this.resources = resources ?? []; + this.URLPatternClass = URLPatternClass; + this.buildIndex(this.resources); + } + + public handle(request: Input): Promise { + return Promise + .resolve() + .then(() => this.#validateRequest(request)) + .then(() => this.search(request)) + .then((result) => + super.sendToNextHandler({ + request, + result, + }) + ); + } + + protected override buildIndex(resources: ResourceClasses[]): void { + for (const Resource of resources) { + if (Array.isArray(Resource)) { + this.buildIndex(Resource); + continue; + } + + const urlPatterns: IURLPattern[] = []; + + const resource = new Resource(); + resource.paths.forEach((path: string) => { + // Add "{/}?" to match possible trailing slashes too. For example, this + // means the following paths point to the same resource: + // + // - /coffee + // - /coffee/ + // + urlPatterns.push( + new this.URLPatternClass({ pathname: path + "{/}?" }), + ); + }); + + this.index.push({ + resource, + path_patterns: urlPatterns, + }); + } + } + + protected search(request: { url: string }): Promise { + const fullyQualifiedUrl = request.url; + + const cachedSearchResult = this.#getCachedSearchResult(fullyQualifiedUrl); + if (cachedSearchResult) { + return Promise.resolve(cachedSearchResult); + } + + for (const resourceURLPatterns of this.index.values()) { + for (const pattern of resourceURLPatterns.path_patterns) { + const result = pattern.exec(fullyQualifiedUrl); + + // No resource? Check the next one. + if (result === null) { + continue; + } + + const resource = resourceURLPatterns.resource; + + this.#cached_search_results[fullyQualifiedUrl] = { + path_params: result.pathname?.groups || {}, + resource, + }; + + return Promise.resolve(this.#cached_search_results[fullyQualifiedUrl]); + } + } + + this.#cached_search_results[fullyQualifiedUrl] = null; + return Promise.resolve(this.#cached_search_results[fullyQualifiedUrl]); + } + + #getCachedSearchResult(fullyQualifiedUrl: string): SearchResult | null { + if (this.#cached_search_results[fullyQualifiedUrl]) { + const cachedResult = this.#cached_search_results[fullyQualifiedUrl]; + + if (cachedResult) { + return cachedResult; + } + } + + return null; + } + + #validateRequest(request: unknown): void { + if (!request || typeof request !== "object") { + throw new HTTPError( + Status.InternalServerError, + "Request could not be read", + ); + } + + if (!("url" in request) || typeof request.url !== "string") { + throw new HTTPError( + Status.InternalServerError, + "Request URL could not be read", + ); + } + } +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { type Input, ResourcesIndex, type SearchResult, type URLPatternClass }; diff --git a/src/standard/http/Middleware.ts b/src/standard/http/Middleware.ts new file mode 100644 index 000000000..7727d4be2 --- /dev/null +++ b/src/standard/http/Middleware.ts @@ -0,0 +1,158 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { MethodOf } from "../../core/Types.ts"; +import { HTTPError } from "../../core/errors/HTTPError.ts"; +import { Resource } from "../../core/http/Resource.ts"; +import { Status } from "../../core/http/response/Status.ts"; + +// Imports > Core + +// Imports > Standard + +class Middleware extends Resource { + public original?: Resource; + + public setOriginal(original: Resource) { + this.original = original; + } + + public next(input: unknown): ReturnValue { + if ( + !input || + (typeof input !== "object") || + !("method" in input) || + (typeof input.method !== "string") || + !this.original || + (typeof this.original !== "object") || + !(input.method in this.original) || + (typeof this.original[input.method as MethodOf]) !== "function" + ) { + throw new Error("Middleware could not process request further"); + } + + const method = input.method as MethodOf; + + const response = this.original[method](input); + + if (!response) { + throw new HTTPError( + Status.InternalServerError, + "The server was unable to generate a response", + ); + } + + return response as ReturnValue; + } + + /** + * Use this method to intercept the request before it is passed to the + * resource's HTTP method. With this method, you can short-circuit the + * request, modify the request, send the request to the resource based on + * conditions, etc. + * + * To send the request to the resource (or next middleware), call + * `this.next(input)`. See example for more details. + * + * @param input The input to help produce an output. + * @returns An output based on the input. + * + * @example + * ```typescript + * class MyMiddleware extends Middleware { + * public ALL(request) { + * + * // If the `x-hello` header is valid ... + * if (request.headers.get("x-hello") === "world") { + * + * // ..., then allow the request further down the request + * // chain. Also, use `` to specify that the value + * // returned from `this.next()` is a `Response`. + * return this.next(request); + * } + * + * // Otherwise, short-circuit the request by throwing a 401 + * // error to the caller + * throw new HTTPError(Status.Unauthorized); + * } + * } + * ``` + */ + public ALL(input: unknown) { + return this.next(input); + } + + public CONNECT(input: unknown): unknown { + return this.#delegate(input, "CONNECT"); + } + + public DELETE(input: unknown): unknown { + return this.#delegate(input, "DELETE"); + } + + public GET(input: unknown): unknown { + return this.#delegate(input, "GET"); + } + + public HEAD(input: unknown): unknown { + return this.#delegate(input, "HEAD"); + } + + public OPTIONS(input: unknown): unknown { + return this.#delegate(input, "OPTIONS"); + } + + public PATCH(input: unknown): unknown { + return this.#delegate(input, "PATCH"); + } + + public POST(input: unknown): unknown { + return this.#delegate(input, "POST"); + } + + public PUT(input: unknown): unknown { + return this.#delegate(input, "PUT"); + } + + public TRACE(input: unknown): unknown { + return this.#delegate(input, "TRACE"); + } + + #delegate(input: unknown, method: MethodOf): unknown { + if (!this.original) { + throw new Error("Failed to create middleware. No original."); + } + + if ( + "ALL" in this && + this.ALL && + typeof this.ALL === "function" + ) { + return this.ALL(input); + } + + return this.original![method as MethodOf](input); + } +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { Middleware }; diff --git a/src/standard/http/ResourceGroup.ts b/src/standard/http/ResourceGroup.ts new file mode 100644 index 000000000..ab267fecb --- /dev/null +++ b/src/standard/http/ResourceGroup.ts @@ -0,0 +1,351 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +// Imports > Core +import { Resource } from "../../core/http/Resource.ts"; +import type { IBuilder } from "../../core/interfaces/IBuilder.ts"; + +// Imports > Standard +import { Middleware } from "./Middleware.ts"; + +type ResourceClasses = (typeof Resource | typeof Resource[])[]; + +/** + * Builder for building a resource group. Resources in a resource group can + * share the following: + * + * - Middleware + * - Path prefixes + */ +class Builder implements IBuilder { + #path_prefixes: string[] = []; + #middleware: Middleware[] = []; + #resources: typeof Resource[] = []; + + /** + * Set the resources for this group so they can share functionality. + * @param resources The resources to group together. + * @returns This instance so you can chain more methods. + * --- + * @example + * ```ts + * class Coffees extends Resource { + * public paths = ["/coffees"]; + * } + * + * class Teas extends Resource { + * public paths = ["/teas"]; + * } + * + * const group = ResourceGroup + * .builder() + * .resources( // The resources here will share ... + * Coffees, + * Teas + * ) + * .pathPrefixes( // ... the path prefixes here ... + * "/api/v1", + * "/v1" + * ) + * .middleware( // ... and use the middleware here. + * MiddlewareA, + * MiddlewareB, + * ) + * .build(); + * ``` + */ + resources(...resources: ResourceClasses): this { + const ret = []; + + for (const input of resources) { + if (Array.isArray(input)) { + ret.push(...input); + continue; + } + + ret.push(input); + } + + this.#resources = ret; + + return this; + } + + /** + * Set the path prefixes for all resources in this group. + * @param pathPrefixes The path prefixes the resources will use. + * @returns This instance so you can chain more methods. + * + * @example + * ```ts + * class Coffees extends Resource { + * public paths = ["/coffees"]; + * } + * + * const group = ResourceGroup + * .builder() + * .pathPrefixes("/api/v1") + * .resources(Coffees) // Coffees.paths becomes ["/api/v1/coffees"] + * .build(); + * ``` + */ + pathPrefixes(...pathPrefixes: string[]): this { + this.#path_prefixes = pathPrefixes; + return this; + } + + /** + * Set the middlware for all resources in this group. + * @param middleware The middleware the resources will use. + * @returns This instance so you can chain more methods. + * + * @example + * ```ts + * class Coffees extends Resource { ... } + * class Teas extends Resource { ... } + * class MiddlewareA extends Middleware { ... } + * class MiddlewareB extends Middleware { ... } + * + * const group = ResourceGroup + * .builder() + * .middleware( // This middleware ... + * MiddlewareA, + * MiddlewareB + * ) + * .resources( // ... will be used by these resources + * Coffees, + * Teas + * ) + * .build(); + * ``` + */ + middleware( + ...middleware: Middleware[] + ): this { + this.#middleware = middleware; + return this; + } + + /** + * Build the resource group. + * @returns An array of all the resources passed to the `this.resources()` + * call with shared functionality (path prefixes, middleware, etc.). + */ + build(): typeof Resource[] { + let ret = []; + + ret = createGroupWithMiddleware( + this.#middleware, + this.#resources, + ); + + ret = createGroupWithPrefixes( + this.#path_prefixes, + ret, + ); + + return ret; + } +} + +function createGroupWithPrefixes( + prefixes: string[], + resourceClasses: typeof Resource[], +): typeof Resource[] { + if (!prefixes || !prefixes.length) { + return resourceClasses; + } + + return resourceClasses.map((ResourceClass: typeof Resource) => { + // Here we are creating the proxy that will be used by the client. The + // only purpose of this proxy is to be instantiable so it can be + // instantiated by the client and have the paths set using the prefixes. + return class PrefixedResourceProxy extends ResourceClass { + constructor() { + super(); + this.paths = this.#getPathsWithPrefixes(); + } + + #getPathsWithPrefixes(): string[] { + if (!prefixes || prefixes.length <= 0) { + return this.paths; + } + + const paths: string[] = []; + + for (const prefix of prefixes) { + for (const path of this.paths) { + paths.push(prefix + path); + } + } + + return paths; + } + }; + }); +} + +function createGroupWithMiddleware( + middlewareClasses: Middleware[], + resourceClasses: typeof Resource[], +): typeof Resource[] { + if (arrayEmpty(middlewareClasses)) { + return resourceClasses; + } + + if (arrayEmpty(resourceClasses)) { + return resourceClasses; + } + + const wrapped = resourceClasses.map((ResourceClass) => { + const middlewareCopies = middlewareClasses.slice(); + let first: Middleware; + + const resourceInstance = new ResourceClass(); + + if (middlewareClasses.length <= 1) { + first = middlewareCopies[0]; + first.setOriginal(resourceInstance); + } else { + // If more than one middleware instance exists, then we link them together + // from top top bottom. For example, if the below was given ... + // + // [ + // MiddlewareA, + // MiddlewareB, + // MiddlewareC, + // MiddlewareZ, + // ] + // + // ..., then they would be linked together like ... + // + // MiddlewareEntryPoint { + // original: MiddlewareA { + // original: MiddlewareB { + // original: MiddlewareC { + // original: MiddlewareZ { original: TheOriginalResource } + // } + // } + // } + // } + // + + const firstMiddlewareInstance = middlewareCopies.shift(); + + if (firstMiddlewareInstance) { + first = firstMiddlewareInstance; + + middlewareCopies.reduce( + (previousMiddlewareInstance, currentMiddlewareInstance, index) => { + // Last middleware instance wraps the resource + if (index + 1 === middlewareCopies.length) { + currentMiddlewareInstance.setOriginal(resourceInstance); + } + + previousMiddlewareInstance.setOriginal(currentMiddlewareInstance); + + return currentMiddlewareInstance; + }, + first, + ); + } + } + + // Here we are creating the proxy that will be used by the client. The + // only purpose of this proxy is to be instantiable just like a resource and + // is an extension of the resource it is proxying. + + // We extend the original resource class so the paths are kept intact + const p = class MiddlewareEntryPoint extends ResourceClass { + public CONNECT(input: unknown): unknown { + return first.CONNECT(input); + } + + public DELETE(input: unknown): unknown { + return first.DELETE(input); + } + + public GET(input: unknown): unknown { + return first.GET(input); + } + + public HEAD(input: unknown): unknown { + return first.HEAD(input); + } + + public OPTIONS(input: unknown): unknown { + return first.OPTIONS(input); + } + + public PATCH(input: unknown): unknown { + return first.PATCH(input); + } + + public POST(input: unknown): unknown { + return first.POST(input); + } + + public PUT(input: unknown): unknown { + return first.PUT(input); + } + + public TRACE(input: unknown): unknown { + return first.TRACE(input); + } + }; + + Object.defineProperty(p, "name", { + value: resourceInstance.constructor.name + "MiddlewareProxy", + }); + + return p; + }); + + return wrapped; +} + +class ResourceGroup { + static Builder = Builder; + + static builder(): Builder { + return new Builder(); + } +} + +function arrayEmpty(value: unknown[]): boolean { + if (!value) { + return true; + } + + if (!Array.isArray(value)) { + return true; + } + + if (value.length === 0) { + return true; + } + + return false; +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { type Builder, ResourceGroup }; diff --git a/src/standard/http/ResourceProxy.ts b/src/standard/http/ResourceProxy.ts new file mode 100644 index 000000000..7d9cb9c63 --- /dev/null +++ b/src/standard/http/ResourceProxy.ts @@ -0,0 +1,74 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +// Imports > Core +import { Resource } from "../../core/http/Resource.ts"; + +class ResourceProxy extends Resource { + public paths: string[] = []; + protected original_instance?: Resource; + + public setOriginal(originalInstance: Resource) { + this.paths = originalInstance.paths; + this.original_instance = originalInstance; + return this.original_instance; + } + + public CONNECT(request: unknown): unknown { + return this.original_instance?.CONNECT(request); + } + + public DELETE(request: unknown): unknown { + return this.original_instance?.DELETE(request); + } + + public GET(request: unknown): unknown { + return this.original_instance?.GET(request); + } + + public HEAD(request: unknown): unknown { + return this.original_instance?.HEAD(request); + } + + public OPTIONS(request: unknown): unknown { + return this.original_instance?.OPTIONS(request); + } + + public PATCH(request: unknown): unknown { + return this.original_instance?.PATCH(request); + } + + public POST(request: unknown): unknown { + return this.original_instance?.POST(request); + } + + public PUT(request: unknown): unknown { + return this.original_instance?.PUT(request); + } + + public TRACE(request: unknown): unknown { + return this.original_instance?.TRACE(request); + } +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { ResourceProxy }; diff --git a/src/standard/log/AbstractLogger.ts b/src/standard/log/AbstractLogger.ts new file mode 100644 index 000000000..9d1c2bbfe --- /dev/null +++ b/src/standard/log/AbstractLogger.ts @@ -0,0 +1,161 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +// Imports > Standard +import { Level } from "./Level.ts"; +import { Logger } from "./Logger.ts"; +import type { LogLevel } from "./LogLevel.ts"; + +/** + * Base logger for logger classes. + */ +abstract class AbstractLogger implements Logger { + /** + * The name of this logger. Can be used when writing messsages. + */ + protected name: string; + + /** + * The highest level log message this logger can write. + */ + protected level: LogLevel; + + constructor(name: string, level: LogLevel) { + this.name = name; + this.level = level; + } + + public debug(message: unknown, ...replacements: unknown[]): unknown { + if (!this.canLog(Level.Debug)) { + return; + } + return this.write("DEBUG", message, replacements); + } + + public error(message: unknown, ...replacements: unknown[]): unknown { + if (!this.canLog(Level.Error)) { + return; + } + return this.write("ERROR", message, replacements); + } + + public fatal(message: unknown, ...replacements: unknown[]): unknown { + if (!this.canLog(Level.Fatal)) { + return; + } + return this.write("FATAL", message, replacements); + } + + public info(message: unknown, ...replacements: unknown[]): unknown { + if (!this.canLog(Level.Info)) { + return; + } + return this.write("INFO", message, replacements); + } + + public trace(message: unknown, ...replacements: unknown[]): unknown { + if (!this.canLog(Level.Trace)) { + return; + } + return this.write("TRACE", message, replacements); + } + + public warn(message: unknown, ...replacements: unknown[]): unknown { + if (!this.canLog(Level.Warn)) { + return; + } + return this.write("WARN", message, replacements); + } + + /** + * Can this logger log the given message level? + * @param messageLevel The mesage level in question. + * @returns True if yes, false if no. + */ + protected canLog(messageLevel: LogLevel): boolean { + // return true; + return this.level >= messageLevel; + } + + /** + * Get the prefix to write before the log message. + * @param messageLevel The message's level to write as part of the prefix. + * @returns The prefix: `[this.name] [messageLevel]` + */ + protected getMessagePrefix(messageLevel: string): string { + const repeatLength = 25 - this.name.length; + const repeat = repeatLength > 0 ? ".".repeat(repeatLength) : ""; + const nameForPrefix = this.name.substring(0, 25) + repeat; + + return `[${nameForPrefix}] [${messageLevel}] `; + } + + /** + * @param level The message's log level. + * @param message The message. + * @param replacements An array of values to replace `{}` placeholders in the + * `message`. + * @returns + */ + protected getFormattedMessage( + level: string, + message: unknown, + replacements: unknown[], + ): string { + const messagePrefix = this.getMessagePrefix(level); + + if (typeof message !== "string") { + return messagePrefix + message; + } + + if (!replacements || !replacements.length) { + return messagePrefix + message; + } + + const replacedMessage = message + .replace(/\{\}/g, "{}{remove}") + .split("{remove}") + .map((value, index) => { + if (index + 1 > replacements.length) { + return value.replace(/\{\}/, "{}"); + } + + const replacement = replacements[index]; + + let cleanReplacement = `${replacement}`; + + if (Array.isArray(replacement) || typeof replacement === "object") { + cleanReplacement = JSON.stringify(replacement); + } + + return value.replace(/\{\}/, cleanReplacement); + }) + .join(""); + + return messagePrefix + replacedMessage; + } + + protected abstract write(...messages: unknown[]): unknown; +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { AbstractLogger }; diff --git a/src/standard/log/ConsoleLogger.ts b/src/standard/log/ConsoleLogger.ts new file mode 100644 index 000000000..8d85934e0 --- /dev/null +++ b/src/standard/log/ConsoleLogger.ts @@ -0,0 +1,49 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +// Imports > Standard +import { AbstractLogger } from "./AbstractLogger.ts"; +import { Level } from "./Level.ts"; +import type { LogLevel } from "./LogLevel.ts"; + +class ConsoleLogger extends AbstractLogger { + /** + * Create this logger. + * @param name + * @param level The highest log message level this logger can write. + * @returns + */ + static create(name: string, level: LogLevel = Level.Off): ConsoleLogger { + return new ConsoleLogger(name, level); + } + + protected write( + level: string, + message: string, + replacements: unknown[], + ): void { + console.log(this.getFormattedMessage(level, message, replacements)); + } +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { ConsoleLogger, Level }; diff --git a/src/standard/log/GroupConsoleLogger.ts b/src/standard/log/GroupConsoleLogger.ts new file mode 100644 index 000000000..7bead7f88 --- /dev/null +++ b/src/standard/log/GroupConsoleLogger.ts @@ -0,0 +1,62 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +// Imports > Standard +import { AbstractLogger } from "./AbstractLogger"; +import { Level } from "./Level.ts"; +import type { LogLevel } from "./LogLevel.ts"; + +class GroupConsoleLogger extends AbstractLogger { + #loggers: Record = {}; + + /** + * Create this logger. + * @param name + * @param level The highest log message level this logger can write. + * @returns + */ + static create(name: string, level: LogLevel = Level.Off): GroupConsoleLogger { + return new GroupConsoleLogger(name, level); + } + + public logger(name: string): GroupConsoleLogger { + if (!this.#loggers[name]) { + this.#loggers[name] = new GroupConsoleLogger( + `${this.name}:${name}`, + this.level, + ); + } + + return this.#loggers[name]; + } + + protected write( + level: string, + message: string, + replacements: unknown[], + ): void { + console.log(this.getFormattedMessage(level, message, replacements)); + } +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { GroupConsoleLogger, Level }; diff --git a/src/standard/log/Level.ts b/src/standard/log/Level.ts new file mode 100644 index 000000000..2ad69ae9b --- /dev/null +++ b/src/standard/log/Level.ts @@ -0,0 +1,34 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +/** + * Log levels in ascending order. + */ +export const Level = { + Off: 0, + Fatal: 1, + Error: 2, + Warn: 3, + Info: 4, + Debug: 5, + Trace: 6, + All: 7, +} as const; diff --git a/src/standard/log/LogLevel.ts b/src/standard/log/LogLevel.ts new file mode 100644 index 000000000..0d12367a4 --- /dev/null +++ b/src/standard/log/LogLevel.ts @@ -0,0 +1,27 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Level } from "./Level.ts"; + +/** + * Log levels in ascending order. + */ +export type LogLevel = (typeof Level)[keyof typeof Level]; diff --git a/src/standard/log/Logger.ts b/src/standard/log/Logger.ts new file mode 100644 index 000000000..1bac7cb1f --- /dev/null +++ b/src/standard/log/Logger.ts @@ -0,0 +1,56 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +/** + * Base interface for logger classes. + */ +export interface Logger { + /** + * Write `debug` messages. + * @param messages The messages to write. + */ + debug(...messages: unknown[]): unknown; + /** + * Write `error` messages. + * @param messages The messages to write. + */ + error(...messages: unknown[]): unknown; + /** + * Write `fatal` messages. + * @param messages The messages to write. + */ + fatal(...messages: unknown[]): unknown; + /** + * Write `info` messages. + * @param messages The messages to write. + */ + info(...messages: unknown[]): unknown; + /** + * Write `trace` messages. + * @param messages The messages to write. + */ + trace(...messages: unknown[]): unknown; + /** + * Write `warn` messages. + * @param messages The messages to write. + */ + warn(...messages: unknown[]): unknown; +} diff --git a/src/standard/polyfill/URLPatternPolyfill.ts b/src/standard/polyfill/URLPatternPolyfill.ts new file mode 100644 index 000000000..b91ea24e2 --- /dev/null +++ b/src/standard/polyfill/URLPatternPolyfill.ts @@ -0,0 +1,227 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +const REGEX_URI_MATCHES = new RegExp(/(:[^(/]+|{[^0-9][^}]*})/, "g"); +const REGEX_URI_REPLACEMENT = "([^/]+)"; + +type ExecResult = { + pathname?: { + groups: Record; + }; +}; + +type ResourcePathPatterns = { + og_path: string; + regex_path: string; + params: string[]; +}; + +class URLPatternPolyfill { + #resource_path_patterns: ResourcePathPatterns[] = []; + + public readonly pathname: string; + + constructor(options: { pathname: string }) { + options.pathname = options.pathname.replace("{/}?", ""); + this.#resource_path_patterns.push(this.#getResourcePaths(options.pathname)); + this.#resource_path_patterns.push( + this.#getResourcePathsUsingOptionalParams(options.pathname), + ); + this.#resource_path_patterns.push( + this.#getResourcePathsUsingWildcard(options.pathname), + ); + this.pathname = options.pathname; + } + + exec(url: string): ExecResult | null { + const { pathname } = new URL(url); + + for (const pathObj of this.#resource_path_patterns) { + const matches = pathname.match(pathObj.regex_path); + + if (matches == null) { + continue; + } + + return { + pathname: { + groups: this.#getPathParams(pathObj, matches), + }, + }; + } + + return null; + } + + /** + * Get resource paths for the path in question. These paths are used to match + * request URIs to a resource. + * + * @param path - The path to parse into parsable pieces. + * + * @return A resource paths object. + */ + #getResourcePaths(path: string): ResourcePathPatterns { + return { + og_path: path, + regex_path: `^${ + path.replace( + REGEX_URI_MATCHES, + REGEX_URI_REPLACEMENT, + ) + }/?$`, + params: (path.match(REGEX_URI_MATCHES) || []).map( + (element: string) => { + return element.replace(/:|{|}/g, ""); + }, + ), + }; + } + + /** + * Get resource paths for the path in question. The path in question should + * have at least one optional param. An optiona param is like :id in the + * following path: + * + * /my-path/:id? + * + . These paths use * to match request URIs to a resource. + * + * @param path - The path to parse into parsable pieces. + * + * @return A resource paths object. + */ + #getResourcePathsUsingOptionalParams(path: string): ResourcePathPatterns { + // Edward Bebbington is the mastermind + // behind this work. Big ups! + let tmpPath = path; + // Replace required params, in preparation to create the `regex_path`, just + // like how we do in the below else block + const numberOfRequiredParams = path.split("/").filter((param) => { + // Ignores optional (`?`) params and only pulls how many required + // parameters the resource path contains, eg: + // :age? --> ignore, :age --> dont ignore, {age} --> dont ignore + // /users/:age/{name}/:city? --> returns 2 required params + return (param.includes(":") || param.includes("{")) && + !param.includes("?"); + }).length; + for (let i = 0; i < numberOfRequiredParams; i++) { + tmpPath = tmpPath.replace( + /(:[^(/]+|{[^0-9][^}]*})/, // same as REGEX_URI_MATCHES but not global + REGEX_URI_REPLACEMENT, + ); + } + // Replace optional path params + const maxOptionalParams = path.split("/").filter((param) => { + return param.includes("?"); + }).length; + // Description for the below for loop and why we use it to create the regex + // for optional parameters: For each optional parameter in the path, we + // replace it with custom regex. Similar to how other blocks construct the + // `regex_path`, but in this case, it isn't as easy as a simple `replace` + // one-liner, due to needing to account for optional parameters (:name?), + // and required parameters before optional params. This is what we do to + // construct the `regex_path`. I haven't been able to come up with a regex + // that would replace all instances and work, which is why a loop is being + // used here, to replace the first instance of an optional parameter (to + // account for a possible required parameter before), and then replace the + // rest of the occurrences. It's slightly tricky because the path + // `/users/:name?/:age?/:city?` should match `/users`. + for (let i = 0; i < maxOptionalParams; i++) { + // We need to mark the start for the first optional param + if (i === 0) { + // The below regex is very similar to `REGEX_URI_MATCHES` but this regex + // isn't global, and accounts for there being a required parameter + // before + tmpPath = tmpPath.replace( + /\/(:[^(/]+|{[^0-9][^}]*}\?)\/?/, + // A `/` being optional, as well as the param being optional, and a + // ending `/` being optional + "/?([^/]+)?/?", + ); + } else { + // We can now create the replace regex for the rest taking into + // consideration the above replace regex + tmpPath = tmpPath.replace( + /\/?(:[^(/]+|{[^0-9][^}]*}\?)\/?/, + "([^/]+)?/?", + ); + } + } + + return { + og_path: path, + regex_path: `^${tmpPath}$`, + // Regex is same as other blocks, but we also strip the `?`. + params: (path.match(REGEX_URI_MATCHES) || []).map( + (element: string) => { + return element.replace(/:|{|}|\?/g, ""); + }, + ), + }; + } + + /** + * Get resource paths for the wildcard path in question. These paths are use + * to match request URIs to a resource. + * + * @param path - The path to parse into parsable pieces. + * + * @return A resource paths object. + */ + #getResourcePathsUsingWildcard(path: string): ResourcePathPatterns { + return { + og_path: path, + regex_path: `^.${ + path.replace( + REGEX_URI_MATCHES, + REGEX_URI_REPLACEMENT, + ) + }/?$`, + params: (path.match(REGEX_URI_MATCHES) || []).map( + (element: string) => { + return element.replace(/:|{|}/g, ""); + }, + ), + }; + } + + #getPathParams( + pathObj: ResourcePathPatterns, + matches: string[], + ): Record { + const matchesCopy = matches.slice(); + + matchesCopy.shift(); + + const pathParams: Record = {}; + + pathObj.params.forEach((paramName: string, index: number) => { + pathParams[paramName] = matchesCopy[index]; + }); + + return pathParams; + } +} + +// FILE MARKER - PUBLIC API //////////////////////////////////////////////////// + +export { URLPatternPolyfill }; diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100644 index 000000000..0210a68ad --- /dev/null +++ b/src/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "emitDeclarationOnly": true, + "declaration": true, + "allowImportingTsExtensions": true, + "target": "es2015" + }, + "include": [ + "../test/**/*.ts", + "./core/**/*.ts", + "./modules/**/*.ts", + "./standard/**/*.ts" + ] +} diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 07046a7df..000000000 --- a/src/types.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Request, Resource, Response } from "../mod.ts"; - -/** - * The allowed types for an HTTP method on a resource. - */ -export type HttpMethodName = - | "CONNECT" - | "DELETE" - | "GET" - | "HEAD" - | "OPTIONS" - | "PATCH" - | "POST" - | "PUT" - | "TRACE"; - -export type BodyFile = { - content: Uint8Array; - size: number; - type: string; - filename: string; -}; - -export type HttpHeadersKeyValuePairs = Record; - -export type ResourcesAndPatternsMap = Map; - -export type ResourceHttpMethodHandler = ( - request: Request, - response: Response, -) => Promise | void; - -/** - * Request options to use when creating the `Drash.Server` object. - * - * @example - * - * ```typescript - * const server = new Drash.Server({ - * ... - * ... - * ... - * request: { - * read_body: false, - * } - * }); - * ``` - */ -export type RequestOptions = Partial<{ - /** Should incoming requests have their bodies read? */ - read_body: boolean; -}>; diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 000000000..2332a28b7 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,68 @@ +# Drash Tests + +This document outlines: + +- descriptions of the test directories; +- intent behind the tests; and +- how to run the tests. + +## Compat + +Compat tests assert compatibility in runtimes. They assert request flows using native APIs and polyfills provided by Drash. + +### Bun + +#### Assumptions + +You have Bun (latest v0.x) installed. + +#### How to run tests + +``` +$ deno task test:compat:bun +``` + +### Deno + +#### Assumptions + +You have Deno (latest v1.x) installed. + +#### How to run tests + +``` +$ deno task test:compat:deno +``` + +### Node + +#### Assumptions + +- You have Node installed. +- You built the CJS and ESM modules using `deno task build:all`. + - The tests import code from Drash's built modules, not the code in the `src` directory. Reason being we want the compat testing to include importing the modules that will be published to the npm registry. + - The tests will not run without `deno task build:all` being used first. + +### Node versions used + +The tests are split by Node version in separate directories (e.g., `node-v16.x`, `node-v18.x`, etc.). When you run the Node compat tests, the directory that will be used will be based on the Node version you are using. For example, if you are using Node 16, then the `node-v16.x` directory will be used and the `node-v18.x` directory will be ignored. + +Using `nvm` (download it at https://github.com/nvm-sh/nvm) can make it easier for you to switch between Node versions to test in specific Node versions (e.g., `nvm use 16` to use Node 16 or `nvm use 18` to use Node 18). + +#### How to run tests + +``` +$ yarn install +$ deno task build:all +$ deno task test:compat:node +``` + +## Unit + +Unit tests make assertions against Core, Standard, and Modules. + +### How to run tests + +``` +$ deno task test:unit +``` diff --git a/tests/compat/bun/tsconfig.json b/tests/compat/bun/tsconfig.json new file mode 100644 index 000000000..e6ba46d62 --- /dev/null +++ b/tests/compat/bun/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "types": [ + "bun-types" + ] + }, + "include": [ + "../../src/**/*.ts", + "./**/*.ts" + ] +} diff --git a/tests/compat/bun/v0.x/modules/RequestChain/native/default-behavior/URLPattern_not_supported_app_test_.ts b/tests/compat/bun/v0.x/modules/RequestChain/native/default-behavior/URLPattern_not_supported_app_test_.ts new file mode 100644 index 000000000..0312f13ec --- /dev/null +++ b/tests/compat/bun/v0.x/modules/RequestChain/native/default-behavior/URLPattern_not_supported_app_test_.ts @@ -0,0 +1,141 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { describe, expect, it } from "bun:test"; +import { handleRequest, hostname, port, protocol } from "./app"; + +const url = `${protocol}://${hostname}:${port}`; + +describe("Native - Using Request/Response", () => { + describe("Home / paths = /", () => { + for (const testCase of testCases()) { + const { method, expected } = testCase; + it(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + expect(response?.status).toBe(expected.status); + expect(body).toBe(expected.body); + }); + } + }); + + describe("Non-existent endpoints / path = test", () => { + for (const testCase of testCasesNotFound()) { + const { method, expected } = testCase; + it(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/test", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + expect(response?.status).toBe(expected.status); + expect(body).toBe(expected.body); + }); + } + }); +}); + +function testCases() { + return [ + { + method: "GET", + expected: { + status: 200, + body: "Hello from GET.", + }, + }, + { + method: "POST", + expected: { + status: 200, + body: "Hello from POST.", + }, + }, + { + method: "PUT", + expected: { + status: 501, + body: "Not Implemented", + }, + }, + { + method: "DELETE", + expected: { + status: 500, + body: "Hey, I'm the DELETE endpoint. Errrr.", + }, + }, + { + method: "PATCH", + expected: { + status: 405, + body: "Method Not Allowed", + }, + }, + ]; +} + +function testCasesNotFound() { + return [ + { + method: "GET", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "POST", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PUT", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "DELETE", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PATCH", + expected: { + status: 404, + body: "Not Found", + }, + }, + ]; +} diff --git a/tests/compat/bun/v0.x/modules/RequestChain/native/default-behavior/app.ts b/tests/compat/bun/v0.x/modules/RequestChain/native/default-behavior/app.ts new file mode 100644 index 000000000..30afbcf52 --- /dev/null +++ b/tests/compat/bun/v0.x/modules/RequestChain/native/default-behavior/app.ts @@ -0,0 +1,79 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { HTTPError } from "../../../../../../../../.drashland/lib/esm/core/errors/HTTPError"; +import { Status } from "../../../../../../../../.drashland/lib/esm/core/http/response/Status"; +import { StatusCode } from "../../../../../../../../.drashland/lib/esm/core/http/response/StatusCode"; +import { StatusDescription } from "../../../../../../../../.drashland/lib/esm/core/http/response/StatusDescription"; +import * as Chain from "../../../../../../../../.drashland/lib/esm/modules/RequestChain/mod.native"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +class Home extends Chain.Resource { + public paths = ["/"]; + + public GET(_request: Request) { + return new Response("Hello from GET."); + } + + public POST(_request: Request) { + return new Response("Hello from POST."); + } + + public DELETE(_request: Request) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_request: Request) { + throw new HTTPError(Status.MethodNotAllowed); + } +} + +const chain = Chain + .builder() + .resources(Home) + .build(); + +export const handleRequest = ( + request: Request, +): Promise => { + return chain + .handle(request) + .catch((error: Error | HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + return new Response(error.message, { + status: error.status_code, + statusText: error.status_code_description, + }); + } + + return new Response(error.message, { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }); + }); +}; diff --git a/tests/compat/bun/v0.x/modules/RequestChain/native/request-in-context-object/URLPattern_not_supported_app_test_.ts b/tests/compat/bun/v0.x/modules/RequestChain/native/request-in-context-object/URLPattern_not_supported_app_test_.ts new file mode 100644 index 000000000..4f5aba160 --- /dev/null +++ b/tests/compat/bun/v0.x/modules/RequestChain/native/request-in-context-object/URLPattern_not_supported_app_test_.ts @@ -0,0 +1,141 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { describe, expect, it } from "bun:test"; +import { handleRequest, hostname, port, protocol } from "./app"; + +const url = `${protocol}://${hostname}:${port}`; + +describe("Native - Using Request/Response in context object", () => { + describe("Home / paths = /", () => { + for (const testCase of testCases()) { + const { method, expected } = testCase; + it(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + expect(response?.status).toBe(expected.status); + expect(body).toBe(expected.body); + }); + } + }); + + describe("Non-existent endpoints / path = test", () => { + for (const testCase of testCasesNotFound()) { + const { method, expected } = testCase; + it(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/test", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + expect(response?.status).toBe(expected.status); + expect(body).toBe(expected.body); + }); + } + }); +}); + +function testCases() { + return [ + { + method: "GET", + expected: { + status: 200, + body: "Hello from GET.", + }, + }, + { + method: "POST", + expected: { + status: 200, + body: "Hello from POST.", + }, + }, + { + method: "PUT", + expected: { + status: 501, + body: "Not Implemented", + }, + }, + { + method: "DELETE", + expected: { + status: 500, + body: "Hey, I'm the DELETE endpoint. Errrr.", + }, + }, + { + method: "PATCH", + expected: { + status: 405, + body: "Method Not Allowed", + }, + }, + ]; +} + +function testCasesNotFound() { + return [ + { + method: "GET", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "POST", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PUT", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "DELETE", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PATCH", + expected: { + status: 404, + body: "Not Found", + }, + }, + ]; +} diff --git a/tests/compat/bun/v0.x/modules/RequestChain/native/request-in-context-object/app.ts b/tests/compat/bun/v0.x/modules/RequestChain/native/request-in-context-object/app.ts new file mode 100644 index 000000000..800a24ebe --- /dev/null +++ b/tests/compat/bun/v0.x/modules/RequestChain/native/request-in-context-object/app.ts @@ -0,0 +1,112 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { HTTPError } from "../../../../../../../../.drashland/lib/esm/core/errors/HTTPError"; +import { StatusCode } from "../../../../../../../../.drashland/lib/esm/core/http/response/StatusCode"; +import { StatusDescription } from "../../../../../../../../.drashland/lib/esm/core/http/response/StatusDescription"; +import * as Chain from "../../../../../../../../.drashland/lib/esm/modules/RequestChain/mod.native"; +import { Status } from "../../../../../../../../.drashland/lib/esm/core/http/response/Status"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +type WebAPIContext = { + url: string; + method: string; + request: Request; + response?: Response; +}; + +class Home extends Chain.Resource { + public paths = ["/"]; + + public GET(context: WebAPIContext) { + context.response = new Response("Hello from GET."); + return context; + } + + public POST(context: WebAPIContext) { + context.response = new Response("Hello from POST."); + return context; + } + + public DELETE(_context: WebAPIContext) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_context: WebAPIContext) { + throw new HTTPError(Status.MethodNotAllowed); + } +} + +const chain = Chain + .builder() + .resources(Home) + .build(); + +export const handleRequest = ( + request: Request, +): Promise => { + // We will keep the Request intact and provide url and method to let the chain + // know how to route the request + const context = { + request, + url: request.url, + method: request.method, + }; + + return chain + .handle(context) + // Since we are passing in a context and resources are returning the + // context, then we expect to retrieve a Response object from the context to + // use as the Response + .then((returnedContext) => { + if (returnedContext.response) { + return returnedContext.response; + } + + return new Response( + "Response not generated", + { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }, + ); + }) + .catch((error: Error | HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + return new Response(error.message, { + status: error.status_code, + statusText: error.status_code_description, + }); + } + + return new Response(error.message, { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }); + }); +}; diff --git a/tests/compat/bun/v0.x/modules/RequestChain/polyfill/default-behavior/app.ts b/tests/compat/bun/v0.x/modules/RequestChain/polyfill/default-behavior/app.ts new file mode 100644 index 000000000..f3b4cd9de --- /dev/null +++ b/tests/compat/bun/v0.x/modules/RequestChain/polyfill/default-behavior/app.ts @@ -0,0 +1,79 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { HTTPError } from "../../../../../../../../.drashland/lib/esm/core/errors/HTTPError"; +import { StatusCode } from "../../../../../../../../.drashland/lib/esm/core/http/response/StatusCode"; +import { StatusDescription } from "../../../../../../../../.drashland/lib/esm/core/http/response/StatusDescription"; +import * as Chain from "../../../../../../../../.drashland/lib/esm/modules/RequestChain/mod.polyfill"; +import { Status } from "../../../../../../../../.drashland/lib/esm/core/http/response/Status"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +class Home extends Chain.Resource { + public paths = ["/"]; + + public GET(_request: Request) { + return new Response("Hello from GET."); + } + + public POST(_request: Request) { + return new Response("Hello from POST."); + } + + public DELETE(_request: Request) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_request: Request) { + throw new HTTPError(Status.MethodNotAllowed); + } +} + +const chain = Chain + .builder() + .resources(Home) + .build(); + +export const handleRequest = ( + request: Request, +): Promise => { + return chain + .handle(request) + .catch((error: Error | HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + return new Response(error.message, { + status: error.status_code, + statusText: error.status_code_description, + }); + } + + return new Response(error.message, { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }); + }); +}; diff --git a/tests/compat/bun/v0.x/modules/RequestChain/polyfill/default-behavior/app_test.ts b/tests/compat/bun/v0.x/modules/RequestChain/polyfill/default-behavior/app_test.ts new file mode 100644 index 000000000..4dd800c31 --- /dev/null +++ b/tests/compat/bun/v0.x/modules/RequestChain/polyfill/default-behavior/app_test.ts @@ -0,0 +1,141 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { describe, expect, it } from "bun:test"; +import { handleRequest, hostname, port, protocol } from "./app"; + +const url = `${protocol}://${hostname}:${port}`; + +describe("Polyfill - Using Request/Response", () => { + describe("Home / paths = /", () => { + for (const testCase of testCases()) { + const { method, expected } = testCase; + it(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + expect(response?.status).toBe(expected.status); + expect(body).toBe(expected.body); + }); + } + }); + + describe("Non-existent endpoints / path = test", () => { + for (const testCase of testCasesNotFound()) { + const { method, expected } = testCase; + it(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/test", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + expect(response?.status).toBe(expected.status); + expect(body).toBe(expected.body); + }); + } + }); +}); + +function testCases() { + return [ + { + method: "GET", + expected: { + status: 200, + body: "Hello from GET.", + }, + }, + { + method: "POST", + expected: { + status: 200, + body: "Hello from POST.", + }, + }, + { + method: "PUT", + expected: { + status: 501, + body: "Not Implemented", + }, + }, + { + method: "DELETE", + expected: { + status: 500, + body: "Hey, I'm the DELETE endpoint. Errrr.", + }, + }, + { + method: "PATCH", + expected: { + status: 405, + body: "Method Not Allowed", + }, + }, + ]; +} + +function testCasesNotFound() { + return [ + { + method: "GET", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "POST", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PUT", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "DELETE", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PATCH", + expected: { + status: 404, + body: "Not Found", + }, + }, + ]; +} diff --git a/tests/compat/bun/v0.x/modules/RequestChain/polyfill/request-in-context-object/app.ts b/tests/compat/bun/v0.x/modules/RequestChain/polyfill/request-in-context-object/app.ts new file mode 100644 index 000000000..079386a6f --- /dev/null +++ b/tests/compat/bun/v0.x/modules/RequestChain/polyfill/request-in-context-object/app.ts @@ -0,0 +1,107 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { HTTPError } from "../../../../../../../../.drashland/lib/esm/core/errors/HTTPError"; +import { Status } from "../../../../../../../../.drashland/lib/esm/core/http/response/Status"; +import { StatusCode } from "../../../../../../../../.drashland/lib/esm/core/http/response/StatusCode"; +import { StatusDescription } from "../../../../../../../../.drashland/lib/esm/core/http/response/StatusDescription"; +import * as Chain from "../../../../../../../../.drashland/lib/esm/modules/RequestChain/mod.polyfill"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +type WebAPIContext = { + url: string; + method: string; + request: Request; + response?: Response; +}; + +class Home extends Chain.Resource { + public paths = ["/"]; + + public GET(context: WebAPIContext) { + context.response = new Response("Hello from GET."); + return context; + } + + public POST(context: WebAPIContext) { + context.response = new Response("Hello from POST."); + return context; + } + + public DELETE(_context: WebAPIContext) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_context: WebAPIContext) { + throw new HTTPError(Status.MethodNotAllowed); + } +} + +const chain = Chain + .builder() + .resources(Home) + .build(); + +export const handleRequest = ( + request: Request, +): Promise => { + const context = { + request, + url: request.url, + method: request.method, + }; + + return chain + .handle(context) + .then((returnedContext) => { + if (returnedContext.response) { + return returnedContext.response; + } + + return new Response( + "Response not generated", + { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }, + ); + }) + .catch((error: Error | HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + return new Response(error.message, { + status: error.status_code, + statusText: error.status_code_description, + }); + } + + return new Response(error.message, { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }); + }); +}; diff --git a/tests/compat/bun/v0.x/modules/RequestChain/polyfill/request-in-context-object/app_test.ts b/tests/compat/bun/v0.x/modules/RequestChain/polyfill/request-in-context-object/app_test.ts new file mode 100644 index 000000000..18e59891f --- /dev/null +++ b/tests/compat/bun/v0.x/modules/RequestChain/polyfill/request-in-context-object/app_test.ts @@ -0,0 +1,141 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { describe, expect, it } from "bun:test"; +import { handleRequest, hostname, port, protocol } from "./app"; + +const url = `${protocol}://${hostname}:${port}`; + +describe("Polyfill - Using Request/Response in context object", () => { + describe("Home / paths = /", () => { + for (const testCase of testCases()) { + const { method, expected } = testCase; + it(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + expect(response?.status).toBe(expected.status); + expect(body).toBe(expected.body); + }); + } + }); + + describe("Non-existent endpoints / path = test", () => { + for (const testCase of testCasesNotFound()) { + const { method, expected } = testCase; + it(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/test", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + expect(response?.status).toBe(expected.status); + expect(body).toBe(expected.body); + }); + } + }); +}); + +function testCases() { + return [ + { + method: "GET", + expected: { + status: 200, + body: "Hello from GET.", + }, + }, + { + method: "POST", + expected: { + status: 200, + body: "Hello from POST.", + }, + }, + { + method: "PUT", + expected: { + status: 501, + body: "Not Implemented", + }, + }, + { + method: "DELETE", + expected: { + status: 500, + body: "Hey, I'm the DELETE endpoint. Errrr.", + }, + }, + { + method: "PATCH", + expected: { + status: 405, + body: "Method Not Allowed", + }, + }, + ]; +} + +function testCasesNotFound() { + return [ + { + method: "GET", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "POST", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PUT", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "DELETE", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PATCH", + expected: { + status: 404, + body: "Not Found", + }, + }, + ]; +} diff --git a/tests/compat/cloudflare/node-v16.x/modules/base/Chain/app.native.test.ts b/tests/compat/cloudflare/node-v16.x/modules/base/Chain/app.native.test.ts new file mode 100644 index 000000000..65f7f2de1 --- /dev/null +++ b/tests/compat/cloudflare/node-v16.x/modules/base/Chain/app.native.test.ts @@ -0,0 +1,49 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { lstatSync } from "node:fs"; +import { unstable_dev } from "wrangler"; +import type { UnstableDevWorker } from "wrangler"; + +const testName = + "tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.native.ts"; + +lstatSync(testName); + +describe("Wrangler", () => { + let worker: UnstableDevWorker; + + beforeAll(async () => { + worker = await unstable_dev(testName, { + experimental: { disableExperimentalWarning: true }, + }); + }); + + afterAll(async () => { + await worker.stop(); + }); + + it("Should return Hello World", async () => { + const res = await worker.fetch("/"); + expect(res.status).toBe(200); + expect(await res.text()).toBe("Hello from GET."); + }); +}); diff --git a/tests/compat/cloudflare/node-v16.x/modules/base/Chain/app.native.ts b/tests/compat/cloudflare/node-v16.x/modules/base/Chain/app.native.ts new file mode 100644 index 000000000..2c47bffd9 --- /dev/null +++ b/tests/compat/cloudflare/node-v16.x/modules/base/Chain/app.native.ts @@ -0,0 +1,93 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Chain as BaseChain } from "../../../../../../../.drashland/lib/esm/modules/base/Chain"; +import { RequestParamsParser } from "../../../../../../../.drashland/lib/esm/standard/handlers/RequestParamsParser"; +import { RequestValidator } from "../../../../../../../.drashland/lib/esm/standard/handlers/RequestValidator"; +import { ResourceCaller } from "../../../../../../../.drashland/lib/esm/standard/handlers/ResourceCaller"; +import { ResourceNotFoundHandler } from "../../../../../../../.drashland/lib/esm/standard/handlers/ResourceNotFoundHandler"; +import { Status } from "../../../../../../../.drashland/lib/esm/core/http/response/Status"; +import { Resource } from "../../../../../../../.drashland/lib/esm/core/http/Resource"; +import { HTTPError } from "../../../../../../../.drashland/lib/esm/core/errors/HTTPError"; +import { ResourcesIndex } from "../../../../../../../.drashland/lib/esm/standard/handlers/ResourcesIndex"; +import { StatusCode } from "../../../../../../../src/core/http/response/StatusCode"; +import { StatusDescription } from "../../../../../../../.drashland/lib/esm/core/http/response/StatusDescription"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +class Home extends Resource { + public paths = ["/"]; + + public GET(_request: Request) { + return new Response("Hello from GET."); + } + + public POST(_request: Request) { + return new Response("Hello from POST."); + } + + public DELETE(_request: Request) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_request: Request) { + throw new HTTPError(Status.MethodNotAllowed); + } +} + +const chain = BaseChain + .builder() + .handler(new RequestValidator()) + // @ts-ignore URLPattern exists while dev'ing + .handler(new ResourcesIndex(URLPattern, Home)) + .handler(new ResourceNotFoundHandler()) + .handler(new RequestParamsParser()) + .handler(new ResourceCaller()) + .build(); + +export const handleRequest = ( + request: Request, + _bindings: unknown, +) => { + return chain + .handle(request) + .catch((error: Error | HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + return new Response(error.message, { + status: error.status_code, + statusText: error.status_code_description, + }); + } + + return new Response(error.message, { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }); + }); +}; + +export default { fetch: handleRequest }; diff --git a/tests/compat/cloudflare/node-v16.x/modules/base/Chain/app.polyfill.test.ts b/tests/compat/cloudflare/node-v16.x/modules/base/Chain/app.polyfill.test.ts new file mode 100644 index 000000000..e337d073f --- /dev/null +++ b/tests/compat/cloudflare/node-v16.x/modules/base/Chain/app.polyfill.test.ts @@ -0,0 +1,49 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { lstatSync } from "fs"; +import { unstable_dev } from "wrangler"; +import type { UnstableDevWorker } from "wrangler"; + +const testName = + "tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.polyfill.ts"; + +lstatSync(testName); + +describe("Wrangler", () => { + let worker: UnstableDevWorker; + + beforeAll(async () => { + worker = await unstable_dev(testName, { + experimental: { disableExperimentalWarning: true }, + }); + }); + + afterAll(async () => { + await worker.stop(); + }); + + it("Should return Hello World", async () => { + const res = await worker.fetch("/"); + expect(res.status).toBe(200); + expect(await res.text()).toBe("Hello from GET."); + }); +}); diff --git a/tests/compat/cloudflare/node-v16.x/modules/base/Chain/app.polyfill.ts b/tests/compat/cloudflare/node-v16.x/modules/base/Chain/app.polyfill.ts new file mode 100644 index 000000000..11ff069d8 --- /dev/null +++ b/tests/compat/cloudflare/node-v16.x/modules/base/Chain/app.polyfill.ts @@ -0,0 +1,93 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { HTTPError } from "../../../../../../../.drashland/lib/esm/core/errors/HTTPError"; +import { Resource } from "../../../../../../../.drashland/lib/esm/core/http/Resource"; +import { Status } from "../../../../../../../.drashland/lib/esm/core/http/response/Status"; +import { StatusCode } from "../../../../../../../.drashland/lib/esm/core/http/response/StatusCode"; +import { StatusDescription } from "../../../../../../../.drashland/lib/esm/core/http/response/StatusDescription"; +import { Chain as BaseChain } from "../../../../../../../.drashland/lib/esm/modules/base/Chain"; +import { RequestParamsParser } from "../../../../../../../.drashland/lib/esm/standard/handlers/RequestParamsParser"; +import { RequestValidator } from "../../../../../../../.drashland/lib/esm/standard/handlers/RequestValidator"; +import { ResourceCaller } from "../../../../../../../.drashland/lib/esm/standard/handlers/ResourceCaller"; +import { ResourceNotFoundHandler } from "../../../../../../../.drashland/lib/esm/standard/handlers/ResourceNotFoundHandler"; +import { ResourcesIndex } from "../../../../../../../.drashland/lib/esm/standard/handlers/ResourcesIndex"; +import { URLPatternPolyfill } from "../../../../../../../src/standard/polyfill/URLPatternPolyfill"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +class Home extends Resource { + public paths = ["/"]; + + public GET(_request: Request) { + return new Response("Hello from GET."); + } + + public POST(_request: Request) { + return new Response("Hello from POST."); + } + + public DELETE(_request: Request) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_request: Request) { + throw new HTTPError(Status.MethodNotAllowed); + } +} + +const chain = BaseChain + .builder() + .handler(new RequestValidator()) + .handler(new ResourcesIndex(URLPatternPolyfill, Home)) // Using native `URLPattern` + .handler(new ResourceNotFoundHandler()) + .handler(new RequestParamsParser()) + .handler(new ResourceCaller()) + .build(); + +export const handleRequest = ( + request: Request, + _bindings: unknown, +) => { + return chain + .handle(request) + .catch((error: Error | HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + return new Response(error.message, { + status: error.status_code, + statusText: error.status_code_description, + }); + } + + return new Response(error.message, { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }); + }); +}; + +export default { fetch: handleRequest }; diff --git a/tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.native.test.ts b/tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.native.test.ts new file mode 100644 index 000000000..c08a0ee6c --- /dev/null +++ b/tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.native.test.ts @@ -0,0 +1,49 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { lstatSync } from "fs"; +import { unstable_dev } from "wrangler"; +import type { UnstableDevWorker } from "wrangler"; + +const testName = + "tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.native.ts"; + +lstatSync(testName); + +describe("Wrangler", () => { + let worker: UnstableDevWorker; + + beforeAll(async () => { + worker = await unstable_dev(testName, { + experimental: { disableExperimentalWarning: true }, + }); + }); + + afterAll(async () => { + await worker.stop(); + }); + + it("Should return Hello World", async () => { + const res = await worker.fetch("/"); + expect(res.status).toBe(200); + expect(await res.text()).toBe("Hello from GET."); + }); +}); diff --git a/tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.native.ts b/tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.native.ts new file mode 100644 index 000000000..2eb870298 --- /dev/null +++ b/tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.native.ts @@ -0,0 +1,91 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { HTTPError } from "../../../../../../../.drashland/lib/esm/core/errors/HTTPError"; +import { Resource } from "../../../../../../../.drashland/lib/esm/core/http/Resource"; +import { Status } from "../../../../../../../.drashland/lib/esm/core/http/response/Status"; +import { Chain as BaseChain } from "../../../../../../../.drashland/lib/esm/modules/base/Chain"; +import { RequestParamsParser } from "../../../../../../../.drashland/lib/esm/standard/handlers/RequestParamsParser"; +import { RequestValidator } from "../../../../../../../.drashland/lib/esm/standard/handlers/RequestValidator"; +import { ResourceCaller } from "../../../../../../../.drashland/lib/esm/standard/handlers/ResourceCaller"; +import { ResourceNotFoundHandler } from "../../../../../../../.drashland/lib/esm/standard/handlers/ResourceNotFoundHandler"; +import { ResourcesIndex } from "../../../../../../../.drashland/lib/esm/standard/handlers/ResourcesIndex"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +class Home extends Resource { + public paths = ["/"]; + + public GET(_request: Request) { + return new Response("Hello from GET."); + } + + public POST(_request: Request) { + return new Response("Hello from POST."); + } + + public DELETE(_request: Request) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_request: Request) { + throw new HTTPError(Status.MethodNotAllowed); + } +} + +const chain = BaseChain + .builder() + .handler(new RequestValidator()) + // @ts-ignore URLPattern exists while dev'ing + .handler(new ResourcesIndex(URLPattern, Home)) // Using native `URLPattern` + .handler(new ResourceNotFoundHandler()) + .handler(new RequestParamsParser()) + .handler(new ResourceCaller()) + .build(); + +export const handleRequest = ( + request: Request, + _bindings: unknown, +) => { + return chain + .handle(request) + .catch((error: Error | HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + return new Response(error.message, { + status: error.status_code, + statusText: error.status_code_description, + }); + } + + return new Response(error.message, { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }); + }); +}; + +export default { fetch: handleRequest }; diff --git a/tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.polyfill.test.ts b/tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.polyfill.test.ts new file mode 100644 index 000000000..e337d073f --- /dev/null +++ b/tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.polyfill.test.ts @@ -0,0 +1,49 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { lstatSync } from "fs"; +import { unstable_dev } from "wrangler"; +import type { UnstableDevWorker } from "wrangler"; + +const testName = + "tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.polyfill.ts"; + +lstatSync(testName); + +describe("Wrangler", () => { + let worker: UnstableDevWorker; + + beforeAll(async () => { + worker = await unstable_dev(testName, { + experimental: { disableExperimentalWarning: true }, + }); + }); + + afterAll(async () => { + await worker.stop(); + }); + + it("Should return Hello World", async () => { + const res = await worker.fetch("/"); + expect(res.status).toBe(200); + expect(await res.text()).toBe("Hello from GET."); + }); +}); diff --git a/tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.polyfill.ts b/tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.polyfill.ts new file mode 100644 index 000000000..11ff069d8 --- /dev/null +++ b/tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.polyfill.ts @@ -0,0 +1,93 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { HTTPError } from "../../../../../../../.drashland/lib/esm/core/errors/HTTPError"; +import { Resource } from "../../../../../../../.drashland/lib/esm/core/http/Resource"; +import { Status } from "../../../../../../../.drashland/lib/esm/core/http/response/Status"; +import { StatusCode } from "../../../../../../../.drashland/lib/esm/core/http/response/StatusCode"; +import { StatusDescription } from "../../../../../../../.drashland/lib/esm/core/http/response/StatusDescription"; +import { Chain as BaseChain } from "../../../../../../../.drashland/lib/esm/modules/base/Chain"; +import { RequestParamsParser } from "../../../../../../../.drashland/lib/esm/standard/handlers/RequestParamsParser"; +import { RequestValidator } from "../../../../../../../.drashland/lib/esm/standard/handlers/RequestValidator"; +import { ResourceCaller } from "../../../../../../../.drashland/lib/esm/standard/handlers/ResourceCaller"; +import { ResourceNotFoundHandler } from "../../../../../../../.drashland/lib/esm/standard/handlers/ResourceNotFoundHandler"; +import { ResourcesIndex } from "../../../../../../../.drashland/lib/esm/standard/handlers/ResourcesIndex"; +import { URLPatternPolyfill } from "../../../../../../../src/standard/polyfill/URLPatternPolyfill"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +class Home extends Resource { + public paths = ["/"]; + + public GET(_request: Request) { + return new Response("Hello from GET."); + } + + public POST(_request: Request) { + return new Response("Hello from POST."); + } + + public DELETE(_request: Request) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_request: Request) { + throw new HTTPError(Status.MethodNotAllowed); + } +} + +const chain = BaseChain + .builder() + .handler(new RequestValidator()) + .handler(new ResourcesIndex(URLPatternPolyfill, Home)) // Using native `URLPattern` + .handler(new ResourceNotFoundHandler()) + .handler(new RequestParamsParser()) + .handler(new ResourceCaller()) + .build(); + +export const handleRequest = ( + request: Request, + _bindings: unknown, +) => { + return chain + .handle(request) + .catch((error: Error | HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + return new Response(error.message, { + status: error.status_code, + statusText: error.status_code_description, + }); + } + + return new Response(error.message, { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }); + }); +}; + +export default { fetch: handleRequest }; diff --git a/tests/compat/cloudflare/node-v20.x/modules/base/Chain/app.native.test.ts b/tests/compat/cloudflare/node-v20.x/modules/base/Chain/app.native.test.ts new file mode 100644 index 000000000..c08a0ee6c --- /dev/null +++ b/tests/compat/cloudflare/node-v20.x/modules/base/Chain/app.native.test.ts @@ -0,0 +1,49 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { lstatSync } from "fs"; +import { unstable_dev } from "wrangler"; +import type { UnstableDevWorker } from "wrangler"; + +const testName = + "tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.native.ts"; + +lstatSync(testName); + +describe("Wrangler", () => { + let worker: UnstableDevWorker; + + beforeAll(async () => { + worker = await unstable_dev(testName, { + experimental: { disableExperimentalWarning: true }, + }); + }); + + afterAll(async () => { + await worker.stop(); + }); + + it("Should return Hello World", async () => { + const res = await worker.fetch("/"); + expect(res.status).toBe(200); + expect(await res.text()).toBe("Hello from GET."); + }); +}); diff --git a/tests/compat/cloudflare/node-v20.x/modules/base/Chain/app.native.ts b/tests/compat/cloudflare/node-v20.x/modules/base/Chain/app.native.ts new file mode 100644 index 000000000..b2458e48a --- /dev/null +++ b/tests/compat/cloudflare/node-v20.x/modules/base/Chain/app.native.ts @@ -0,0 +1,93 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { HTTPError } from "../../../../../../../.drashland/lib/esm/core/errors/HTTPError"; +import { Resource } from "../../../../../../../.drashland/lib/esm/core/http/Resource"; +import { Status } from "../../../../../../../.drashland/lib/esm/core/http/response/Status"; +import { StatusCode } from "../../../../../../../.drashland/lib/esm/core/http/response/StatusCode"; +import { StatusDescription } from "../../../../../../../.drashland/lib/esm/core/http/response/StatusDescription"; +import { Chain as BaseChain } from "../../../../../../../.drashland/lib/esm/modules/base/Chain"; +import { RequestParamsParser } from "../../../../../../../.drashland/lib/esm/standard/handlers/RequestParamsParser"; +import { RequestValidator } from "../../../../../../../.drashland/lib/esm/standard/handlers/RequestValidator"; +import { ResourceCaller } from "../../../../../../../.drashland/lib/esm/standard/handlers/ResourceCaller"; +import { ResourceNotFoundHandler } from "../../../../../../../.drashland/lib/esm/standard/handlers/ResourceNotFoundHandler"; +import { ResourcesIndex } from "../../../../../../../.drashland/lib/esm/standard/handlers/ResourcesIndex"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +class Home extends Resource { + public paths = ["/"]; + + public GET(_request: Request) { + return new Response("Hello from GET."); + } + + public POST(_request: Request) { + return new Response("Hello from POST."); + } + + public DELETE(_request: Request) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_request: Request) { + throw new HTTPError(Status.MethodNotAllowed); + } +} + +const chain = BaseChain + .builder() + .handler(new RequestValidator()) + // @ts-ignore URLPattern exists + .handler(new ResourcesIndex(URLPattern, Home)) + .handler(new ResourceNotFoundHandler()) + .handler(new RequestParamsParser()) + .handler(new ResourceCaller()) + .build(); + +export const handleRequest = ( + request: Request, + _bindings: unknown, +) => { + return chain + .handle(request) + .catch((error: Error | HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + return new Response(error.message, { + status: error.status_code, + statusText: error.status_code_description, + }); + } + + return new Response(error.message, { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }); + }); +}; + +export default { fetch: handleRequest }; diff --git a/tests/compat/cloudflare/node-v20.x/modules/base/Chain/app.polyfill.test.ts b/tests/compat/cloudflare/node-v20.x/modules/base/Chain/app.polyfill.test.ts new file mode 100644 index 000000000..e337d073f --- /dev/null +++ b/tests/compat/cloudflare/node-v20.x/modules/base/Chain/app.polyfill.test.ts @@ -0,0 +1,49 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { lstatSync } from "fs"; +import { unstable_dev } from "wrangler"; +import type { UnstableDevWorker } from "wrangler"; + +const testName = + "tests/compat/cloudflare/node-v18.x/modules/base/Chain/app.polyfill.ts"; + +lstatSync(testName); + +describe("Wrangler", () => { + let worker: UnstableDevWorker; + + beforeAll(async () => { + worker = await unstable_dev(testName, { + experimental: { disableExperimentalWarning: true }, + }); + }); + + afterAll(async () => { + await worker.stop(); + }); + + it("Should return Hello World", async () => { + const res = await worker.fetch("/"); + expect(res.status).toBe(200); + expect(await res.text()).toBe("Hello from GET."); + }); +}); diff --git a/tests/compat/cloudflare/node-v20.x/modules/base/Chain/app.polyfill.ts b/tests/compat/cloudflare/node-v20.x/modules/base/Chain/app.polyfill.ts new file mode 100644 index 000000000..65f30b7d8 --- /dev/null +++ b/tests/compat/cloudflare/node-v20.x/modules/base/Chain/app.polyfill.ts @@ -0,0 +1,93 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { HTTPError } from "../../../../../../../.drashland/lib/esm/core/errors/HTTPError"; +import { Resource } from "../../../../../../../.drashland/lib/esm/core/http/Resource"; +import { Status } from "../../../../../../../.drashland/lib/esm/core/http/response/Status"; +import { StatusCode } from "../../../../../../../.drashland/lib/esm/core/http/response/StatusCode"; +import { StatusDescription } from "../../../../../../../.drashland/lib/esm/core/http/response/StatusDescription"; +import { Chain as BaseChain } from "../../../../../../../.drashland/lib/esm/modules/base/Chain"; +import { RequestParamsParser } from "../../../../../../../.drashland/lib/esm/standard/handlers/RequestParamsParser"; +import { RequestValidator } from "../../../../../../../.drashland/lib/esm/standard/handlers/RequestValidator"; +import { ResourceCaller } from "../../../../../../../.drashland/lib/esm/standard/handlers/ResourceCaller"; +import { ResourceNotFoundHandler } from "../../../../../../../.drashland/lib/esm/standard/handlers/ResourceNotFoundHandler"; +import { ResourcesIndex } from "../../../../../../../.drashland/lib/esm/standard/handlers/ResourcesIndex"; +import { URLPatternPolyfill } from "../../../../../../../src/standard/polyfill/URLPatternPolyfill"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +class Home extends Resource { + public paths = ["/"]; + + public GET(_request: Request) { + return new Response("Hello from GET."); + } + + public POST(_request: Request) { + return new Response("Hello from POST."); + } + + public DELETE(_request: Request) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_request: Request) { + throw new HTTPError(Status.MethodNotAllowed); + } +} + +const chain = BaseChain + .builder() + .handler(new RequestValidator()) + .handler(new ResourcesIndex(URLPatternPolyfill, Home)) + .handler(new ResourceNotFoundHandler()) + .handler(new RequestParamsParser()) + .handler(new ResourceCaller()) + .build(); + +export const handleRequest = ( + request: Request, + _bindings: unknown, +) => { + return chain + .handle(request) + .catch((error: Error | HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + return new Response(error.message, { + status: error.status_code, + statusText: error.status_code_description, + }); + } + + return new Response(error.message, { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }); + }); +}; + +export default { fetch: handleRequest }; diff --git a/tests/compat/cloudflare/tsconfig.json b/tests/compat/cloudflare/tsconfig.json new file mode 100644 index 000000000..bb88fd469 --- /dev/null +++ b/tests/compat/cloudflare/tsconfig.json @@ -0,0 +1,6 @@ +{ + "include": [ + "../../.drashland/**/*.js", + "./**/*.ts" + ] +} diff --git a/tests/compat/deno/v1.x/modules/RequestChain/native/concurrency/app.ts b/tests/compat/deno/v1.x/modules/RequestChain/native/concurrency/app.ts new file mode 100644 index 000000000..8c2823d77 --- /dev/null +++ b/tests/compat/deno/v1.x/modules/RequestChain/native/concurrency/app.ts @@ -0,0 +1,116 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Status } from "../../../../../../../../src/core/http/response/Status.ts"; +import { StatusCode } from "../../../../../../../../src/core/http/response/StatusCode.ts"; +import { StatusDescription } from "../../../../../../../../src/core/http/response/StatusDescription.ts"; +import * as Chain from "../../../../../../../../src/modules/RequestChain/mod.native.ts"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +class Accounts extends Chain.Resource { + public paths = ["/accounts"]; + + public GET(request: Request) { + if (request.headers.get("x-wait-1")) { + return new Promise((resolve) => { + setTimeout(() => { + resolve( + new Response( + "Waited for x-wait-1!", + { status: 200 }, + ), + ); + }, 2000); + }); + } + + if (request.headers.get("x-wait-2")) { + return new Promise((resolve) => { + setTimeout(() => { + resolve( + new Response( + "Waited for x-wait-2!", + { status: 200 }, + ), + ); + }, 1500); + }); + } + + if (request.headers.get("x-wait-3")) { + return new Promise((resolve) => { + setTimeout(() => { + resolve( + new Response( + "Waited for x-wait-3!", + { status: 200 }, + ), + ); + }, 1250); + }); + } + + return new Response( + "Hello from Accounts.GET(). Didn't wait!", + { status: 200 }, + ); + } +} + +class Users extends Chain.Resource { + public paths = ["/users"]; + + public GET(_request: Request) { + throw new Chain.HTTPError(Status.MethodNotAllowed); + } +} + +const chain = Chain + .builder() + .resources(Accounts, Users) + .build(); + +export const handleRequest = ( + request: Request, +): Promise => { + return chain + .handle(request) + .catch((error: Error | Chain.HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof Chain.HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + return new Response(`error.message: ${error.message}`, { + status: error.status_code, + statusText: error.status_code_description, + }); + } + + return new Response(`error.message: ${error.message}`, { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }); + }); +}; diff --git a/tests/compat/deno/v1.x/modules/RequestChain/native/concurrency/app_test.ts b/tests/compat/deno/v1.x/modules/RequestChain/native/concurrency/app_test.ts new file mode 100644 index 000000000..ac7651315 --- /dev/null +++ b/tests/compat/deno/v1.x/modules/RequestChain/native/concurrency/app_test.ts @@ -0,0 +1,182 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { asserts } from "../../../../../../../deps.ts"; +import { handleRequest, hostname, port, protocol } from "./app.ts"; + +const url = `${protocol}://${hostname}:${port}`; + +Deno.test("Native - Using Request/Response", async (t) => { + await t.step("Accounts /accounts paths = /accounts", async (t) => { + await t.step(`GET does not wait`, () => { + const req = new Request(url + "/accounts", { + method: "GET", + }); + + return handleRequest(req) + .then((response) => { + asserts.assertEquals(response?.status, 200); + return response; + }) + .then((response) => response.text()) + .then((body) => { + asserts.assertEquals(body, "Hello from Accounts.GET(). Didn't wait!"); + }); + }); + + await t.step( + `GET waits with header x-wait-1 (method: Promise.all())`, + () => { + const req1 = new Request(url + "/accounts", { + method: "GET", + headers: { + "x-wait-1": "yup", + }, + }); + + const p1 = handleRequest(req1) + .then((response) => { + asserts.assertEquals(response?.status, 200); + return response; + }) + .then((response) => response.text()); + + const req2 = new Request(url + "/accounts", { + method: "GET", + }); + + const p2 = handleRequest(req2) + .then((response) => { + asserts.assertEquals(response?.status, 200); + return response; + }) + .then((response) => response.text()); + + return Promise.all([ + p1, + p2, + ]) + .then(([res1, res2]) => { + asserts.assertEquals(res1, "Waited for x-wait-1!"); + asserts.assertEquals( + res2, + "Hello from Accounts.GET(). Didn't wait!", + ); + }); + }, + ); + + await t.step( + `GET does not wait for header x-wait-1 request (method: Promise.any())`, + () => { + return new Promise((resolve) => { + // This will go to the `setTimeout()` call in the resource + const req1 = new Request(url + "/accounts", { + method: "GET", + headers: { + "x-wait-1": "yup", + }, + }); + + const req1Promise = handleRequest(req1) + .then((response) => { + asserts.assertEquals(response?.status, 200); + return response; + }) + .then((response) => response.text()) + .then((res) => { + asserts.assertEquals(res, "Waited for x-wait-1!"); + + // We need to resolve here so the test does not finish BEFORE this request's response is + // retrieved. The resource will still be processing the `setTimeout()` call when `req2` + // below resolves. We are only checking to see if `req2` resolves faster than `req1` in + // this test, but we need to make sure both requests resolve before the test finishes. + // Otherwise we run into the "async ops" error. + resolve(); + }); + + // This will not go to the `setTimeout()` call in the resource, so we + // should expect this request to come back to use BEFORE the `req1` + // request above + const req2 = new Request(url + "/accounts", { + method: "GET", + }); + + const req2Promise = handleRequest(req2) + .then((response) => { + asserts.assertEquals(response?.status, 200); + return response; + }) + .then((response) => response.text()); + + return Promise.any([ + req1Promise, // This request is first so we can assert it does not block requests + req2Promise, // This request SHOULD NOT BE BLOCKED by the above request + ]) + .then((req2Response) => { + asserts.assertEquals( + req2Response, + "Hello from Accounts.GET(). Didn't wait!", + ); + }); + }); + }, + ); + + await t.step(`GET waits with header x-wait-2`, () => { + const req = new Request(url + "/accounts", { + method: "GET", + headers: { + "x-wait-2": "yup", + }, + }); + + return handleRequest(req) + .then((response) => { + asserts.assertEquals(response?.status, 200); + return response; + }) + .then((response) => response.text()) + .then((body) => { + asserts.assertEquals(body, "Waited for x-wait-2!"); + }); + }); + + await t.step(`GET waits with header x-wait-3`, () => { + const req = new Request(url + "/accounts", { + method: "GET", + headers: { + "x-wait-3": "yup", + }, + }); + + return handleRequest(req) + .then((response) => { + asserts.assertEquals(response?.status, 200); + return response; + }) + .then((response) => response.text()) + .then((body) => { + asserts.assertEquals(body, "Waited for x-wait-3!"); + }); + }); + }); +}); diff --git a/tests/compat/deno/v1.x/modules/RequestChain/native/default-behavior/app.ts b/tests/compat/deno/v1.x/modules/RequestChain/native/default-behavior/app.ts new file mode 100644 index 000000000..20c239084 --- /dev/null +++ b/tests/compat/deno/v1.x/modules/RequestChain/native/default-behavior/app.ts @@ -0,0 +1,79 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { HTTPError } from "../../../../../../../../src/core/errors/HTTPError.ts"; +import * as Chain from "../../../../../../../../src/modules/RequestChain/mod.native.ts"; +import { StatusCode } from "../../../../../../../../src/core/http/response/StatusCode.ts"; +import { StatusDescription } from "../../../../../../../../src/core/http/response/StatusDescription.ts"; +import { Status } from "../../../../../../../../src/core/http/response/Status.ts"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +class Home extends Chain.Resource { + public paths = ["/"]; + + public GET(_request: Request) { + return new Response("Hello from GET."); + } + + public POST(_request: Request) { + return new Response("Hello from POST."); + } + + public DELETE(_request: Request) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_request: Request) { + throw new HTTPError(Status.MethodNotAllowed); + } +} + +const chain = Chain + .builder() + .resources(Home) + .build(); + +export const handleRequest = ( + request: Request, +): Promise => { + return chain + .handle(request) + .catch((error: Error | HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + return new Response(error.message, { + status: error.status_code, + statusText: error.status_code_description, + }); + } + + return new Response(error.message, { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }); + }); +}; diff --git a/tests/compat/deno/v1.x/modules/RequestChain/native/default-behavior/app_test.ts b/tests/compat/deno/v1.x/modules/RequestChain/native/default-behavior/app_test.ts new file mode 100644 index 000000000..f003afa06 --- /dev/null +++ b/tests/compat/deno/v1.x/modules/RequestChain/native/default-behavior/app_test.ts @@ -0,0 +1,143 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { asserts } from "../../../../../../../deps.ts"; +import { handleRequest, hostname, port, protocol } from "./app.ts"; + +const url = `${protocol}://${hostname}:${port}`; + +Deno.test("Native - Using Request/Response", async (t) => { + await t.step("Home / paths = /", async (t) => { + for (const testCase of testCases()) { + const { method, expected } = testCase; + + await t.step(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + asserts.assertEquals(response?.status, expected.status); + asserts.assertEquals(body, expected.body); + }); + } + }); + + await t.step("Non-existent endpoints", async (t) => { + for (const testCase of testCasesNotFound()) { + const { method, expected } = testCase; + + await t.step(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/test", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + asserts.assertEquals(response?.status, expected.status); + asserts.assertEquals(body, expected.body); + }); + } + }); +}); + +function testCases() { + return [ + { + method: "GET", + expected: { + status: 200, + body: "Hello from GET.", + }, + }, + { + method: "POST", + expected: { + status: 200, + body: "Hello from POST.", + }, + }, + { + method: "PUT", + expected: { + status: 501, + body: "Not Implemented", + }, + }, + { + method: "DELETE", + expected: { + status: 500, + body: "Hey, I'm the DELETE endpoint. Errrr.", + }, + }, + { + method: "PATCH", + expected: { + status: 405, + body: "Method Not Allowed", + }, + }, + ]; +} + +function testCasesNotFound() { + return [ + { + method: "GET", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "POST", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PUT", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "DELETE", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PATCH", + expected: { + status: 404, + body: "Not Found", + }, + }, + ]; +} diff --git a/tests/compat/deno/v1.x/modules/RequestChain/native/request-in-context-object/app.ts b/tests/compat/deno/v1.x/modules/RequestChain/native/request-in-context-object/app.ts new file mode 100644 index 000000000..871e5d541 --- /dev/null +++ b/tests/compat/deno/v1.x/modules/RequestChain/native/request-in-context-object/app.ts @@ -0,0 +1,111 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { StatusCode } from "../../../../../../../../src/core/http/response/StatusCode.ts"; +import { StatusDescription } from "../../../../../../../../src/core/http/response/StatusDescription.ts"; +import * as Chain from "../../../../../../../../src/modules/RequestChain/mod.native.ts"; +import { Status } from "../../../../../../../../src/core/http/response/Status.ts"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +type WebAPIContext = { + url: string; + method: string; + request: Request; + response?: Response; +}; + +class Home extends Chain.Resource { + public paths = ["/"]; + + public GET(context: WebAPIContext) { + context.response = new Response("Hello from GET."); + return context; + } + + public POST(context: WebAPIContext) { + context.response = new Response("Hello from POST."); + return context; + } + + public DELETE(_context: WebAPIContext) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_context: WebAPIContext) { + throw new Chain.HTTPError(Status.MethodNotAllowed); + } +} + +const chain = Chain + .builder() + .resources(Home) + .build(); + +export const handleRequest = ( + request: Request, +): Promise => { + // We will keep the Request intact and provide url and method to let the chain + // know how to route the request + const context = { + request, + url: request.url, + method: request.method, + }; + + return chain + .handle(context) + // Since we are passing in a context and resources are returning the + // context, then we expect to retrieve a Response object from the context to + // use as the Response + .then((returnedContext) => { + if (returnedContext.response) { + return returnedContext.response; + } + + return new Response( + "Response not generated", + { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }, + ); + }) + .catch((error: Error | Chain.HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof Chain.HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + return new Response(error.message, { + status: error.status_code, + statusText: error.status_code_description, + }); + } + + return new Response(error.message, { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }); + }); +}; diff --git a/tests/compat/deno/v1.x/modules/RequestChain/native/request-in-context-object/app_test.ts b/tests/compat/deno/v1.x/modules/RequestChain/native/request-in-context-object/app_test.ts new file mode 100644 index 000000000..421d78078 --- /dev/null +++ b/tests/compat/deno/v1.x/modules/RequestChain/native/request-in-context-object/app_test.ts @@ -0,0 +1,143 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { asserts } from "../../../../../../../deps.ts"; +import { handleRequest, hostname, port, protocol } from "./app.ts"; + +const url = `${protocol}://${hostname}:${port}`; + +Deno.test("Native - Using Request/Response in context object", async (t) => { + await t.step("Home / paths = /", async (t) => { + for await (const testCase of testCases()) { + const { method, expected } = testCase; + + await t.step(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + asserts.assertEquals(body, expected.body); + asserts.assertEquals(response?.status, expected.status); + }); + } + }); + + await t.step("Non-existent endpoints", async (t) => { + for (const testCase of testCasesNotFound()) { + const { method, expected } = testCase; + + await t.step(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/test", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + asserts.assertEquals(response?.status, expected.status); + asserts.assertEquals(body, expected.body); + }); + } + }); +}); + +function testCases() { + return [ + { + method: "GET", + expected: { + status: 200, + body: "Hello from GET.", + }, + }, + { + method: "POST", + expected: { + status: 200, + body: "Hello from POST.", + }, + }, + { + method: "PUT", + expected: { + status: 501, + body: "Not Implemented", + }, + }, + { + method: "DELETE", + expected: { + status: 500, + body: "Hey, I'm the DELETE endpoint. Errrr.", + }, + }, + { + method: "PATCH", + expected: { + status: 405, + body: "Method Not Allowed", + }, + }, + ]; +} + +function testCasesNotFound() { + return [ + { + method: "GET", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "POST", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PUT", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "DELETE", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PATCH", + expected: { + status: 404, + body: "Not Found", + }, + }, + ]; +} diff --git a/tests/compat/deno/v1.x/modules/RequestChain/native/resource-groups/app.ts b/tests/compat/deno/v1.x/modules/RequestChain/native/resource-groups/app.ts new file mode 100644 index 000000000..3d4834fba --- /dev/null +++ b/tests/compat/deno/v1.x/modules/RequestChain/native/resource-groups/app.ts @@ -0,0 +1,268 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { HTTPError } from "../../../../../../../../src/core/errors/HTTPError.ts"; +import * as Chain from "../../../../../../../../src/modules/RequestChain/mod.native.ts"; +import { StatusCode } from "../../../../../../../../src/core/http/response/StatusCode.ts"; +import { StatusDescription } from "../../../../../../../../src/core/http/response/StatusDescription.ts"; +import { ResourceGroup } from "../../../../../../../../src/standard/http/ResourceGroup.ts"; +import { Status } from "../../../../../../../../src/core/http/response/Status.ts"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +class Home extends Chain.Resource { + public paths = ["/home"]; + + public GET(_request: Request) { + return new Response("Hello from GET."); + } + + public POST(_request: Request) { + return new Response("Hello from POST."); + } + + public DELETE(_request: Request) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_request: Request) { + throw new HTTPError(Status.MethodNotAllowed); + } +} + +class UsersAll extends Chain.Resource { + public paths = ["/users-all"]; + + public GET(_request: Request) { + return new Response("Hello from GET."); + } + + public POST(_request: Request) { + return new Response("Hello from POST."); + } + + public DELETE(_request: Request) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_request: Request) { + throw new HTTPError(Status.MethodNotAllowed); + } +} + +class UsersAllGet extends Chain.Resource { + public paths = ["/users-all-get"]; + + public GET(_request: Request) { + return new Response("Hello from GET."); + } + + public POST(_request: Request) { + return new Response("Hello from POST."); + } + + public DELETE(_request: Request) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_request: Request) { + throw new HTTPError(Status.MethodNotAllowed); + } +} + +class UsersAllGetGetAgain extends Chain.Resource { + public paths = ["/users-all-get-get-again"]; + + public GET(_request: Request) { + return new Response("Hello from GET."); + } + + public POST(_request: Request) { + return new Response("Hello from POST."); + } + + public DELETE(_request: Request) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_request: Request) { + throw new HTTPError(Status.MethodNotAllowed); + } +} + +class MiddlewareBlockedMethods extends Chain.Middleware { + // Intentionally not using `public` keyword here to mix and match usages + GET(_request: Request) { + return new Response("Blocked"); + } + + // Intentionally not using `public` keyword here to mix and match usages + POST(_request: Request) { + return new Response("Blocked"); + } + + // Intentionally not using `public` keyword here to mix and match usages + DELETE(_request: Request) { + return new Response("Blocked", { status: 500 }); + } + + // Intentionally not using `public` keyword here to mix and match usages + PATCH(_request: Request) { + return new Response("Blocked", { status: 405 }); + } + + // Intentionally not using `public` keyword here to mix and match usages + PUT(_request: Request) { + return new Response("Blocked", { status: 501 }); + } +} + +class MiddlewareALL extends Chain.Middleware { + public async ALL(request: Request) { + const ogResponse = await super.next(request); + return new Response(`MiddlewareALL touched;` + await ogResponse.text()); + } + + public POST(_request: Request) { + return new Response("Alllllll that", { status: StatusCode.Created }); + } + + public DELETE(_request: Request) { + return new Response("Alllllll that", { status: StatusCode.Created }); + } + + public PATCH(_request: Request) { + return new Response("Alllllll that", { status: StatusCode.Created }); + } + + public PUT(_request: Request) { + return new Response("Alllllll that", { status: StatusCode.Created }); + } +} + +class MiddlewareGET extends Chain.Middleware { + public async GET(request: Request) { + const ogResponse = await super.next(request); + const body = await ogResponse.text(); + return new Response(`MiddlewareGET touched;` + body); + } +} + +class MiddlewareGETAgain extends Chain.Middleware { + public async GET(request: Request) { + const ogResponse = await super.next(request); + const body = await ogResponse.text(); + return new Response(`MiddlewareGETAgain touched;` + body); + } +} + +class MiddlewareGETAgain2 extends Chain.Middleware { + public async GET(request: Request) { + const ogResponse = await super.next(request); + const body = await ogResponse.text(); + return new Response(`MiddlewareGETAgain2 touched;` + body); + } +} + +class MiddlewareGETAgain3 extends Chain.Middleware { + public GET(_request: Request) { + return new Response( + `MiddlewareGETAgain3 touched, but blocking access to the resource`, + ); + } +} + +const groupWithBlockedMethods = ResourceGroup + .builder() + .resources(Home) + .pathPrefixes("/api/v1") + .middleware( + new MiddlewareBlockedMethods(), + ) + .build(); + +const groupWithAll = ResourceGroup + .builder() + .resources(UsersAll) + .pathPrefixes("/api/v2") + .middleware( + new MiddlewareALL(), + ) + .build(); + +const groupWithAllGet = ResourceGroup + .builder() + .resources(UsersAllGet) + .pathPrefixes("/api/v2") + .middleware( + new MiddlewareALL(), + new MiddlewareGET(), + ) + .build(); + +const groupWithAllGetGetAgain = ResourceGroup + .builder() + .resources(UsersAllGetGetAgain) + .pathPrefixes("/api/v2") + .middleware( + new MiddlewareALL(), + new MiddlewareGET(), + new MiddlewareGETAgain(), + new MiddlewareGETAgain2(), + new MiddlewareGETAgain3(), + ) + .build(); + +const chain = Chain + .builder() + .resources( + groupWithBlockedMethods, + groupWithAll, + groupWithAllGet, + groupWithAllGetGetAgain, + ) + .build(); + +export const handleRequest = ( + request: Request, +): Promise => { + return chain + .handle(request) + .catch((error: Error | HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + return new Response(error.message, { + status: error.status_code, + statusText: error.status_code_description, + }); + } + + return new Response(error.message, { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }); + }); +}; diff --git a/tests/compat/deno/v1.x/modules/RequestChain/native/resource-groups/app_test.ts b/tests/compat/deno/v1.x/modules/RequestChain/native/resource-groups/app_test.ts new file mode 100644 index 000000000..50e8989f2 --- /dev/null +++ b/tests/compat/deno/v1.x/modules/RequestChain/native/resource-groups/app_test.ts @@ -0,0 +1,332 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { StatusCode } from "../../../../../../../../src/core/http/response/StatusCode.ts"; +import { asserts } from "../../../../../../../deps.ts"; +import { handleRequest, hostname, port, protocol } from "./app.ts"; + +const url = `${protocol}://${hostname}:${port}`; + +Deno.test("Native - Using Request/Response", async (t) => { + for (const testCase of testCasesWithMiddlewareMethods()) { + const { method, expected, path } = testCase; + + await t.step( + `${method} ${path} -> Status: ${expected.status}; Body: ${expected.body}`, + async () => { + const req = new Request(url + path, { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + asserts.assertEquals(body, expected.body); + asserts.assertEquals(response?.status, expected.status); + }, + ); + } + + for (const testCase of testCasesWithMiddlewareAll()) { + const { method, expected } = testCase; + + const path = "/api/v2/users-all"; + + await t.step( + `${method} ${path} -> Status: ${expected.status}; Body: ${expected.body}`, + async () => { + const req = new Request(url + path, { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + asserts.assertEquals(body, expected.body); + asserts.assertEquals(response?.status, expected.status); + }, + ); + } + + for (const testCase of testCasesWithMiddlewareAllGet()) { + const { method, expected } = testCase; + + const path = "/api/v2/users-all-get"; + + await t.step( + `${method} ${path} -> Status: ${expected.status}; Body: ${expected.body}`, + async () => { + const req = new Request(url + path, { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + asserts.assertEquals(body, expected.body); + asserts.assertEquals(response?.status, expected.status); + }, + ); + } + + for (const testCase of testCasesWithMiddlewareAllGetGetAgain()) { + const { method, expected } = testCase; + + const path = "/api/v2/users-all-get-get-again"; + + await t.step( + `${method} ${path} -> Status: ${expected.status}; Body: ${expected.body}`, + async () => { + const req = new Request(url + path, { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + asserts.assertEquals(body, expected.body); + asserts.assertEquals(response?.status, expected.status); + }, + ); + } + + for (const testCase of testCasesNotFound()) { + const { method, expected } = testCase; + + await t.step(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/home", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + asserts.assertEquals(response?.status, expected.status); + asserts.assertEquals(body, expected.body); + }); + } +}); + +function testCasesWithMiddlewareMethods() { + return [ + { + method: "GET", + path: "/api/v1/home", + expected: { + status: 200, + body: "Blocked", + }, + }, + { + method: "POST", + path: "/api/v1/home", + expected: { + status: 200, + body: "Blocked", + }, + }, + { + method: "PUT", + path: "/api/v1/home", + expected: { + status: 501, + body: "Blocked", + }, + }, + { + method: "DELETE", + path: "/api/v1/home", + expected: { + status: 500, + body: "Blocked", + }, + }, + { + method: "PATCH", + path: "/api/v1/home", + expected: { + status: 405, + body: "Blocked", + }, + }, + ]; +} + +function testCasesWithMiddlewareAll() { + return [ + { + method: "GET", + expected: { + status: StatusCode.OK, + body: "MiddlewareALL touched;Hello from GET.", + }, + }, + { + method: "POST", + expected: { + status: StatusCode.Created, + body: "Alllllll that", + }, + }, + { + method: "PUT", + expected: { + status: StatusCode.Created, + body: "Alllllll that", + }, + }, + { + method: "DELETE", + expected: { + status: StatusCode.Created, + body: "Alllllll that", + }, + }, + { + method: "PATCH", + expected: { + status: StatusCode.Created, + body: "Alllllll that", + }, + }, + ]; +} + +function testCasesWithMiddlewareAllGetGetAgain() { + return [ + { + method: "GET", + expected: { + status: StatusCode.OK, + body: + "MiddlewareALL touched;MiddlewareGET touched;MiddlewareGETAgain touched;MiddlewareGETAgain2 touched;MiddlewareGETAgain3 touched, but blocking access to the resource", + }, + }, + { + method: "POST", + expected: { + status: StatusCode.Created, + body: "Alllllll that", + }, + }, + { + method: "PUT", + expected: { + status: StatusCode.Created, + body: "Alllllll that", + }, + }, + { + method: "DELETE", + expected: { + status: StatusCode.Created, + body: "Alllllll that", + }, + }, + { + method: "PATCH", + expected: { + status: StatusCode.Created, + body: "Alllllll that", + }, + }, + ]; +} + +function testCasesWithMiddlewareAllGet() { + return [ + { + method: "GET", + expected: { + status: StatusCode.OK, + body: "MiddlewareALL touched;MiddlewareGET touched;Hello from GET.", + }, + }, + { + method: "POST", + expected: { + status: StatusCode.Created, + body: "Alllllll that", + }, + }, + { + method: "PUT", + expected: { + status: StatusCode.Created, + body: "Alllllll that", + }, + }, + { + method: "DELETE", + expected: { + status: StatusCode.Created, + body: "Alllllll that", + }, + }, + { + method: "PATCH", + expected: { + status: StatusCode.Created, + body: "Alllllll that", + }, + }, + ]; +} + +function testCasesNotFound() { + return [ + { + method: "GET", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "POST", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PUT", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "DELETE", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PATCH", + expected: { + status: 404, + body: "Not Found", + }, + }, + ]; +} diff --git a/tests/compat/deno/v1.x/modules/RequestChain/polyfill/concurrency/app.ts b/tests/compat/deno/v1.x/modules/RequestChain/polyfill/concurrency/app.ts new file mode 100644 index 000000000..e6db2a743 --- /dev/null +++ b/tests/compat/deno/v1.x/modules/RequestChain/polyfill/concurrency/app.ts @@ -0,0 +1,116 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Status } from "../../../../../../../../src/core/http/response/Status.ts"; +import { StatusCode } from "../../../../../../../../src/core/http/response/StatusCode.ts"; +import { StatusDescription } from "../../../../../../../../src/core/http/response/StatusDescription.ts"; +import * as Chain from "../../../../../../../../src/modules/RequestChain/mod.polyfill.ts"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +class Accounts extends Chain.Resource { + public paths = ["/accounts"]; + + public GET(request: Request) { + if (request.headers.get("x-wait-1")) { + return new Promise((resolve) => { + setTimeout(() => { + resolve( + new Response( + "Waited for x-wait-1!", + { status: 200 }, + ), + ); + }, 250); + }); + } + + if (request.headers.get("x-wait-2")) { + return new Promise((resolve) => { + setTimeout(() => { + resolve( + new Response( + "Waited for x-wait-2!", + { status: 200 }, + ), + ); + }, 1000); + }); + } + + if (request.headers.get("x-wait-3")) { + return new Promise((resolve) => { + setTimeout(() => { + resolve( + new Response( + "Waited for x-wait-3!", + { status: 200 }, + ), + ); + }, 1000); + }); + } + + return new Response( + "Hello from Accounts.GET(). Didn't wait!", + { status: 200 }, + ); + } +} + +class Users extends Chain.Resource { + public paths = ["/users"]; + + public GET(_request: Request) { + throw new Chain.HTTPError(Status.MethodNotAllowed); + } +} + +const chain = Chain + .builder() + .resources(Accounts, Users) + .build(); + +export const handleRequest = ( + request: Request, +): Promise => { + return chain + .handle(request) + .catch((error: Error | Chain.HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof Chain.HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + return new Response(`error.message: ${error.message}`, { + status: error.status_code, + statusText: error.status_code_description, + }); + } + + return new Response(`error.message: ${error.message}`, { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }); + }); +}; diff --git a/tests/compat/deno/v1.x/modules/RequestChain/polyfill/concurrency/app_test.ts b/tests/compat/deno/v1.x/modules/RequestChain/polyfill/concurrency/app_test.ts new file mode 100644 index 000000000..aa5f49a05 --- /dev/null +++ b/tests/compat/deno/v1.x/modules/RequestChain/polyfill/concurrency/app_test.ts @@ -0,0 +1,182 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { asserts } from "../../../../../../../deps.ts"; +import { handleRequest, hostname, port, protocol } from "./app.ts"; + +const url = `${protocol}://${hostname}:${port}`; + +Deno.test("Polyfill - Using Request/Response", async (t) => { + await t.step("Accounts /accounts paths = /accounts", async (t) => { + await t.step(`GET does not wait`, () => { + const req = new Request(url + "/accounts", { + method: "GET", + }); + + return handleRequest(req) + .then((response) => { + asserts.assertEquals(response?.status, 200); + return response; + }) + .then((response) => response.text()) + .then((body) => { + asserts.assertEquals(body, "Hello from Accounts.GET(). Didn't wait!"); + }); + }); + + await t.step( + `GET waits with header x-wait-1 (method: Promise.all())`, + () => { + const req1 = new Request(url + "/accounts", { + method: "GET", + headers: { + "x-wait-1": "yup", + }, + }); + + const p1 = handleRequest(req1) + .then((response) => { + asserts.assertEquals(response?.status, 200); + return response; + }) + .then((response) => response.text()); + + const req2 = new Request(url + "/accounts", { + method: "GET", + }); + + const p2 = handleRequest(req2) + .then((response) => { + asserts.assertEquals(response?.status, 200); + return response; + }) + .then((response) => response.text()); + + return Promise.all([ + p1, + p2, + ]) + .then(([res1, res2]) => { + asserts.assertEquals(res1, "Waited for x-wait-1!"); + asserts.assertEquals( + res2, + "Hello from Accounts.GET(). Didn't wait!", + ); + }); + }, + ); + + await t.step( + `GET does not wait for header x-wait-1 request (method: Promise.any())`, + () => { + return new Promise((resolve) => { + // This will go to the `setTimeout()` call in the resource + const req1 = new Request(url + "/accounts", { + method: "GET", + headers: { + "x-wait-1": "yup", + }, + }); + + const req1Promise = handleRequest(req1) + .then((response) => { + asserts.assertEquals(response?.status, 200); + return response; + }) + .then((response) => response.text()) + .then((res) => { + asserts.assertEquals(res, "Waited for x-wait-1!"); + + // We need to resolve here so the test does not finish BEFORE this request's response is + // retrieved. The resource will still be processing the `setTimeout()` call when `req2` + // below resolves. We are only checking to see if `req2` resolves faster than `req1` in + // this test, but we need to make sure both requests resolve before the test finishes. + // Otherwise we run into the "async ops" error. + resolve(); + }); + + // This will not go to the `setTimeout()` call in the resource, so we + // should expect this request to come back to use BEFORE the `req1` + // request above + const req2 = new Request(url + "/accounts", { + method: "GET", + }); + + const req2Promise = handleRequest(req2) + .then((response) => { + asserts.assertEquals(response?.status, 200); + return response; + }) + .then((response) => response.text()); + + return Promise.any([ + req1Promise, // This request is first so we can assert it does not block requests + req2Promise, // This request SHOULD NOT BE BLOCKED by the above request + ]) + .then((req2Response) => { + asserts.assertEquals( + req2Response, + "Hello from Accounts.GET(). Didn't wait!", + ); + }); + }); + }, + ); + + await t.step(`GET waits with header x-wait-2`, () => { + const req = new Request(url + "/accounts", { + method: "GET", + headers: { + "x-wait-2": "yup", + }, + }); + + return handleRequest(req) + .then((response) => { + asserts.assertEquals(response?.status, 200); + return response; + }) + .then((response) => response.text()) + .then((body) => { + asserts.assertEquals(body, "Waited for x-wait-2!"); + }); + }); + + await t.step(`GET waits with header x-wait-3`, () => { + const req = new Request(url + "/accounts", { + method: "GET", + headers: { + "x-wait-3": "yup", + }, + }); + + return handleRequest(req) + .then((response) => { + asserts.assertEquals(response?.status, 200); + return response; + }) + .then((response) => response.text()) + .then((body) => { + asserts.assertEquals(body, "Waited for x-wait-3!"); + }); + }); + }); +}); diff --git a/tests/compat/deno/v1.x/modules/RequestChain/polyfill/default-behavior/app.ts b/tests/compat/deno/v1.x/modules/RequestChain/polyfill/default-behavior/app.ts new file mode 100644 index 000000000..da3c13f50 --- /dev/null +++ b/tests/compat/deno/v1.x/modules/RequestChain/polyfill/default-behavior/app.ts @@ -0,0 +1,78 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { StatusCode } from "../../../../../../../../src/core/http/response/StatusCode.ts"; +import { StatusDescription } from "../../../../../../../../src/core/http/response/StatusDescription.ts"; +import * as Chain from "../../../../../../../../src/modules/RequestChain/mod.polyfill.ts"; +import { Status } from "../../../../../../../../src/core/http/response/Status.ts"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +class Home extends Chain.Resource { + public paths = ["/"]; + + public GET(_request: Request) { + return new Response("Hello from GET."); + } + + public POST(_request: Request) { + return new Response("Hello from POST."); + } + + public DELETE(_request: Request) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_request: Request) { + throw new Chain.HTTPError(Status.MethodNotAllowed); + } +} + +const chain = Chain + .builder() + .resources(Home) + .build(); + +export const handleRequest = ( + request: Request, +): Promise => { + return chain + .handle(request) + .catch((error: Error | Chain.HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof Chain.HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + return new Response(error.message, { + status: error.status_code, + statusText: error.status_code_description, + }); + } + + return new Response(error.message, { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }); + }); +}; diff --git a/tests/compat/deno/v1.x/modules/RequestChain/polyfill/default-behavior/app_test.ts b/tests/compat/deno/v1.x/modules/RequestChain/polyfill/default-behavior/app_test.ts new file mode 100644 index 000000000..f1d506558 --- /dev/null +++ b/tests/compat/deno/v1.x/modules/RequestChain/polyfill/default-behavior/app_test.ts @@ -0,0 +1,143 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { asserts } from "../../../../../../../deps.ts"; +import { handleRequest, hostname, port, protocol } from "./app.ts"; + +const url = `${protocol}://${hostname}:${port}`; + +Deno.test("Polyfill - Using Request/Response", async (t) => { + await t.step("Home / paths = /", async (t) => { + for (const testCase of testCases()) { + const { method, expected } = testCase; + + await t.step(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + asserts.assertEquals(response?.status, expected.status); + asserts.assertEquals(body, expected.body); + }); + } + }); + + await t.step("Non-existent endpoints", async (t) => { + for (const testCase of testCasesNotFound()) { + const { method, expected } = testCase; + + await t.step(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/test", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + asserts.assertEquals(response?.status, expected.status); + asserts.assertEquals(body, expected.body); + }); + } + }); +}); + +function testCases() { + return [ + { + method: "GET", + expected: { + status: 200, + body: "Hello from GET.", + }, + }, + { + method: "POST", + expected: { + status: 200, + body: "Hello from POST.", + }, + }, + { + method: "PUT", + expected: { + status: 501, + body: "Not Implemented", + }, + }, + { + method: "DELETE", + expected: { + status: 500, + body: "Hey, I'm the DELETE endpoint. Errrr.", + }, + }, + { + method: "PATCH", + expected: { + status: 405, + body: "Method Not Allowed", + }, + }, + ]; +} + +function testCasesNotFound() { + return [ + { + method: "GET", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "POST", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PUT", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "DELETE", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PATCH", + expected: { + status: 404, + body: "Not Found", + }, + }, + ]; +} diff --git a/tests/compat/deno/v1.x/modules/RequestChain/polyfill/request-in-context-object/app.ts b/tests/compat/deno/v1.x/modules/RequestChain/polyfill/request-in-context-object/app.ts new file mode 100644 index 000000000..bf7ec7a14 --- /dev/null +++ b/tests/compat/deno/v1.x/modules/RequestChain/polyfill/request-in-context-object/app.ts @@ -0,0 +1,106 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { StatusCode } from "../../../../../../../../src/core/http/response/StatusCode.ts"; +import { StatusDescription } from "../../../../../../../../src/core/http/response/StatusDescription.ts"; +import * as Chain from "../../../../../../../../src/modules/RequestChain/mod.polyfill.ts"; +import { Status } from "../../../../../../../../src/core/http/response/Status.ts"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +type WebAPIContext = { + url: string; + method: string; + request: Request; + response?: Response; +}; + +class Home extends Chain.Resource { + public paths = ["/"]; + + public GET(context: WebAPIContext) { + context.response = new Response("Hello from GET."); + return context; + } + + public POST(context: WebAPIContext) { + context.response = new Response("Hello from POST."); + return context; + } + + public DELETE(_context: WebAPIContext) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_context: WebAPIContext) { + throw new Chain.HTTPError(Status.MethodNotAllowed); + } +} + +const chain = Chain + .builder() + .resources(Home) + .build(); + +export const handleRequest = ( + request: Request, +): Promise => { + const context = { + request, + url: request.url, + method: request.method, + }; + + return chain + .handle(context) + .then((returnedContext) => { + if (returnedContext.response) { + return returnedContext.response; + } + + return new Response( + "Response not generated", + { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }, + ); + }) + .catch((error: Error | Chain.HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof Chain.HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + return new Response(error.message, { + status: error.status_code, + statusText: error.status_code_description, + }); + } + + return new Response(error.message, { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }); + }); +}; diff --git a/tests/compat/deno/v1.x/modules/RequestChain/polyfill/request-in-context-object/app_test.ts b/tests/compat/deno/v1.x/modules/RequestChain/polyfill/request-in-context-object/app_test.ts new file mode 100644 index 000000000..11333afcc --- /dev/null +++ b/tests/compat/deno/v1.x/modules/RequestChain/polyfill/request-in-context-object/app_test.ts @@ -0,0 +1,143 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { asserts } from "../../../../../../../deps.ts"; +import { handleRequest, hostname, port, protocol } from "./app.ts"; + +const url = `${protocol}://${hostname}:${port}`; + +Deno.test("Polyfill - Using Request/Response in context object", async (t) => { + await t.step("Home / paths = /", async (t) => { + for await (const testCase of testCases()) { + const { method, expected } = testCase; + + await t.step(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + asserts.assertEquals(body, expected.body); + asserts.assertEquals(response?.status, expected.status); + }); + } + }); + + await t.step("Non-existent endpoints", async (t) => { + for (const testCase of testCasesNotFound()) { + const { method, expected } = testCase; + + await t.step(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/test", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + asserts.assertEquals(response?.status, expected.status); + asserts.assertEquals(body, expected.body); + }); + } + }); +}); + +function testCases() { + return [ + { + method: "GET", + expected: { + status: 200, + body: "Hello from GET.", + }, + }, + { + method: "POST", + expected: { + status: 200, + body: "Hello from POST.", + }, + }, + { + method: "PUT", + expected: { + status: 501, + body: "Not Implemented", + }, + }, + { + method: "DELETE", + expected: { + status: 500, + body: "Hey, I'm the DELETE endpoint. Errrr.", + }, + }, + { + method: "PATCH", + expected: { + status: 405, + body: "Method Not Allowed", + }, + }, + ]; +} + +function testCasesNotFound() { + return [ + { + method: "GET", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "POST", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PUT", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "DELETE", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PATCH", + expected: { + status: 404, + body: "Not Found", + }, + }, + ]; +} diff --git a/tests/compat/deno/v1.x/modules/base/Chain/app_native.ts b/tests/compat/deno/v1.x/modules/base/Chain/app_native.ts new file mode 100644 index 000000000..9a0dbfe31 --- /dev/null +++ b/tests/compat/deno/v1.x/modules/base/Chain/app_native.ts @@ -0,0 +1,90 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Chain as BaseChain } from "../../../../../../../src/modules/base/Chain.ts"; +import { RequestParamsParser } from "../../../../../../../src/standard/handlers/RequestParamsParser.ts"; +import { RequestValidator } from "../../../../../../../src/standard/handlers/RequestValidator.ts"; +import { ResourceCaller } from "../../../../../../../src/standard/handlers/ResourceCaller.ts"; +import { ResourceNotFoundHandler } from "../../../../../../../src/standard/handlers/ResourceNotFoundHandler.ts"; +import { StatusCode } from "../../../../../../../src/core/http/response/StatusCode.ts"; +import { StatusDescription } from "../../../../../../../src/core/http/response/StatusDescription.ts"; +import { HTTPError } from "../../../../../../../src/core/errors/HTTPError.ts"; +import { ResourcesIndex } from "../../../../../../../src/standard/handlers/ResourcesIndex.ts"; +import { Resource } from "../../../../../../../src/core/http/Resource.ts"; +import { Status } from "../../../../../../../src/core/http/response/Status.ts"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +class Home extends Resource { + public paths = ["/"]; + + public GET(_request: Request) { + return new Response("Hello from GET."); + } + + public POST(_request: Request) { + return new Response("Hello from POST."); + } + + public DELETE(_request: Request) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_request: Request) { + throw new HTTPError(Status.MethodNotAllowed); + } +} + +const chain = BaseChain + .builder() + .handler(new RequestValidator()) + // @ts-ignore We know URLPattern exists when dev'ing with Deno + .handler(new ResourcesIndex(URLPattern, Home)) + .handler(new ResourceNotFoundHandler()) + .handler(new RequestParamsParser()) + .handler(new ResourceCaller()) + .build(); + +export const handleRequest = ( + request: Request, +): Promise => { + return chain + .handle(request) + .catch((error: Error | HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + return new Response(error.message, { + status: error.status_code, + statusText: error.status_code_description, + }); + } + + return new Response(error.message, { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }); + }); +}; diff --git a/tests/compat/deno/v1.x/modules/base/Chain/app_native_test.ts b/tests/compat/deno/v1.x/modules/base/Chain/app_native_test.ts new file mode 100644 index 000000000..247935ab1 --- /dev/null +++ b/tests/compat/deno/v1.x/modules/base/Chain/app_native_test.ts @@ -0,0 +1,143 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { asserts } from "../../../../../../deps.ts"; +import { handleRequest, hostname, port, protocol } from "./app_native.ts"; + +const url = `${protocol}://${hostname}:${port}`; + +Deno.test("Native - Using Request/Response", async (t) => { + await t.step("Home / paths = /", async (t) => { + for (const testCase of testCases()) { + const { method, expected } = testCase; + + await t.step(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + asserts.assertEquals(response?.status, expected.status); + asserts.assertEquals(body, expected.body); + }); + } + }); + + await t.step("Non-existent endpoints", async (t) => { + for (const testCase of testCasesNotFound()) { + const { method, expected } = testCase; + + await t.step(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/test", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + asserts.assertEquals(response?.status, expected.status); + asserts.assertEquals(body, expected.body); + }); + } + }); +}); + +function testCases() { + return [ + { + method: "GET", + expected: { + status: 200, + body: "Hello from GET.", + }, + }, + { + method: "POST", + expected: { + status: 200, + body: "Hello from POST.", + }, + }, + { + method: "PUT", + expected: { + status: 501, + body: "Not Implemented", + }, + }, + { + method: "DELETE", + expected: { + status: 500, + body: "Hey, I'm the DELETE endpoint. Errrr.", + }, + }, + { + method: "PATCH", + expected: { + status: 405, + body: "Method Not Allowed", + }, + }, + ]; +} + +function testCasesNotFound() { + return [ + { + method: "GET", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "POST", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PUT", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "DELETE", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PATCH", + expected: { + status: 404, + body: "Not Found", + }, + }, + ]; +} diff --git a/tests/compat/deno/v1.x/modules/base/Chain/app_native_use.ts b/tests/compat/deno/v1.x/modules/base/Chain/app_native_use.ts new file mode 100644 index 000000000..b5a9319e2 --- /dev/null +++ b/tests/compat/deno/v1.x/modules/base/Chain/app_native_use.ts @@ -0,0 +1,168 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Chain as BaseChain } from "../../../../../../../src/modules/base/Chain.ts"; +import { Handler } from "../../../../../../../src/standard/handlers/Handler.ts"; +import { Resource } from "../../../../../../../src/modules/RequestChain/mod.native.ts"; +import { ResourceNotFoundHandler } from "../../../../../../../src/standard/handlers/ResourceNotFoundHandler.ts"; +import { StatusDescription } from "../../../../../../../src/core/http/response/StatusDescription.ts"; +import { StatusCode } from "../../../../../../../src/core/http/response/StatusCode.ts"; +import { HTTPError } from "../../../../../../../src/core/errors/HTTPError.ts"; +import { + ResourcesIndex, + SearchResult, +} from "../../../../../../../src/standard/handlers/ResourcesIndex.ts"; +import { Status } from "../../../../../../../src/core/http/response/Status.ts"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +type Ctx = { + request: Request; + response?: Response; + resource?: Resource; +}; + +class Home extends Resource { + public paths = ["/"]; + + public GET(ctx: Ctx) { + ctx.response = new Response(`Hello from GET.`); + } + + public POST(ctx: Ctx) { + ctx.response = new Response("Hello from POST."); + } + + public DELETE(_ctx: Ctx) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_ctx: Ctx) { + throw new HTTPError(Status.MethodNotAllowed); + } +} + +// Set up a wrapper to use `.use( (...) => {...} )` instead of `.handler(new Handler())` + +class UseInsteadOfHandleBuilder extends BaseChain.Builder { + public use(handlerFn: (ctx: Ctx) => void): this { + class UseHandler extends Handler { + handle(ctx: Ctx) { + return Promise + .resolve() + .then(() => handlerFn(ctx)) + .then(() => { + if (this.next !== null) { + return super.sendToNextHandler(ctx); + } + + return ctx as Output; // Intentional cast for now + }); + } + } + + const handler = new UseHandler(); + + Object.defineProperty(handler.constructor, "name", { + value: handlerFn.name, + }); + + return super.handler(handler); + } +} + +// Build the chain + +// @ts-ignore We know URLPattern exists if dev'ing with Deno's extension +const resourceIndex = new ResourcesIndex(URLPattern, Home); +const resourceNotFoundHandler = new ResourceNotFoundHandler(); +class ReturnSearchResult extends Handler { + handle(result: Output): Promise { + return Promise.resolve(result); + } +} + +resourceIndex + .setNext(resourceNotFoundHandler) + .setNext(new ReturnSearchResult()); + +const chain = (new UseInsteadOfHandleBuilder()) + .use(function ReceiveRequest(ctx: Ctx) { + if (!ctx.request) { + throw new Error("No request found"); + } + }) + .use(function FindResource(ctx) { + return resourceIndex + .handle(ctx.request) // Last handler is `ReturnSearchResult` and we can chain `then` from it + .then((result) => { + ctx.resource = result?.resource; + }); + }) + .use(function CallResource(ctx: Ctx) { + if (!ctx.resource) { + throw new HTTPError(Status.InternalServerError, "No resource"); + } + const method = ctx.request.method?.toUpperCase(); + // @ts-ignore We know this exists + return ctx.resource[method](ctx); + }) + .use(function SendResponse(ctx) { + if (!ctx.response) { + ctx.response = new Response("Woops", { status: 500 }); + } + }) + .build(); + +export const handleRequest = ( + request: Request, +): Promise => { + const ctx: Ctx = { request }; + + return chain + .handle(ctx) + .then(() => { + if (!ctx.response) { + return new Response("No response", { status: 500 }); + } + + return ctx.response; + }) + .catch((error: Error | HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + return new Response(error.message, { + status: error.status_code, + statusText: error.status_code_description, + }); + } + + return new Response(error.message, { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }); + }); +}; diff --git a/tests/compat/deno/v1.x/modules/base/Chain/app_native_use_test.ts b/tests/compat/deno/v1.x/modules/base/Chain/app_native_use_test.ts new file mode 100644 index 000000000..939491f08 --- /dev/null +++ b/tests/compat/deno/v1.x/modules/base/Chain/app_native_use_test.ts @@ -0,0 +1,143 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { asserts } from "../../../../../../deps.ts"; +import { handleRequest, hostname, port, protocol } from "./app_native_use.ts"; + +const url = `${protocol}://${hostname}:${port}`; + +Deno.test("Native - Using Request/Response", async (t) => { + await t.step("Home / paths = /", async (t) => { + for (const testCase of testCases()) { + const { method, expected } = testCase; + + await t.step(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + asserts.assertEquals(response?.status, expected.status); + asserts.assertEquals(body, expected.body); + }); + } + }); + + await t.step("Non-existent endpoints", async (t) => { + for (const testCase of testCasesNotFound()) { + const { method, expected } = testCase; + + await t.step(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/test", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + asserts.assertEquals(response?.status, expected.status); + asserts.assertEquals(body, expected.body); + }); + } + }); +}); + +function testCases() { + return [ + { + method: "GET", + expected: { + status: 200, + body: "Hello from GET.", + }, + }, + { + method: "POST", + expected: { + status: 200, + body: "Hello from POST.", + }, + }, + { + method: "PUT", + expected: { + status: 501, + body: "Not Implemented", + }, + }, + { + method: "DELETE", + expected: { + status: 500, + body: "Hey, I'm the DELETE endpoint. Errrr.", + }, + }, + { + method: "PATCH", + expected: { + status: 405, + body: "Method Not Allowed", + }, + }, + ]; +} + +function testCasesNotFound() { + return [ + { + method: "GET", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "POST", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PUT", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "DELETE", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PATCH", + expected: { + status: 404, + body: "Not Found", + }, + }, + ]; +} diff --git a/tests/compat/deno/v1.x/modules/base/Chain/app_polyfill.ts b/tests/compat/deno/v1.x/modules/base/Chain/app_polyfill.ts new file mode 100644 index 000000000..d677a409d --- /dev/null +++ b/tests/compat/deno/v1.x/modules/base/Chain/app_polyfill.ts @@ -0,0 +1,90 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Chain as BaseChain } from "../../../../../../../src/modules/base/Chain.ts"; +import { RequestParamsParser } from "../../../../../../../src/standard/handlers/RequestParamsParser.ts"; +import { RequestValidator } from "../../../../../../../src/standard/handlers/RequestValidator.ts"; +import { Resource } from "../../../../../../../src/core/http/Resource.ts"; +import { ResourceCaller } from "../../../../../../../src/standard/handlers/ResourceCaller.ts"; +import { ResourceNotFoundHandler } from "../../../../../../../src/standard/handlers/ResourceNotFoundHandler.ts"; +import { ResourcesIndex } from "../../../../../../../src/standard/handlers/ResourcesIndex.ts"; +import { StatusCode } from "../../../../../../../src/core/http/response/StatusCode.ts"; +import { StatusDescription } from "../../../../../../../src/core/http/response/StatusDescription.ts"; +import { URLPatternPolyfill } from "../../../../../../../src/standard/polyfill/URLPatternPolyfill.ts"; +import { HTTPError } from "../../../../../../../src/core/errors/HTTPError.ts"; +import { Status } from "../../../../../../../src/core/http/response/Status.ts"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +class Home extends Resource { + public paths = ["/"]; + + public GET(_request: Request) { + return new Response("Hello from GET."); + } + + public POST(_request: Request) { + return new Response("Hello from POST."); + } + + public DELETE(_request: Request) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_request: Request) { + throw new HTTPError(Status.MethodNotAllowed); + } +} + +const chain = BaseChain + .builder() + .handler(new RequestValidator()) + .handler(new ResourcesIndex(URLPatternPolyfill, Home)) // Using the `URLPattern` polyfill from Drash v1 + .handler(new ResourceNotFoundHandler()) + .handler(new RequestParamsParser()) + .handler(new ResourceCaller()) + .build(); + +export const handleRequest = ( + request: Request, +): Promise => { + return chain + .handle(request) + .catch((error: Error | HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + return new Response(error.message, { + status: error.status_code, + statusText: error.status_code_description, + }); + } + + return new Response(error.message, { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }); + }); +}; diff --git a/tests/compat/deno/v1.x/modules/base/Chain/app_polyfill_test.ts b/tests/compat/deno/v1.x/modules/base/Chain/app_polyfill_test.ts new file mode 100644 index 000000000..4d7aa1634 --- /dev/null +++ b/tests/compat/deno/v1.x/modules/base/Chain/app_polyfill_test.ts @@ -0,0 +1,143 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { asserts } from "../../../../../../deps.ts"; +import { handleRequest, hostname, port, protocol } from "./app_polyfill.ts"; + +const url = `${protocol}://${hostname}:${port}`; + +Deno.test("Native - Using Request/Response", async (t) => { + await t.step("Home / paths = /", async (t) => { + for (const testCase of testCases()) { + const { method, expected } = testCase; + + await t.step(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + asserts.assertEquals(response?.status, expected.status); + asserts.assertEquals(body, expected.body); + }); + } + }); + + await t.step("Non-existent endpoints", async (t) => { + for (const testCase of testCasesNotFound()) { + const { method, expected } = testCase; + + await t.step(`${method} returns ${expected.status}`, async () => { + const req = new Request(url + "/test", { + method, + }); + + const response = await handleRequest(req); + const body = await response?.text(); + + asserts.assertEquals(response?.status, expected.status); + asserts.assertEquals(body, expected.body); + }); + } + }); +}); + +function testCases() { + return [ + { + method: "GET", + expected: { + status: 200, + body: "Hello from GET.", + }, + }, + { + method: "POST", + expected: { + status: 200, + body: "Hello from POST.", + }, + }, + { + method: "PUT", + expected: { + status: 501, + body: "Not Implemented", + }, + }, + { + method: "DELETE", + expected: { + status: 500, + body: "Hey, I'm the DELETE endpoint. Errrr.", + }, + }, + { + method: "PATCH", + expected: { + status: 405, + body: "Method Not Allowed", + }, + }, + ]; +} + +function testCasesNotFound() { + return [ + { + method: "GET", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "POST", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PUT", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "DELETE", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PATCH", + expected: { + status: 404, + body: "Not Found", + }, + }, + ]; +} diff --git a/tests/compat/node/node-v16.x/modules/RequestChain/polyfill/node-http-in-context-object/app.test.ts b/tests/compat/node/node-v16.x/modules/RequestChain/polyfill/node-http-in-context-object/app.test.ts new file mode 100644 index 000000000..2e9470c48 --- /dev/null +++ b/tests/compat/node/node-v16.x/modules/RequestChain/polyfill/node-http-in-context-object/app.test.ts @@ -0,0 +1,154 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Socket } from "node:net"; +import { IncomingMessage, ServerResponse } from "node:http"; + +import { handleRequest } from "./app"; + +describe("Polyfill - Using IncomingMessage/ServerResponse in context object", () => { + describe.each(testCases())( + "Home / paths = /", + ({ method, expected }) => { + it(`${method} returns ${expected.status}`, async () => { + const req = new IncomingMessage(new Socket()); + req.method = method; + const res = new ServerResponse(req); + + await handleRequest(req, res); + const body = getBody(res); + + expect(body).toBe(expected.body); + expect(res.statusCode).toBe(expected.status); + }); + }, + ); + + describe.each(testCasesNotFound())( + "Non-existent endpoints / path = test", + ({ method, expected }) => { + it(`${method} returns ${expected.status}`, async () => { + const req = new IncomingMessage(new Socket()); + req.url = `/test`; + req.method = method; + const res = new ServerResponse(req); + + await handleRequest(req, res); + const body = getBody(res); + + expect(body).toBe(expected.body); + expect(res.statusCode).toBe(expected.status); + }); + }, + ); +}); + +function getBody(response: ServerResponse) { + const json = JSON.parse(JSON.stringify(response)); + + const body: string[] = []; + + for (const output of json.outputData) { + body.push(output.data.replace(json._header, "")); + } + + return body.join(""); +} + +function testCases() { + return [ + { + method: "GET", + expected: { + status: 200, + body: "Hello from GET.", + }, + }, + { + method: "POST", + expected: { + status: 200, + body: "Hello from POST.", + }, + }, + { + method: "PUT", + expected: { + status: 501, + body: "Not Implemented", + }, + }, + { + method: "DELETE", + expected: { + status: 500, + body: "Hey, I'm the DELETE endpoint. Errrr.", + }, + }, + { + method: "PATCH", + expected: { + status: 405, + body: "Method Not Allowed", + }, + }, + ]; +} + +function testCasesNotFound() { + return [ + { + method: "GET", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "POST", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PUT", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "DELETE", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PATCH", + expected: { + status: 404, + body: "Not Found", + }, + }, + ]; +} diff --git a/tests/compat/node/node-v16.x/modules/RequestChain/polyfill/node-http-in-context-object/app.ts b/tests/compat/node/node-v16.x/modules/RequestChain/polyfill/node-http-in-context-object/app.ts new file mode 100644 index 000000000..8eb72ff7c --- /dev/null +++ b/tests/compat/node/node-v16.x/modules/RequestChain/polyfill/node-http-in-context-object/app.ts @@ -0,0 +1,105 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { IncomingMessage, ServerResponse } from "node:http"; + +import { HTTPError } from "../../../../../../../../.drashland/lib/esm/core/errors/HTTPError"; +import { StatusCode } from "../../../../../../../../.drashland/lib/esm/core/http/response/StatusCode"; +import { StatusDescription } from "../../../../../../../../.drashland/lib/esm/core/http/response/StatusDescription"; +import * as Chain from "../../../../../../../../.drashland/lib/esm/modules/RequestChain/mod.polyfill"; +import { Status } from "../../../../../../../../.drashland/lib/esm/core/http/response/Status"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +type NodeContext = { + url: string; + method?: string; + request: IncomingMessage; + response: ServerResponse; +}; + +class Home extends Chain.Resource { + public paths = ["/"]; + + public GET(context: NodeContext) { + context.response.setHeader("x-drash", "Home.GET()"); + context.response.write("Hello from GET."); + return context; + } + + public POST(context: NodeContext) { + context.response.setHeader("x-drash", "Home.POST()"); + context.response.write("Hello from POST."); + } + + public DELETE(context: NodeContext) { + context.response.setHeader("x-drash", "Home.DELETE()"); + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(context: NodeContext) { + context.response.setHeader("x-drash", "Home.PATCH()"); + throw new HTTPError(Status.MethodNotAllowed); + } +} + +const chain = Chain + .builder() + .resources(Home) + .build(); + +export const handleRequest = ( + request: IncomingMessage, + response: ServerResponse, +): Promise => { + // We will keep the IncomingMessage and ServerResponse objects intact and just + // provide url and method to let the chain know how to route the request + const context = { + url: `${protocol}://${hostname}:${port}${request.url}`, + method: request.method, + request, + response, + }; + + return chain + .handle(context) + // There is no `.then((response) => { ... })` block here because resources + // use `context.response.end()` which tells Node the ServerResponse ended + .catch((error: Error | HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + context.response.statusCode = error.status_code; + context.response.statusMessage = error.status_code_description; + context.response.end(error.message); + } else { + context.response.statusCode = StatusCode.InternalServerError; + context.response.statusMessage = StatusDescription.InternalServerError; + context.response.end(error.message); + } + + return context; + }); +}; diff --git a/tests/compat/node/node-v18.x/modules/RequestChain/polyfill/node-http-converted-to-web-api/app.test.ts b/tests/compat/node/node-v18.x/modules/RequestChain/polyfill/node-http-converted-to-web-api/app.test.ts new file mode 100644 index 000000000..0c5a1b04a --- /dev/null +++ b/tests/compat/node/node-v18.x/modules/RequestChain/polyfill/node-http-converted-to-web-api/app.test.ts @@ -0,0 +1,154 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Socket } from "node:net"; +import { IncomingMessage, ServerResponse } from "node:http"; + +import { handleRequest } from "./app"; + +describe.only("Native - Convert IncomingMessage/ServerResponse to Request/Response", () => { + describe.each(testCases())( + "Home / paths = /", + ({ method, expected }) => { + it(`${method} returns ${expected.status}`, async () => { + const req = new IncomingMessage(new Socket()); + req.method = method; + const res = new ServerResponse(req); + + await handleRequest(req, res); + const body = getBody(res); + + expect(body).toBe(expected.body); + expect(res.statusCode).toBe(expected.status); + }); + }, + ); + + describe.each(testCasesNotFound())( + "Non-existent endpoints / path = test", + ({ method, expected }) => { + it(`${method} returns ${expected.status}`, async () => { + const req = new IncomingMessage(new Socket()); + req.url = `/test`; + req.method = method; + const res = new ServerResponse(req); + + await handleRequest(req, res); + const body = getBody(res); + + expect(body).toBe(expected.body); + expect(res.statusCode).toBe(expected.status); + }); + }, + ); +}); + +function getBody(response: ServerResponse) { + const json = JSON.parse(JSON.stringify(response)); + + const body: string[] = []; + + for (const output of json.outputData) { + body.push(output.data.replace(json._header, "")); + } + + return body.join(""); +} + +function testCases() { + return [ + { + method: "GET", + expected: { + status: 200, + body: "Hello from GET.", + }, + }, + { + method: "POST", + expected: { + status: 200, + body: "Hello from POST.", + }, + }, + { + method: "PUT", + expected: { + status: 501, + body: "Not Implemented", + }, + }, + { + method: "DELETE", + expected: { + status: 500, + body: "Hey, I'm the DELETE endpoint. Errrr.", + }, + }, + { + method: "PATCH", + expected: { + status: 405, + body: "Method Not Allowed", + }, + }, + ]; +} + +function testCasesNotFound() { + return [ + { + method: "GET", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "POST", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PUT", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "DELETE", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PATCH", + expected: { + status: 404, + body: "Not Found", + }, + }, + ]; +} diff --git a/tests/compat/node/node-v18.x/modules/RequestChain/polyfill/node-http-converted-to-web-api/app.ts b/tests/compat/node/node-v18.x/modules/RequestChain/polyfill/node-http-converted-to-web-api/app.ts new file mode 100644 index 000000000..6eaeab801 --- /dev/null +++ b/tests/compat/node/node-v18.x/modules/RequestChain/polyfill/node-http-converted-to-web-api/app.ts @@ -0,0 +1,95 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { IncomingMessage, ServerResponse } from "node:http"; + +import { HTTPError } from "../../../../../../../../.drashland/lib/esm/core/errors/HTTPError"; +import { StatusCode } from "../../../../../../../../.drashland/lib/esm/core/http/response/StatusCode"; +import { StatusDescription } from "../../../../../../../../.drashland/lib/esm/core/http/response/StatusDescription"; +import * as Chain from "../../../../../../../../.drashland/lib/esm/modules/RequestChain/mod.polyfill"; +import { Status } from "../../../../../../../../.drashland/lib/esm/core/http/response/Status"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +class Home extends Chain.Resource { + public paths = ["/"]; + + public GET(_request: Request) { + return new Response("Hello from GET."); + } + + public POST(_request: Request) { + return new Response("Hello from POST."); + } + + public DELETE(_request: Request) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_request: Request) { + throw new HTTPError(Status.MethodNotAllowed); + } +} + +const chain = Chain + .builder() + .resources(Home) + .build(); + +export const handleRequest = ( + req: IncomingMessage, + res: ServerResponse, +): Promise => { + // Convert the IncomingMessage object to a Request object + const request = new Request(`${protocol}://${hostname}:${port}${req.url}`, { + method: req.method, + }); + + return chain + .handle(request) + // All resources will return a Response object that we can use to build the + // ServerResponse object + .then((response) => { + res.statusCode = response.status; + return response.text(); + }) + .then((body) => { + res.end(body); + }) + .catch((error: Error | HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + res.statusCode = error.status_code; + res.statusMessage = error.status_code_description; + res.end(error.message); + return; + } + + res.statusCode = StatusCode.InternalServerError; + res.statusMessage = StatusDescription.InternalServerError; + res.end(error.message); + }); +}; diff --git a/tests/compat/node/node-v18.x/modules/RequestChain/polyfill/node-http-in-context-object/app.test.ts b/tests/compat/node/node-v18.x/modules/RequestChain/polyfill/node-http-in-context-object/app.test.ts new file mode 100644 index 000000000..2e9470c48 --- /dev/null +++ b/tests/compat/node/node-v18.x/modules/RequestChain/polyfill/node-http-in-context-object/app.test.ts @@ -0,0 +1,154 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Socket } from "node:net"; +import { IncomingMessage, ServerResponse } from "node:http"; + +import { handleRequest } from "./app"; + +describe("Polyfill - Using IncomingMessage/ServerResponse in context object", () => { + describe.each(testCases())( + "Home / paths = /", + ({ method, expected }) => { + it(`${method} returns ${expected.status}`, async () => { + const req = new IncomingMessage(new Socket()); + req.method = method; + const res = new ServerResponse(req); + + await handleRequest(req, res); + const body = getBody(res); + + expect(body).toBe(expected.body); + expect(res.statusCode).toBe(expected.status); + }); + }, + ); + + describe.each(testCasesNotFound())( + "Non-existent endpoints / path = test", + ({ method, expected }) => { + it(`${method} returns ${expected.status}`, async () => { + const req = new IncomingMessage(new Socket()); + req.url = `/test`; + req.method = method; + const res = new ServerResponse(req); + + await handleRequest(req, res); + const body = getBody(res); + + expect(body).toBe(expected.body); + expect(res.statusCode).toBe(expected.status); + }); + }, + ); +}); + +function getBody(response: ServerResponse) { + const json = JSON.parse(JSON.stringify(response)); + + const body: string[] = []; + + for (const output of json.outputData) { + body.push(output.data.replace(json._header, "")); + } + + return body.join(""); +} + +function testCases() { + return [ + { + method: "GET", + expected: { + status: 200, + body: "Hello from GET.", + }, + }, + { + method: "POST", + expected: { + status: 200, + body: "Hello from POST.", + }, + }, + { + method: "PUT", + expected: { + status: 501, + body: "Not Implemented", + }, + }, + { + method: "DELETE", + expected: { + status: 500, + body: "Hey, I'm the DELETE endpoint. Errrr.", + }, + }, + { + method: "PATCH", + expected: { + status: 405, + body: "Method Not Allowed", + }, + }, + ]; +} + +function testCasesNotFound() { + return [ + { + method: "GET", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "POST", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PUT", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "DELETE", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PATCH", + expected: { + status: 404, + body: "Not Found", + }, + }, + ]; +} diff --git a/tests/compat/node/node-v18.x/modules/RequestChain/polyfill/node-http-in-context-object/app.ts b/tests/compat/node/node-v18.x/modules/RequestChain/polyfill/node-http-in-context-object/app.ts new file mode 100644 index 000000000..2bfbfc2ba --- /dev/null +++ b/tests/compat/node/node-v18.x/modules/RequestChain/polyfill/node-http-in-context-object/app.ts @@ -0,0 +1,105 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { IncomingMessage, ServerResponse } from "node:http"; + +import { HTTPError } from "../../../../../../../../.drashland/lib/esm/core/errors/HTTPError"; +import { StatusCode } from "../../../../../../../../.drashland/lib/esm/core/http/response/StatusCode"; +import { StatusDescription } from "../../../../../../../../.drashland/lib/esm/core/http/response/StatusDescription"; +import * as Chain from "../../../../../../../../.drashland/lib/esm/modules/RequestChain/mod.polyfill"; +import { Status } from "../../../../../../../../.drashland/lib/esm/core/http/response/Status"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +type NodeContext = { + url: string; + method?: string; + request: IncomingMessage; + response: ServerResponse; +}; + +class Home extends Chain.Resource { + public paths = ["/"]; + + public GET(context: NodeContext) { + context.response.setHeader("x-drash", "Home.GET()"); + context.response.write("Hello from GET."); + return context; + } + + public POST(context: NodeContext) { + context.response.setHeader("x-drash", "Home.POST()"); + context.response.write("Hello from POST."); + } + + public DELETE(context: NodeContext) { + context.response.setHeader("x-drash", "Home.DELETE()"); + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(context: NodeContext) { + context.response.setHeader("x-drash", "Home.PATCH()"); + throw new HTTPError(Status.MethodNotAllowed); + } +} + +const chain = Chain + .builder() + .resources(Home) + .build(); + +export const handleRequest = ( + request: IncomingMessage, + response: ServerResponse, +): Promise => { + // We will keep the IncomingMessage and ServerResponse objects intact and just + // provide url and method to let the chain know how to route the request + const context = { + url: `${protocol}://${hostname}:${port}${request.url}`, + method: request.method, + request, + response, + }; + + return chain + .handle(context) + // There is no `.then((response) => { ... })` block here because resources + // use `context.response.end()` which tells Node the ServerResponse ended + .catch((error: Error | HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + context.response.statusCode = error.status_code; + context.response.statusMessage = error.status_code_description; + context.response.end(error.message); + } else { + context.response.statusCode = StatusCode.InternalServerError; + context.response.statusMessage = StatusDescription.InternalServerError; + context.response.end(error.message); + } + + return context; + }); +}; diff --git a/tests/compat/node/node-v20.x/modules/RequestChain/polyfill/node-http-converted-to-web-api/app.test.ts b/tests/compat/node/node-v20.x/modules/RequestChain/polyfill/node-http-converted-to-web-api/app.test.ts new file mode 100644 index 000000000..0c5a1b04a --- /dev/null +++ b/tests/compat/node/node-v20.x/modules/RequestChain/polyfill/node-http-converted-to-web-api/app.test.ts @@ -0,0 +1,154 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Socket } from "node:net"; +import { IncomingMessage, ServerResponse } from "node:http"; + +import { handleRequest } from "./app"; + +describe.only("Native - Convert IncomingMessage/ServerResponse to Request/Response", () => { + describe.each(testCases())( + "Home / paths = /", + ({ method, expected }) => { + it(`${method} returns ${expected.status}`, async () => { + const req = new IncomingMessage(new Socket()); + req.method = method; + const res = new ServerResponse(req); + + await handleRequest(req, res); + const body = getBody(res); + + expect(body).toBe(expected.body); + expect(res.statusCode).toBe(expected.status); + }); + }, + ); + + describe.each(testCasesNotFound())( + "Non-existent endpoints / path = test", + ({ method, expected }) => { + it(`${method} returns ${expected.status}`, async () => { + const req = new IncomingMessage(new Socket()); + req.url = `/test`; + req.method = method; + const res = new ServerResponse(req); + + await handleRequest(req, res); + const body = getBody(res); + + expect(body).toBe(expected.body); + expect(res.statusCode).toBe(expected.status); + }); + }, + ); +}); + +function getBody(response: ServerResponse) { + const json = JSON.parse(JSON.stringify(response)); + + const body: string[] = []; + + for (const output of json.outputData) { + body.push(output.data.replace(json._header, "")); + } + + return body.join(""); +} + +function testCases() { + return [ + { + method: "GET", + expected: { + status: 200, + body: "Hello from GET.", + }, + }, + { + method: "POST", + expected: { + status: 200, + body: "Hello from POST.", + }, + }, + { + method: "PUT", + expected: { + status: 501, + body: "Not Implemented", + }, + }, + { + method: "DELETE", + expected: { + status: 500, + body: "Hey, I'm the DELETE endpoint. Errrr.", + }, + }, + { + method: "PATCH", + expected: { + status: 405, + body: "Method Not Allowed", + }, + }, + ]; +} + +function testCasesNotFound() { + return [ + { + method: "GET", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "POST", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PUT", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "DELETE", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PATCH", + expected: { + status: 404, + body: "Not Found", + }, + }, + ]; +} diff --git a/tests/compat/node/node-v20.x/modules/RequestChain/polyfill/node-http-converted-to-web-api/app.ts b/tests/compat/node/node-v20.x/modules/RequestChain/polyfill/node-http-converted-to-web-api/app.ts new file mode 100644 index 000000000..6eaeab801 --- /dev/null +++ b/tests/compat/node/node-v20.x/modules/RequestChain/polyfill/node-http-converted-to-web-api/app.ts @@ -0,0 +1,95 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { IncomingMessage, ServerResponse } from "node:http"; + +import { HTTPError } from "../../../../../../../../.drashland/lib/esm/core/errors/HTTPError"; +import { StatusCode } from "../../../../../../../../.drashland/lib/esm/core/http/response/StatusCode"; +import { StatusDescription } from "../../../../../../../../.drashland/lib/esm/core/http/response/StatusDescription"; +import * as Chain from "../../../../../../../../.drashland/lib/esm/modules/RequestChain/mod.polyfill"; +import { Status } from "../../../../../../../../.drashland/lib/esm/core/http/response/Status"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +class Home extends Chain.Resource { + public paths = ["/"]; + + public GET(_request: Request) { + return new Response("Hello from GET."); + } + + public POST(_request: Request) { + return new Response("Hello from POST."); + } + + public DELETE(_request: Request) { + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(_request: Request) { + throw new HTTPError(Status.MethodNotAllowed); + } +} + +const chain = Chain + .builder() + .resources(Home) + .build(); + +export const handleRequest = ( + req: IncomingMessage, + res: ServerResponse, +): Promise => { + // Convert the IncomingMessage object to a Request object + const request = new Request(`${protocol}://${hostname}:${port}${req.url}`, { + method: req.method, + }); + + return chain + .handle(request) + // All resources will return a Response object that we can use to build the + // ServerResponse object + .then((response) => { + res.statusCode = response.status; + return response.text(); + }) + .then((body) => { + res.end(body); + }) + .catch((error: Error | HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + res.statusCode = error.status_code; + res.statusMessage = error.status_code_description; + res.end(error.message); + return; + } + + res.statusCode = StatusCode.InternalServerError; + res.statusMessage = StatusDescription.InternalServerError; + res.end(error.message); + }); +}; diff --git a/tests/compat/node/node-v20.x/modules/RequestChain/polyfill/node-http-in-context-object/app.test.ts b/tests/compat/node/node-v20.x/modules/RequestChain/polyfill/node-http-in-context-object/app.test.ts new file mode 100644 index 000000000..2e9470c48 --- /dev/null +++ b/tests/compat/node/node-v20.x/modules/RequestChain/polyfill/node-http-in-context-object/app.test.ts @@ -0,0 +1,154 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Socket } from "node:net"; +import { IncomingMessage, ServerResponse } from "node:http"; + +import { handleRequest } from "./app"; + +describe("Polyfill - Using IncomingMessage/ServerResponse in context object", () => { + describe.each(testCases())( + "Home / paths = /", + ({ method, expected }) => { + it(`${method} returns ${expected.status}`, async () => { + const req = new IncomingMessage(new Socket()); + req.method = method; + const res = new ServerResponse(req); + + await handleRequest(req, res); + const body = getBody(res); + + expect(body).toBe(expected.body); + expect(res.statusCode).toBe(expected.status); + }); + }, + ); + + describe.each(testCasesNotFound())( + "Non-existent endpoints / path = test", + ({ method, expected }) => { + it(`${method} returns ${expected.status}`, async () => { + const req = new IncomingMessage(new Socket()); + req.url = `/test`; + req.method = method; + const res = new ServerResponse(req); + + await handleRequest(req, res); + const body = getBody(res); + + expect(body).toBe(expected.body); + expect(res.statusCode).toBe(expected.status); + }); + }, + ); +}); + +function getBody(response: ServerResponse) { + const json = JSON.parse(JSON.stringify(response)); + + const body: string[] = []; + + for (const output of json.outputData) { + body.push(output.data.replace(json._header, "")); + } + + return body.join(""); +} + +function testCases() { + return [ + { + method: "GET", + expected: { + status: 200, + body: "Hello from GET.", + }, + }, + { + method: "POST", + expected: { + status: 200, + body: "Hello from POST.", + }, + }, + { + method: "PUT", + expected: { + status: 501, + body: "Not Implemented", + }, + }, + { + method: "DELETE", + expected: { + status: 500, + body: "Hey, I'm the DELETE endpoint. Errrr.", + }, + }, + { + method: "PATCH", + expected: { + status: 405, + body: "Method Not Allowed", + }, + }, + ]; +} + +function testCasesNotFound() { + return [ + { + method: "GET", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "POST", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PUT", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "DELETE", + expected: { + status: 404, + body: "Not Found", + }, + }, + { + method: "PATCH", + expected: { + status: 404, + body: "Not Found", + }, + }, + ]; +} diff --git a/tests/compat/node/node-v20.x/modules/RequestChain/polyfill/node-http-in-context-object/app.ts b/tests/compat/node/node-v20.x/modules/RequestChain/polyfill/node-http-in-context-object/app.ts new file mode 100644 index 000000000..2bfbfc2ba --- /dev/null +++ b/tests/compat/node/node-v20.x/modules/RequestChain/polyfill/node-http-in-context-object/app.ts @@ -0,0 +1,105 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { IncomingMessage, ServerResponse } from "node:http"; + +import { HTTPError } from "../../../../../../../../.drashland/lib/esm/core/errors/HTTPError"; +import { StatusCode } from "../../../../../../../../.drashland/lib/esm/core/http/response/StatusCode"; +import { StatusDescription } from "../../../../../../../../.drashland/lib/esm/core/http/response/StatusDescription"; +import * as Chain from "../../../../../../../../.drashland/lib/esm/modules/RequestChain/mod.polyfill"; +import { Status } from "../../../../../../../../.drashland/lib/esm/core/http/response/Status"; + +export const protocol = "http"; +export const hostname = "localhost"; +export const port = 1447; + +type NodeContext = { + url: string; + method?: string; + request: IncomingMessage; + response: ServerResponse; +}; + +class Home extends Chain.Resource { + public paths = ["/"]; + + public GET(context: NodeContext) { + context.response.setHeader("x-drash", "Home.GET()"); + context.response.write("Hello from GET."); + return context; + } + + public POST(context: NodeContext) { + context.response.setHeader("x-drash", "Home.POST()"); + context.response.write("Hello from POST."); + } + + public DELETE(context: NodeContext) { + context.response.setHeader("x-drash", "Home.DELETE()"); + throw new Error("Hey, I'm the DELETE endpoint. Errrr."); + } + + public PATCH(context: NodeContext) { + context.response.setHeader("x-drash", "Home.PATCH()"); + throw new HTTPError(Status.MethodNotAllowed); + } +} + +const chain = Chain + .builder() + .resources(Home) + .build(); + +export const handleRequest = ( + request: IncomingMessage, + response: ServerResponse, +): Promise => { + // We will keep the IncomingMessage and ServerResponse objects intact and just + // provide url and method to let the chain know how to route the request + const context = { + url: `${protocol}://${hostname}:${port}${request.url}`, + method: request.method, + request, + response, + }; + + return chain + .handle(context) + // There is no `.then((response) => { ... })` block here because resources + // use `context.response.end()` which tells Node the ServerResponse ended + .catch((error: Error | HTTPError) => { + if ( + (error.name === "HTTPError" || error instanceof HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + context.response.statusCode = error.status_code; + context.response.statusMessage = error.status_code_description; + context.response.end(error.message); + } else { + context.response.statusCode = StatusCode.InternalServerError; + context.response.statusMessage = StatusDescription.InternalServerError; + context.response.end(error.message); + } + + return context; + }); +}; diff --git a/tests/compat/node/tsconfig.json b/tests/compat/node/tsconfig.json new file mode 100644 index 000000000..bb88fd469 --- /dev/null +++ b/tests/compat/node/tsconfig.json @@ -0,0 +1,6 @@ +{ + "include": [ + "../../.drashland/**/*.js", + "./**/*.ts" + ] +} diff --git a/tests/data/example_code/class_one_p_property_one.ts b/tests/data/example_code/class_one_p_property_one.ts deleted file mode 100644 index 1d1ef4d0c..000000000 --- a/tests/data/example_code/class_one_p_property_one.ts +++ /dev/null @@ -1,4 +0,0 @@ -// deno-lint-ignore no-unused-vars -import { Drash } from "https://deno.land/x/drash/mod.ts"; - -// This is just an example file for testing... nothing else to see here. diff --git a/tests/data/example_code/class_one_p_property_two.ts b/tests/data/example_code/class_one_p_property_two.ts deleted file mode 100644 index fef83a9cf..000000000 --- a/tests/data/example_code/class_one_p_property_two.ts +++ /dev/null @@ -1 +0,0 @@ -// comment diff --git a/tests/data/index.html b/tests/data/index.html deleted file mode 100644 index 5c9dede5c..000000000 --- a/tests/data/index.html +++ /dev/null @@ -1 +0,0 @@ -This is the index.html file for testing pretty links diff --git a/tests/data/resources/test_resource_1.ts b/tests/data/resources/test_resource_1.ts deleted file mode 100644 index 3499a9ea8..000000000 --- a/tests/data/resources/test_resource_1.ts +++ /dev/null @@ -1,5 +0,0 @@ -import members from "../../../members.ts"; - -export default class TestResource1 extends members.Drash.Http.Resource() { - paths = ["/test-resource-1"]; -} diff --git a/tests/data/resources/test_resource_2.ts b/tests/data/resources/test_resource_2.ts deleted file mode 100644 index 1032699e1..000000000 --- a/tests/data/resources/test_resource_2.ts +++ /dev/null @@ -1,5 +0,0 @@ -import members from "../../../members.ts"; - -export default class TestResource2 extends members.Drash.Http.Resource() { - paths = ["/test-resource-2"]; -} diff --git a/tests/data/sample_1.txt b/tests/data/sample_1.txt deleted file mode 100644 index 8c7a1c204..000000000 --- a/tests/data/sample_1.txt +++ /dev/null @@ -1,35 +0,0 @@ -----------------------------434049563556637648550474 -content-disposition: form-data; name="foo" -content-type: application/octet-stream - -foo -----------------------------434049563556637648550474 -content-disposition: form-data; name="bar" -content-type: application/octet-stream - -bar -----------------------------434049563556637648550474 -content-disposition: form-data; name="file"; filename="tsconfig.json" -content-type: application/octet-stream - -{ - "compilerOptions": { - "target": "es2018", - "baseUrl": ".", - "paths": { - "deno": ["./deno.d.ts"], - "https://*": ["../../.deno/deps/https/*"], - "http://*": ["../../.deno/deps/http/*"] - } - } -} - -----------------------------434049563556637648550474 -content-disposition: form-data; name="file2"; filename="中文.json" -content-type: application/octet-stream - -{ - "test": "filename" -} - -----------------------------434049563556637648550474-- diff --git a/tests/data/sample_2.txt b/tests/data/sample_2.txt deleted file mode 100644 index 2b481de53..000000000 --- a/tests/data/sample_2.txt +++ /dev/null @@ -1,6 +0,0 @@ -----------------------------434049563556637648550474 -content-disposition: form-data; name="hello" -content-type: application/octet-stream - -world -----------------------------434049563556637648550474-- diff --git a/tests/data/sample_3.txt b/tests/data/sample_3.txt deleted file mode 100644 index 3a0de020f..000000000 --- a/tests/data/sample_3.txt +++ /dev/null @@ -1,6 +0,0 @@ -----------------------------434049563556637648550474 -content-disposition: form-data; name="foo" -content-type: application/octet-stream - -foo -----------------------------434049563556637648550474-- diff --git a/tests/data/server.crt b/tests/data/server.crt deleted file mode 100644 index 0c67ccb69..000000000 --- a/tests/data/server.crt +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDajCCAlKgAwIBAgIJAOPyQVdy/UpPMA0GCSqGSIb3DQEBCwUAMCcxCzAJBgNV -BAYTAlVTMRgwFgYDVQQDDA9FeGFtcGxlLVJvb3QtQ0EwIBcNMTkxMDIxMTYyODU4 -WhgPMjExODA5MjcxNjI4NThaMG0xCzAJBgNVBAYTAlVTMRIwEAYDVQQIDAlZb3Vy -U3RhdGUxETAPBgNVBAcMCFlvdXJDaXR5MR0wGwYDVQQKDBRFeGFtcGxlLUNlcnRp -ZmljYXRlczEYMBYGA1UEAwwPbG9jYWxob3N0LmxvY2FsMIIBIjANBgkqhkiG9w0B -AQEFAAOCAQ8AMIIBCgKCAQEAz9svjVdf5jihUBtofd84XKdb8dEHQRJfDNKaJ4Ar -baqMHAdnqi/fWtlqEEMn8gweZ7+4hshECY5mnx4Hhy7IAbePDsTTbSm01dChhlxF -uvd9QuvzvrqSjSq+v4Jlau+pQIhUzzV12dF5bFvrIrGWxCZp+W7lLDZI6Pd6Su+y -ZIeiwrUaPMzdUePNf2hZI/IvWCUMCIyoqrrKHdHoPuvQCW17IyxsnFQJNbmN+Rtp -BQilhtwvBbggCBWhHxEdiqBaZHDw6Zl+bU7ejx1mu9A95wpQ9SCL2cRkAlz2LDOy -wznrTAwGcvqvFKxlV+3HsaD7rba4kCA1Ihp5mm/dS2k94QIDAQABo1EwTzAfBgNV -HSMEGDAWgBTzut+pwwDfqmMYcI9KNWRDhxcIpTAJBgNVHRMEAjAAMAsGA1UdDwQE -AwIE8DAUBgNVHREEDTALgglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggEBAKVu -vVpu5nPGAGn1SX4FQUcbn9Z5wgBkjnZxfJHJQX4sYIRlcirZviPHCZGPWex4VHC+ -lFMm+70YEN2uoe5jGrdgcugzx2Amc7/mLrsvvpMsaS0PlxNMcqhdM1WHbGjjdNln -XICVITSKnB1fSGH6uo9CMCWw5kgPS9o4QWrLLkxnds3hoz7gVEUyi/6V65mcfFNA -lof9iKcK9JsSHdBs35vpv7UKLX+96RM7Nm2Mu0yue5JiS79/zuMA/Kryxot4jv5z -ecdWFl0eIyQBZmBzMw2zPUqkxEnXLiKjV8jutEg/4qovTOB6YiA41qbARXdzNA2V -FYuchcTcWmnmVVRFyyU= ------END CERTIFICATE----- diff --git a/tests/data/server.key b/tests/data/server.key deleted file mode 100644 index 6c4569b69..000000000 --- a/tests/data/server.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDP2y+NV1/mOKFQ -G2h93zhcp1vx0QdBEl8M0pongCttqowcB2eqL99a2WoQQyfyDB5nv7iGyEQJjmaf -HgeHLsgBt48OxNNtKbTV0KGGXEW6931C6/O+upKNKr6/gmVq76lAiFTPNXXZ0Xls -W+sisZbEJmn5buUsNkjo93pK77Jkh6LCtRo8zN1R481/aFkj8i9YJQwIjKiqusod -0eg+69AJbXsjLGycVAk1uY35G2kFCKWG3C8FuCAIFaEfER2KoFpkcPDpmX5tTt6P -HWa70D3nClD1IIvZxGQCXPYsM7LDOetMDAZy+q8UrGVX7cexoPuttriQIDUiGnma -b91LaT3hAgMBAAECggEBAJABfn+BQorBP1m9s3ZJmcXvmW7+7/SwYrQCkRS+4te2 -6h1dMAAj7K4HpUkhDeLPbJ1aoeCXjTPFuemRp4uL6Lvvzahgy059L7FXOyFYemMf -pmQgDx5cKr6tF7yc/eDJrExuZ7urgTvouiRNxqmhuh+psZBDuXkZHwhwtQSH7uNg -KBDKu0qWO73vFLcLckdGEU3+H9oIWs5xcvvOkWzyvHbRGFJSihgcRpPPHodF5xB9 -T/gZIoJHMmCbUMlWaSasUyNXTuvCnkvBDol8vXrMJCVzKZj9GpPDcIFdc08GSn4I -pTdSNwzUcHbdERzdVU28Xt+t6W5rvp/4FWrssi4IzkUCgYEA//ZcEcBguRD4OFrx -6wbSjzCcUW1NWhzA8uTOORZi4SvndcH1cU4S2wznuHNubU1XlrGwJX6PUGebmY/l -53B5PJvStbVtZCVIxllR+ZVzRuL8wLodRHzlYH8GOzHwoa4ivSupkzl72ij1u/tI -NMLGfYEKVdNd8zXIESUY88NszvsCgYEAz+MDp3xOhFaCe+CPv80A592cJcfzc8Al -+rahEOu+VdN2QBZf86PIf2Bfv/t0QvnRvs1z648TuH6h83YSggOAbmfHyd789jkq -UWlktIaXbVn+VaHmPTcBWTg3ZTlvG+fiFCbZXiYhm+UUf1MDqZHdiifAoyVIjV/Z -YhCNJo3q39MCgYEAknrpK5t9fstwUcfyA/9OhnVaL9suVjB4V0iLn+3ovlXCywgp -ryLv9X3IKi2c9144jtu3I23vFCOGz3WjKzSZnQ7LogNmy9XudNxu5jcZ1mpWHPEl -iKk1F2j6Juwoek5OQRX4oHFYKHwiTOa75r3Em9Q6Fu20KVgQ24bwZafj3/sCgYAy -k0AoVw2jFIjaKl/Ogclen4OFjYek+XJD9Hpq62964d866Dafx5DXrFKfGkXGpZBp -owI4pK5fjC9KU8dc6g0szwLEEgPowy+QbtuZL8VXTTWbD7A75E3nrs2LStXFLDzM -OkdXqF801h6Oe1vAvUPwgItVJZTpEBCK0wwD/TLPEQKBgQDRkhlTtAoHW7W6STd0 -A/OWc0dxhzMurpxg0bLgCqUjw1ESGrSCGhffFn0IWa8sv19VWsZuBhTgjNatZsYB -AhDs/6OosT/3nJoh2/t0hYDj1FBI0lPXWYD4pesuZ5yIMrmSaAOtIzp4BGY7ui8N -wOqcq/jdiHj/MKEdqOXy3YAJrA== ------END PRIVATE KEY----- diff --git a/tests/data/static_file.html b/tests/data/static_file.html deleted file mode 100644 index 9daeafb98..000000000 --- a/tests/data/static_file.html +++ /dev/null @@ -1 +0,0 @@ -test diff --git a/tests/data/static_file.json b/tests/data/static_file.json deleted file mode 100644 index de2aa2e06..000000000 --- a/tests/data/static_file.json +++ /dev/null @@ -1 +0,0 @@ -{ "test": "test" } diff --git a/tests/data/static_file.txt b/tests/data/static_file.txt deleted file mode 100644 index 9daeafb98..000000000 --- a/tests/data/static_file.txt +++ /dev/null @@ -1 +0,0 @@ -test diff --git a/tests/data/views/tengine_index.html b/tests/data/views/tengine_index.html deleted file mode 100644 index c82c5e788..000000000 --- a/tests/data/views/tengine_index.html +++ /dev/null @@ -1 +0,0 @@ -<% greeting %> \ No newline at end of file diff --git a/tests/deps.ts b/tests/deps.ts index bb7f20dae..7de2a91a8 100644 --- a/tests/deps.ts +++ b/tests/deps.ts @@ -1,13 +1,22 @@ -export * as Drash from "../mod.ts"; -export * as path from "https://deno.land/std@0.175.0/path/mod.ts"; -export * as TestHelpers from "./test_helpers.ts"; -export { - assert, - assertEquals, - assertNotEquals, -} from "https://deno.land/std@0.175.0/testing/asserts.ts"; +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ -export { green, red } from "https://deno.land/std@0.175.0/fmt/colors.ts"; -export { delay } from "https://deno.land/std@0.175.0/async/delay.ts"; - -export { deferred } from "https://deno.land/std@0.175.0/async/deferred.ts"; +export * as asserts from "https://deno.land/std@v0.199.0/assert/mod.ts"; diff --git a/tests/integration/browser_request_resource_test.ts b/tests/integration/browser_request_resource_test.ts deleted file mode 100644 index 398fabe01..000000000 --- a/tests/integration/browser_request_resource_test.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * This test addresses an issue where someone on the discord had their default - * content type set, but on browser requests the response was "null". This is - * because originally, the response class didn't fully take into account the - * config AND the accept headers. Essentially meaning, returning text/html (as - * this was the first type on the request) - */ - -import { assertEquals, TestHelpers } from "../deps.ts"; -import { Request, Resource, Response, Server } from "../../mod.ts"; - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - APP SETUP ///////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -class BrowserRequestResource extends Resource { - paths = ["/browser-request"]; - - public GET(_request: Request, response: Response) { - response.text("hello"); - } -} - -const server = new Server({ - resources: [ - BrowserRequestResource, - ], - protocol: "http", - hostname: "localhost", - port: 3000, -}); - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - TESTS ///////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -Deno.test("GET /browser-request", async (t) => { - await t.step("Response should be JSON", async () => { - server.run(); - // Example browser request - const response = await TestHelpers.makeRequest.get( - "http://localhost:3000/browser-request", - { - headers: { - Accept: "*/*", - }, - }, - ); - await server.close(); - assertEquals(await response.text(), "hello"); - assertEquals( - response.headers.get("Content-Type"), - "text/plain", - ); - }); -}); diff --git a/tests/integration/coffee_resource_test.ts b/tests/integration/coffee_resource_test.ts deleted file mode 100644 index 510f7dd5c..000000000 --- a/tests/integration/coffee_resource_test.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { assertEquals, TestHelpers } from "../deps.ts"; -import { - Errors, - IResource, - Request, - Resource, - Response, - Server, -} from "../../mod.ts"; - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - APP SETUP ///////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -interface ICoffee { - name: string; -} - -export class CoffeeResource extends Resource implements IResource { - paths = ["/coffee", "/coffee/:id"]; - - protected coffee = new Map([ - [17, { name: "Light" }], - [18, { name: "Medium" }], - [19, { name: "Dark" }], - ]); - - public GET(request: Request, response: Response) { - // @ts-ignore Ignoring because we don't care - let coffeeId: string | null | undefined = request.pathParam("id"); - const location = request.queryParam("location"); - if (location) { - if (location == "from_query") { - coffeeId = request.queryParam("id"); - } - } - - if (!coffeeId) { - response.text("Please specify a coffee ID."); - return; - } - - response.text(JSON.stringify(this.getCoffee(parseInt(coffeeId)))); - } - - protected getCoffee(coffeeId: number): ICoffee { - let coffee = null; - - try { - coffee = this.coffee.get(coffeeId); - } catch (error) { - throw new Errors.HttpError( - 400, - `Error getting coffee with ID "${coffeeId}". Error: ${error.message}.`, - ); - } - - if (!coffee) { - throw new Errors.HttpError( - 404, - `Coffee with ID "${coffeeId}" not found.`, - ); - } - - return coffee as ICoffee; - } -} - -const server = new Server({ - resources: [ - CoffeeResource, - ], - protocol: "http", - hostname: "localhost", - port: 3000, -}); - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - TESTS ///////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -Deno.test("coffee_resource_test.ts", async (t) => { - await t.step("/coffee (path params)", async (t) => { - await t.step("works as expected with path params", async () => { - server.run(); - - let response; - - // response = await fetch("http://localhost:3000/coffee"); - // assertEquals( - // await response.text(), - // 'Please specify a coffee ID.', - // ); - - // response = await fetch("http://localhost:3000/coffee/"); - // assertEquals( - // await response.text(), - // 'Please specify a coffee ID.', - // ); - - // response = await fetch("http://localhost:3000/coffee//"); - // assertEquals((await response.text()).startsWith("Error: Not Found"), true); - - // response = await fetch("http://localhost:3000/coffee/17"); - // assertEquals(await response.text(), '{"name":"Light"}'); - - response = await fetch("http://localhost:3000/coffee/17/", { - headers: { - Accept: "text/plain", - }, - }); - assertEquals(await response.text(), '{"name":"Light"}'); - - response = await fetch("http://localhost:3000/coffee/18", { - headers: { - Accept: "text/plain", - }, - }); - assertEquals(await response.text(), '{"name":"Medium"}'); - - response = await fetch("http://localhost:3000/coffee/18/", { - headers: { - Accept: "text/plain", - }, - }); - assertEquals(await response.text(), '{"name":"Medium"}'); - - response = await fetch("http://localhost:3000/coffee/19", { - headers: { - Accept: "text/plain", - }, - }); - assertEquals(await response.text(), '{"name":"Dark"}'); - - response = await fetch("http://localhost:3000/coffee/19/", { - headers: { - Accept: "text/plain", - }, - }); - assertEquals(await response.text(), '{"name":"Dark"}'); - - response = await fetch("http://localhost:3000/coffee/20", { - headers: { - Accept: "text/plain", - }, - }); - assertEquals( - (await response.text()).startsWith( - 'Error: Coffee with ID "20" not found.', - ), - true, - ); - - response = await TestHelpers.makeRequest.post( - "http://localhost:3000/coffee/17/", - { - headers: { - Accept: "text/plain", - }, - }, - ); - assertEquals( - (await response.text()).startsWith("Error: Method Not Allowed"), - true, - ); - - await server.close(); - }); - }); - - await t.step("/coffee (url query params)", async (t) => { - await t.step("works as expected with URL query params", async () => { - server.run(); - - const response = await fetch( - "http://localhost:3000/coffee/19?location=from_query&id=18", - { - method: "GET", - headers: { - Accept: "text/plain", - }, - }, - ); - const t = await response.text(); - assertEquals(t, '{"name":"Medium"}'); - - await server.close(); - }); - }); -}); diff --git a/tests/integration/cookie_resource_test.ts b/tests/integration/cookie_resource_test.ts deleted file mode 100644 index 4dd227e1a..000000000 --- a/tests/integration/cookie_resource_test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { assertEquals, TestHelpers } from "../deps.ts"; -import { Request, Resource, Response, Server } from "../../mod.ts"; - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - APP SETUP ///////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -class CookieResource extends Resource { - paths = ["/cookie", "/cookie/"]; - - public GET(request: Request, response: Response) { - const cookieValue = request.getCookie("testCookie"); - response.text(cookieValue); - } - - public POST(_request: Request, response: Response) { - response.setCookie({ name: "testCookie", value: "Drash" }); - response.text("Saved your cookie!"); - } - - public DELETE(_request: Request, response: Response) { - response.text("DELETE request received!"); - response.deleteCookie("testCookie"); - } -} - -const server = new Server({ - resources: [ - CookieResource, - ], - protocol: "http", - hostname: "localhost", - port: 3000, -}); - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - TESTS ///////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -Deno.test("cookie_resource_test.ts", async (t) => { - await t.step("/cookie", async (t) => { - await t.step("cookie can be created, retrieved, and deleted", async () => { - server.run(); - - let response; - const cookie = { name: "testCookie", value: "Drash" }; - - // Post - response = await TestHelpers.makeRequest.post( - "http://localhost:3000/cookie", - { - headers: { - "Content-Type": "application/json", - Accept: "text/plain", - }, - body: cookie, - }, - ); - assertEquals(await response.text(), "Saved your cookie!"); - - // Get - Dependent on the above post request saving a cookie - response = await TestHelpers.makeRequest.get( - "http://localhost:3000/cookie", - { - credentials: "same-origin", - headers: { - Cookie: "testCookie=Drash", - Accept: "text/plain", - }, - }, - ); - await assertEquals(await response.text(), "Drash"); - - // Remove - Dependent on the above post request saving a cookie - response = await TestHelpers.makeRequest.delete( - "http://localhost:3000/cookie", - { - headers: { - Accept: "text/plain", - }, - }, - ); - const cookies = response.headers.get("set-cookie") || ""; - const cookieVal = cookies.split(";")[0].split("=")[1]; - assertEquals(cookieVal, ""); - await response.arrayBuffer(); - //await response.body.close() - - await server.close(); - }); - }); -}); diff --git a/tests/integration/error_handler_test.ts b/tests/integration/error_handler_test.ts deleted file mode 100644 index abc02ec99..000000000 --- a/tests/integration/error_handler_test.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { assertEquals, TestHelpers } from "../deps.ts"; -import type { ConnInfo } from "../../deps.ts"; -import { - ErrorHandler, - Errors, - IErrorHandler, - Response, - Server, -} from "../../mod.ts"; - -class MyErrorHandler extends ErrorHandler { - public catch(error: Error, _request: Request, response: Response) { - let code = 0; - if (error instanceof Errors.HttpError) { - code = error.code; - } else { - code = 500; - } - response.json({ error: error.message }, code); - } -} - -class MyHttpErrorErrorHandler extends ErrorHandler { - public catch(_error: Error, _request: Request, _response: Response) { - throw new Errors.HttpError(500, "error on ErrorHandler"); - } -} - -class MyOwnErrorHandler { - public catch(error: Error, _request: Request, response: Response) { - let code = 0; - if (error instanceof Errors.HttpError) { - code = error.code; - } else { - code = 500; - } - response.json({ error: error.message }, code); - } -} - -class MyAsyncErrorHandler extends ErrorHandler { - public async catch(error: Error, _request: Request, response: Response) { - let code = 0; - if (error instanceof Errors.HttpError) { - code = error.code; - } else { - code = 500; - } - await new Promise((resolve) => setTimeout(resolve, 200)); - response.json({ error: error.message }, code); - } -} - -class MySimpleErrorErrorHandler { - public catch(_error: Error, _request: Request, _response: Response) { - throw new Error("My Simple Error"); - } -} - -class _TypeCheckConnInfoInErrorHandler { - public catch( - _error: Error, - _request: Request, - response: Response, - connInfo: ConnInfo, - ) { - return response.json(connInfo); - } -} - -class MyConnInfoErrorErrorHandler implements IErrorHandler { - public catch( - _error: Error, - _request: Request, - response: Response, - connInfo: ConnInfo, - ) { - return response.json(connInfo); - } -} - -Deno.test("error_handler_test.ts", async (t) => { - await t.step("GET /", async (t) => { - await t.step("default ErrorHandler", async () => { - const server = new Server({ - protocol: "http", - hostname: "localhost", - port: 3000, - resources: [], - }); - server.run(); - const res = await TestHelpers.makeRequest.get(server.address); - await server.close(); - - assertEquals(res.status, 404); - assertEquals( - (await res.text()).includes("Error: Not Found\n"), - true, - ); - }); - - await t.step("custom ErrorHandler", async () => { - const server = new Server({ - protocol: "http", - hostname: "localhost", - port: 3000, - resources: [], - error_handler: MyErrorHandler, - }); - server.run(); - const res = await TestHelpers.makeRequest.get(server.address); - await server.close(); - - assertEquals(res.status, 404); - assertEquals(await res.json(), { error: "Not Found" }); - }); - - await t.step("custom ErrorHandler thrown error", async () => { - const server = new Server({ - protocol: "http", - hostname: "localhost", - port: 3000, - resources: [], - error_handler: MyHttpErrorErrorHandler, - }); - server.run(); - const res = await TestHelpers.makeRequest.get(server.address); - await server.close(); - - assertEquals(res.status, 500); - assertEquals( - (await res.text()).includes("Error: error on ErrorHandler\n"), - true, - ); - }); - - await t.step("custom ErrorHandler without extends", async () => { - const server = new Server({ - protocol: "http", - hostname: "localhost", - port: 3000, - resources: [], - error_handler: MyOwnErrorHandler, - }); - server.run(); - const res = await TestHelpers.makeRequest.get(server.address); - await server.close(); - - assertEquals(res.status, 404); - assertEquals(await res.json(), { error: "Not Found" }); - }); - - await t.step("custom ErrorHandler with async catch", async () => { - const server = new Server({ - protocol: "http", - hostname: "localhost", - port: 3000, - resources: [], - error_handler: MyAsyncErrorHandler, - }); - server.run(); - const res = await TestHelpers.makeRequest.get(server.address); - await server.close(); - - assertEquals(res.status, 404); - assertEquals(await res.json(), { error: "Not Found" }); - }); - - await t.step("custom ErrorHandler simple Error thrown", async () => { - const server = new Server({ - protocol: "http", - hostname: "localhost", - port: 3000, - resources: [], - error_handler: MySimpleErrorErrorHandler, - }); - server.run(); - const res = await TestHelpers.makeRequest.get(server.address); - await server.close(); - - assertEquals(res.status, 500); - assertEquals( - (await res.text()).includes("Error: My Simple Error\n"), - true, - ); - }); - - await t.step("connInfo error handler gets connInfo", async () => { - const server = new Server({ - protocol: "http", - hostname: "localhost", - port: 3000, - resources: [], - error_handler: MyConnInfoErrorErrorHandler, - }); - server.run(); - const res = await TestHelpers.makeRequest.get(server.address); - await server.close(); - const json = await res.json(); - assertEquals(res.status, 200); - assertEquals(json.localAddr.port, 3000); - assertEquals(json.remoteAddr.transport, "tcp"); - }); - }); -}); diff --git a/tests/integration/etag_test.ts b/tests/integration/etag_test.ts deleted file mode 100644 index 53c9a78ca..000000000 --- a/tests/integration/etag_test.ts +++ /dev/null @@ -1,124 +0,0 @@ -/** - * This test addresses an issue where someone on the discord had their default - * content type set, but on browser requests the response was "null". This is - * because originally, the response class didn't fully take into account the - * config AND the accept headers. Essentially meaning, returning text/html (as - * this was the first type on the request) - */ - -import { assert, assertEquals, assertNotEquals, delay } from "../deps.ts"; -import { Interfaces, Request, Resource, Response, Server } from "../../mod.ts"; -import { ETagService } from "../../src/services/etag/etag.ts"; - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - APP SETUP ///////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -class ETagResource extends Resource { - paths = ["/etag/:name?"]; - - public GET(request: Request, response: Response) { - const name = request.pathParam("name") ?? ""; - response.text("hello " + name); - } -} - -const configs: Interfaces.IServerOptions = { - resources: [ - ETagResource, - ], - protocol: "http", - hostname: "localhost", - port: 3000, -}; - -function makeServer(weak = false) { - return new Server({ - ...configs, - services: [new ETagService({ weak })], - }); -} - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - TESTS ///////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -Deno.test("etag_test.ts", async (t) => { - await t.step("GET /etag", async (t) => { - await t.step("Should set the header and default to strong", async () => { - const strongServer = makeServer(); - strongServer.run(); - // Example browser request - const response = await fetch( - `${strongServer.address}/etag`, - ); - await strongServer.close(); - assertEquals(await response.text(), "hello "); - const header = response.headers.get("etag") ?? ""; - assert(header.match(/\"\d-.*\"/)); - assert(response.headers.get("last-modified")); - }); - await t.step( - "Should set the header and be weak if specified", - async () => { - const weakServer = makeServer(true); - weakServer.run(); - // Example browser request - const response = await fetch( - `${weakServer.address}/etag`, - ); - await weakServer.close(); - assertEquals(await response.text(), "hello "); - const header = response.headers.get("etag") ?? ""; - assert(header.match(/W\/\"\d-.*\"/)); - assert(response.headers.get("last-modified")); - }, - ); - await t.step( - "Header values stay the same after 2 reqs with same body", - async () => { - const strongServer = makeServer(); - strongServer.run(); - // Example browser request - const response1 = await fetch( - `${strongServer.address}/etag`, - ); - await response1.text(); - const response2 = await fetch( - `${strongServer.address}/etag`, - ); - await response2.text(); - await strongServer.close(); - const lastModified1 = response1.headers.get("last-modified"); - const etag1 = response1.headers.get("etag"); - const lastModified2 = response1.headers.get("last-modified"); - const etag2 = response1.headers.get("etag"); - assertEquals(lastModified1, lastModified2); - assertEquals(etag1, etag2); - }, - ); - await t.step( - "Header values are different after 2nd req has different body", - async () => { - const strongServer = makeServer(); - strongServer.run(); - const response1 = await fetch( - `${strongServer.address}/etag`, - ); - await response1.text(); - await delay(1500); - const response2 = await fetch( - `${strongServer.address}/etag/edward`, - ); - await response2.text(); - await strongServer.close(); - const lastModified1 = response1.headers.get("last-modified"); - const etag1 = response1.headers.get("etag"); - const lastModified2 = response2.headers.get("last-modified"); - const etag2 = response2.headers.get("etag"); - assertNotEquals(lastModified1, lastModified2); - assertNotEquals(etag1, etag2); - }, - ); - }); -}); diff --git a/tests/integration/files_resource_test.ts b/tests/integration/files_resource_test.ts deleted file mode 100644 index 9da930c85..000000000 --- a/tests/integration/files_resource_test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { assertEquals } from "../deps.ts"; -import { Request, Resource, Response, Server } from "../../mod.ts"; - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - APP SETUP ///////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -class FilesResource extends Resource { - paths = ["/files"]; - - public POST(request: Request, response: Response) { - response.text(request.bodyParam("value_1") ?? "No body param was set."); - } -} - -const server = new Server({ - resources: [ - FilesResource, - ], - protocol: "http", - hostname: "localhost", - port: 3000, -}); - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - TESTS ///////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -Deno.test("files_resource_test.ts", async (t) => { - await t.step("/files", async (t) => { - await t.step("multipart/form-data works", async () => { - server.run(); - - const formData = new FormData(); - formData.append("value_1", "John"); - - const response = await fetch("http://localhost:3000/files", { - method: "POST", - body: formData, - headers: { - Accept: "text/plain", - }, - }); - assertEquals(await response.text(), "John"); - - await server.close(); - }); - }); -}); diff --git a/tests/integration/graphql_test.ts b/tests/integration/graphql_test.ts deleted file mode 100644 index 540e6f702..000000000 --- a/tests/integration/graphql_test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { assertEquals } from "../deps.ts"; -import * as Drash from "../../mod.ts"; -import { GraphQL, GraphQLService } from "../../src/services/graphql/graphql.ts"; - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - TEST DATA ///////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -const schema = GraphQL.buildSchema(` - type Query { - hello: String - } -`); - -const root = { - hello: () => { - return "Hello world!"; - }, -}; - -const graphQL = new GraphQLService({ schema, graphiql: true, rootValue: root }); - -async function serverAction( - action: "close", - server: Drash.Server, -): Promise; - -async function serverAction(action: "run"): Promise; - -async function serverAction( - action: "run" | "close", - server?: Drash.Server, -): Promise { - if (action === "run") { - const server = new Drash.Server({ - resources: [], - protocol: "http", - port: 1337, - hostname: "localhost", - services: [graphQL], - }); - server.run(); - return server; - } - if (action === "close" && server) { - await server.close(); - } -} - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - TESTS ///////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -Deno.test("graphql_test.ts", async (t) => { - await t.step("GraphQL", async (t) => { - await t.step( - "Can respond with a playground when used as middleware", - async () => { - const server = await serverAction("run"); - const res = await fetch("http://localhost:1337/graphql"); - await serverAction("close", server); - const text = await res.text(); - assertEquals( - text.indexOf("GraphQL Playground") > -1, - true, - ); - assertEquals(res.status, 200); - assertEquals(res.headers.get("Content-Type"), "text/html"); - }, - ); - await t.step( - "Will make a query on a request when used as middleware", - async () => { - const server = await serverAction("run"); - const res = await fetch("http://localhost:1337/graphql", { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: "{ hello }", - }), - }); - await serverAction("close", server); - const json = await res.json(); - assertEquals(res.status, 200); - assertEquals( - res.headers.get("Content-Type"), - "application/json", - ); - assertEquals(json, { data: { hello: "Hello world!" } }); - }, - ); - }); -}); diff --git a/tests/integration/home_resource_test.ts b/tests/integration/home_resource_test.ts deleted file mode 100644 index 6ca9a279a..000000000 --- a/tests/integration/home_resource_test.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { assertEquals, TestHelpers } from "../deps.ts"; -import { Request, Resource, Response, Server } from "../../mod.ts"; - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - APP SETUP ///////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -class HomeResource extends Resource { - paths = ["/", "/home"]; - - public GET(_request: Request, response: Response) { - response.text("GET request received!"); - } - - public POST(_request: Request, response: Response) { - response.text("POST request received!"); - } - - public PUT(_request: Request, response: Response) { - response.text("PUT request received!"); - } - - public DELETE(_request: Request, response: Response) { - response.text("DELETE request received!"); - } -} - -const server = new Server({ - resources: [ - HomeResource, - ], - protocol: "http", - hostname: "localhost", - port: 3000, -}); - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - TESTS ///////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -Deno.test("home_resource_test.ts", async (t) => { - await t.step("/", async (t) => { - await t.step("only defined methods are accessible", async () => { - server.run(); - - let response; - - response = await TestHelpers.makeRequest.get("http://localhost:3000", { - headers: { - Accept: "text/plain", - }, - }); - assertEquals( - await response.text(), - "GET request received!", - ); - - response = await TestHelpers.makeRequest.get( - "http://localhost:3000/home", - { - headers: { - Accept: "text/plain", - }, - }, - ); - assertEquals( - await response.text(), - "GET request received!", - ); - - response = await TestHelpers.makeRequest.get( - "http://localhost:3000/home/", - { - headers: { - Accept: "text/plain", - }, - }, - ); - assertEquals( - await response.text(), - "GET request received!", - ); - - response = await TestHelpers.makeRequest.get( - "http://localhost:3000/home//", - { - headers: { - Accept: "text/plain", - }, - }, - ); - assertEquals( - (await response.text()).startsWith("Error: Not Found"), - true, - ); - - response = await TestHelpers.makeRequest.post("http://localhost:3000", { - headers: { - Accept: "text/plain", - }, - }); - assertEquals( - await response.text(), - "POST request received!", - ); - - response = await TestHelpers.makeRequest.put("http://localhost:3000", { - headers: { - Accept: "text/plain", - }, - }); - assertEquals( - await response.text(), - "PUT request received!", - ); - - response = await TestHelpers.makeRequest.delete("http://localhost:3000", { - headers: { - Accept: "text/plain", - }, - }); - assertEquals( - await response.text(), - "DELETE request received!", - ); - - response = await TestHelpers.makeRequest.patch("http://localhost:3000", { - headers: { - Accept: "text/plain", - }, - }); - assertEquals( - (await response.text()).startsWith("Error: Method Not Allowed"), - true, - ); - - await server.close(); - }); - }); -}); diff --git a/tests/integration/https_test.ts b/tests/integration/https_test.ts deleted file mode 100644 index 67c32fbee..000000000 --- a/tests/integration/https_test.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * This test addresses an issue where someone on the discord had their default - * content type set, but on browser requests the response was "null". This is - * because originally, the response class didn't fully take into account the - * config AND the accept headers. Essentially meaning, returning text/html (as - * this was the first type on the request) - */ - -import { assertEquals, TestHelpers } from "../deps.ts"; -import { Request, Resource, Response, Server } from "../../mod.ts"; - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - APP SETUP ///////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -class BrowserRequestResource extends Resource { - paths = ["/browser-request"]; - - public GET(_request: Request, response: Response) { - response.text("hello"); - } -} - -const server = new Server({ - resources: [ - BrowserRequestResource, - ], - protocol: "https", - hostname: "localhost", - port: 3000, - key_file: "./tests/data/server.key", - cert_file: "./tests/data/server.crt", -}); - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - TESTS ///////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -Deno.test("browser_request_resource.ts", async (t) => { - await t.step("GET (https) /browser-request", async (t) => { - await t.step("Response should be JSON", async () => { - server.run(); - // Example browser request - const response = await TestHelpers.makeRequest.get( - "https://localhost:3000/browser-request", - { - headers: { - Accept: "*/*", - }, - }, - ); - await server.close(); - assertEquals(await response.text(), "hello"); - assertEquals( - response.headers.get("Content-Type"), - "text/plain", - ); - }); - }); -}); diff --git a/tests/integration/optional_path_params_resource_test.ts b/tests/integration/optional_path_params_resource_test.ts deleted file mode 100644 index 405216896..000000000 --- a/tests/integration/optional_path_params_resource_test.ts +++ /dev/null @@ -1,435 +0,0 @@ -import { assertEquals, TestHelpers } from "../deps.ts"; -import { Request, Resource, Response, Server } from "../../mod.ts"; - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - APP SETUP ///////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -class OptionalPathParamsResource extends Resource { - paths = [ - "/oppWithoutRequired/:name?/:age_of_person?/:city?", - "/oppWithRequired/:name/:age_of_person?", - ]; - - public GET(request: Request, response: Response) { - const name = request.pathParam("name"); - // deno-lint-ignore camelcase - const age_of_person = request.pathParam("age_of_person"); - const city = request.pathParam("city"); - - response.json({ - message: "Successfully handled optional path params", - data: { - name, - age_of_person, - city, - }, - }); - } -} - -const server = new Server({ - resources: [ - OptionalPathParamsResource, - ], - protocol: "http", - hostname: "localhost", - port: 3000, -}); - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - TESTS ///////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -Deno.test("optional_path_params_test.ts", async (t) => { - await t.step("/oppWithoutRequired", async (t) => { - await t.step("Resource should handle request", async () => { - server.run(); - - const response = await TestHelpers.makeRequest.get( - "http://localhost:3000/oppWithoutRequired", - { - headers: { - Accept: "application/json", - }, - }, - ); - const json = await response.json(); - assertEquals( - json.message, - "Successfully handled optional path params", - ); - assertEquals(json.data.name, undefined); - assertEquals(json.data.age_of_person, undefined); - assertEquals(json.data.city, undefined); - - await server.close(); - }); - }); - await t.step("/oppWithoutRequired/", async (t) => { - await t.step("Resource should handle request", async () => { - server.run(); - - const response = await TestHelpers.makeRequest.get( - "http://localhost:3000/oppWithoutRequired/", - { - headers: { - Accept: "application/json", - }, - }, - ); - const json = await response.json(); - assertEquals( - json.message, - "Successfully handled optional path params", - ); - assertEquals(json.data.name, undefined); - assertEquals(json.data.age_of_person, undefined); - assertEquals(json.data.city, undefined); - - await server.close(); - }); - }); - await t.step("/oppWithoutRequired/:name", async (t) => { - await t.step("Resource should handle request", async () => { - server.run(); - - const response = await TestHelpers.makeRequest.get( - "http://localhost:3000/oppWithoutRequired/edward", - { - headers: { - Accept: "application/json", - }, - }, - ); - const json = await response.json(); - assertEquals( - json.message, - "Successfully handled optional path params", - ); - assertEquals(json.data.name, "edward"); - assertEquals(json.data.age_of_person, undefined); - assertEquals(json.data.city, undefined); - - await server.close(); - }); - }); - await t.step("/oppWithoutRequired/:name/", async (t) => { - await t.step("Resource should handle request", async () => { - server.run(); - - const response = await TestHelpers.makeRequest.get( - "http://localhost:3000/oppWithoutRequired/edward/", - { - headers: { - Accept: "application/json", - }, - }, - ); - const json = await response.json(); - assertEquals( - json.message, - "Successfully handled optional path params", - ); - assertEquals(json.data.name, "edward"); - assertEquals(json.data.age_of_person, undefined); - assertEquals(json.data.city, undefined); - - await server.close(); - }); - }); - await t.step("/oppWithoutRequired/:name/:age_of_person", async (t) => { - await t.step("Resource should handle request", async () => { - server.run(); - - const response = await TestHelpers.makeRequest.get( - "http://localhost:3000/oppWithoutRequired/edward/999", - { - headers: { - Accept: "application/json", - }, - }, - ); - const json = await response.json(); - assertEquals( - json.message, - "Successfully handled optional path params", - ); - assertEquals(json.data.name, "edward"); - assertEquals(json.data.age_of_person, "999"); - assertEquals(json.data.city, undefined); - - await server.close(); - }); - }); - await t.step("/oppWithoutRequired/:name/:age_of_person/", async (t) => { - await t.step("Resource should handle request", async () => { - server.run(); - - const response = await TestHelpers.makeRequest.get( - "http://localhost:3000/oppWithoutRequired/edward/999/", - { - headers: { - Accept: "application/json", - }, - }, - ); - const json = await response.json(); - assertEquals( - json.message, - "Successfully handled optional path params", - ); - assertEquals(json.data.name, "edward"); - assertEquals(json.data.age_of_person, "999"); - assertEquals(json.data.city, undefined); - - await server.close(); - }); - }); - await t.step("/oppWithoutRequired/:name/:age_of_person/:city", async (t) => { - await t.step("Resource should handle request", async () => { - server.run(); - const response = await TestHelpers.makeRequest.get( - "http://localhost:3000/oppWithoutRequired/edward/999/UK", - { - headers: { - Accept: "application/json", - }, - }, - ); - const json = await response.json(); - assertEquals( - json.message, - "Successfully handled optional path params", - ); - assertEquals(json.data.name, "edward"); - assertEquals(json.data.age_of_person, "999"); - assertEquals(json.data.city, "UK"); - - await server.close(); - }); - }); - await t.step("/oppWithoutRequired/:name/:age_of_person/:city/", async (t) => { - await t.step("Resource should handle request", async () => { - server.run(); - - const response = await TestHelpers.makeRequest.get( - "http://localhost:3000/oppWithoutRequired/edward/999/UK/", - { - headers: { - Accept: "application/json", - }, - }, - ); - const json = await response.json(); - assertEquals( - json.message, - "Successfully handled optional path params", - ); - assertEquals(json.data.name, "edward"); - assertEquals(json.data.age_of_person, "999"); - assertEquals(json.data.city, "UK"); - - await server.close(); - }); - }); - await t.step( - "/oppWithoutRequired/:name/:age_of_person/:city/:other", - async (t) => { - await t.step("Resource should NOT handle request", async () => { - server.run(); - - const response = await TestHelpers.makeRequest.get( - "http://localhost:3000/oppWithoutRequired/edward/999/UK/other", - { - headers: { - Accept: "application/json", - }, - }, - ); - assertEquals( - (await response.text()).startsWith("Error: Not Found"), - true, - ); - await server.close(); - }); - }, - ); - await t.step("/oppWithoutRequired/:name/", async (t) => { - await t.step( - "Resource should handle request", - async () => { - server.run(); - - const response = await TestHelpers.makeRequest.get( - "http://localhost:3000/oppWithoutRequired/edward/", - { - headers: { - Accept: "application/json", - }, - }, - ); - const json = await response.json(); - assertEquals( - json.message, - "Successfully handled optional path params", - ); - assertEquals(json.data.name, "edward"); - assertEquals(json.data.age_of_person, undefined); - assertEquals(json.data.city, undefined); - - await server.close(); - }, - ); - }); - await t.step("/oppWithRequired", async (t) => { - await t.step("Resource should NOT handle request", async () => { - server.run(); - - const response = await TestHelpers.makeRequest.get( - "http://localhost:3000/oppWithRequired", - { - headers: { - Accept: "application/json", - }, - }, - ); - assertEquals( - (await response.text()).startsWith("Error: Not Found"), - true, - ); - - await server.close(); - }); - }); - await t.step("/oppWithRequired/", async (t) => { - await t.step("Resource should NOT handle request", async () => { - server.run(); - - const response = await TestHelpers.makeRequest.get( - "http://localhost:3000/oppWithRequired/", - { - headers: { - Accept: "application/json", - }, - }, - ); - assertEquals( - (await response.text()).startsWith("Error: Not Found"), - true, - ); - - await server.close(); - }); - }); - await t.step("/oppWithRequired/edward", async (t) => { - await t.step("Resource should handle request", async () => { - server.run(); - - const response = await TestHelpers.makeRequest.get( - "http://localhost:3000/oppWithRequired/edward", - { - headers: { - Accept: "application/json", - }, - }, - ); - await server.close(); - const json = await response.json(); - assertEquals( - json.message, - "Successfully handled optional path params", - ); - assertEquals(json.data.name, "edward"); - assertEquals(json.data.age_of_person, undefined); - }); - }); - await t.step("/oppWithRequired/edward/", async (t) => { - await t.step("Resource should handle request", async () => { - server.run(); - - const response = await TestHelpers.makeRequest.get( - "http://localhost:3000/oppWithRequired/edward/", - { - headers: { - Accept: "application/json", - }, - }, - ); - await server.close(); - const json = await response.json(); - assertEquals( - json.message, - "Successfully handled optional path params", - ); - assertEquals(json.data.name, "edward"); - assertEquals(json.data.age_of_person, undefined); - }); - }); - await t.step("/oppWithRequired/ed-123/22", async (t) => { - await t.step("Resource should handle request", async () => { - server.run(); - - const response = await TestHelpers.makeRequest.get( - "http://localhost:3000/oppWithRequired/ed-123/22-22", - { - headers: { - Accept: "application/json", - }, - }, - ); - const json = await response.json(); - assertEquals( - json.message, - "Successfully handled optional path params", - ); - assertEquals(json.data.name, "ed-123"); - assertEquals(json.data.age_of_person, "22-22"); - - await server.close(); - }); - }); - await t.step("/oppWithRequired/edward/22/", async (t) => { - await t.step("Resource should handle request", async () => { - server.run(); - - const response = await TestHelpers.makeRequest.get( - "http://localhost:3000/oppWithRequired/edward/22/", - { - headers: { - Accept: "application/json", - }, - }, - ); - const json = await response.json(); - assertEquals( - json.message, - "Successfully handled optional path params", - ); - assertEquals(json.data.name, "edward"); - assertEquals(json.data.age_of_person, "22"); - - await server.close(); - }); - }); - await t.step("/oppWithRequired/edward/22/other", async (t) => { - await t.step("Resource should NOT handle request", async () => { - server.run(); - - const response = await TestHelpers.makeRequest.get( - "http://localhost:3000/oppWithRequired/edward/22/other", - { - headers: { - Accept: "application/json", - }, - }, - ); - assertEquals( - (await response.text()).startsWith("Error: Not Found"), - true, - ); - - await server.close(); - }); - }); -}); diff --git a/tests/integration/post_no_body_test.ts b/tests/integration/post_no_body_test.ts deleted file mode 100644 index c93db3473..000000000 --- a/tests/integration/post_no_body_test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { assertEquals, TestHelpers } from "../deps.ts"; -import { Request, Resource, Response, Server } from "../../mod.ts"; - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - APP SETUP ///////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -class PostNoBodyResource extends Resource { - paths = ["/post-no-body"]; - - public POST(_request: Request, response: Response) { - response.text("POST request received!"); - } -} - -const server = new Server({ - resources: [ - PostNoBodyResource, - ], - protocol: "http", - hostname: "localhost", - port: 3000, -}); - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - TESTS ///////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -Deno.test("post_no_body_test.ts", async (t) => { - await t.step("POST /post-no-body", async (t) => { - /** - * See the following for reasons why this test was added: - * - * - https://github.com/drashland/drash/pull/691 - */ - await t.step("Does not throw if a body is not defined", async () => { - server.run(); - - const response = await TestHelpers.makeRequest.post( - "http://localhost:3000/post-no-body", - ); - const text = await response.text(); - await server.close(); - assertEquals( - text, - "POST request received!", - ); - }); - }); -}); diff --git a/tests/integration/posting_invalid_json_test.ts b/tests/integration/posting_invalid_json_test.ts deleted file mode 100644 index 1594dba06..000000000 --- a/tests/integration/posting_invalid_json_test.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * This test addresses an issue where someone on the discord had their default - * content type set, but on browser requests the response was "null". This is - * because originally, the response class didn't fully take into account the - * config AND the accept headers. Essentially meaning, returning text/html (as - * this was the first type on the request) - */ - -import { assertEquals } from "../deps.ts"; -import { Request, Resource, Response, Server } from "../../mod.ts"; - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - APP SETUP ///////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -class Res extends Resource { - paths = ["/"]; - - public POST(request: Request, response: Response) { - response.text(request.bodyParam("name") ?? ""); - } -} - -const server = new Server({ - resources: [ - Res, - ], - protocol: "http", - hostname: "localhost", - port: 3000, -}); - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - TESTS ///////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -Deno.test("posting_invlaid_json_test.ts", async (t) => { - await t.step("POST /", async (t) => { - await t.step("Should return error when json body is invalid", async () => { - server.run(); - const response = await fetch( - server.address, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: '{"name": "ed"}},,', - }, - ); - await server.close(); - assertEquals( - (await response.text()).startsWith( - "Error: Unprocessable Entity. The request body seems to be invalid as there was an error parsing it.", - ), - true, - ); - assertEquals( - response.status, - 422, - ); - }); - }); -}); diff --git a/tests/integration/rate_limiter_test.ts b/tests/integration/rate_limiter_test.ts deleted file mode 100644 index 4341d9160..000000000 --- a/tests/integration/rate_limiter_test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { assert, assertEquals } from "../deps.ts"; -import { RateLimiterService } from "../../src/services/rate_limiter/rate_limiter.ts"; -import * as Drash from "../../mod.ts"; - -class Resource extends Drash.Resource { - paths = ["/"]; - public GET(_request: Drash.Request, response: Drash.Response) { - return response.text("Hello world"); - } -} - -Deno.test("rate_limiter_test", async (t) => { - await t.step("Not hit limit", async (t) => { - await t.step( - "Header should be set correctly when you request and haven't hit the limit", - async () => { - const rateLimiter = new RateLimiterService({ - timeframe: 15 * 60 * 1000, - max_requests: 3, - }); - const server = new Drash.Server({ - resources: [Resource], - services: [rateLimiter], - protocol: "http", - port: 1667, - hostname: "localhost", - }); - server.run(); - const res1 = await fetch(server.address); - assertEquals(res1.status, 200); - assertEquals(await res1.text(), "Hello world"); - assert(res1.headers.get("date")); - assertEquals(res1.headers.get("x-ratelimit-limit"), "3"); - assertEquals( - res1.headers.get("x-ratelimit-remaining"), - "2", - ); - assert(res1.headers.get("x-ratelimit-reset")); - const res2 = await fetch(server.address); - await server.close(); - assertEquals(res2.status, 200); - assertEquals(await res2.text(), "Hello world"); - assert(res2.headers.get("date")); - assertEquals(res2.headers.get("x-ratelimit-limit"), "3"); - assertEquals( - res2.headers.get("x-ratelimit-remaining"), - "1", - ); - assert(res2.headers.get("x-ratelimit-reset")); - rateLimiter.cleanup(); - }, - ); - }); - await t.step("Has hit limit", async (t) => { - await t.step( - "Header should be set correctly when you request and have hit the limit", - async () => { - const rateLimiter = new RateLimiterService({ - timeframe: 15 * 60 * 1000, - max_requests: 3, - }); - const server = new Drash.Server({ - resources: [Resource], - services: [rateLimiter], - protocol: "http", - port: 1667, - hostname: "localhost", - }); - server.run(); - const res1 = await fetch(server.address); - await res1.arrayBuffer(); - const res2 = await fetch(server.address); - await res2.arrayBuffer(); - const res3 = await fetch(server.address); - assertEquals(res3.status, 200); - assertEquals(await res3.text(), "Hello world"); - assert(res3.headers.get("date")); - assertEquals(res3.headers.get("x-ratelimit-limit"), "3"); - assertEquals( - res3.headers.get("x-ratelimit-remaining"), - "0", - ); - assert(res3.headers.get("x-ratelimit-reset")); - assertEquals(res3.headers.get("x-retry-after"), null); - const res4 = await fetch(server.address); - await server.close(); - assertEquals(res4.status, 429); - const text = await res4.text(); - const retryAfter = res4.headers.get("x-retry-after"); - assert( - text.startsWith( - "Error: Too Many Requests. Please try again after " + retryAfter, - ), - ); - assert(res4.headers.get("date")); - assertEquals(res4.headers.get("x-ratelimit-limit"), "3"); - assertEquals( - res4.headers.get("x-ratelimit-remaining"), - "0", - ); - assert(res4.headers.get("x-ratelimit-reset")); - assertEquals(res4.headers.get("x-retry-after"), "900s"); - rateLimiter.cleanup(); - }, - ); - }); -}); diff --git a/tests/integration/redirect_test.ts b/tests/integration/redirect_test.ts deleted file mode 100644 index 238f529b9..000000000 --- a/tests/integration/redirect_test.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * This test addresses an issue where someone on the discord had their default - * content type set, but on browser requests the response was "null". This is - * because originally, the response class didn't fully take into account the - * config AND the accept headers. Essentially meaning, returning text/html (as - * this was the first type on the request) - */ - -import { assertEquals } from "../deps.ts"; -import { Request, Resource, Response, Server } from "../../mod.ts"; - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - APP SETUP ///////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -class Res0 extends Resource { - paths = ["/"]; - - public GET(_request: Request, response: Response) { - this.redirect("http://localhost:3000/redirect", response); - } -} - -class Res1 extends Resource { - paths = ["/redirects-with-307"]; - - public GET(_request: Request, response: Response) { - this.redirect("http://localhost:3000/redirect", response, 307); - } -} - -class Res2 extends Resource { - paths = ["/redirect"]; - - public GET(_request: Request, response: Response) { - response.text("hello"); - } -} - -const server = new Server({ - resources: [ - Res0, - Res1, - Res2, - ], - protocol: "http", - hostname: "localhost", - port: 3000, -}); - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - TESTS ///////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -Deno.test("redirect_test.ts", async (t) => { - await t.step("GET /", async (t) => { - await t.step("Should redirect to another resource", async () => { - server.run(); - // Example browser request - const response = await fetch(server.address); - await server.close(); - assertEquals(await response.text(), "hello"); - assertEquals(response.status, 200); - }); - await t.step( - "Should respect the status code during redirection", - async () => { - server.run(); - // Example browser request - const response = await fetch(server.address + "/redirects-with-307", { - redirect: "manual", - }); - await server.close(); - assertEquals(await response.text(), ""); - assertEquals(response.status, 307); - }, - ); - }); -}); diff --git a/tests/integration/request_accepts_resource_test.ts b/tests/integration/request_accepts_resource_test.ts deleted file mode 100644 index de5f0ac25..000000000 --- a/tests/integration/request_accepts_resource_test.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { assertEquals, TestHelpers } from "../deps.ts"; -import { Request, Resource, Response, Server } from "../../mod.ts"; - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - APP SETUP ///////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -class RequestAcceptsUseCaseOneResource extends Resource { - paths = ["/request-accepts-use-case-one"]; - - public GET(request: Request, response: Response) { - const typeToRequest = request.queryParam("typeToCheck"); - - let matchedType; - if (typeToRequest) { - if (request.accepts(typeToRequest)) { - matchedType = typeToRequest; - } - } else { - if (request.accepts("text/html")) { - matchedType = "text/html"; - } - } - - if (!matchedType) { - response.json({ success: false }); - return; - } - - response.json( - { success: true, message: matchedType }, - ); - } -} - -class RequestAcceptsUseCaseTwoResource extends Resource { - paths = ["/request-accepts-use-case-two"]; - - public GET(request: Request, response: Response) { - const acceptHeader = request.headers.get("Accept"); - if (!acceptHeader) { - return this.jsonResponse(response); - } - const contentTypes: string[] = acceptHeader.split(";"); - for (let content of contentTypes) { - content = content.trim(); - if (content.indexOf("application/json") != -1) { - return this.jsonResponse(response); - } - if (content.indexOf("text/html") != -1) { - return this.htmlResponse(response); - } - if (content.indexOf("text/xml") != -1) { - return this.xmlResponse(response); - } - } - } - - protected htmlResponse(response: Response) { - response.html("
response: text/html
"); - return; - } - - protected jsonResponse(response: Response) { - response.json({ response: "application/json" }); - return; - } - - protected xmlResponse(response: Response) { - response.xml("text/xml"); - } -} - -const server = new Server({ - resources: [ - RequestAcceptsUseCaseOneResource, - RequestAcceptsUseCaseTwoResource, - ], - protocol: "http", - hostname: "localhost", - port: 3000, -}); - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - TESTS ///////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -Deno.test("request_accepts_resource_test.ts", async (t) => { - await t.step("/request-accepts-use-case-one", async (t) => { - await t.step("request accepts one type", async () => { - server.run(); - // Accepts the correct type the resource will give - tests calling the `accepts` method with a string and finds a match - const typeToCheck = "application/json"; - const response = await TestHelpers.makeRequest.get( - "http://localhost:3000/request-accepts-use-case-one?typeToCheck=" + - typeToCheck, - { - headers: { - Accept: "application/json", - }, - }, - ); - const res = await response.json(); - const json = res; - assertEquals(json.success, true); - assertEquals(json.message, "application/json"); - - await server.close(); - }); - - await t.step( - "request accepts multiple types: text/xml first", - async () => { - server.run(); - - // Accepts the first content type - tests when calling the `accepts` method with an array and finds a match - const response = await TestHelpers.makeRequest.get( - "http://localhost:3000/request-accepts-use-case-one", - { - headers: { - Accept: "text/xml,text/html,application/json;0.5;something", - }, - }, - ); - const json = await response.json(); - assertEquals(json.success, true); - assertEquals(json.message, "text/html"); - await server.close(); - }, - ); - - await t.step("request accepts multiple types: text/js first", async () => { - server.run(); - - // Accepts the first content type - tests when calling the `accepts` method with an array with no match - const response = await TestHelpers.makeRequest.get( - "http://localhost:3000/request-accepts-use-case-one", - { - headers: { - Accept: "text/js,text/php,text/python;0.5;something", // random stuff the resource isn't looking for - }, - }, - ); - const text = await response.text(); - assertEquals(text.startsWith("Error: "), true); - await server.close(); - }); - }); - - await t.step("/request-accepts-use-case-two", async (t) => { - await t.step("accepts one and multiple types", async () => { - server.run(); - - let response; - - response = await TestHelpers.makeRequest.get( - "http://localhost:3000/request-accepts-use-case-two", - { - headers: { - Accept: "text/html;application/json", - }, - }, - ); - assertEquals( - await response.text(), - `
response: text/html
`, - ); - - response = await TestHelpers.makeRequest.get( - "http://localhost:3000/request-accepts-use-case-two", - { - headers: { - Accept: "application/json;text/xml", - }, - }, - ); - assertEquals( - await response.text(), - `{"response":"application/json"}`, - ); - - response = await TestHelpers.makeRequest.get( - "http://localhost:3000/request-accepts-use-case-two", - { - headers: { - Accept: "text/xml", - }, - }, - ); - assertEquals( - await response.text(), - `text/xml`, - ); - - await server.close(); - }); - }); -}); diff --git a/tests/integration/response_time_test.ts b/tests/integration/response_time_test.ts deleted file mode 100644 index 4ac950994..000000000 --- a/tests/integration/response_time_test.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * This test addresses an issue where someone on the discord had their default - * content type set, but on browser requests the response was "null". This is - * because originally, the response class didn't fully take into account the - * config AND the accept headers. Essentially meaning, returning text/html (as - * this was the first type on the request) - */ - -import { assert, delay } from "../deps.ts"; -import { Request, Resource, Response, Server } from "../../mod.ts"; -import { ResponseTimeService } from "../../src/services/response_time/response_time.ts"; - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - APP SETUP ///////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -class ResponseTimeResource extends Resource { - paths = ["/response-time"]; - - public async GET(_request: Request, response: Response) { - await delay(400); - response.text("hello"); - } -} - -const server = new Server({ - resources: [ - ResponseTimeResource, - ], - services: [new ResponseTimeService()], - protocol: "http", - hostname: "localhost", - port: 3000, -}); - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - TESTS ///////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -Deno.test("response_time_test.ts", async (t) => { - await t.step("GET /response-time", async (t) => { - await t.step("Should set the response time header", async () => { - server.run(); - // Example browser request - const response = await fetch( - `${server.address}/response-time`, - ); - await response.text(); - await server.close(); - const value = response.headers.get("x-response-time") ?? ""; - assert(value.match(/\d\d\dms/)); - }); - }); -}); diff --git a/tests/integration/server_with_optionals_request_test.ts b/tests/integration/server_with_optionals_request_test.ts deleted file mode 100644 index d8cf3c60a..000000000 --- a/tests/integration/server_with_optionals_request_test.ts +++ /dev/null @@ -1,289 +0,0 @@ -import * as Drash from "../../mod.ts"; -import { assertEquals } from "../deps.ts"; - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - APP SETUP ///////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -class HomeResource extends Drash.Resource { - public paths = [ - "/", - ]; - - public async POST( - request: Drash.Request, - response: Drash.Response, - ): Promise { - const methodToExecute = request.queryParam("method_to_execute"); - - let body: unknown; - - try { - switch (methodToExecute) { - case "arrayBuffer": - body = await request.arrayBuffer(); - break; - case "blob": - body = await request.blob(); - break; - case "formData": - body = await request.formData(); - break; - case "json": - body = JSON.stringify(await request.json()); - break; - case "text": - default: - body = await request.text(); - break; - } - response.text(body as string); - } catch (error) { - response.text(error.message); - } - } -} - -const createServer = (readBody: boolean) => - new Drash.Server({ - hostname: "localhost", - port: 1447, - protocol: "http", - resources: [ - HomeResource, - ], - request: { - read_body: readBody, - }, - }); - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - TESTS ///////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -Deno.test("Drash.Server#options.request", async (t) => { - await t.step("read_body: true", async (t) => { - await t.step( - "calling request.blob() returns 'Body already consumed' response", - async () => { - const server = createServer(true); - server.run(); - - const response = await fetch( - "http://localhost:1447?method_to_execute=blob", - { - method: "POST", - body: JSON.stringify({ - something: "random", - }), - }, - ); - const res = await response.text(); - assertEquals(res, "Body already consumed."); - await server.close(); - }, - ); - - await t.step( - "calling request.json() returns 'Body already consumed' response", - async () => { - const server = createServer(true); - server.run(); - - const response = await fetch( - "http://localhost:1447?method_to_execute=json", - { - method: "POST", - body: JSON.stringify({ - something: "random", - }), - }, - ); - const res = await response.text(); - assertEquals(res, "Body already consumed."); - await server.close(); - }, - ); - - await t.step( - "calling request.arrayBuffer() returns 'Body already consumed' response", - async () => { - const server = createServer(true); - server.run(); - - const response = await fetch( - "http://localhost:1447?method_to_execute=arrayBuffer", - { - method: "POST", - body: JSON.stringify({ - something: "random", - }), - }, - ); - const res = await response.text(); - assertEquals(res, "Body already consumed."); - await server.close(); - }, - ); - - await t.step( - "calling request.text() returns 'Body already consumed' response", - async () => { - const server = createServer(true); - server.run(); - - const response = await fetch( - "http://localhost:1447?method_to_execute=text", - { - method: "POST", - body: JSON.stringify({ - something: "random", - }), - }, - ); - const res = await response.text(); - assertEquals(res, "Body already consumed."); - await server.close(); - }, - ); - - await t.step( - "calling request.formData() returns 'Body already consumed' response", - async () => { - const server = createServer(true); - server.run(); - - const response = await fetch( - "http://localhost:1447?method_to_execute=formData", - { - method: "POST", - body: JSON.stringify({ - something: "random", - }), - }, - ); - const res = await response.text(); - assertEquals(res, "Body already consumed."); - await server.close(); - }, - ); - }); - - await t.step("read_body: false", async (t) => { - await t.step( - "calling request.blob() returns body that was sent in request", - async () => { - const server = createServer(false); - server.run(); - - const response = await fetch( - "http://localhost:1447?method_to_execute=blob", - { - method: "POST", - body: JSON.stringify({ - something: "random", - }), - }, - ); - const res = await response.text(); - assertEquals(res, `{"something":"random"}`); - await server.close(); - }, - ); - - await t.step( - "calling request.json() returns body that was sent in request", - async () => { - const server = createServer(false); - server.run(); - - const response = await fetch( - "http://localhost:1447?method_to_execute=json", - { - method: "POST", - body: JSON.stringify({ - something: "random", - }), - }, - ); - const res = await response.text(); - assertEquals(res, `{"something":"random"}`); - await server.close(); - }, - ); - - await t.step( - "calling request.arrayBuffer() returns body that was sent in request", - async () => { - const server = createServer(false); - server.run(); - - const response = await fetch( - "http://localhost:1447?method_to_execute=arrayBuffer", - { - method: "POST", - body: JSON.stringify({ - something: "random", - }), - }, - ); - const res = await response.text(); - assertEquals(res, `{"something":"random"}`); - await server.close(); - }, - ); - - await t.step( - "calling request.text() returns body that was sent in request", - async () => { - const server = createServer(false); - server.run(); - - const response = await fetch( - "http://localhost:1447?method_to_execute=text", - { - method: "POST", - body: JSON.stringify({ - something: "random", - }), - }, - ); - const res = await response.text(); - assertEquals(res, `{"something":"random"}`); - await server.close(); - }, - ); - - await t.step( - "calling request.formData() returns body that was sent in request", - async () => { - const server = createServer(false); - server.run(); - - const formData = new FormData(); - const file = new Blob([ - new File([new ArrayBuffer(10)], "mod.ts"), - ], { - type: "application/javascript", - }); - formData.append("foo[]", file, "mod.ts"); - - const response = await fetch( - "http://localhost:1447?method_to_execute=formData", - { - method: "POST", - body: formData, - }, - ); - - const res = await response.text(); - const cd = `Content-Disposition: form-data;`; - const name = `name="foo[]"; filename="mod.ts"`; - - assertEquals(res.includes(cd), true); - assertEquals(res.includes(name), true); - await server.close(); - }, - ); - }); -}); diff --git a/tests/integration/service_ends_lifecycle_test.ts b/tests/integration/service_ends_lifecycle_test.ts deleted file mode 100644 index fa636c72b..000000000 --- a/tests/integration/service_ends_lifecycle_test.ts +++ /dev/null @@ -1,471 +0,0 @@ -import { - Errors, - IResource, - IService, - Request, - Resource, - Response, - Server, - Service, -} from "../../mod.ts"; -import { assertEquals } from "../deps.ts"; - -class MethodService extends Service implements IService { - async runBeforeResource(request: Request, response: Response) { - response.headers.set("x-method-service-before", "started"); - if (request.queryParam("method-before") === "wait") { - request.end(); - const p = new Promise((resolve) => { - setTimeout(() => { - resolve(true); - }, 2000); - }); - await p; - return; - } - if (request.queryParam("method-before") === "throw") { - throw new Errors.HttpError(419, "Method Service Before threw"); - } - if (request.queryParam("method-before") === "end") { - response.text("method before ended"); - return request.end(); - } - response.headers.set("x-method-service-before", "finished"); - } - - runAfterResource(request: Request, response: Response) { - response.headers.set("x-method-service-after", "started"); - if (request.queryParam("method-after") === "throw") { - throw new Errors.HttpError(419, "Method Service After threw"); - } - if (request.queryParam("method-after") === "end") { - response.text("method after ended"); - return request.end(); - } - response.headers.set("x-method-service-after", "finished"); - } -} - -class ServerService extends Service implements IService { - async runBeforeResource(request: Request, response: Response) { - response.headers.set("x-server-service-before", "started"); - if (request.queryParam("server-before") === "wait") { - request.end(); - const p = new Promise((resolve) => { - setTimeout(() => { - resolve(true); - }, 2000); - }); - await p; - return; - } - if (request.queryParam("server-before") === "throw") { - throw new Errors.HttpError(419, "Server Service Before threw"); - } - if (request.queryParam("server-before") === "end") { - response.text("server before ended"); - return request.end(); - } - response.headers.set("x-server-service-before", "finished"); - } - runAfterResource(request: Request, response: Response) { - response.headers.set("x-server-service-after", "started"); - if (request.queryParam("server-after") === "throw") { - throw new Errors.HttpError(419, "Server Service After threw"); - } - if (request.queryParam("server-after") === "end") { - response.text("server after ended"); - return request.end(); - } - response.headers.set("x-server-service-after", "finished"); - } -} - -class ClassService extends Service implements IService { - async runBeforeResource(request: Request, response: Response) { - response.headers.set("x-class-service-before", "started"); - if (request.queryParam("class-before") === "wait") { - request.end(); - const p = new Promise((resolve) => { - setTimeout(() => { - resolve(true); - }, 2000); - }); - await p; - return; - } - if (request.queryParam("class-before") === "throw") { - throw new Errors.HttpError(419, "Class Service Before threw"); - } - if (request.queryParam("class-before") === "end") { - response.text("class before ended"); - return request.end(); - } - response.headers.set("x-class-service-before", "finished"); - } - runAfterResource(request: Request, response: Response) { - response.headers.set("x-class-service-after", "started"); - if (request.queryParam("class-after") === "throw") { - throw new Errors.HttpError(419, "Class Service After threw"); - } - if (request.queryParam("class-after") === "end") { - response.text("class after ended"); - return request.end(); - } - response.headers.set("x-class-service-after", "finished"); - } -} - -class Resource1 extends Resource implements IResource { - paths = ["/"]; - - public services = { - "GET": [new MethodService()], - ALL: [new ClassService()], - }; - - public GET(_request: Request, response: Response) { - response.text("GET called"); - response.headers.set("x-resource-called", "true"); - } -} - -function getServer(port?: number) { - return new Server({ - protocol: "http", - port: port ?? 1234, - hostname: "localhost", - resources: [Resource1], - services: [new ServerService()], - }); -} - -Deno.test( - 'Server before services should have "end lifecycle" contexts separated', - async () => { - const server = getServer(1667); - server.run(); - - // Make the first request. This request will end early, but it also has a - // timeout set at 5 seconds. The second request below this code will also end - // early, but should not know this this first request is also ending early. - // They should have two completely different contexts. - const p = new Promise((resolve) => { - fetch(`${server.address}?server-before=wait`) - .then(async (res) => { - resolve(await res.text()); - }); - }); - - // This request will end early as well, but it has a different context. So - // even though the first request is ending early, the first request's "end - // early" context should have no effect on the request below. - const res = await fetch(`${server.address}?server-before=throw`); - assertEquals(res.headers.get("x-server-service-before"), "started"); - assertEquals(res.headers.get("x-server-service-after"), null); - assertEquals(res.headers.get("x-class-service-before"), null); - assertEquals(res.headers.get("x-class-service-after"), null); - assertEquals(res.headers.get("x-method-service-before"), null); - assertEquals(res.headers.get("x-method-service-after"), null); - assertEquals(res.headers.get("x-resource-called"), null); - assertEquals(res.status, 419); - assertEquals( - (await res.text()).startsWith("Error: Server Service Before threw"), - true, - ); - - // Await on the first request to resolve so that we do not leak ops. - await p; - - await server.close(); - }, -); - -Deno.test("Server before services should throw and end lifecycle", async () => { - const server = getServer(); - server.run(); - const res = await fetch(`${server.address}?server-before=throw`); - await server.close(); - assertEquals(res.headers.get("x-server-service-before"), "started"); - assertEquals(res.headers.get("x-server-service-after"), null); - assertEquals(res.headers.get("x-class-service-before"), null); - assertEquals(res.headers.get("x-class-service-after"), null); - assertEquals(res.headers.get("x-method-service-before"), null); - assertEquals(res.headers.get("x-method-service-after"), null); - assertEquals(res.headers.get("x-resource-called"), null); - assertEquals(res.status, 419); - assertEquals( - (await res.text()).startsWith("Error: Server Service Before threw"), - true, - ); -}); - -Deno.test("Server before services should end lifecycle", async () => { - const server = getServer(); - server.run(); - const res = await fetch(`${server.address}?server-before=end`); - await server.close(); - assertEquals(res.headers.get("x-server-service-before"), "started"); - assertEquals(res.headers.get("x-server-service-after"), null); - assertEquals(res.headers.get("x-class-service-before"), null); - assertEquals(res.headers.get("x-class-service-after"), null); - assertEquals(res.headers.get("x-method-service-before"), null); - assertEquals(res.headers.get("x-method-service-after"), null); - assertEquals(res.headers.get("x-resource-called"), null); - assertEquals(res.status, 200); - assertEquals(await res.text(), "server before ended"); -}); - -Deno.test("Server after services should throw and end lifecycle", async () => { - const server = getServer(); - server.run(); - const res = await fetch(`${server.address}?server-after=throw`); - await server.close(); - assertEquals(res.headers.get("x-server-service-before"), "finished"); - assertEquals(res.headers.get("x-server-service-after"), "started"); - assertEquals(res.headers.get("x-class-service-before"), "finished"); - assertEquals(res.headers.get("x-class-service-after"), "finished"); - assertEquals(res.headers.get("x-method-service-before"), "finished"); - assertEquals(res.headers.get("x-method-service-after"), "finished"); - assertEquals(res.headers.get("x-resource-called"), "true"); - assertEquals(res.status, 419); - assertEquals( - (await res.text()).startsWith("Error: Server Service After threw"), - true, - ); -}); - -Deno.test("Server after services should end lifecycle", async () => { - const server = getServer(); - server.run(); - const res = await fetch(`${server.address}?server-after=end`); - await server.close(); - assertEquals(res.headers.get("x-server-service-before"), "finished"); - assertEquals(res.headers.get("x-server-service-after"), "started"); - assertEquals(res.headers.get("x-class-service-before"), "finished"); - assertEquals(res.headers.get("x-class-service-after"), "finished"); - assertEquals(res.headers.get("x-method-service-before"), "finished"); - assertEquals(res.headers.get("x-method-service-after"), "finished"); - assertEquals(res.headers.get("x-resource-called"), "true"); - assertEquals(res.status, 200); - assertEquals(await res.text(), "server after ended"); -}); - -Deno.test( - 'Class before services should have "end lifecycle" contexts separated', - async () => { - const server = getServer(1667); - server.run(); - - // Make the first request. This request will end early, but it also has a - // timeout set at 5 seconds. The second request below this code will also end - // early, but should not know this this first request is also ending early. - // They should have two completely different contexts. - const p = new Promise((resolve) => { - fetch(`${server.address}?server-before=wait`) - .then(async (res) => { - resolve(await res.text()); - }); - }); - - // This request will end early as well, but it has a different context. So - // even though the first request is ending early, the first request's "end - // early" context should have no effect on the request below. - const res = await fetch(`${server.address}?class-before=throw`); - assertEquals(res.headers.get("x-server-service-before"), "finished"); - assertEquals(res.headers.get("x-server-service-after"), null); - assertEquals(res.headers.get("x-class-service-before"), "started"); - assertEquals(res.headers.get("x-class-service-after"), null); - assertEquals(res.headers.get("x-method-service-before"), null); - assertEquals(res.headers.get("x-method-service-after"), null); - assertEquals(res.headers.get("x-resource-called"), null); - assertEquals(res.status, 419); - assertEquals( - (await res.text()).startsWith("Error: Class Service Before threw"), - true, - ); - - // Await on the first request to resolve so that we do not leak ops. - await p; - - await server.close(); - }, -); - -Deno.test("Class before services should throw and end lifecycle", async () => { - const server = getServer(); - server.run(); - const res = await fetch(`${server.address}?class-before=throw`); - await server.close(); - assertEquals(res.headers.get("x-server-service-before"), "finished"); - assertEquals(res.headers.get("x-server-service-after"), null); - assertEquals(res.headers.get("x-class-service-before"), "started"); - assertEquals(res.headers.get("x-class-service-after"), null); - assertEquals(res.headers.get("x-method-service-before"), null); - assertEquals(res.headers.get("x-method-service-after"), null); - assertEquals(res.headers.get("x-resource-called"), null); - assertEquals(res.status, 419); - assertEquals( - (await res.text()).startsWith("Error: Class Service Before threw"), - true, - ); -}); - -Deno.test("Class before services should end lifecycle", async () => { - const server = getServer(); - server.run(); - const res = await fetch(`${server.address}?class-before=end`); - await server.close(); - assertEquals(res.headers.get("x-server-service-before"), "finished"); - assertEquals(res.headers.get("x-server-service-after"), null); - assertEquals(res.headers.get("x-class-service-before"), "started"); - assertEquals(res.headers.get("x-class-service-after"), null); - assertEquals(res.headers.get("x-method-service-before"), null); - assertEquals(res.headers.get("x-method-service-after"), null); - assertEquals(res.headers.get("x-resource-called"), null); - assertEquals(res.status, 200); - assertEquals(await res.text(), "class before ended"); -}); - -Deno.test("Class after services should throw and end lifecycle", async () => { - const server = getServer(); - server.run(); - const res = await fetch(`${server.address}?class-after=throw`); - await server.close(); - assertEquals(res.headers.get("x-server-service-before"), "finished"); - assertEquals(res.headers.get("x-server-service-after"), null); - assertEquals(res.headers.get("x-class-service-before"), "finished"); - assertEquals(res.headers.get("x-class-service-after"), "started"); - assertEquals(res.headers.get("x-method-service-before"), "finished"); - assertEquals(res.headers.get("x-method-service-after"), "finished"); - assertEquals(res.headers.get("x-resource-called"), "true"); - assertEquals(res.status, 419); - assertEquals( - (await res.text()).startsWith("Error: Class Service After threw"), - true, - ); -}); - -Deno.test("Class after services should end lifecycle", async () => { - const server = getServer(); - server.run(); - const res = await fetch(`${server.address}?class-after=end`); - await server.close(); - assertEquals(res.headers.get("x-server-service-before"), "finished"); - assertEquals(res.headers.get("x-server-service-after"), null); - assertEquals(res.headers.get("x-class-service-before"), "finished"); - assertEquals(res.headers.get("x-class-service-after"), "started"); - assertEquals(res.headers.get("x-method-service-before"), "finished"); - assertEquals(res.headers.get("x-method-service-after"), "finished"); - assertEquals(res.headers.get("x-resource-called"), "true"); - assertEquals(res.status, 200); - assertEquals(await res.text(), "class after ended"); -}); - -Deno.test( - 'Method before services should have "end lifecycle" contexts separated', - async () => { - const server = getServer(1887); - server.run(); - - // Make the first request. This request will end early, but it also has a - // timeout set at 5 seconds. The second request below this code will also end - // early, but should not know this this first request is also ending early. - // They should have two completely different contexts. - const p = new Promise((resolve) => { - fetch(`${server.address}?method-before=wait`) - .then(async (res) => { - resolve(await res.text()); - }); - }); - - const res = await fetch(`${server.address}?method-before=throw`); - assertEquals(res.headers.get("x-server-service-before"), "finished"); - assertEquals(res.headers.get("x-server-service-after"), null); - assertEquals(res.headers.get("x-class-service-before"), "finished"); - assertEquals(res.headers.get("x-class-service-after"), null); - assertEquals(res.headers.get("x-method-service-before"), "started"); - assertEquals(res.headers.get("x-method-service-after"), null); - assertEquals(res.headers.get("x-resource-called"), null); - assertEquals(res.status, 419); - assertEquals( - (await res.text()).startsWith("Error: Method Service Before threw"), - true, - ); - - // Await on the first request to resolve so that we do not leak ops. - await p; - - await server.close(); - }, -); - -Deno.test("Method before services should throw and end lifecycle", async () => { - const server = getServer(); - server.run(); - const res = await fetch(`${server.address}?method-before=throw`); - await server.close(); - assertEquals(res.headers.get("x-server-service-before"), "finished"); - assertEquals(res.headers.get("x-server-service-after"), null); - assertEquals(res.headers.get("x-class-service-before"), "finished"); - assertEquals(res.headers.get("x-class-service-after"), null); - assertEquals(res.headers.get("x-method-service-before"), "started"); - assertEquals(res.headers.get("x-method-service-after"), null); - assertEquals(res.headers.get("x-resource-called"), null); - assertEquals(res.status, 419); - assertEquals( - (await res.text()).startsWith("Error: Method Service Before threw"), - true, - ); -}); - -Deno.test("Method before services should end lifecycle", async () => { - const server = getServer(); - server.run(); - const res = await fetch(`${server.address}?method-before=end`); - await server.close(); - assertEquals(res.headers.get("x-server-service-before"), "finished"); - assertEquals(res.headers.get("x-server-service-after"), null); - assertEquals(res.headers.get("x-class-service-before"), "finished"); - assertEquals(res.headers.get("x-class-service-after"), null); - assertEquals(res.headers.get("x-method-service-before"), "started"); - assertEquals(res.headers.get("x-method-service-after"), null); - assertEquals(res.headers.get("x-resource-called"), null); - assertEquals(res.status, 200); - assertEquals(await res.text(), "method before ended"); -}); - -Deno.test("Method after services should throw and end lifecycle", async () => { - const server = getServer(); - server.run(); - const res = await fetch(`${server.address}?method-after=throw`); - await server.close(); - assertEquals(res.headers.get("x-server-service-before"), "finished"); - assertEquals(res.headers.get("x-server-service-after"), null); - assertEquals(res.headers.get("x-class-service-before"), "finished"); - assertEquals(res.headers.get("x-class-service-after"), null); - assertEquals(res.headers.get("x-method-service-before"), "finished"); - assertEquals(res.headers.get("x-method-service-after"), "started"); - assertEquals(res.headers.get("x-resource-called"), "true"); - assertEquals(res.status, 419); - assertEquals( - (await res.text()).startsWith("Error: Method Service After threw"), - true, - ); -}); - -Deno.test("Method after services should end lifecycle", async () => { - const server = getServer(); - server.run(); - const res = await fetch(`${server.address}?method-after=end`); - await server.close(); - assertEquals(res.headers.get("x-server-service-before"), "finished"); - assertEquals(res.headers.get("x-server-service-after"), null); - assertEquals(res.headers.get("x-class-service-before"), "finished"); - assertEquals(res.headers.get("x-class-service-after"), null); - assertEquals(res.headers.get("x-method-service-before"), "finished"); - assertEquals(res.headers.get("x-method-service-after"), "started"); - assertEquals(res.headers.get("x-resource-called"), "true"); - assertEquals(res.status, 200); - assertEquals(await res.text(), "method after ended"); -}); diff --git a/tests/integration/service_run_at_startup.ts b/tests/integration/service_run_at_startup.ts deleted file mode 100644 index 0c1dd793c..000000000 --- a/tests/integration/service_run_at_startup.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { - Interfaces, - Request, - Resource, - Response, - Server, - Service, -} from "../../mod.ts"; -import { assertEquals } from "../deps.ts"; - -// Default resource to add to the server (all tests should be able to see this resource) -class DefaultResource extends Resource { - paths = ["/"]; - public GET(_request: Request, response: Response) { - response.text("Hello from DefaultResource"); - } -} - -// This resource should be added when `ServerService` is plugged into the server -class AddedResource extends Resource { - public paths = ["/added-resource"]; - - public GET(request: Request, response: Response): void { - const queryParam = request.queryParam("some_query_param"); - if (queryParam) { - return response.text( - `Hello from AddedResource. You passed in a "some_query_param" value: ${queryParam}`, - ); - } - - response.text( - "Hello from AddedResource. You did not pass in a query param.", - ); - } -} - -// This service should add `AddedResource` to the server -class ServerService extends Service { - runAtStartup(options: Interfaces.IServiceStartupOptions) { - const { server } = options; - server.addResource(AddedResource); - } -} - -/** - * Get the fully qualified URL given the URI. - * - * @param uri - The URI to add to the serverURL. - * @returns The fully qualified URL. - */ -function url(server: Server, uri: string): string { - return server.address + uri; -} - -Deno.test("ServerService should add AddedResource at startup", async () => { - const server = new Server({ - protocol: "http", - port: 1234, - hostname: "localhost", - // `AddedResource` is not present, but it should exist when `ServerService` is instantiated - resources: [DefaultResource], - services: [new ServerService()], - }); - - server.run(); - - // Assert that the `DeafultResource` is accessible - const res1 = await fetch(url(server, "/")); - const res1Text = await res1.text(); - assertEquals(res1Text, "Hello from DefaultResource"); - - // Assert that the `AddedResource` is accessible since `ServerService` added it - const res2 = await fetch(url(server, "/added-resource")); - const res2Text = await res2.text(); - assertEquals( - res2Text, - "Hello from AddedResource. You did not pass in a query param.", - ); - - // Assert (for sanity check) that `AddedResource` takes query params as expected - const res3 = await fetch(url(server, "/added-resource?some_query_param=sup")); - const res3Text = await res3.text(); - assertEquals( - res3Text, - `Hello from AddedResource. You passed in a "some_query_param" value: sup`, - ); - - // Assert (for sanity check) that `AddedResource` works with other query param values as expected - const res4 = await fetch( - url(server, "/added-resource?some_query_param=anotha one"), - ); - const res4Text = await res4.text(); - assertEquals( - res4Text, - `Hello from AddedResource. You passed in a "some_query_param" value: anotha one`, - ); - - await server.close(); -}); diff --git a/tests/integration/service_test.ts b/tests/integration/service_test.ts deleted file mode 100644 index bb9b59fa9..000000000 --- a/tests/integration/service_test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { - IResource, - IService, - Request, - Resource, - Response, - Server, - Service, -} from "../../mod.ts"; -import { assertEquals } from "../deps.ts"; - -class ServerService extends Service implements IService { - runBeforeResource(_request: Request, response: Response) { - response.headers.set("X-SERVER-SERVICE-BEFORE", "hi"); - } - runAfterResource(_request: Request, response: Response) { - response.headers.set("X-SERVER-SERVICE-AFTER", "hi"); - } -} - -const serverService = new ServerService(); - -class ClassService extends Service implements IService { - runBeforeResource(_request: Request, response: Response) { - response.headers.set("X-CLASS-SERVICE-BEFORE", "hi"); - } - runAfterResource(_request: Request, response: Response) { - response.headers.set("X-CLASS-SERVICE-AFTER", "hi"); - } -} - -const classService = new ClassService(); - -class MethodService extends Service implements IService { - runBeforeResource(_request: Request, response: Response) { - response.headers.set("X-METHOD-SERVICE-BEFORE", "hi"); - } - runAfterResource(_request: Request, response: Response) { - response.headers.set("X-METHOD-SERVICE-AFTER", "hi"); - } -} - -const methodService = new MethodService(); - -class Resource1 extends Resource implements IResource { - paths = ["/"]; - - public services = { - "ALL": [classService], - "GET": [methodService], - }; - - public GET(_request: Request, response: Response) { - response.text("done"); - } -} - -const server = new Server({ - protocol: "http", - port: 1234, - hostname: "localhost", - resources: [Resource1], - services: [serverService], -}); - -Deno.test("Class middleware should run", async () => { - server.run(); - const res = await fetch(server.address, { - headers: { - Accept: "text/plain", - }, - }); - await res.text(); - await server.close(); - assertEquals(res.headers.get("X-CLASS-SERVICE-BEFORE"), "hi"); - assertEquals(res.headers.get("X-CLASS-SERVICE-AFTER"), "hi"); -}); - -Deno.test("Method middleware should run", async () => { - server.run(); - const res = await fetch(server.address, { - headers: { - Accept: "text/plain", - }, - }); - await res.text(); - await server.close(); - assertEquals(res.headers.get("X-METHOD-SERVICE-BEFORE"), "hi"); - assertEquals(res.headers.get("X-METHOD-SERVICE-AFTER"), "hi"); -}); - -Deno.test("Server middleware should run", async () => { - server.run(); - const res = await fetch(server.address, { - headers: { - Accept: "text/plain", - }, - }); - await res.text(); - await server.close(); - assertEquals(res.headers.get("X-SERVER-SERVICE-BEFORE"), "hi"); - assertEquals(res.headers.get("X-SERVER-SERVICE-AFTER"), "hi"); -}); diff --git a/tests/integration/services/resource_loader/resource_loader_test.ts b/tests/integration/services/resource_loader/resource_loader_test.ts deleted file mode 100644 index d48c6c6cb..000000000 --- a/tests/integration/services/resource_loader/resource_loader_test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { assertEquals, Drash } from "../../../deps.ts"; -import { ResourceLoaderService } from "../../../../src/services/resource_loader/resource_loader.ts"; - -/** - * Helper function to start the server in a test that can close it to prevent - * leaking async ops. The server needs to start and stop in the same test. - * @returns The server so it can be closed in the test. - */ -async function startServer(port: number) { - const resourceLoader = new ResourceLoaderService({ - paths_to_resources: [ - "./tests/integration/services/resource_loader/resources/api", - "./tests/integration/services/resource_loader/resources/ssr", - ], - }); - - const server = new Drash.Server({ - protocol: "http", - hostname: "localhost", - port: port, - services: [ - resourceLoader, - ], - }); - - await server.run(); - - return server; -} - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - TESTS ///////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -Deno.test("resource_loader_test.ts", async (t) => { - await t.step("GET /home", async (t) => { - await t.step("should return the homepage", async () => { - const server = await startServer(3000); - const res = await fetch(`${server.address}/home`); - await server.close(); - const actual = await res.text(); - assertEquals(res.headers.get("content-type"), "text/html"); - assertEquals(actual, "
Homepage
"); - }); - }); - - await t.step("GET /api/users", async (t) => { - await t.step("should return a users array", async () => { - const server = await startServer(3001); - - const resGet = await fetch(`${server.address}/api/users`); - assertEquals(resGet.headers.get("content-type"), "application/json"); - assertEquals(await resGet.json(), [ - { - id: 1, - name: "Ed", - }, - { - id: 2, - name: "Breno", - }, - ]); - - const resPost = await fetch(`${server.address}/api/users`, { - method: "POST", - }); - assertEquals(resPost.headers.get("content-type"), "application/json"); - assertEquals(await resPost.json(), [ - { - id: 1, - name: "Eric", - }, - { - id: 2, - name: "Sara", - }, - ]); - - await server.close(); - }); - }); -}); diff --git a/tests/integration/services/resource_loader/resources/api/users_resource.ts b/tests/integration/services/resource_loader/resources/api/users_resource.ts deleted file mode 100644 index eadc1e2a7..000000000 --- a/tests/integration/services/resource_loader/resources/api/users_resource.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Drash } from "../../../../../deps.ts"; - -export class UsersResource extends Drash.Resource { - paths = ["/api/users"]; - - public GET(_request: Drash.Request, response: Drash.Response) { - response.json([ - { - id: 1, - name: "Ed", - }, - { - id: 2, - name: "Breno", - }, - ]); - } - - public POST(_request: Drash.Request, response: Drash.Response) { - response.json([ - { - id: 1, - name: "Eric", - }, - { - id: 2, - name: "Sara", - }, - ]); - } -} diff --git a/tests/integration/services/resource_loader/resources/ssr/home_resource.ts b/tests/integration/services/resource_loader/resources/ssr/home_resource.ts deleted file mode 100644 index b93ed6a46..000000000 --- a/tests/integration/services/resource_loader/resources/ssr/home_resource.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Drash } from "../../../../../deps.ts"; - -export class HomeResource extends Drash.Resource { - paths = ["/home"]; - - public GET(_request: Drash.Request, response: Drash.Response) { - response.html("
Homepage
"); - } -} diff --git a/tests/integration/tengine_test.ts b/tests/integration/tengine_test.ts deleted file mode 100644 index b32370836..000000000 --- a/tests/integration/tengine_test.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * This test addresses an issue where someone on the discord had their default - * content type set, but on browser requests the response was "null". This is - * because originally, the response class didn't fully take into account the - * config AND the accept headers. Essentially meaning, returning text/html (as - * this was the first type on the request) - */ - -import { assertEquals } from "../deps.ts"; -import { Request, Resource, Response, Server } from "../../mod.ts"; -import { TengineService } from "../../src/services/tengine/tengine.ts"; - -const tengine = new TengineService({ - views_path: "./tests/data/views", -}); - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - APP SETUP ///////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -class TengineResource extends Resource { - paths = ["/tengine"]; - - services = { - "GET": [tengine], - }; - - public GET(_request: Request, response: Response) { - response.html(response.render("/tengine_index.html", { - greeting: "Gday", - }) as string); - } -} - -const server = new Server({ - resources: [ - TengineResource, - ], - protocol: "http", - hostname: "localhost", - port: 3000, -}); - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - TESTS ///////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -Deno.test("tengine_test.ts", async (t) => { - await t.step("GET /tengine", async (t) => { - await t.step("Tengine should handle the request", async () => { - server.run(); - const res = await fetch(`${server.address}/tengine`); - await server.close(); - assertEquals(res.headers.get("content-type"), "text/html"); - assertEquals(await res.text(), "Gday"); - }); - }); -}); diff --git a/tests/integration/upgrade_websocket_test.ts b/tests/integration/upgrade_websocket_test.ts deleted file mode 100644 index a6bf8ec53..000000000 --- a/tests/integration/upgrade_websocket_test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { assertEquals, deferred } from "../deps.ts"; -import { Request, Resource, Response, Server } from "../../mod.ts"; - -const messages: MessageEvent[] = []; -let globalResolve: ((arg: unknown) => void) | null = null; - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - APP SETUP ///////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -class HomeResource extends Resource { - paths = ["/"]; - - public GET(request: Request, response: Response) { - const { - socket, - response: upgradedResponse, - } = Deno.upgradeWebSocket(request); - - socket.onmessage = (message) => { - messages.push(message.data); - if (globalResolve) { - globalResolve(messages); - } - }; - - return response.upgrade(upgradedResponse); - } -} - -const server = new Server({ - resources: [ - HomeResource, - ], - protocol: "http", - hostname: "localhost", - port: 3000, -}); - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - TESTS ///////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -Deno.test("integration/upgrade_websocket_test.ts", async () => { - server.run(); - - const socket = new WebSocket("ws://localhost:3000"); - - const hydratedMessages = await new Promise((resolve, _reject) => { - // We pass the `resolve` function to the `globalResolve` variable so that - // the `socket.onmessage()` call in the `GET()` method in the resource can - // use it to resolve the `Promise`. This makes the `Promise` truly wait - // until the `messages` array has the message that was sent from the client. - globalResolve = resolve; - socket.onopen = () => { - socket.send("this is a message from the client"); - // Close the connection so that this test doesn't leak async ops - socket.close(); - }; - }); - - const p = deferred(); - socket.onclose = () => { - p.resolve(); - }; - assertEquals( - (hydratedMessages as MessageEvent[])[0] as unknown as string, - "this is a message from the client", - ); - await server.close(); - await p; -}); diff --git a/tests/integration/users.json b/tests/integration/users.json deleted file mode 100644 index 9e74fc111..000000000 --- a/tests/integration/users.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "17": { - "id": 17, - "name": "Thor" - }, - "28": { - "id": 28, - "name": "Iron Man" - }, - "32": { - "id": 32, - "name": "Captain America" - } -} diff --git a/tests/integration/users_resource_test.ts b/tests/integration/users_resource_test.ts deleted file mode 100644 index 7379788a6..000000000 --- a/tests/integration/users_resource_test.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { assertEquals, TestHelpers } from "../deps.ts"; -import * as Drash from "../../mod.ts"; -import { Request, Resource, Response } from "../../mod.ts"; - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - APP SETUP ///////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -class UsersResource extends Resource { - paths = ["/users", "/users/:id"]; - - public GET(request: Request, response: Response) { - const userId = request.pathParam("id"); - - if (!userId) { - response.text("Please specify a user ID."); - return; - } - - response.text(JSON.stringify(this.getUser(parseInt(userId)))); - return; - } - - public POST(_request: Request, response: Response) { - response.text("POST request received!"); - return; - } - - protected getUser(userId: number) { - let user = null; - - try { - let users = this.readFileContents( - "users.json", - ); - users = JSON.parse(users); - user = users[userId]; - } catch (error) { - throw new Drash.Errors.HttpError( - 400, - `Error getting user with ID "${userId}". Error: ${error.message}.`, - ); - } - - if (!user) { - throw new Drash.Errors.HttpError( - 404, - `User with ID "${userId}" not found.`, - ); - } - - return user; - } - - protected readFileContents(file: string) { - const fileContentsRaw = Deno.readFileSync(file); - const decoder = new TextDecoder(); - const decoded = decoder.decode(fileContentsRaw); - return decoded; - } -} - -const server = new Drash.Server({ - resources: [ - UsersResource, - ], - protocol: "http", - hostname: "localhost", - port: 3000, -}); - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - TESTS ///////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -Deno.test("users_resource_test.ts", async (t) => { - await t.step("/users", async (t) => { - await t.step("user data can be retrieved", async () => { - server.run(); - - let response; - const currentDir = Deno.cwd(); - Deno.chdir("./tests/integration"); - response = await TestHelpers.makeRequest.get( - "http://localhost:3000/users", - { - headers: { - Accept: "text/plain", - }, - }, - ); - assertEquals( - await response.text(), - "Please specify a user ID.", - ); - - response = await TestHelpers.makeRequest.get( - "http://localhost:3000/users/", - { - headers: { - Accept: "text/plain", - }, - }, - ); - assertEquals( - await response.text(), - "Please specify a user ID.", - ); - - response = await TestHelpers.makeRequest.get( - "http://localhost:3000/users//", - { - headers: { - Accept: "text/plain", - }, - }, - ); - assertEquals( - (await response.text()).startsWith("Error: Not Found"), - true, - ); - - response = await TestHelpers.makeRequest.get( - "http://localhost:3000/users/17", - { - headers: { - Accept: "text/plain", - }, - }, - ); - assertEquals( - await response.text(), - '{"id":17,"name":"Thor"}', - ); - - response = await TestHelpers.makeRequest.get( - "http://localhost:3000/users/17/", - { - headers: { - Accept: "text/plain", - }, - }, - ); - assertEquals( - await response.text(), - '{"id":17,"name":"Thor"}', - ); - - response = await TestHelpers.makeRequest.get( - "http://localhost:3000/users/18", - { - headers: { - Accept: "text/plain", - }, - }, - ); - assertEquals( - (await response.text()).startsWith( - `Error: User with ID "18" not found.`, - ), - true, - ); - - // Change back to what the current working directory was so that other - // tests that try to open files use the current working directory and - // not "./tests/integration". - Deno.chdir(currentDir); - await server.close(); - }); - }); -}); diff --git a/tests/middleware/deno/README.md b/tests/middleware/deno/README.md new file mode 100644 index 000000000..56927a128 --- /dev/null +++ b/tests/middleware/deno/README.md @@ -0,0 +1,17 @@ +# Tests / Middleware / Deno + +## Configuring TypeScript + +This directory depends on you having: + +- the official Deno extension installed; and +- `/.vscode/settings.json` file with the following configurations at a minimum: + + ```json + { + "deno.enable": true, + "deno.enablePaths": [ + "./tests/middleware/deno" + ] + } + ``` diff --git a/tests/middleware/deno/utils.ts b/tests/middleware/deno/utils.ts new file mode 100644 index 000000000..f80decd27 --- /dev/null +++ b/tests/middleware/deno/utils.ts @@ -0,0 +1,134 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { HTTPError } from "../../../src/core/errors/HTTPError.ts"; +import { StatusCode } from "../../../src/core/http/response/StatusCode.ts"; +import { StatusDescription } from "../../../src/core/http/response/StatusDescription.ts"; +import * as Chain from "../../../src/modules/RequestChain/mod.native.ts"; +import { ResourceGroup } from "../../../src/standard/http/ResourceGroup.ts"; + +export function assertionMessage(...message: string[]): string { + return `\n\n +------------------------------ Test Error/Failure ------------------------------ + +${message.join("\n")} + +--------------------------------------------------------------------------------`; +} + +export function catchError(error: Error | HTTPError): Response { + if ( + (error.name === "HTTPError" || error instanceof HTTPError) && + "status_code" in error && + "status_code_description" in error + ) { + return new Response(error.message, { + status: error.status_code, + statusText: error.status_code_description, + }); + } + + return new Response(error.message, { + status: StatusCode.InternalServerError, + statusText: StatusDescription.InternalServerError, + }); +} + +export function testCaseName(n: number) { + return `[Test case ${n}${n < 10 ? " " : ""}]`; +} + +/** + * Take the given `kvp` object and convert it to a URL query param string. + * @param kvp A key-value pair object where the key is the URL query param name + * and the value is the value of the query param. + * @returns The given `kvp` in string format. + * + * @example + * ```typescript + * const queryString = query({ + * ok: "then", + * hello: "goodbye" + * }); // => "?ok=then&hello=goodbye" + * ``` + */ +export function query(kvp?: Record) { + if (!kvp) { + return ""; + } + + return "?" + Object + .keys(kvp) + .map((key) => `${key}=${kvp[key]}`) + .join("&"); +} + +/** + * Get a chain with a simple `/` path resource. Middleware and resources can be + * added to it if passed in the `options` param. + * + * @param options + * @returns The `RequestChain`'s handler. + * + * @example + * ```typescript + * const myChain = chain({ + * middleware: [ + * SomeMiddleware() + * ], + * resources: [ + * class MyResource extends Chain.Resource { ... } + * ], + * }) + * + * const req = new Request("/some-path"), reqOptions); + * const res = await myChain.handle(req); // => a `Response` object + * ``` + */ +export function chain(options: { + middleware?: Chain.Middleware[]; + resources?: typeof Chain.Resource[]; +}) { + const { + middleware = [], + resources = [], + } = options; + + return Chain + .builder() + .resources( + ResourceGroup + .builder() + .resources( + class Home extends Chain.Resource { + public paths = ["/"]; + + public GET(_request: Chain.Request) { + return new Response("Hello from Home.GET()!"); + } + }, + ...resources, + ) + .middleware(...middleware) + .build(), + ) + .build(); +} diff --git a/tests/middleware/deno/v1.x/modules/middleware/AcceptHeader/mod_test.ts b/tests/middleware/deno/v1.x/modules/middleware/AcceptHeader/mod_test.ts new file mode 100644 index 000000000..b73977070 --- /dev/null +++ b/tests/middleware/deno/v1.x/modules/middleware/AcceptHeader/mod_test.ts @@ -0,0 +1,390 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { asserts } from "../../../../../../deps.ts"; +import { + assertionMessage, + catchError, + chain, + query, + testCaseName, +} from "../../../../utils.ts"; +import { Method } from "../../../../../../../src/core/http/request/Method.ts"; +import { + AcceptHeader, + AcceptHeaderMiddleware, + defaultOptions, + type Options, +} from "../../../../../../../src/modules/middleware/AcceptHeader/mod.ts"; +import * as Chain from "../../../../../../../src/modules/RequestChain/mod.native.ts"; +import { resource } from "../../../../../../../src/modules/builders/ResourceBuilder.ts"; +import { HTTPError } from "../../../../../../../src/core/errors/HTTPError.ts"; +import { Status } from "../../../../../../../src/core/http/response/Status.ts"; +import { StatusCode } from "../../../../../../../src/core/http/response/StatusCode.ts"; +import { StatusDescription } from "../../../../../../../src/core/http/response/StatusDescription.ts"; +import { Handler } from "../../../../../../../src/standard/handlers/Handler.ts"; + +type TestCase = { + chain: Handler; + requests: { + request: RequestInfo; + expected_response: ExpectedCombined; + }[]; +}; + +type RequestInfo = + & RequestInit + & { + path: string; + query?: Record; + }; + +type Expected = + & ResponseInit + & { + body: unknown; + headers?: Record; + }; + +type ExpectedCombined = Expected | { deno: Expected; drash: Expected }; + +const protocol = "http"; +const hostname = "localhost"; +const port = 1447; +const url = `${protocol}://${hostname}:${port}`; + +// This variable gets set by each test case so that each test case uses the same +// chain +const globals: { + current_chain: Handler | null; +} = { + current_chain: null, +}; + +const serverController = new AbortController(); + +Deno.serve( + { + port, + hostname, + onListen: () => runTests(), + signal: serverController.signal, + }, + (request: Request): Promise => { + if (!globals.current_chain) { + throw new Error(`Var \`globals.current_chain\` was not set by the test`); + } + + return globals.current_chain + .handle(request) + .catch(catchError); + }, +); + +function runTests() { + Deno.test("AcceptHeader", async (t) => { + await t.step("Deno Tests (using the chain in a Deno server)", async (t) => { + const testCases = getTestCases(); + + for (const [testCaseIndex, testCase] of testCases.entries()) { + const { chain, requests } = testCase; + + // Set the current chain to be the chain in this test case so the server (outside this + // test function) can use it. + globals.current_chain = chain; + + for ( + const [requestIndex, { request, expected_response }] of requests + .entries() + ) { + await t.step( + `${testCaseName(testCaseIndex)} ${request.method} ${request.path}`, + async () => { + const requestOptions: Record = { + method: request.method, + }; + + requestOptions.headers = request.headers ?? {}; + const fullUrl = url + request.path + query(request.query); + + const req = new Request(fullUrl, requestOptions); + + const response = await fetch(req); + + await assert( + "Deno", + testCaseIndex, + req, + requestIndex, + response, + ("deno" in expected_response) + ? expected_response.deno + : expected_response, + ); + }, + ); + } + } + }); + + await t.step("Drash Tests (using only the chain)", async (t) => { + const testCases = getTestCases(); + + for (const [testCaseIndex, testCase] of testCases.entries()) { + const { chain, requests } = testCase; + + for ( + const [requestIndex, { request, expected_response }] of requests + .entries() + ) { + await t.step( + `${testCaseName(testCaseIndex)} ${request.method} ${request.path}`, + async () => { + const requestOptions: Record = { + method: request.method, + }; + + requestOptions.headers = request.headers ?? {}; + const fullUrl = url + request.path + query(request.query); + + const req = new Request(fullUrl, requestOptions); + + const response = await chain + .handle(req) + .catch(catchError); + + await assert( + "Drash", + testCaseIndex, + req, + requestIndex, + response, + ("drash" in expected_response) + ? expected_response.drash + : expected_response, + ); + }, + ); + } + } + }); + }); +} + +async function assert( + system: "Drash" | "Deno", + testCaseIndex: number, + request: Request, + requestIndex: number, + actualResponse: Response, + expectedResponse: Expected, +) { + asserts.assertEquals( + await actualResponse.clone().text(), + expectedResponse.body, + assertionMessage( + `AcceptHeader test failed in ${system}:`, + `\n Response body does not match expected.`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); + + asserts.assertEquals( + actualResponse.status, + expectedResponse.status, + assertionMessage( + `AcceptHeader test failed in ${system}:`, + `\n Response status does not match expected.`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); + + asserts.assertEquals( + actualResponse.statusText, + expectedResponse.statusText, + assertionMessage( + `AcceptHeader test failed in ${system}:`, + `\n Response statusText does not match expected.`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); +} + +function getTestCases(): TestCase[] { + return [ + { + chain: chain({ + middleware: [getAcceptHeaderMiddleware({ logs: false })], + resources: [ + resource() + .paths([ + "/accept-header", + ]) + .GET((_request: Chain.Request) => { + return new Response("Hello from GET."); + }) + .POST((_request: Chain.Request) => { + return new Response( + JSON.stringify({ message: "Hello from POST." }), + { + status: StatusCode.OK, + statusText: StatusDescription.OK, + headers: { + "content-type": "application/json", + }, + }, + ); + }) + .DELETE((_request: Chain.Request) => { + return new Response("Deleted!"); + }) + .PATCH((_request: Chain.Request) => { + throw new HTTPError(Status.MethodNotAllowed); + }) + .build(), + ], + }), + requests: [ + { + request: { + method: Method.GET, + path: "/accept-header", + headers: { + accept: "application/json", + }, + }, + expected_response: { + status: 422, + statusText: "Unprocessable Entity", + headers: {}, + body: + "The server did not generate a response matching the request's Accept header", + }, + }, + { + request: { + method: Method.POST, + path: "/accept-header", + headers: { + accept: "application/json", + }, + }, + expected_response: { + status: 200, + statusText: "OK", + body: `{"message":"Hello from POST."}`, + }, + }, + { + request: { + method: Method.PUT, + path: "/accept-header", + headers: { + accept: "*/*", + }, + }, + expected_response: { + status: 501, + statusText: "Not Implemented", + body: "Not Implemented", + }, + }, + { + request: { + method: Method.DELETE, + path: "/accept-header", + headers: { + accept: "*/*", + }, + }, + expected_response: { + deno: { + status: 200, + statusText: "OK", // Deno magically sets this + body: "Deleted!", + }, + drash: { + status: 200, + statusText: "", // No statusText was set in the resource, so we expect blank. We do not magically set it. + body: "Deleted!", + }, + }, + }, + { + request: { + method: Method.PATCH, + path: "/accept-header", + headers: { + accept: "*/*", + }, + }, + expected_response: { + status: 405, + statusText: "Method Not Allowed", + body: "Method Not Allowed", + }, + }, + ], + }, + ]; +} + +/** + * Helper function to use a decorated AcceptHeader middleware (with logs for + * debugging purposes) or the original AcceptHeader middleware. + */ +function getAcceptHeaderMiddleware( + options: Options & { logs?: boolean } = defaultOptions, +) { + if (!options.logs) { + return AcceptHeader(options); + } + + return new class AcceptHeaderLogged extends AcceptHeaderMiddleware { + constructor() { + super(options); + } + + ALL(request: Request): Promise { + return Promise + .resolve() + .then(() => { + console.log(`Calling handleIfAcceptHeaderMissing()`); + return this.handleIfAcceptHeaderMissing(request); + }) + .then(() => super.next(request)) + .then((response) => ({ request, response })) + .then((context) => { + console.log(`Calling handleHeaders()`); + return this.handleHeaders(context); + }) + .then((context) => { + console.log(`Calling sendResponse()`); + const r = this.sendResponse(context); + console.log(`Response received:`, r); + return r; + }); + } + }(); +} diff --git a/tests/middleware/deno/v1.x/modules/middleware/CORS/mod_test.ts b/tests/middleware/deno/v1.x/modules/middleware/CORS/mod_test.ts new file mode 100644 index 000000000..feb2aa19d --- /dev/null +++ b/tests/middleware/deno/v1.x/modules/middleware/CORS/mod_test.ts @@ -0,0 +1,1239 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { Method } from "../../../../../../../src/core/http/request/Method.ts"; +import { Status } from "../../../../../../../src/core/http/response/Status.ts"; +import { + CORS, + CORSMiddleware, + defaultOptions, + Options, +} from "../../../../../../../src/modules/middleware/CORS/mod.ts"; +import { Handler } from "../../../../../../../src/standard/handlers/Handler.ts"; +import { asserts } from "../../../../../../deps.ts"; +import { catchError, query } from "../../../../utils.ts"; +import { assertionMessage, chain, testCaseName } from "../../../../utils.ts"; + +type TestCase = { + chain: Handler; + requests: { + request: RequestInfo; + expected_response: ExpectedCombined; + }[]; +}; + +type RequestInfo = + & RequestInit + & { + path: string; + query?: Record; + }; + +type Expected = + & ResponseInit + & { + body?: unknown; + headers?: Record; + }; + +type ExpectedCombined = Expected | { deno: Expected; drash: Expected }; + +const protocol = "http"; +const hostname = "localhost"; +const port = 1447; +const url = `${protocol}://${hostname}:${port}`; + +// This variable gets set by each test case so that each test case uses the same +// chain +// +const globals: { + current_chain: Handler | null; +} = { + current_chain: null, +}; + +const serverController = new AbortController(); + +Deno.serve( + { + port, + hostname, + onListen: () => runTests(), + signal: serverController.signal, + }, + (request: Request): Promise => { + if (!globals.current_chain) { + throw new Error(`Var \`globals.current_chain\` was not set by the test`); + } + + return globals.current_chain + .handle(request) + .catch(catchError); + }, +); + +function runTests() { + Deno.test("CORS", async (t) => { + await t.step("Deno Tests (using Deno server and chain)", async (t) => { + const testCases = getTestCases(); + + for (const [testCaseIndex, testCase] of testCases.entries()) { + const { chain, requests } = testCase; + + // Set the current chain to be the chain in this test case so the server (outside this + // test function) can use it. + globals.current_chain = chain; + + for ( + const [requestIndex, { request, expected_response }] of requests + .entries() + ) { + await t.step( + `${testCaseName(testCaseIndex)} ${request.method} ${request.path}`, + async () => { + const requestOptions: Record = { + method: request.method, + }; + + requestOptions.headers = request.headers ?? {}; + const fullUrl = url + request.path + query(request.query); + + const req = new Request(fullUrl, requestOptions); + + const response = await fetch(req); + + await assert( + "Deno", + testCaseIndex, + req, + requestIndex, + response, + ("deno" in expected_response) + ? expected_response.deno + : expected_response, + ); + }, + ); + } + } + }); + + await t.step("Drash Tests (using chain only)", async (t) => { + const testCases = getTestCases(); + + for (const [testCaseIndex, testCase] of testCases.entries()) { + const { chain, requests } = testCase; + + for ( + const [requestIndex, { request, expected_response }] of requests + .entries() + ) { + await t.step( + `${testCaseName(testCaseIndex)} ${request.method} ${request.path}`, + async () => { + const requestOptions: Record = { + method: request.method, + }; + + requestOptions.headers = request.headers ?? {}; + const fullUrl = url + request.path + query(request.query); + + const req = new Request(fullUrl, requestOptions); + + const response = await chain.handle(req); + + await assert( + "Drash", + testCaseIndex, + req, + requestIndex, + response, + ("drash" in expected_response) + ? expected_response.deno + : expected_response, + ); + }, + ); + } + } + }); + }); +} + +async function assert( + system: "Drash" | "Deno", + testCaseIndex: number, + request: Request, + requestIndex: number, + actualResponse: Response, + expectedResponse: Expected, +) { + if (expectedResponse.body === null) { + const body = await actualResponse.clone().body; + asserts.assertEquals( + body, + expectedResponse.body, + assertionMessage( + `CORS test failed in ${system}:`, + `\n Response body ${body} does not match expected null`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); + } else { + const body = await actualResponse.clone().text(); + asserts.assertEquals( + body, + expectedResponse.body, + assertionMessage( + `CORS test failed in ${system}:`, + `\n Response body ${body} does not match expected ${expectedResponse.body}`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); + } + + asserts.assertEquals( + actualResponse.status, + expectedResponse.status, + assertionMessage( + `CORS test failed in ${system}:`, + `\n Response status does not match expected.`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); + + asserts.assertEquals( + actualResponse.statusText, + expectedResponse.statusText, + assertionMessage( + `CORS test failed in ${system}`, + `\n Response statusText does not match expected.`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); +} + +function getTestCases(): TestCase[] { + return [ + // 0 + { + chain: chain({ + middleware: [getCorsMiddleware()], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "*", + "content-length": "0", + }, + }, + }, + ], + }, + + // 1 + { + chain: chain({ + middleware: [getCorsMiddleware( + {}, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { "access-control-request-headers": "x-yezzir" }, + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "*", + "content-length": "0", + "vary": "Access-Control-Request-Headers", + }, + }, + }, + ], + }, + + // 2 + { + chain: chain({ + middleware: [getCorsMiddleware( + { + access_control_allow_origin: [ + "http://test", + "https://woopwoop2", + /.*woopwoop3\.local.*/, + new RegExp("http(s)?://slowbro$"), + ], + }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { origin: "http://woopwoop" }, + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "false", + "content-length": "0", + "vary": "Origin", + }, + }, + }, + ], + }, + + // 3 + { + chain: chain({ + middleware: [getCorsMiddleware( + { + access_control_allow_origin: [ + "http://test", + "https://woopwoop2", + /.*woopwoop3\.local.*/, + new RegExp("http(s)?://slowbro$"), + ], + }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { origin: "http://test" }, + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "http://test", + "content-length": "0", + "vary": "Origin", + }, + }, + }, + ], + }, + + // 4 + { + chain: chain({ + middleware: [getCorsMiddleware( + { + access_control_allow_origin: [ + "http://test", + "https://woopwoop2", + /.*woopwoop3\.local.*/, + new RegExp("http(s)?://slowbro$"), + ], + }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { origin: "https://woopwoop2" }, + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "https://woopwoop2", + "content-length": "0", + "vary": "Origin", + }, + }, + }, + ], + }, + + // 5 + { + chain: chain({ + middleware: [getCorsMiddleware( + { + access_control_allow_origin: [ + "http://test", + "https://woopwoop2", + /.*woopwoop3\.local.*/, + new RegExp("http(s)?://slowbro$"), + ], + }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { origin: "http://woopwoop3.local" }, + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "http://woopwoop3.local", + "content-length": "0", + "vary": "Origin", + }, + }, + }, + ], + }, + + // 6 + { + chain: chain({ + middleware: [getCorsMiddleware( + { + access_control_allow_origin: [ + "http://test", + "https://woopwoop2", + /.*woopwoop3\.local.*/, + new RegExp("http(s)?://slowbro$"), + ], + }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { origin: "http://slowbro" }, + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "http://slowbro", + "content-length": "0", + "vary": "Origin", + }, + }, + }, + ], + }, + + // 7 + { + chain: chain({ + middleware: [getCorsMiddleware( + { + access_control_allow_origin: [ + "http://test", + "https://woopwoop2", + /.*woopwoop3\.local.*/, + new RegExp("http(s)?://slowbro$"), + ], + }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { origin: "http://slowbros" }, // The last `s` character should cause a `false` origin, + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "false", + "content-length": "0", + "vary": "Origin", + }, + }, + }, + ], + }, + + // 8 + { + chain: chain({ + middleware: [getCorsMiddleware( + { + access_control_allow_origin: [ + "http://test", + "https://woopwoop2", + /.*woopwoop3\.local.*/, + new RegExp("http(s)?://slowbro$"), + ], + }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { origin: "https://slowbro" }, // https should work too + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "https://slowbro", + "content-length": "0", + "vary": "Origin", + }, + }, + }, + ], + }, + + // 9 + { + chain: chain({ + middleware: [getCorsMiddleware( + { + access_control_allow_origin: [ + "http://test", + "https://woopwoop2", + /.*woopwoop3\.local.*/, + new RegExp("http(s)?://slowbro$"), + ], + }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { origin: "https://slowbros" }, // The last `s` character should cause a `false` origin again + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "false", + "content-length": "0", + "vary": "Origin", + }, + }, + }, + ], + }, + + // 10 + { + chain: chain({ + middleware: [getCorsMiddleware( + { options_success_status: Status.OK }, // This results in a "" response body in Deno + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + }, + expected_response: { + deno: { + status: 200, + statusText: "OK", + body: "", // Deno automagically sets the response + headers: { + "access-control-allow-methods": + "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "*", + "content-length": "0", + }, + }, + drash: { + status: 200, + statusText: "OK", + body: null, + headers: { + "access-control-allow-methods": + "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "*", + "content-length": "0", + }, + }, + }, + }, + ], + }, + + // 11 + { + chain: chain({ + middleware: [getCorsMiddleware( + { options_success_status: Status.OK }, // This results in a "" response body in Deno + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { origin: "https://woopwoop2" }, + }, + expected_response: { + deno: { + status: 200, + statusText: "OK", + body: "", + headers: { + "access-control-allow-methods": + "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "*", + "content-length": "0", + }, + }, + drash: { + status: 200, + statusText: "OK", + body: "", + headers: { + "access-control-allow-methods": + "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "*", + "content-length": "0", + }, + }, + }, + }, + ], + }, + + // 12 + { + chain: chain({ + middleware: [getCorsMiddleware( + { options_success_status: Status.OK }, // This results in a "" response body in Deno + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { origin: "http://woopwoop3.local" }, + }, + expected_response: { + deno: { + status: 200, + statusText: "OK", + body: "", + headers: { + "access-control-allow-methods": + "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "*", + "content-length": "0", + }, + }, + drash: { + status: 200, + statusText: "OK", + body: null, + headers: { + "access-control-allow-methods": + "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "*", + "content-length": "0", + }, + }, + }, + }, + ], + }, + + // 13 + { + chain: chain({ + middleware: [getCorsMiddleware( + { options_success_status: Status.OK }, // This results in a "" response body in Deno + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { origin: "http://slowbro" }, + }, + expected_response: { + deno: { + status: 200, + statusText: "OK", + body: "", + headers: { + "access-control-allow-methods": + "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "*", + "content-length": "0", + }, + }, + drash: { + status: 200, + statusText: "OK", + body: null, + headers: { + "access-control-allow-methods": + "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "*", + "content-length": "0", + }, + }, + }, + }, + ], + }, + + // 14 + { + chain: chain({ + middleware: [getCorsMiddleware( + { access_control_allow_methods: ["GET"] }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET", + "access-control-allow-origin": "*", + "content-length": "0", + }, + }, + }, + ], + }, + + // 15 + { + chain: chain({ + middleware: [getCorsMiddleware( + { access_control_allow_methods: ["GET"] }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { origin: "https://woopwoop2" }, + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET", + "access-control-allow-origin": "*", + "content-length": "0", + }, + }, + }, + ], + }, + + // 16 + { + chain: chain({ + middleware: [getCorsMiddleware( + { access_control_allow_methods: ["GET"] }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { origin: "http://woopwoop3.local" }, + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET", + "access-control-allow-origin": "*", + "content-length": "0", + }, + }, + }, + ], + }, + + // 17 + { + chain: chain({ + middleware: [getCorsMiddleware( + { access_control_allow_methods: ["GET"] }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { origin: "http://slowbro" }, + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET", + "access-control-allow-origin": "*", + "content-length": "0", + }, + }, + }, + ], + }, + + // 18 + { + chain: chain({ + middleware: [getCorsMiddleware( + { access_control_max_age: 1000 }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "*", + "access-control-max-age": "1000", + "content-length": "0", + }, + }, + }, + ], + }, + + // 19 + { + chain: chain({ + middleware: [getCorsMiddleware( + { access_control_max_age: 1000 }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { origin: "https://woopwoop2" }, + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "*", + "access-control-max-age": "1000", + "content-length": "0", + }, + }, + }, + ], + }, + + // 20 + { + chain: chain({ + middleware: [getCorsMiddleware( + { access_control_max_age: 1000 }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { origin: "http://woopwoop3.local" }, + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "*", + "access-control-max-age": "1000", + "content-length": "0", + }, + }, + }, + ], + }, + + // 21 + { + chain: chain({ + middleware: [getCorsMiddleware( + { access_control_max_age: 1000 }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { origin: "http://slowbro" }, + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "*", + "access-control-max-age": "1000", + "content-length": "0", + }, + }, + }, + ], + }, + + // 22 + { + chain: chain({ + middleware: [getCorsMiddleware( + { access_control_allow_headers: ["x-hello", "x-nah-brah"] }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-headers": "x-hello,x-nah-brah", + "access-control-allow-origin": "*", + "content-length": "0", + }, + }, + }, + ], + }, + + // 23 + { + chain: chain({ + middleware: [getCorsMiddleware( + { access_control_allow_headers: ["x-hello", "x-nah-brah"] }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { + origin: "https://woopwoop2", + "access-control-request-headers": "x-test", + }, + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-headers": "x-hello,x-nah-brah,x-test", + "access-control-allow-origin": "*", + "content-length": "0", + "vary": "Access-Control-Request-Headers", + }, + }, + }, + ], + }, + + // 24 + { + chain: chain({ + middleware: [getCorsMiddleware( + { access_control_allow_headers: ["x-hello", "x-nah-brah"] }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { origin: "http://woopwoop3.local" }, + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-headers": "x-hello,x-nah-brah", + "access-control-allow-origin": "*", + "content-length": "0", + }, + }, + }, + ], + }, + + // 25 + { + chain: chain({ + middleware: [getCorsMiddleware( + { access_control_allow_headers: ["x-hello", "x-nah-brah"] }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { origin: "http://slowbro" }, + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-headers": "x-hello,x-nah-brah", + "access-control-allow-origin": "*", + "content-length": "0", + }, + }, + }, + ], + }, + + // 26 + { + chain: chain({ + middleware: [getCorsMiddleware( + { access_control_expose_headers: ["x-pose-headers", "x-anotha-one"] }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-expose-headers": "x-pose-headers,x-anotha-one", + "access-control-allow-origin": "*", + "content-length": "0", + }, + }, + }, + ], + }, + + // 27 + { + chain: chain({ + middleware: [getCorsMiddleware( + { access_control_expose_headers: ["x-pose-headers", "x-anotha-one"] }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { + origin: "https://woopwoop2", + "access-control-request-headers": "x-test", + }, + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-expose-headers": "x-pose-headers,x-anotha-one", + "access-control-allow-origin": "*", + "content-length": "0", + "vary": "Access-Control-Request-Headers", + }, + }, + }, + ], + }, + + // 28 + { + chain: chain({ + middleware: [getCorsMiddleware( + { access_control_expose_headers: ["x-pose-headers", "x-anotha-one"] }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { origin: "http://woopwoop3.local" }, + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-expose-headers": "x-pose-headers,x-anotha-one", + "access-control-allow-origin": "*", + "content-length": "0", + }, + }, + }, + ], + }, + + // 29 + { + chain: chain({ + middleware: [getCorsMiddleware( + { access_control_expose_headers: ["x-pose-headers", "x-anotha-one"] }, + )], + }), + requests: [ + { + request: { + method: Method.OPTIONS, + path: "/", + headers: { origin: "http://slowbro" }, + }, + expected_response: { + status: 204, + statusText: "No Content", + body: null, + headers: { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-expose-headers": "x-pose-headers,x-anotha-one", + "access-control-allow-origin": "*", + "content-length": "0", + }, + }, + }, + ], + }, + ]; +} + +/** + * Helper function to use a decorated ETag middleware (with logs for debugging + * purposes) or the original ETag middleware. + */ +function getCorsMiddleware( + options: Options & { logs?: boolean } = defaultOptions, +) { + if (!options.logs) { + return CORS(options); + } + + return new class CORSLogged extends CORSMiddleware { + constructor() { + super(options); + } + + ALL(request: Request): Response { + return super.ALL(request); + } + + OPTIONS(request: Request) { + return super.OPTIONS(request); + } + }(); +} diff --git a/tests/middleware/deno/v1.x/modules/middleware/ETag/mod_test.ts b/tests/middleware/deno/v1.x/modules/middleware/ETag/mod_test.ts new file mode 100644 index 000000000..3c0ef3e8a --- /dev/null +++ b/tests/middleware/deno/v1.x/modules/middleware/ETag/mod_test.ts @@ -0,0 +1,495 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { asserts } from "../../../../../../deps.ts"; +import { + assertionMessage, + catchError, + chain, + query, + testCaseName, +} from "../../../../utils.ts"; +import { StatusCode } from "../../../../../../../src/core/http/response/StatusCode.ts"; +import { StatusDescription } from "../../../../../../../src/core/http/response/StatusDescription.ts"; +import { Method } from "../../../../../../../src/core/http/request/Method.ts"; +import * as Chain from "../../../../../../../src/modules/RequestChain/mod.native.ts"; +import { + defaultOptions, + ETag, + ETagMiddleware, + type Options, +} from "../../../../../../../src/modules/middleware/ETag/mod.ts"; +import { Handler } from "../../../../../../../src/standard/handlers/Handler.ts"; + +type TestCase = { + chain: Handler; + requests: { + request: RequestInfo; + expected_response: ExpectedCombined; + }[]; +}; + +type RequestInfo = + & RequestInit + & { + path: string; + query?: Record; + }; + +type Expected = + & ResponseInit + & { + body: unknown; + headers?: Record; + }; + +type ExpectedCombined = Expected | { deno: Expected; drash: Expected }; + +const protocol = "http"; +const hostname = "localhost"; +const port = 1447; +const url = `${protocol}://${hostname}:${port}`; + +// This variable gets set by each test case so that each test case uses the same +// chain. If the chain is recreated during each test case, then the ETag +// middleware will lose its cache of generated etags. Without this cache, the +// tests will fail. Reason being the tests need to exercise subsequent requests +// to make sure the ETag middleare is doing its job. For example, one request +// will be sent and it will be given an ETag header value. That value will be +// cached by the ETag middleware. When a second request is sent, the ETag +// middleware will: +// +// - hash the response of the request; +// - use the hash to see if it exists in its cache; and +// - send a 304 response if the hash exists. +// +const globals: { + current_chain: Handler | null; +} = { + current_chain: null, +}; + +const serverController = new AbortController(); + +Deno.serve( + { + port, + hostname, + onListen: () => runTests(), + signal: serverController.signal, + }, + (request: Request): Promise => { + if (!globals.current_chain) { + throw new Error(`Var \`globals.current_chain\` was not set by the test`); + } + + return globals.current_chain + .handle(request) + .catch(catchError); + }, +); + +function runTests() { + Deno.test("ETag", async (t) => { + await t.step("Deno Tests (using the chain in a Deno server)", async (t) => { + const testCases = getTestCases(); + + for (const [testCaseIndex, testCase] of testCases.entries()) { + const { chain, requests } = testCase; + + // Set the current chain to be the chain in this test case so the server (outside this + // test function) can use it. + globals.current_chain = chain; + + for ( + const [requestIndex, { request, expected_response }] of requests + .entries() + ) { + await t.step( + `${testCaseName(testCaseIndex)} ${request.method} ${request.path}`, + async () => { + const requestOptions: Record = { + method: request.method, + }; + + requestOptions.headers = request.headers ?? {}; + const fullUrl = url + request.path + query(request.query); + + const req = new Request(fullUrl, requestOptions); + + const response = await fetch(req); + + await assert( + "Deno", + testCaseIndex, + req, + requestIndex, + response, + ("deno" in expected_response) + ? expected_response.deno + : expected_response, + ); + }, + ); + } + } + }); + + await t.step("Drash Tests (using only the chain)", async (t) => { + const testCases = getTestCases(); + + for (const [testCaseIndex, testCase] of testCases.entries()) { + const { chain, requests } = testCase; + + for ( + const [requestIndex, { request, expected_response }] of requests + .entries() + ) { + await t.step( + `${testCaseName(testCaseIndex)} ${request.method} ${request.path}`, + async () => { + const requestOptions: Record = { + method: request.method, + }; + + requestOptions.headers = request.headers ?? {}; + const fullUrl = url + request.path + query(request.query); + + const req = new Request(fullUrl, requestOptions); + + const response = await chain.handle(req); + + await assert( + "Drash", + testCaseIndex, + req, + requestIndex, + response, + ("drash" in expected_response) + ? expected_response.drash + : expected_response, + ); + }, + ); + } + } + }); + }); +} + +async function assert( + system: "Drash" | "Deno", + testCaseIndex: number, + request: Request, + requestIndex: number, + actualResponse: Response, + expectedResponse: Expected, +) { + asserts.assertEquals( + await actualResponse.clone().text(), + expectedResponse.body, + assertionMessage( + `ETag test failed in ${system}:`, + `\n Response body does not match expected.`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); + + asserts.assertEquals( + actualResponse.headers.get("etag"), + expectedResponse.headers?.etag, + assertionMessage( + `ETag test failed in ${system}:`, + `\n Response "etag" header does not match expected.`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); + + const actualLastModifiedDate = new Date( + actualResponse.headers.get("last-modified")!, + ); + + // The `last-modified` header should be dated as "right now" because + asserts.assertEquals( + actualLastModifiedDate.toISOString().replace( + /:[0-9]+\.+[0-9]+Z/, + "", + ), + date(), + assertionMessage( + `ETag test failed in ${system}:`, + `\n Response "last-modified" header does not match expected.`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); + + asserts.assertEquals( + actualResponse.status, + expectedResponse.status, + assertionMessage( + `ETag test failed in ${system}:`, + `\n Response status does not match expected.`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); + + asserts.assertEquals( + actualResponse.statusText, + expectedResponse.statusText, + assertionMessage( + `Test failed in ${system}` + + `\n\nResponse statusText does not match expected.` + + `\n\nSee getTestCases()[${testCaseIndex}].requests[${requestIndex}]`, + ), + ); +} + +/** + * A date to use for asserting the "last-modified" header. + * + * Assertions for "last-modified" headers are done in the test. The assertion is + * just, "... the `last-modified` header should be NOW," so it is just doing a + * `new Date()` comparison with the seconds taken off. + */ +function date() { + return new Date().toISOString().replace(/:[0-9]+\.+[0-9]+Z/, ""); +} + +function getTestCases(): TestCase[] { + return [ + { + chain: chain({ middleware: [getEtagMiddleware()] }), + requests: [ + // Send the first request + { + request: { + method: Method.GET, + path: "/", + }, + expected_response: { + body: "Hello from Home.GET()!", + status: StatusCode.OK, + statusText: StatusDescription.OK, + headers: { + etag: `"16-SGVsbG8gZnJvbSBIb21lLkdFVCgpIQ=="`, + }, + }, + }, + // ETag middleware should keep track of the etag header it made above + { + request: { + headers: { + "if-none-match": `"16-SGVsbG8gZnJvbSBIb21lLkdFVCgpIQ=="`, + }, + method: Method.GET, + path: "/", + }, + expected_response: { + body: "", // Body should be empty when using `.text()` + headers: { + etag: `"16-SGVsbG8gZnJvbSBIb21lLkdFVCgpIQ=="`, // We should get the same etag back + }, + status: StatusCode.NotModified, // Response should be considered "not modified" + statusText: StatusDescription.NotModified, + }, + }, + // If we send the same request without the `if-none-match` header, then ... + { + request: { + method: Method.GET, + path: "/", + }, + expected_response: { + body: "Hello from Home.GET()!", // Body should be the body in the first request + headers: { + etag: `"16-SGVsbG8gZnJvbSBIb21lLkdFVCgpIQ=="`, // We should get the same etag back + }, + status: StatusCode.OK, // Response is new so it SHOULD NOT be considered "not modified" + statusText: StatusDescription.OK, + }, + }, + ], + }, + { + chain: chain({ + middleware: [getEtagMiddleware({ logs: false })], + resources: [ + class Users extends Chain.Resource { + public paths = ["/users/:id?"]; + #number_of_requests_received = 0; + + public GET(request: Chain.Request) { + const id = request.params.pathParam("id"); + + if (id) { + this.#number_of_requests_received++; + + if (this.#number_of_requests_received === 3) { + return; + } + + return new Response(`Hello user #${id}`); + } + + return new Response("Hello from Users.GET()!"); + } + }, + ], + }), + requests: [ + // Send the first request with its etag that does not exist yet + { + request: { + method: Method.GET, + path: "/users", + headers: { + "if-none-match": `"17-SGVsbG8gZnJvbSBVc2Vycy5HRVQoKSE="`, + }, + }, + expected_response: { + body: "Hello from Users.GET()!", // This the etag did not exist, this response should contain the body + status: StatusCode.OK, // It should not have a 304 Not Modified + statusText: StatusDescription.OK, // It should not have a status code description associated with 304 Not Modified + headers: { + etag: `"17-SGVsbG8gZnJvbSBVc2Vycy5HRVQoKSE="`, + }, + }, + }, + { + request: { + method: Method.GET, + path: "/users", + }, + expected_response: { + body: "Hello from Users.GET()!", + status: StatusCode.OK, + statusText: StatusDescription.OK, + headers: { + etag: `"17-SGVsbG8gZnJvbSBVc2Vycy5HRVQoKSE="`, + }, + }, + }, + // ETag middleware should keep track of the etag header it made above + { + request: { + headers: { + "if-none-match": `"17-SGVsbG8gZnJvbSBVc2Vycy5HRVQoKSE="`, + }, + method: Method.GET, + path: "/users", + }, + expected_response: { + body: "", // Body should be empty when using `.text()` + status: StatusCode.NotModified, // Response should be considered "not modified" + statusText: StatusDescription.NotModified, + headers: { + etag: `"17-SGVsbG8gZnJvbSBVc2Vycy5HRVQoKSE="`, // We should get the same etag back + }, + }, + }, + // If we hit the same endpoint and provide a path param, then ... + { + request: { + method: Method.GET, + path: "/users/1", + }, + expected_response: { + body: "Hello user #1", // Body should be the one set in the `if (id) { ... }` conditional in the resource + status: StatusCode.OK, // Response is new so it SHOULD NOT be considered "not modified" + statusText: StatusDescription.OK, + headers: { + etag: `"d-SGVsbG8gdXNlciAjMQ=="`, // We should have a different etag + }, + }, + }, + // If we send the first request again with the "if-none-match" etag, then ... + { + request: { + method: Method.GET, + path: "/users", + headers: { + "if-none-match": `"17-SGVsbG8gZnJvbSBVc2Vycy5HRVQoKSE="`, + }, + }, + expected_response: { + body: "", // The body should be empty because the response to the request has not been modified + status: StatusCode.NotModified, // The response should have the Not Modified status + statusText: StatusDescription.NotModified, + headers: { + etag: `"17-SGVsbG8gZnJvbSBVc2Vycy5HRVQoKSE="`, // We should get the same etag back + }, + }, + }, + ], + }, + ]; +} + +/** + * Helper function to use a decorated ETag middleware (with logs for debugging + * purposes) or the original ETag middleware. + */ +function getEtagMiddleware( + options: Options & { logs?: boolean } = defaultOptions, +) { + if (!options.logs) { + return ETag(options); + } + + return new class ETagLogged extends ETagMiddleware { + constructor() { + super(options); + } + + ALL(request: Request): Promise { + return Promise + .resolve() + .then(() => this.next(request)) + .then((response) => ({ request, response })) + .then((context) => { + console.log(`Calling handleIfResponseEmpty()`); + const ret = this.handleIfResponseEmpty(context); + if (ret.done) { + console.log(`done`); + } + return ret; + }) + .then((context) => this.createEtagHeader(context)) + .then((context) => { + console.log(`Calling handleEtagMatchesRequestIfNoneMatchHeader()`); + const ret = this.handleEtagMatchesRequestIfNoneMatchHeader(context); + if (ret.done) { + console.log(`done`); + } + return ret; + }) + .then((context) => { + console.log(`Calling sendResponse()`); + console.log({ response: context.response }); + return this.sendResponse(context); + }); + } + }(); +} diff --git a/tests/middleware/deno/v1.x/modules/middleware/RateLimiter/mod_test.ts b/tests/middleware/deno/v1.x/modules/middleware/RateLimiter/mod_test.ts new file mode 100644 index 000000000..4e1eefe79 --- /dev/null +++ b/tests/middleware/deno/v1.x/modules/middleware/RateLimiter/mod_test.ts @@ -0,0 +1,661 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { asserts } from "../../../../../../deps.ts"; +import { + assertionMessage, + chain, + query, + testCaseName, +} from "../../../../utils.ts"; +import { StatusCode } from "../../../../../../../src/core/http/response/StatusCode.ts"; +import { StatusDescription } from "../../../../../../../src/core/http/response/StatusDescription.ts"; +import { Method } from "../../../../../../../src/core/http/request/Method.ts"; +import * as Chain from "../../../../../../../src/modules/RequestChain/mod.native.ts"; +import { Handler } from "../../../../../../../src/standard/handlers/Handler.ts"; +import { + type Options, + RateLimiter, + RateLimiterMiddleware, +} from "../../../../../../../src/modules/middleware/RateLimiter/mod.ts"; +import { RateLimiterErrorResponse } from "../../../../../../../src/modules/middleware/RateLimiter/RateLimiterErrorResponse.ts"; +import { Header } from "../../../../../../../src/core/http/Header.ts"; + +type TestCase = { + chain: Handler; + middleware_options: Options; + requests: { + request: RequestInfo; + expected_response: ExpectedCombined; + }[]; +}; + +type RequestInfo = + & RequestInit + & { + path: string; + query?: Record; + }; + +type Expected = + & ResponseInit + & { + body: unknown; + headers: Headers; + }; + +type ExpectedCombined = Expected | { deno: Expected; drash: Expected }; + +const protocol = "http"; +const hostname = "localhost"; +const port = 1447; +const url = `${protocol}://${hostname}:${port}`; + +// This variable gets set by each test case so that each test case uses the same +// chain. If the chain is recreated during each test case, then the ETag +// middleware will lose its cache of generated etags. Without this cache, the +// tests will fail. Reason being the tests need to exercise subsequent requests +// to make sure the ETag middleare is doing its job. For example, one request +// will be sent and it will be given an ETag header value. That value will be +// cached by the ETag middleware. When a second request is sent, the ETag +// middleware will: +// +// - hash the response of the request; +// - use the hash to see if it exists in its cache; and +// - send a 304 response if the hash exists. +// +const globals: { + current_chain: Handler | null; +} = { + current_chain: null, +}; + +const serverController = new AbortController(); + +Deno.serve( + { + port, + hostname, + onListen: () => runTests(), + signal: serverController.signal, + }, + (request: Request): Promise => { + if (!globals.current_chain) { + throw new Error(`Var \`globals.current_chain\` was not set by the test`); + } + + return globals.current_chain + .handle(request) + .catch((error: RateLimiterErrorResponse) => { + return error.response; + }); + }, +); + +function runTests() { + Deno.test("RateLimiter", async (t) => { + await t.step("Deno Tests (using the chain in a Deno server)", async (t) => { + const testCases = getTestCases(); + + for (const [testCaseIndex, entry] of testCases.entries()) { + const testCase = (typeof entry === "function") ? entry() : entry; + + const { chain, requests } = testCase; + + // Set the current chain to be the chain in this test case so the server (outside this + // test function) can use it. + globals.current_chain = chain; + + for ( + const [requestIndex, { request, expected_response }] of requests + .entries() + ) { + await t.step( + `${testCaseName(testCaseIndex)} ${request.method} ${request.path}`, + async () => { + const requestOptions: Record = { + method: request.method, + }; + + requestOptions.headers = request.headers ?? {}; + const fullUrl = url + request.path + query(request.query); + + const req = new Request(fullUrl, requestOptions); + + const response = await fetch(req); + + await assert( + "Deno", + testCaseIndex, + testCase.middleware_options, + req, + requestIndex, + response, + ("deno" in expected_response) + ? expected_response.deno + : expected_response, + ); + }, + ); + } + } + }); + + await t.step("Drash Tests (using only the chain)", async (t) => { + const testCases = getTestCases(); + + for (const [testCaseIndex, entry] of testCases.entries()) { + const testCase = (typeof entry === "function") ? entry() : entry; + + const { chain, requests } = testCase; + + for ( + const [requestIndex, { request, expected_response }] of requests + .entries() + ) { + await t.step( + `${testCaseName(testCaseIndex)} ${request.method} ${request.path}`, + async () => { + const requestOptions: Record = { + method: request.method, + }; + + requestOptions.headers = request.headers ?? {}; + const fullUrl = url + request.path + query(request.query); + + const req = new Request(fullUrl, requestOptions); + + const response = await chain + .handle(req) + .catch((error: RateLimiterErrorResponse) => { + return error.response; + }); + + await assert( + "Drash", + testCaseIndex, + testCase.middleware_options, + req, + requestIndex, + response, + ("drash" in expected_response) + ? expected_response.drash + : expected_response, + ); + }, + ); + } + } + }); + }); +} + +async function assert( + system: "Drash" | "Deno", + testCaseIndex: number, + middlewareOptions: Options, + request: Request, + requestIndex: number, + actualResponse: Response, + expectedResponse: Expected, +) { + asserts.assertEquals( + await actualResponse.clone().text(), + expectedResponse.body, + assertionMessage( + `RateLimiter test failed in ${system}:`, + `\n Response body does not match expected.`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); + + asserts.assertEquals( + actualResponse.headers.get("x-ratelimit-limit"), + expectedResponse.headers.get("x-ratelimit-limit"), + assertionMessage( + `RateLimiter test failed in ${system}:`, + `\n Response "x-ratelimit-limit" header does not match expected.`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); + + asserts.assertEquals( + actualResponse.headers.get("x-ratelimit-remaining"), + expectedResponse.headers.get("x-ratelimit-remaining"), + assertionMessage( + `RateLimiter test failed in ${system}:`, + `\n Response "x-ratelimit-remaining" header does not match expected.`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); + + const rateLimitResetActual = actualResponse.headers.get( + "x-ratelimit-reset", + ); + const rateLimitResetExpected = expectedResponse.headers.get( + "x-ratelimit-reset", + ); + + if (rateLimitResetActual === null || rateLimitResetExpected === null) { + asserts.assertEquals( + rateLimitResetActual, // This should be null ... + rateLimitResetExpected, // ... and this should be null + assertionMessage( + `RateLimiter test failed in ${system}:`, + `\n Actual "x-ratelimit-reset" header ${rateLimitResetActual} does not match expected (${rateLimitResetExpected}).`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); + } else { + // If the "x-ratelimit-reset" header is not null, then we assert the seconds + // that have gone by since the test started to when this assertion is made + + const timeSetInExpectedHeader = parseInt(rateLimitResetExpected); // This occurs first (when the test runs) + const timeWhenMiddlewareProcessedRequest = parseInt(rateLimitResetActual); // This occurs second (during the test) + + asserts.assertEquals( + timeSetInExpectedHeader <= timeWhenMiddlewareProcessedRequest, + true, + assertionMessage( + `RateLimiter test failed in ${system}:`, + `\n Expected "x-ratelimit-reset" header time is not BEFORE actual time.\n`, + ` Time set in expected header: ${timeSetInExpectedHeader}`, + ` Time set in middleware: ${timeWhenMiddlewareProcessedRequest}`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); + + const timeNow = Date.now() + + middlewareOptions.rate_limit_time_window_length; + + asserts.assertEquals( + timeWhenMiddlewareProcessedRequest <= timeNow, + true, + assertionMessage( + `RateLimiter test failed in ${system}:`, + `\n Actual "x-ratelimit-reset" header time is not BEFORE time NOW.\n`, + ` Time now: ${timeNow} (+${middlewareOptions.rate_limit_time_window_length} added)`, + ` Time set in middleware: ${timeWhenMiddlewareProcessedRequest}`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); + + asserts.assertEquals( + new Date(timeWhenMiddlewareProcessedRequest).toUTCString(), + actualResponse.headers.get("retry-after")!, + assertionMessage( + `RateLimiter test failed in ${system}:`, + `\n Actual "x-ratelimit-reset" header time is not BEFORE time NOW.\n`, + ` Time now: ${timeNow} (+${middlewareOptions.rate_limit_time_window_length} added)`, + ` Time set in middleware: ${timeWhenMiddlewareProcessedRequest}`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); + } + + const actualContentType = actualResponse.headers.get("content-type"); + + if (actualContentType) { + asserts.assertEquals( + actualContentType, + expectedResponse.headers.get("content-type"), + assertionMessage( + `RateLimiter test failed in ${system}:`, + `\n Actual "content-type" header does not match expected.\n`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); + } + + const xThrottledHeader = actualResponse.headers.get("x-throttled"); + + if (xThrottledHeader) { + asserts.assertEquals( + xThrottledHeader, + expectedResponse.headers.get("x-throttled"), + assertionMessage( + `RateLimiter test failed in ${system}:`, + `\n Actual "x-throttled" header does not match expected.\n`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); + } + + asserts.assertEquals( + actualResponse.headers.get("x-retry-after"), + expectedResponse.headers.get("x-retry-after"), + assertionMessage( + `RateLimiter test failed in ${system}:`, + `\n Response "x-retry-after" header does not match expected.`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); + + asserts.assertEquals( + actualResponse.status, + expectedResponse.status, + assertionMessage( + `RateLimiter test failed in ${system}:`, + `\n Response status does not match expected.`, + `\nSee test case index [${testCaseIndex}] request index [${requestIndex}] containing:`, + `\n ${request.method} ${request.url.replace(url, "")}`, + ), + ); + + asserts.assertEquals( + actualResponse.statusText, + expectedResponse.statusText, + assertionMessage( + `RateLimiter test failed in ${system}` + + `\n\nResponse statusText does not match expected.` + + `\n\nSee getTestCases()[${testCaseIndex}].requests[${requestIndex}]`, + ), + ); +} + +function getTestCases(): (() => TestCase)[] { + return [ + () => { + const options = { + // Middleware options + client_id_header_name: "x-connecto-patronum", + max_requests: 3, + rate_limit_time_window_length: 1000 * 60 * 2, // 2 minute time window + throw_if_connection_header_name_missing: false, + + // Middleware decorator options + logs: false, + }; + + const justBeforeEndTime = Date.now() + + options.rate_limit_time_window_length; + const rateLimitResetHeader = justBeforeEndTime.toString(); + + return { + chain: chain({ + middleware: [getRateLimiterMiddleware(options)], + resources: [ + class Throttled extends Chain.Resource { + public paths = ["/throttled/:id?"]; + + public GET(request: Chain.Request) { + const id = request.params.pathParam("id"); + + if (id) { + return new Response( + JSON.stringify({ message: "My headers should persist" }), + { + status: 200, + statusText: "OK", + headers: { + [Header.ContentType]: "application/json", + "x-throttled": "I should persist!", + }, + }, + ); + } + + return new Response("Hello from Throttled.GET()!", { + status: 200, + statusText: "OK", + }); + } + }, + ], + }), + middleware_options: options, + requests: [ + // Request 0 + { + // Given ... + request: { + method: Method.GET, + path: "/throttled", + headers: new Headers({ + // ... this request does not contain the `x-connecto-patronum` header + "x-connecto": `this will not result in x-ratelimit-* headers`, + }), + }, + // When the request is made, then ... + expected_response: { + body: "Hello from Throttled.GET()!", + status: StatusCode.OK, // ... the response is OK because `throw_if_connection_header_name_missing` is `false` + statusText: StatusDescription.OK, + headers: new Headers({ + "content-type": "text/plain;charset=UTF-8", + // ... no x-ratelimit-* headers are present + }), + }, + }, + + // Request 1 + { + // Given ... + request: { + method: Method.GET, + path: "/throttled", + headers: new Headers({ + // ... this request contains the `x-connecto-patronum` header as `artic` + "x-connecto-patronum": "artic", + }), + }, + // When the request is made, then ... + expected_response: { + body: "Hello from Throttled.GET()!", + status: StatusCode.OK, + statusText: StatusDescription.OK, + headers: new Headers({ + "content-type": "text/plain;charset=UTF-8", + "x-ratelimit-limit": "3", + "x-ratelimit-remaining": "2", // ... this value should be the max requests allowed value minus 1 + "x-ratelimit-reset": rateLimitResetHeader, + }), + }, + }, + + // Request 2 + { + // Given ... + request: { + method: Method.GET, + path: "/throttled", + headers: { + // ... this is the same client making another request + "x-connecto-patronum": "artic", + }, + }, + // When the request is made, then ... + expected_response: { + body: "Hello from Throttled.GET()!", + status: StatusCode.OK, + statusText: StatusDescription.OK, + headers: new Headers({ + "content-type": "text/plain;charset=UTF-8", + "x-ratelimit-limit": "3", + "x-ratelimit-remaining": "1", // ... this value should be artic's last response's value minus 1 + "x-ratelimit-reset": rateLimitResetHeader, + }), + }, + }, + + // Request 3 + { + // Given ... + request: { + method: Method.GET, + path: "/throttled", + headers: { + // ... this is a different client: `zap` + "x-connecto-patronum": "zap", + }, + }, + // When the request is made, then ... + expected_response: { + body: "Hello from Throttled.GET()!", + status: StatusCode.OK, + statusText: StatusDescription.OK, + headers: new Headers({ + "content-type": "text/plain;charset=UTF-8", + "x-ratelimit-limit": "3", + "x-ratelimit-remaining": "2", // ... this should be the max requests allowed value minus 1 + "x-ratelimit-reset": rateLimitResetHeader, + }), + }, + }, + + // Request 4 + { + // Given ... + request: { + method: Method.GET, + path: "/throttled", + headers: { + // ... this is the `artic` client again making its 3rd request in its rate limit time window + "x-connecto-patronum": "artic", + }, + }, + // When the request is made, then ... + expected_response: { + body: "Hello from Throttled.GET()!", + status: StatusCode.OK, + statusText: StatusDescription.OK, + headers: new Headers({ + "content-type": "text/plain;charset=UTF-8", + "x-ratelimit-limit": "3", + "x-ratelimit-remaining": "0", // ... this value should be artic's last response's value minus 1 + "x-ratelimit-reset": rateLimitResetHeader, + }), + }, + }, + + // Request 5 + { + // Given ... + request: { + method: Method.GET, + path: "/throttled", + headers: { + // ... this is the `artic` client again making its 4th request in its rate limit time window + // ... and 3 is the maximum number of requests that can be made in a rate limit time window + "x-connecto-patronum": "artic", + }, + }, + // When the request is made, then ... + expected_response: { + body: + "Too many requests. Next request can be made after the time set in the Retry-After header.", + status: StatusCode.TooManyRequests, // ... this should be a 429 + statusText: StatusDescription.TooManyRequests, // ... this should be the 429's description + headers: new Headers({ + "content-type": "text/plain;charset=UTF-8", + "x-ratelimit-limit": "3", + "x-ratelimit-remaining": "0", // ... this value should still be 0 because a client cannot have negative requests remaining + "x-ratelimit-reset": rateLimitResetHeader, + }), + }, + }, + + // Request 6 + { + // Given ... + request: { + method: Method.GET, + path: "/throttled/1337", + headers: { + // ... this is a new client: `mol` + "x-connecto-patronum": "mol", + }, + }, + // When the request is made, then ... + expected_response: { + body: JSON.stringify({ message: "My headers should persist" }), + status: StatusCode.OK, + statusText: StatusDescription.OK, + headers: new Headers({ + "x-throttled": "I should persist!", + "content-type": "application/json", + "x-ratelimit-limit": "3", + "x-ratelimit-remaining": "2", // ... this value should be the max number of requests minus 1 + "x-ratelimit-reset": rateLimitResetHeader, + }), + }, + }, + ], + }; + }, + ]; +} + +const defaultOptions: Options = { + rate_limit_time_window_length: 10000, // 10 seconds + max_requests: 3, + client_id_header_name: "x-client-id", + throw_if_connection_header_name_missing: false, +}; + +/** + * Helper function to use a decorated AcceptHeader middleware (with logs for + * debugging purposes) or the original AcceptHeader middleware. + */ +function getRateLimiterMiddleware( + options: Options & { logs?: boolean } = defaultOptions, +) { + if (!options.logs) { + return RateLimiter(options); + } + + return new class RateLimiterLogged extends RateLimiterMiddleware { + constructor() { + super(options); + } + + public ALL(request: Request): Promise { + return Promise + .resolve() + .then(() => { + console.log(`Calling setClientInContext()`); + return this.setClientInContext(request); + }) + .then((context) => { + console.log(`Calling throwIfRateLimited()`); + return this.throwIfRateLimited(context); + }) + .then((context) => { + console.log(`Calling sendToNext()`); + return this.sendToNext(context); + }) + .then((context) => { + console.log(`Calling sendResponse()`); + return this.sendResponse(context); + }); + } + }(); +} diff --git a/tests/test_helpers.ts b/tests/test_helpers.ts deleted file mode 100644 index d6334185c..000000000 --- a/tests/test_helpers.ts +++ /dev/null @@ -1,54 +0,0 @@ -// deno-lint-ignore-file -import { Drash } from "./deps.ts"; -const decoder = new TextDecoder("utf-8"); - -interface IMakeRequestOptions { - body?: any; - headers?: any; - credentials?: any; -} - -export const makeRequest = { - get(url: string, options: IMakeRequestOptions = {}) { - options = Object.assign(options, { - method: "GET", - }); - return fetch(url, options); - }, - post(url: string, options: IMakeRequestOptions = {}) { - options = Object.assign(options, { - method: "POST", - }); - if (options.body) { - options.body = JSON.stringify(options.body); - } - return fetch(url, options); - }, - put(url: string, options: IMakeRequestOptions = {}) { - options = Object.assign(options, { - method: "PUT", - }); - if (options.body) { - options.body = JSON.stringify(options.body); - } - return fetch(url, options); - }, - delete(url: string, options: IMakeRequestOptions = {}) { - options = Object.assign(options, { - method: "DELETE", - }); - if (options.body) { - options.body = JSON.stringify(options.body); - } - return fetch(url, options); - }, - patch(url: string, options: IMakeRequestOptions = {}) { - options = Object.assign(options, { - method: "PATCH", - }); - if (options.body) { - options.body = JSON.stringify(options.body); - } - return fetch(url, options); - }, -}; diff --git a/tests/unit/http/error_handler_test.ts b/tests/unit/http/error_handler_test.ts deleted file mode 100644 index 548f56458..000000000 --- a/tests/unit/http/error_handler_test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import * as Drash from "../../../mod.ts"; -import type { ConnInfo } from "../../../deps.ts"; -import { assertEquals } from "../../deps.ts"; - -const connInfo: ConnInfo = { - localAddr: { - transport: "tcp", - hostname: "localhost", - port: 1337, - }, - remoteAddr: { - transport: "udp", - hostname: "localhost", - port: 1337, - }, -}; - -function request() { - const req = new Request("https://drash.land", { - headers: { - Accept: "application/json;text/html", - }, - }); - - return new Drash.Request( - req, - new Map(), - connInfo, - ); -} - -const errorHandler = new Drash.ErrorHandler(); - -Deno.test("catch()", async (t) => { - await t.step("new Error()", () => { - const res = new Drash.Response(); - errorHandler.catch( - new Error(), - request(), - res, - connInfo, - ); - assertEquals(res.status, 500); - }); - - await t.step("Built-in JS Errors", () => { - const errors = [ - new EvalError(), - new RangeError(), - new ReferenceError(), - new SyntaxError(), - new TypeError(), - new URIError(), - ]; - - errors.forEach((error: Error) => { - const res = new Drash.Response(); - errorHandler.catch( - error, - request(), - res, - connInfo, - ); - assertEquals(res.status, 500); - }); - }); - - await t.step("{ code: 'Hello' }", () => { - const res = new Drash.Response(); - errorHandler.catch( - new ErrorWithRandomCodeString("Hello"), - request(), - res, - connInfo, - ); - assertEquals(res.status, 500); - }); - - await t.step("{ code: '500' }", () => { - const res = new Drash.Response(); - errorHandler.catch( - new ErrorWithRandomCodeString("SQL15023"), - request(), - res, - connInfo, - ); - assertEquals(res.status, 500); - }); - - await t.step("{ code: 400 }", () => { - const res = new Drash.Response(); - errorHandler.catch( - new ErrorWithRandomCodeNumber(400), - request(), - res, - connInfo, - ); - assertEquals(res.status, 400); - }); - - await t.step("new Drash.Errors.HttpError(401)", () => { - const res = new Drash.Response(); - errorHandler.catch( - new Drash.Errors.HttpError(401), - request(), - res, - connInfo, - ); - assertEquals(res.status, 401); - }); -}); - -class ErrorWithRandomCodeNumber extends Error { - public code: number; - constructor(code: number, message?: string) { - super(message ?? "(no error message provided)"); - this.code = code; - } -} - -class ErrorWithRandomCodeString extends Error { - public code: string; - constructor(code: string, message?: string) { - super(message ?? "(no error message provided)"); - this.code = code; - } -} diff --git a/tests/unit/http/request_test.ts b/tests/unit/http/request_test.ts deleted file mode 100644 index 91781d0ff..000000000 --- a/tests/unit/http/request_test.ts +++ /dev/null @@ -1,804 +0,0 @@ -import { assertEquals } from "../../deps.ts"; -import * as Drash from "../../../mod.ts"; -import type { ConnInfo } from "../../../deps.ts"; - -const connInfo: ConnInfo = { - localAddr: { - transport: "tcp", - hostname: "localhost", - port: 1337, - }, - remoteAddr: { - transport: "udp", - hostname: "localhost", - port: 1337, - }, -}; - -Deno.test("http/request_test.ts", async (t) => { - await t.step("original", async (t) => { - await originalRequestTests(t); - }); - - await t.step("accepts()", async (t) => { - await acceptsTests(t); - }); - - await t.step("getCookie()", async (t) => { - await getCookieTests(t); - }); - - await t.step("bodyParam()", async (t) => { - await bodyTests(t); - }); - - await t.step("pathParam()", async (t) => { - await paramTests(t); - }); - - await t.step("queryParam()", async (t) => { - await queryTests(t); - }); - - await t.step("static create()", async (t) => { - await staticCreateTests(t); - }); -}); - -//////////////////////////////////////////////////////////////////////////////// -// FILE MARKER - TEST CASES //////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -async function acceptsTests(t: Deno.TestContext) { - await t.step( - "accepts the single type if it is present in the header", - () => { - const req = new Request("https://drash.land", { - headers: { - Accept: "application/json;text/html", - }, - }); - const request = new Drash.Request( - req, - new Map(), - connInfo, - ); - let actual; - actual = request.accepts("application/json"); - assertEquals(actual, true); - actual = request.accepts("text/html"); - assertEquals(actual, true); - }, - ); - await t.step( - "rejects the single type if it is not present in the header", - () => { - const req = new Request("https://drash.land", { - headers: { - Accept: "application/json;text/html", - }, - }); - const request = new Drash.Request( - req, - new Map(), - connInfo, - ); - const actual = request.accepts("text/xml"); - assertEquals(actual, false); - }, - ); -} - -async function getCookieTests(t: Deno.TestContext) { - await t.step("Returns the cookie value if it exists", () => { - const req = new Request("https://drash.land", { - headers: { - Accept: "application/json;text/html", - Cookie: "test_cookie=test_cookie_value", - credentials: "include", - }, - }); - const request = new Drash.Request( - req, - new Map(), - connInfo, - ); - const cookieValue = request.getCookie("test_cookie"); - assertEquals(cookieValue, "test_cookie_value"); - }); - await t.step("Returns undefined if the cookie does not exist", () => { - const req = new Request("https://drash.land", { - headers: { - Accept: "application/json;text/html", - Cookie: "test_cookie=test_cookie_value", - credentials: "include", - }, - }); - const request = new Drash.Request( - req, - new Map(), - connInfo, - ); - const cookieValue = request.getCookie("cookie_doesnt_exist"); - assertEquals(cookieValue, undefined); - }); -} - -async function bodyTests(t: Deno.TestContext) { - await t.step("Can return multiple files", async () => { - const formData = new FormData(); - const file1 = new Blob([ - new File([Deno.readFileSync("./mod.ts")], "mod.ts"), - ], { - type: "application/javascript", - }); - const file2 = new Blob([ - new File([Deno.readFileSync("./mod.ts")], "mod.ts"), - ], { - type: "application/javascript", - }); - formData.append("foo[]", file1, "mod.ts"); - formData.append("foo[]", file2, "mod2.ts"); - const serverRequest = new Request("https://drash.land", { - headers: { - // We use `"Content-Length": "1"` to tell Drash.Request that there is - // a request body. This is a hack just for unit testing. In the real - // world, the Content-Length header will be defined (at least it - // should be) by the client. - "Content-Length": "1", - }, - body: formData, - method: "POST", - }); - const request = await Drash.Request.create( - serverRequest, - new Map(), - connInfo, - ); - - // make sure that if a requets has multiple files, we can get each one eh - const size = Deno.build.os === "windows" ? 1471 : 1433; - const file = request.bodyParam("foo") ?? []; - assertEquals(file[0].content, Deno.readFileSync("./mod.ts")); - assertEquals(file[0].size, size); - assertEquals(file[0].type, "application/javascript"); - assertEquals(file[0].filename, "mod.ts"); - assertEquals(file[1].content, Deno.readFileSync("./mod.ts")); - assertEquals(file[1].size, size); - assertEquals(file[1].type, "application/javascript"); - assertEquals(file[1].filename, "mod2.ts"); - }); - - // Reason: `this.request.getBodyParam()` didn't work for multipart/form-data requests - await t.step("Returns the file object if the file exists", async () => { - const formData = new FormData(); - const file = new Blob([ - new File([Deno.readFileSync("./logo.svg")], "logo.svg"), - ], { - type: "image/svg", - }); - formData.append("foo", file, "logo.svg"); - const serverRequest = new Request("https://drash.land", { - headers: { - // We use `"Content-Length": "1"` to tell Drash.Request that there is - // a request body. This is a hack just for unit testing. In the real - // world, the Content-Length header will be defined (at least it - // should be) by the client. - "Content-Length": "1", - }, - body: formData, - method: "POST", - }); - const request = await Drash.Request.create( - serverRequest, - new Map(), - connInfo, - ); - // deno-lint-ignore no-explicit-any - const bodyFile = request.bodyParam("foo") as any; - assertEquals( - bodyFile.content, - Deno.readFileSync("./logo.svg"), - ); - assertEquals(bodyFile.type, "image/svg"); - assertEquals(bodyFile.filename, "logo.svg"); - assertEquals( - bodyFile.size > 3000 && bodyFile.size < 3200, - true, - ); // Should be 3099, but on windows it 3119, so just do a basic check on size to avoid bloated test code - }); - - await t.step( - "Returns the value of a normal field for formdata requests", - async () => { - const formData = new FormData(); - formData.append("user", "Drash"); - const req = new Request("https://drash.land", { - headers: { - // We use `"Content-Length": "1"` to tell Drash.Request that there is - // a request body. This is a hack just for unit testing. In the real - // world, the Content-Length header will be defined (at least it - // should be) by the client. - "Content-Length": "1", - }, - body: formData, - method: "POST", - }); - const request = await Drash.Request.create( - req, - new Map(), - connInfo, - ); - assertEquals(request.bodyParam("user"), "Drash"); - }, - ); - - await t.step("Returns undefined if the file does not exist", async () => { - const formData = new FormData(); - const file = new Blob([JSON.stringify({ hello: "world" }, null, 2)], { - type: "application/json", - }); - formData.append("foo[]", file, "hello.json"); - const serverRequest = new Request("https://drash.land", { - headers: { - // We use `"Content-Length": "1"` to tell Drash.Request that there is - // a request body. This is a hack just for unit testing. In the real - // world, the Content-Length header will be defined (at least it - // should be) by the client. - "Content-Length": "1", - }, - body: formData, - method: "POST", - }); - const request = await Drash.Request.create( - serverRequest, - new Map(), - connInfo, - ); - assertEquals(request.bodyParam("dontexist"), undefined); - }); - await t.step( - "Returns the value for the parameter when the data exists for application/json", - async () => { - const req = new Request("https://drash.land", { - headers: { - // We use `"Content-Length": "1"` to tell Drash.Request that there is - // a request body. This is a hack just for unit testing. In the real - // world, the Content-Length header will be defined (at least it - // should be) by the client. - "Content-Length": "1", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - hello: "world", - }), - method: "POST", - }); - const request = await Drash.Request.create( - req, - new Map(), - connInfo, - ); - const actual = request.bodyParam("hello"); - assertEquals("world", actual); - }, - ); - await t.step( - "Returns null when the data doesn't exist for application/json", - async () => { - const serverRequest = new Request("https://drash.land", { - headers: { - // We use `"Content-Length": "1"` to tell Drash.Request that there is - // a request body. This is a hack just for unit testing. In the real - // world, the Content-Length header will be defined (at least it - // should be) by the client. - "Content-Length": "1", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - hello: "world", - }), - method: "POST", - }); - const request = await Drash.Request.create( - serverRequest, - new Map(), - connInfo, - ); - const actual = request.bodyParam("dont_exist"); - assertEquals(undefined, actual); - }, - ); - - await t.step( - "Should be consistent with falsey return values (null value returns null)", - async () => { - // This test case was added due to https://github.com/drashland/drash/issues/623 - - const serverRequest = new Request("https://drash.land", { - headers: { - // We use `"Content-Length": "1"` to tell Drash.Request that there is - // a request body. This is a hack just for unit testing. In the real - // world, the Content-Length header will be defined (at least it - // should be) by the client. - "Content-Length": "1", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - foo: null, - }), - method: "POST", - }); - const request = await Drash.Request.create( - serverRequest, - new Map(), - connInfo, - ); - const body = request.bodyAll() as { - foo: null; - }; - assertEquals(body, { foo: null }); - assertEquals(body["foo"], null); - assertEquals(request.bodyParam("foo"), null); - // As an edge case check, make sure bodyAll() returns the expected - assertEquals( - (request.bodyAll() as Partial<{ foo: unknown }>)["foo"], - null, - ); - }, - ); - - await t.step( - "Should be consistent with falsey return values (false value returns false)", - async () => { - // This test case was added due to https://github.com/drashland/drash/issues/623 - - const serverRequest = new Request("https://drash.land", { - headers: { - // We use `"Content-Length": "1"` to tell Drash.Request that there is - // a request body. This is a hack just for unit testing. In the real - // world, the Content-Length header will be defined (at least it - // should be) by the client. - "Content-Length": "1", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - foo: false, - }), - method: "POST", - }); - const request = await Drash.Request.create( - serverRequest, - new Map(), - connInfo, - ); - const body = request.bodyAll() as Partial<{ - foo: unknown; - }>; - assertEquals(body, { foo: false }); - assertEquals(body["foo"], false); - assertEquals(request.bodyParam("foo"), false); - // As an edge case check, make sure bodyAll() returns the expected - assertEquals( - (request.bodyAll() as Partial<{ foo: unknown }>)["foo"], - false, - ); - }, - ); - - await t.step( - "Should be consistent with falsey return values (undefined value returns undefined)", - async () => { - // This test case was added due to https://github.com/drashland/drash/issues/623 - - const serverRequest = new Request("https://drash.land", { - headers: { - // We use `"Content-Length": "1"` to tell Drash.Request that there is - // a request body. This is a hack just for unit testing. In the real - // world, the Content-Length header will be defined (at least it - // should be) by the client. - "Content-Length": "1", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - foo: undefined, - }), - method: "POST", - }); - const request = await Drash.Request.create( - serverRequest, - new Map(), - connInfo, - ); - const body = request.bodyAll() as Partial<{ - foo: unknown; - }>; - assertEquals(body, {}); - assertEquals(body["foo"], undefined); - assertEquals(request.bodyParam("foo"), undefined); - // As an edge case check, make sure bodyAll() returns the expected - assertEquals( - (request.bodyAll() as Partial<{ foo: unknown }>)["foo"], - undefined, - ); - }, - ); - - await t.step( - "Should be consistent with falsey return values (no value returns undefined)", - async () => { - // This test case was added due to https://github.com/drashland/drash/issues/623 - - const serverRequest = new Request("https://drash.land", { - headers: { - // We use `"Content-Length": "1"` to tell Drash.Request that there is - // a request body. This is a hack just for unit testing. In the real - // world, the Content-Length header will be defined (at least it - // should be) by the client. - "Content-Length": "1", - "Content-Type": "application/json", - }, - body: JSON.stringify({}), - method: "POST", - }); - const request = await Drash.Request.create( - serverRequest, - new Map(), - connInfo, - ); - const body = request.bodyAll() as Partial<{ - foo: unknown; - }>; - assertEquals(body, {}); - assertEquals(body["foo"], undefined); - assertEquals(request.bodyParam("foo"), undefined); - // As an edge case check, make sure bodyAll() returns the expected - assertEquals( - (request.bodyAll() as Partial<{ foo: unknown }>)["foo"], - undefined, - ); - }, - ); - - await t.step( - "Returns the value for the parameter when it exists and request is multipart/form-data when using generics", - async () => { - const formData = new FormData(); - const file = new Blob([ - new File([Deno.readFileSync("./logo.svg")], "logo.svg"), - ], { - type: "image/svg", - }); - formData.append("foo", file, "logo.svg"); - formData.append("user", "drash"); - const serverRequest = new Request("https://drash.land", { - headers: { - // We use `"Content-Length": "1"` to tell Drash.Request that there is - // a request body. This is a hack just for unit testing. In the real - // world, the Content-Length header will be defined (at least it - // should be) by the client. - "Content-Length": "1", - }, - body: formData, - method: "POST", - }); - const request = await Drash.Request.create( - serverRequest, - new Map(), - connInfo, - ); - const param = request.bodyParam<{ - content: Uint8Array; - filename: string; - size: string; - type: string; - }>("foo"); - assertEquals( - param!.content, - Deno.readFileSync("./logo.svg"), - ); - assertEquals(request.bodyParam("user"), "drash"); - }, - ); - // Before the date of 5th, Oct 2020, type errors were thrown for objects because the return value of `getBodyParam` was either a string or null - await t.step("Can handle when a body param is an object", async () => { - const serverRequest = new Request("https://drash.land", { - headers: { - // We use `"Content-Length": "1"` to tell Drash.Request that there is - // a request body. This is a hack just for unit testing. In the real - // world, the Content-Length header will be defined (at least it - // should be) by the client. - "Content-Length": "1", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - user: { - name: "Edward", - location: "UK", - }, - }), - method: "POST", - }); - const request = await Drash.Request.create( - serverRequest, - new Map(), - connInfo, - ); - const actual = request.bodyParam<{ - name: string; - location: string; - }>("user")!; - assertEquals({ - name: "Edward", - location: "UK", - }, actual); - const name = actual.name; // Ensuring we can access it and TS doesn't throw errors - assertEquals(name, "Edward"); - }); - await t.step("Can handle when a body param is an array", async () => { - const serverRequest = new Request("https://drash.land", { - headers: { - // We use `"Content-Length": "1"` to tell Drash.Request that there is - // a request body. This is a hack just for unit testing. In the real - // world, the Content-Length header will be defined (at least it - // should be) by the client. - "Content-Length": "1", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - usernames: ["Edward", "John Smith", "Lord Voldemort", "Count Dankula"], - }), - method: "POST", - }); - const request = await Drash.Request.create( - serverRequest, - new Map(), - connInfo, - ); - const actual = request.bodyParam("usernames"); - assertEquals( - ["Edward", "John Smith", "Lord Voldemort", "Count Dankula"], - actual, - ); - const firstName = (actual as Array)[0]; - assertEquals(firstName, "Edward"); - }); - await t.step("Can handle when a body param is a boolean", async () => { - const serverRequest = new Request("https://drash.land", { - headers: { - // We use `"Content-Length": "1"` to tell Drash.Request that there is - // a request body. This is a hack just for unit testing. In the real - // world, the Content-Length header will be defined (at least it - // should be) by the client. - "Content-Length": "1", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - authenticated: false, - }), - method: "POST", - }); - const request = await Drash.Request.create( - serverRequest, - new Map(), - connInfo, - ); - const actual = request.bodyParam("authenticated"); - assertEquals(actual, false); - const authenticated = actual as boolean; - assertEquals(authenticated, false); - }); -} - -async function originalRequestTests(t: Deno.TestContext) { - await t.step( - "body is kept intact when Drash.Request body is parsed", - async () => { - const serverRequest = new Request("https://drash.land", { - headers: { - // We use `"Content-Length": "1"` to tell Drash.Request that there is - // a request body. This is a hack just for unit testing. In the real - // world, the Content-Length header will be defined (at least it - // should be) by the client. - "Content-Length": "1", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - hello: "world", - }), - method: "POST", - }); - - // When creating a Drash.Request object, the body is automatically parsed - // and causes `Drash.Request.bodyUsed` to be `true` - const request = await Drash.Request.create( - serverRequest, - new Map(), - connInfo, - ); - - // Check that the Drash.Request body has the `hello` param - const hello = request.bodyParam("hello"); - assertEquals(hello, "world"); - assertEquals(request.bodyUsed, true); - - // Check that the original request body was kept intact - assertEquals(request.original.bodyUsed, false); - - // Now read the original request body - assertEquals(await request.original.json(), { hello: "world" }); - assertEquals(request.original.bodyUsed, true); - }, - ); - - await t.step( - "can be retrieved via request.original and has { bodyUsed: false } (POST no body)", - async () => { - // We expect this to be cloned in the `Drash.Request.create()` call - const serverRequest = new Request("https://drash.land", { - headers: { "x-hello": "goodbye", "x-goodbye": "hello" }, - method: "POST", - redirect: "error", - }); - - // When creating a Drash.Request object, the body is automatically parsed - // and causes `Drash.Request.bodyUsed` to be `true` - const request = await Drash.Request.create( - serverRequest, - new Map(), - connInfo, - ); - - // Assert some equality between the two requests - assertEquals(request.original.bodyUsed, false); - assertEquals(request.original.bodyUsed, serverRequest.bodyUsed); - assertEquals(request.original.headers, serverRequest.headers); - assertEquals(request.original.method, serverRequest.method); - assertEquals(request.original.redirect, serverRequest.redirect); - assertEquals(request.original.url, serverRequest.url); - }, - ); -} - -async function paramTests(t: Deno.TestContext) { - await t.step( - "Returns the value for the header param when it exists", - () => { - const serverRequest = new Request("https://drash.land"); - const request = new Drash.Request( - serverRequest, - new Map().set("hello", "world"), - connInfo, - ); - const actual = request.pathParam("hello"); - assertEquals("world", actual); - }, - ); - - await t.step( - "Returns null when the path param doesn't exist", - () => { - const serverRequest = new Request("https://drash.land"); - const request = new Drash.Request( - serverRequest, - new Map().set("hello", "world"), - connInfo, - ); - const actual = request.pathParam("dont-exist"); - assertEquals(actual, undefined); - }, - ); -} - -async function queryTests(t: Deno.TestContext) { - await t.step( - "Returns the value for the query param when it exists", - () => { - const serverRequest = new Request("https://drash.land/?hello=world"); - const request = new Drash.Request( - serverRequest, - new Map(), - connInfo, - ); - const actual = request.queryParam("hello"); - assertEquals(actual, "world"); - }, - ); - - await t.step( - "Returns null when the query data doesn't exist", - () => { - const serverRequest = new Request("https://drash.land/?hello=world"); - const request = new Drash.Request( - serverRequest, - new Map(), - connInfo, - ); - const actual = request.queryParam("dont_exist"); - assertEquals(undefined, actual); - }, - ); -} - -async function staticCreateTests(t: Deno.TestContext) { - await t.step( - "option { read_body: false } keeps body intact", - async () => { - const serverRequest = new Request("https://drash.land", { - headers: { - // We use `"Content-Length": "1"` to tell Drash.Request that there is - // a request body. This is a hack just for unit testing. In the real - // world, the Content-Length header will be defined (at least it - // should be) by the client. - // - // Since we are passing in { read_body: false }, the body should not - // be read regardless of Content-Length being 1. - "Content-Length": "1", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - hello: "world", - }), - method: "POST", - }); - - // This call should cause the body to be left intact. - // `DrashRequest.parseBody()` should not be called. - const request = await Drash.Request.create( - serverRequest, - new Map(), - connInfo, - { - read_body: false, - }, - ); - - // Assert the bodies have not been read because a clone of `serverRequest` - // was passed in to `super()` in `DrashRequest` - assertEquals(request.bodyUsed, false); - assertEquals(request.original.bodyUsed, false); - - // Now read the original request body - assertEquals(await request.original.json(), { hello: "world" }); - assertEquals(request.original.bodyUsed, true); - }, - ); - - await t.step( - "empty POST body does not throw an error", - async () => { - const serverRequest = new Request("https://drash.land", { - method: "POST", - }); - - const request = await Drash.Request.create( - serverRequest, - new Map(), - connInfo, - ); - - // Assert the bodies have not been read because a clone of `serverRequest` - // was passed in to `super()` in `DrashRequest` - assertEquals(request.bodyUsed, false); - assertEquals(request.original.bodyUsed, false); - - // Now read the bodies - assertEquals(await request.text(), ""); - - // This should work because the above call should not affect the original - assertEquals(await request.original.text(), ""); - - // The body should be null for both according to spec - // (https://developer.mozilla.org/en-US/docs/Web/API/Request/body). Body - // contents should not have been added to the requests, so null is what we - // should expect. - assertEquals(request.body, null); - assertEquals(request.original.body, null); - - // According to spec, these should still be false because the bodies were - // null. See https://fetch.spec.whatwg.org/#dom-body-bodyused. - assertEquals(request.bodyUsed, false); - assertEquals(request.original.bodyUsed, false); - }, - ); -} diff --git a/tests/unit/http/response_test.ts b/tests/unit/http/response_test.ts deleted file mode 100644 index 347c738ad..000000000 --- a/tests/unit/http/response_test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import * as Drash from "../../../mod.ts"; -import { assertEquals } from "../../deps.ts"; -Deno.test("tests/unit/http/response_test.ts | setCookie()", () => { - const response = new Drash.Response(); - response.setCookie({ - name: "Repo", - value: "Drash", - }); - assertEquals(response.headers.get("Set-cookie"), "Repo=Drash"); -}); -Deno.test("deleteCookie", () => { - const response = new Drash.Response(); - response.setCookie({ - name: "Repo", - value: "Drash", - }); - assertEquals(response.headers.get("Set-cookie"), "Repo=Drash"); - response.deleteCookie("Repo"); - assertEquals( - response.headers.get("Set-cookie")?.includes("Repo=Drash, Repo=; Expires="), - true, - ); -}); -Deno.test("download()", () => { - const response = new Drash.Response(); - const filepath = Deno.cwd() + "/tests/data/static_file.txt"; - response.download(filepath, "text/plain"); - assertEquals( - response.headers.get("Content-Disposition"), - `attachment; filename="static_file.txt"`, - ); - assertEquals(response.headers.get("Content-Type"), "text/plain"); - assertEquals( - (new TextDecoder().decode(response.body as Uint8Array)).startsWith("test"), - true, - ); -}); -Deno.test("text()", () => { - const response = new Drash.Response(); - response.text("hello", 419, { - user: "name", - }); - assertEquals(response.body, "hello"); - assertEquals(response.headers.get("content-type"), "text/plain"); - assertEquals(response.status, 419); - assertEquals(response.headers.get("user"), "name"); -}); - -Deno.test("html()", () => { - const response = new Drash.Response(); - response.html("hello", 419, { - user: "name", - }); - assertEquals(response.body, "hello"); - assertEquals(response.headers.get("content-type"), "text/html"); - assertEquals(response.status, 419); - assertEquals(response.headers.get("user"), "name"); -}); - -Deno.test("xml()", () => { - const response = new Drash.Response(); - response.xml("hello", 419, { - user: "name", - }); - assertEquals(response.body, "hello"); - assertEquals(response.headers.get("content-type"), "text/xml"); - assertEquals(response.status, 419); - assertEquals(response.headers.get("user"), "name"); -}); - -Deno.test("json()", () => { - const response = new Drash.Response(); - response.json({ name: "Drash" }, 419, { - user: "name", - }); - assertEquals(JSON.parse(response.body as string), { "name": "Drash" }); - assertEquals(response.headers.get("content-type"), "application/json"); - assertEquals(response.status, 419); - assertEquals(response.headers.get("user"), "name"); -}); - -Deno.test("file()", () => { - const response = new Drash.Response(); - const filepath = Deno.cwd() + "/tests/data/index.html"; - response.file(filepath, 419, { - user: "name", - }); - assertEquals( - (new TextDecoder().decode(response.body as Uint8Array)).startsWith( - `This is the index.html file for testing pretty links`, - ), - true, - ); - assertEquals(response.headers.get("content-type"), "text/html"); - response.file("./logo.svg"); - assertEquals(response.headers.get("content-type"), "image/svg+xml"); - assertEquals(response.headers.get("user"), "name"); -}); diff --git a/tests/unit/http/server_test.ts b/tests/unit/http/server_test.ts deleted file mode 100644 index b603a7338..000000000 --- a/tests/unit/http/server_test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { assertEquals } from "../../deps.ts"; -import { Server } from "../../../src/http/server.ts"; -import { Request, Resource, Response } from "../../../mod.ts"; - -class HomeResource extends Resource { - paths = ["/"]; - public GET(_request: Request, response: Response) { - response.json({ - success: true, - }); - } - public POST(request: Request, response: Response) { - response.text(request.bodyParam("name") ?? "No body param passed in."); - } -} - -const server = new Server({ - port: 1234, - resources: [HomeResource], - protocol: "http", - hostname: "localhost", -}); - -Deno.test("http/server_test.ts", async (t) => { - await t.step("address", async (t) => { - await t.step("Should correctly format the address", () => { - const server1 = new Server({ - protocol: "https", - hostname: "hosty", - port: 1234, - resources: [HomeResource], - }); - const server2 = new Server({ - port: 1234, - resources: [HomeResource], - protocol: "http", - hostname: "ello", - }); - server1.close(); - server2.close(); - assertEquals(server1.address, "https://hosty:1234"); - assertEquals(server2.address, "http://ello:1234"); - }); - }); - - await t.step("close()", async (t) => { - await t.step("Closes the server", async () => { - server.run(); - // can connect - const conn = await Deno.connect({ - hostname: "localhost", - port: 1234, - }); - conn.close(); - // and then close - await server.close(); - let errorThrown = false; - try { - await Deno.connect({ - hostname: "localhost", - port: 1234, - }); - } catch (_e) { - errorThrown = true; - } - assertEquals(errorThrown, true); - }); - }); - - await t.step("run()", async (t) => { - await t.step( - "Will listen correctly and send the proper response", - async () => { - server.run(); - const res = await fetch("http://localhost:1234", { - headers: { - Accept: "application/json", - }, - }); - await server.close(); - assertEquals(await res.json(), { - success: true, - }); - assertEquals(res.status, 200); - }, - ); - await t.step( - "Will throw a 404 if no resource found matching the uri", - async () => { - server.run(); - const res = await fetch("http://localhost:1234/dont/exist"); - await res.text(); - await server.close(); - assertEquals(res.status, 404); - }, - ); - await t.step( - "Will throw a 405 if the req method isnt found on the resource", - async () => { - server.run(); - const res = await fetch("http://localhost:1234", { - method: "OPTIONS", - }); - await res.text(); - await server.close(); - assertEquals(res.status, 405); - }, - ); - await t.step( - "Will parse the body if it exists on the request", - async () => { - server.run(); - const res = await fetch("http://localhost:1234", { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "text/plain", - }, - body: JSON.stringify({ name: "Drash" }), - }); - await server.close(); - assertEquals(await res.text(), "Drash"); - assertEquals(res.status, 200); - }, - ); - }); -}); diff --git a/tests/unit/services/cors_test.ts b/tests/unit/services/cors_test.ts deleted file mode 100644 index 91f605c0c..000000000 --- a/tests/unit/services/cors_test.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { assertEquals } from "../../deps.ts"; -import { - IResource, - Request, - Resource, - Response, - Server, -} from "../../../mod.ts"; -import { CORSService } from "../../../src/services/cors/cors.ts"; - -class FailedOptionCORSMiddlewareResource extends Resource implements IResource { - paths = ["/cors"]; - public GET(_request: Request, response: Response) { - response.text("GET request received!"); - } - public OPTIONS(_request: Request, response: Response) { - response.headers.set("content-type", "text/plain"); - } -} - -function runServer(allowAll = true): Server { - const cors = allowAll - ? new CORSService() - : new CORSService({ origin: "localhost" }); - const server = new Server({ - services: [cors], - resources: [ - FailedOptionCORSMiddlewareResource, - ], - port: 1447, - hostname: "127.0.0.1", - protocol: "http", - }); - server.run(); - return server; -} - -Deno.test("cors/tests/mod_test.ts", async (t) => { - // Also covers unit tests - await t.step("Integration", async (t) => { - await t.step("Should shortcircuit preflight requests", async () => { - const server = runServer(); - const response = await fetch("http://localhost:1447/cors", { - method: "OPTIONS", - headers: { - "Origin": "localhost", - "Access-Control-Request-Method": "GET", - Accept: "text/plain", - }, - }); - assertEquals( - response.status, - 204, - ); - assertEquals( - response.headers.get("access-control-allow-origin"), - "*", - ); - assertEquals( - response.headers.get("access-control-allow-methods"), - "GET,HEAD,PUT,PATCH,POST,DELETE", - ); - assertEquals( - response.headers.get("vary"), - "Accept-Encoding, origin", - ); - assertEquals(response.headers.get("content-length"), null); - await server.close(); - }); - await t.step("Should always set the vary header", async () => { - const server = runServer(); - const response = await fetch("http://localhost:1447/cors", { - method: "OPTIONS", - headers: { - "Origin": "localhost", - "Access-Control-Request-Method": "GET", - Accept: "text/plain", - }, - }); - await server.close(); - assertEquals( - response.headers.get("vary"), - "Accept-Encoding, origin", - ); - }); - await t.step( - "Only sets the vary header if Origin header is not set", - async () => { - const server = runServer(); - const response = await fetch("http://localhost:1447/cors", { - method: "OPTIONS", - headers: { - "Access-Control-Request-Method": "GET", - Accept: "text/plain", - }, - }); - await response.arrayBuffer(); - await server.close(); - assertEquals( - response.headers.get("vary"), - "Accept-Encoding, origin", - ); - assertEquals( - response.headers.get("access-control-allow-origin"), - null, - ); - assertEquals( - response.headers.get("access-control-allow-methods"), - null, - ); - }, - ); - await t.step( - "Should not allow request when origins do not match", - async () => { - const server = runServer(false); - const response = await fetch("http://localhost:1447/cors", { - method: "OPTIONS", - headers: { - "Origin": "the big bang", - "Access-Control-Request-Method": "GET", - Accept: "text/plain", - }, - }); - await response.arrayBuffer(); - await server.close(); - assertEquals( - response.headers.get("vary"), - "Accept-Encoding, origin", - ); - assertEquals( - response.headers.get("access-control-allow-origin"), - null, - ); - }, - ); - await t.step( - "Sets Allow Headers header when Request Header header is set", - async () => { - const server = runServer(false); - const response = await fetch("http://localhost:1447/cors", { - method: "OPTIONS", - headers: { - "Origin": "localhost", - "Access-Control-Request-Method": "GET", - "Access-Control-Request-Headers": "hello world", - Accept: "text/plain", - }, - }); - await response.arrayBuffer(); - await server.close(); - assertEquals( - response.headers.get("access-control-allow-headers"), - "hello world", - ); - }, - ); - await t.step( - "Realworld example - CORS not enabled for request", - async () => { - // Failed request - access control header is not present - const server = runServer(false); - const res = await fetch("http://localhost:1447", { - method: "GET", - headers: { - Origin: "https://google.com", - Accept: "text/plain", - }, - }); - await res.text(); - await server.close(); - assertEquals( - res.headers.get("access-control-allow-origin"), - null, - ); - }, - ); - await t.step( - "Realworld example - CORS enabled for a single origin", - async () => { - // Successful request - access control header is present and the value of the origin - const server = runServer(false); - const res = await fetch("http://localhost:1447/cors", { - method: "GET", - headers: { - Origin: "localhost", - Accept: "text/plain", // As server two is setting cors origin as localhost - }, - }); - await res.text(); - await server.close(); - assertEquals( - res.headers.get("access-control-allow-origin"), - "localhost", - ); - }, - ); - await t.step( - "Realworld example - CORS enabled for every origin", - async () => { - // Another successful request, but the origin allows anything - const server = runServer(true); - const res = await fetch("http://localhost:1447/cors", { - method: "GET", - headers: { - Origin: "https://anything.com", - Accept: "text/plain", - }, - }); - await res.text(); - await server.close(); - assertEquals( - res.headers.get("access-control-allow-origin"), - "*", - ); - }, - ); - }); -}); diff --git a/tests/unit/services/csrf_test.ts b/tests/unit/services/csrf_test.ts deleted file mode 100644 index c01e2dd9c..000000000 --- a/tests/unit/services/csrf_test.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { CSRFService } from "../../../src/services/csrf/csrf.ts"; -import { assertEquals } from "../../deps.ts"; -import { - IResource, - Request, - Resource, - Response, - Server, -} from "../../../mod.ts"; - -const csrfWithoutCookie = new CSRFService(); -const csrfWithCookie = new CSRFService({ cookie: true }); - -/** - * This resource resembles the following: - * 1. On any route other than login/register/etc, supply the csrf token on GET requests - * 2. On requests MADE to the server, check the token was passed in - */ -class ResourceNoCookie extends Resource implements IResource { - paths = ["/"]; - - public services = { - "POST": [csrfWithoutCookie], - }; - - public GET(_request: Request, response: Response) { - // Give token to the 'view' - response.headers.set("X-CSRF-TOKEN", csrfWithoutCookie.token); - response.text(csrfWithoutCookie.token); - } - - public POST(_request: Request, response: Response) { - // request should have token - response.text("Success; " + csrfWithoutCookie.token); - } -} - -class ResourceWithCookie extends Resource { - paths = ["/cookie"]; - - public services = { - "POST": [csrfWithCookie], - }; - - public GET(_request: Request, response: Response) { - // Give token to the 'view' - response.setCookie({ - name: "X-CSRF-TOKEN", - value: csrfWithCookie.token, - }); - response.text(csrfWithCookie.token); - } - - public POST(_request: Request, response: Response) { - // request should have token - response.text("Success; " + csrfWithCookie.token); - } -} - -const server = new Server({ - resources: [ResourceNoCookie, ResourceWithCookie], - protocol: "http", - port: 1337, - hostname: "localhost", -}); - -Deno.test("CSRF - mod_test.ts", async (t) => { - await t.step("csrf", async (t) => { - await t.step("`csrf.token` Should return a valid token", () => { - assertEquals( - csrfWithoutCookie.token.match("[a-zA-Z0-9]{43}") !== null, - true, - ); - }); - await t.step( - "Token should be the same for different requests", - async () => { - server.run(); - const firstRes = await fetch("http://localhost:1337", { - headers: { - Accept: "text/plain", - }, - }); - await firstRes.arrayBuffer(); - assertEquals( - firstRes.headers.get("X-CSRF-TOKEN") === csrfWithoutCookie.token, - true, - ); - const secondRes = await fetch("http://localhost:1337", { - headers: { - Accept: "text/plain", - }, - }); - await secondRes.arrayBuffer(); - assertEquals( - secondRes.headers.get("X-CSRF-TOKEN") === csrfWithoutCookie.token, - true, - ); - await server.close(); - }, - ); - await t.step("Token can be used for other requests", async () => { // eg get it from a route, and use it in the view for sending other requests - server.run(); - const firstRes = await fetch("http://localhost:1337", { - headers: { - Accept: "text/plain", - }, - }); - const token = firstRes.headers.get("X-CSRF-TOKEN"); - await firstRes.arrayBuffer(); - const secondRes = await fetch("http://localhost:1337", { - method: "POST", - headers: { - "X-CSRF-TOKEN": token || "", // bypass annoying tsc warnings - Accept: "text/plain", - }, - }); - assertEquals( - await secondRes.text(), - "Success; " + token, - ); - assertEquals(secondRes.status, 200); - await server.close(); - }); - await t.step( - "Route with CSRF should throw a 400 when no token", - async () => { - server.run(); - const res = await fetch("http://localhost:1337", { - method: "POST", - headers: { - Accept: "text/plain", - }, - }); - assertEquals(res.status, 400); - assertEquals( - (await res.text()).startsWith("Error: No CSRF token was passed in"), - true, - ); - await server.close(); - }, - ); - await t.step( - "Route with CSRF should throw 403 for an invalid token", - async () => { - server.run(); - const res = await fetch("http://localhost:1337", { - method: "POST", - headers: { - "X-CSRF-TOKEN": csrfWithoutCookie.token.substr(1), - Accept: "text/plain", - }, - }); - assertEquals(res.status, 403); - assertEquals( - (await res.text()).startsWith( - "Error: The CSRF tokens do not match", - ), - true, - ); - await server.close(); - }, - ); - // This test asserts that the token is consistent when passed about, and will not change - await t.step( - "Route should respond with success when passing in token", - async () => { - server.run(); - const res = await fetch("http://localhost:1337", { - method: "POST", - headers: { - "X-CSRF-TOKEN": csrfWithoutCookie.token, - Accept: "text/plain", - }, - }); - assertEquals(res.status, 200); - assertEquals( - await res.text(), - "Success; " + csrfWithoutCookie.token, - ); - await server.close(); - }, - ); - await t.step("Should allow to set the token as a cookie", async () => { - server.run(); - const res = await fetch("http://localhost:1337/cookie", { - headers: { - Accept: "text/plain", - }, - }); - await res.text(); - const headers = res.headers; - const token = headers.get("set-cookie")!.split("=")[1]; - assertEquals(token, csrfWithCookie.token); - await server.close(); - }); - }); -}); diff --git a/tests/unit/services/dexter_test.ts b/tests/unit/services/dexter_test.ts deleted file mode 100644 index 840f1f609..000000000 --- a/tests/unit/services/dexter_test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { assertEquals } from "../../deps.ts"; -import { DexterService } from "../../../src/services/dexter/dexter.ts"; - -Deno.test("Dexter - mod_test.ts", async (t) => { - await t.step("Dexter", async (t) => { - await t.step("is configurable", () => { - let dexter = new DexterService(); - assertEquals(dexter.configs.enabled, true); - dexter = new DexterService({ - enabled: false, - }); - assertEquals(dexter.configs.enabled, false); - }); - await t.step( - "logger and all of its log functions are exposed", - () => { - const dexter = new DexterService({ - enabled: true, - }); - assertEquals(typeof dexter.logger.debug, "function"); - assertEquals(typeof dexter.logger.error, "function"); - assertEquals(typeof dexter.logger.fatal, "function"); - assertEquals(typeof dexter.logger.info, "function"); - assertEquals(typeof dexter.logger.trace, "function"); - assertEquals(typeof dexter.logger.warn, "function"); - }, - ); - await t.step("logger can be used to write messages", () => { - const dexter = new DexterService({ - enabled: true, - }); - let actual; - actual = dexter.logger.debug("test") as string; - assertEquals( - actual.match( - /.*\[DEBUG\].*\s\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \| test/, - )?.length, - 1, - ); - actual = dexter.logger.error("test") as string; - assertEquals( - actual.match( - /.*\[ERROR\].*\s\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \| test/, - )?.length, - 1, - ); - actual = dexter.logger.fatal("test") as string; - assertEquals( - actual.match( - /.*\[FATAL\].*\s\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \| test/, - )?.length, - 1, - ); - actual = dexter.logger.info("test") as string; - assertEquals( - actual.match( - /.*\[INFO\].*\s\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \| test/, - )?.length, - 1, - ); - actual = dexter.logger.trace("test") as string; - assertEquals( - actual.match( - /.*\[TRACE\].*\s\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \| test/, - )?.length, - 1, - ); - actual = dexter.logger.warn("test") as string; - assertEquals( - actual.match( - /.*\[WARN\].*\s\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \| test/, - )?.length, - 1, - ); - }); - }); -}); diff --git a/tests/unit/services/jae_test.ts b/tests/unit/services/jae_test.ts deleted file mode 100644 index 01b8b8072..000000000 --- a/tests/unit/services/jae_test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Jae } from "../../../src/services/tengine/jae.ts"; - -Deno.test("render()", async (t) => { - await t.step("Should handle no trailing or leading slashes", () => { - const jae = new Jae("./tests/data"); - jae.render("index.html", {}); - }); - await t.step("Should handle both trailing and leading slashes", () => { - const jae = new Jae("./tests/data/"); - jae.render("/index.html", {}); - }); - await t.step("Should handle trailing or leading slashes", () => { - const jae = new Jae("./tests/data"); - jae.render("/index.html", {}); - const jae2 = new Jae("./tests/data/"); - jae2.render("index.html", {}); - }); -}); diff --git a/tests/unit/services/paladin_test.ts b/tests/unit/services/paladin_test.ts deleted file mode 100644 index 9d7392096..000000000 --- a/tests/unit/services/paladin_test.ts +++ /dev/null @@ -1,513 +0,0 @@ -import { assertEquals } from "../../deps.ts"; -import { PaladinService } from "../../../src/services/paladin/paladin.ts"; -import { - IResource, - Request, - Resource, - Response, - Server, -} from "../../../mod.ts"; - -class Res extends Resource implements IResource { - paths = ["/"]; - public GET(_request: Request, response: Response) { - response.text("Hello world!"); - } -} - -function runServer( - paladin: PaladinService, // eg `const paladin = new new PaladinService()`, imported from paladin/mod.ts - port: number, -): Server { - const server = new Server({ - resources: [Res], - services: [paladin], - protocol: "http", - hostname: "localhost", - port, - }); - server.run(); - return server; -} - -Deno.test("Paladin - mod_test.ts", async (t) => { - await t.step("X-XSS-Protection Header", async (t) => { - await t.step("Sets the header by Default", async () => { - const paladin = new PaladinService(); - const server = runServer(paladin, 1667); - const res = await fetch("http://localhost:1667/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("X-XSS-Protection"); - assertEquals(header, "1; mode=block"); - await server.close(); - }); - await t.step("Sets the header when config is true", async () => { - const paladin = new PaladinService({ - "X-XSS-Protection": true, - }); - const server = runServer(paladin, 1668); - const res = await fetch("http://localhost:1668/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("X-XSS-Protection"); - assertEquals(header, "1; mode=block"); - await server.close(); - }); - await t.step("Does not set the header when config is false", async () => { - const paladin = new PaladinService({ - "X-XSS-Protection": false, - }); - const server = runServer(paladin, 1669); - const res = await fetch("http://localhost:1669/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("X-XSS-Protection"); - assertEquals(header, null); - await server.close(); - }); - }); - await t.step("Referrer-Policy header", async (t) => { - await t.step("Does not set the header by default", async () => { - const paladin = new PaladinService(); - const server = runServer(paladin, 1670); - const res = await fetch("http://localhost:1670/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("Referrer-Policy"); - assertEquals(header, null); - await server.close(); - }); - await t.step("Sets the header when passed in", async () => { - const paladin = new PaladinService({ - "Referrer-Policy": "origin", - }); - const server = runServer(paladin, 1670); - const res = await fetch("http://localhost:1670/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("Referrer-Policy"); - assertEquals(header, "origin"); - await server.close(); - }); - }); - await t.step("X-Content-Type-Options header", async (t) => { - await t.step("Sets the header by Default", async () => { - const paladin = new PaladinService(); - const server = runServer(paladin, 1650); - const res = await fetch("http://localhost:1650/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("X-Content-Type-Options"); - assertEquals(header, "nosniff"); - await server.close(); - }); - await t.step("Sets the header when config is true", async () => { - const paladin = new PaladinService({ - "X-Content-Type-Options": true, - }); - const server = runServer(paladin, 1651); - const res = await fetch("http://localhost:1651/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("X-Content-Type-Options"); - assertEquals(header, "nosniff"); - await server.close(); - }); - await t.step("Does not set the header when config is false", async () => { - const paladin = new PaladinService({ - "X-Content-Type-Options": false, - }); - const server = runServer(paladin, 1652); - const res = await fetch("http://localhost:1652/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("X-Content-Type-Options"); - assertEquals(header, null); - await server.close(); - }); - }); - await t.step("Strict-Transport-Security header", async (t) => { - await t.step("Is set by default", async () => { - const paladin = new PaladinService(); - const server = runServer(paladin, 1671); - const res = await fetch("http://localhost:1671/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("Strict-Transport-Security"); - assertEquals(header, "max-age=5184000; include_sub_domains"); - await server.close(); - }); - await t.step("Is set when max_age is set", async () => { - const paladin = new PaladinService({ - hsts: { - max_age: 101, - }, - }); - const server = runServer(paladin, 1671); - const res = await fetch("http://localhost:1671/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("Strict-Transport-Security"); - assertEquals(header, "max-age=101; include_sub_domains"); - await server.close(); - }); - await t.step("Not set when max_age is false", async () => { - const paladin = new PaladinService({ - hsts: { - max_age: false, - }, - }); - const server = runServer(paladin, 1671); - const res = await fetch("http://localhost:1671/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("Strict-Transport-Security"); - assertEquals(header, null); - await server.close(); - }); - await t.step("Set header but disable include_sub_domains", async () => { - const paladin = new PaladinService({ - hsts: { - include_sub_domains: false, - }, - }); - const server = runServer(paladin, 1672); - const res = await fetch("http://localhost:1672/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("Strict-Transport-Security"); - assertEquals(header, "max-age=5184000"); - await server.close(); - }); - await t.step("Set and explicitly enable include_sub_domains", async () => { - const paladin = new PaladinService({ - hsts: { - include_sub_domains: true, - }, - }); - const server = runServer(paladin, 1673); - const res = await fetch("http://localhost:1673/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("Strict-Transport-Security"); - assertEquals(header, "max-age=5184000; include_sub_domains"); - await server.close(); - }); - await t.step( - "Set header and explicitly set preload to false", - async () => { - const paladin = new PaladinService({ - hsts: { - preload: false, - }, - }); - const server = runServer(paladin, 1674); - const res = await fetch("http://localhost:1674/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("Strict-Transport-Security"); - assertEquals( - header, - "max-age=5184000; include_sub_domains", - ); - await server.close(); - }, - ); - await t.step("Set header and set preload", async () => { - const paladin = new PaladinService({ - hsts: { - preload: true, - }, - }); - const server = runServer(paladin, 1675); - const res = await fetch("http://localhost:1675/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("Strict-Transport-Security"); - assertEquals( - header, - "max-age=5184000; include_sub_domains; preload", - ); - await server.close(); - }); - }); - await t.step("X-Powered-By header", async (t) => { - await t.step("Header removed by default", async () => { - const paladin = new PaladinService(); - const server = runServer(paladin, 1675); - const res = await fetch("http://localhost:1675/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("X-Powered-By"); - assertEquals(header, null); - await server.close(); - }); - await t.step("Header removed when explicitly asked to", async () => { - const paladin = new PaladinService({ - "X-Powered-By": false, - }); - const server = runServer(paladin, 1675); - const res = await fetch("http://localhost:1675/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("X-Powered-By"); - assertEquals(header, null); - await server.close(); - }); - await t.step("Header can be modified", async () => { - const paladin = new PaladinService({ - "X-Powered-By": "You will never know, mwuahaha", - }); - const server = runServer(paladin, 1676); - const res = await fetch("http://localhost:1676/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("X-Powered-By"); - assertEquals(header, "You will never know, mwuahaha"); - await server.close(); - }); - }); - await t.step("X-Frame-Options header", async (t) => { - await t.step("Sets the header by default", async () => { - const paladin = new PaladinService(); - const server = runServer(paladin, 1677); - const res = await fetch("http://localhost:1677/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("X-Frame-Options"); - assertEquals(header, "SAMEORIGIN"); - await server.close(); - }); - await t.step("Will not set the header if config is false", async () => { - const paladin = new PaladinService({ - "X-Frame-Options": false, - }); - const server = runServer(paladin, 1678); - const res = await fetch("http://localhost:1678/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("X-Frame-Options"); - assertEquals(header, null); - await server.close(); - }); - await t.step("Sets the head when explicitly done so", async () => { - const paladin = new PaladinService({ - "X-Frame-Options": "DENY", - }); - const server = runServer(paladin, 1679); - const res = await fetch("http://localhost:1679/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("X-Frame-Options"); - assertEquals(header, "DENY"); - await server.close(); - }); - }); - await t.step("Expect-CT header", async (t) => { - await t.step("Does not set the header by default", async () => { - const paladin = new PaladinService(); - const server = runServer(paladin, 1680); - const res = await fetch("http://localhost:1680/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("Expect-CT"); - assertEquals(header, null); - await server.close(); - }); - await t.step("Set the header and set the max age", async () => { - const paladin = new PaladinService({ - expect_ct: { - max_age: 30, - }, - }); - const server = runServer(paladin, 1681); - const res = await fetch("http://localhost:1681/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("Expect-CT"); - assertEquals(header, "max-age=30"); - await server.close(); - }); - await t.step("Set the header and set enforce", async () => { - const paladin = new PaladinService({ - expect_ct: { - max_age: 30, - enforce: true, - }, - }); - const server = runServer(paladin, 1682); - const res = await fetch("http://localhost:1682/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("Expect-CT"); - assertEquals(header, "max-age=30; enforce"); - await server.close(); - }); - await t.step("set the header and set report_uri", async () => { - const paladin = new PaladinService({ - expect_ct: { - max_age: 30, - report_uri: "hello", - }, - }); - const server = runServer(paladin, 1683); - const res = await fetch("http://localhost:1683/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("Expect-CT"); - assertEquals(header, "max-age=30; hello"); - await server.close(); - }); - }); - await t.step("X-DNS-Prefetch-Control header", async (t) => { - await t.step("Is set by default", async () => { - const paladin = new PaladinService(); - const server = runServer(paladin, 1684); - const res = await fetch("http://localhost:1684/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("X-DNS-Prefetch-Control"); - assertEquals(header, "off"); - await server.close(); - }); - await t.step("Can explicitly be set to off", async () => { - const paladin = new PaladinService({ - "X-DNS-Prefetch-Control": false, - }); - const server = runServer(paladin, 1685); - const res = await fetch("http://localhost:1685/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("X-DNS-Prefetch-Control"); - assertEquals(header, "off"); - await server.close(); - }); - await t.step("Can be set to on", async () => { - const paladin = new PaladinService({ - "X-DNS-Prefetch-Control": true, - }); - const server = runServer(paladin, 1686); - const res = await fetch("http://localhost:1686/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("X-DNS-Prefetch-Control"); - assertEquals(header, "on"); - await server.close(); - }); - }); - await t.step("Content-Security-Policy header", async (t) => { - await t.step("Is not set by default", async () => { - const paladin = new PaladinService(); - const server = runServer(paladin, 1687); - const res = await fetch("http://localhost:1687/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("Content-Security-Policy"); - assertEquals(header, null); - await server.close(); - }); - await t.step("Can be set if config is set", async () => { - const paladin = new PaladinService({ - "Content-Security-Policy": "Something something", - }); - const server = runServer(paladin, 1688); - const res = await fetch("http://localhost:1688/", { - headers: { - Accept: "text/plain", - }, - }); - await res.arrayBuffer(); - const header = res.headers.get("Content-Security-Policy"); - assertEquals(header, "Something something"); - await server.close(); - }); - }); -}); diff --git a/tests/unit/standard/handlers/RequestValidator_test.ts b/tests/unit/standard/handlers/RequestValidator_test.ts new file mode 100644 index 000000000..68367ea85 --- /dev/null +++ b/tests/unit/standard/handlers/RequestValidator_test.ts @@ -0,0 +1,114 @@ +/** + * Drash - A microframework for building JavaScript/TypeScript HTTP systems. + * Copyright (C) 2023 Drash authors. The Drash authors are listed in the + * AUTHORS file at . This notice + * applies to Drash version 3.X.X and any later version. + * + * This file is part of Drash. See . + * + * Drash is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Drash is distributed 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. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Drash. If not, see . + */ + +import { asserts } from "../../../deps.ts"; +import { RequestValidator } from "../../../../src/standard/handlers/RequestValidator.ts"; + +const testCasesThrow = [ + { + input: null, + expected: "Request could not be read", + }, + { + input: undefined, + expected: "Request could not be read", + }, + { + input: {}, + expected: "Request HTTP method could not be read", + }, + { + input: { url: "yep" }, + expected: "Request HTTP method could not be read", + }, + { + input: { method: "yep" }, + expected: "Request URL could not be read", + }, + { + input: { url: true }, + expected: "Request HTTP method could not be read", + }, + { + input: { method: false }, + expected: "Request HTTP method could not be read", + }, + { + input: { url: true, method: true }, + expected: "Request HTTP method could not be read", + }, + { + input: { hello: "yep" }, + expected: "Request HTTP method could not be read", + }, + { + input: true, + expected: "Request could not be read", + }, + { + input: false, + expected: "Request could not be read", + }, +]; + +Deno.test("RequestValidator", async (t) => { + for await (const request of testCasesThrow) { + const testName = JSON.stringify(request); + + await t.step(`throws if request is \`${testName}\``, async () => { + const requestValidator = new RequestValidator(); + try { + // @ts-ignore: Igorning because we want to test passing in incorrect + // values + await requestValidator.handle(request.input); + } catch (e) { + asserts.assertEquals(e.message, request.expected); + } + }); + } + + await t.step("throws if request is not provided", async () => { + const requestValidator = new RequestValidator(); + try { + // @ts-ignore: Ignorning because we want to test not passing in an arg for + // cases where TypeScript is not being used + await requestValidator.handle(); + } catch (e) { + asserts.assertEquals(e.message, "Request could not be read"); + } + }); + + await t.step( + "does not throw if the object is `{ url: string; method: string }`", + () => { + const requestValidator = new RequestValidator(); + try { + requestValidator.handle({ url: "", method: "" }); + asserts.assert(true); // Asserting just so we assert something in this test + } catch (e) { + throw new Error( + "Request object is valid, but the test failed. Error message: " + + e.messages, + ); + } + }, + ); +}); diff --git a/tsconfig.build.base.json b/tsconfig.build.base.json new file mode 100644 index 000000000..5cc7aa0dd --- /dev/null +++ b/tsconfig.build.base.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "declaration": true, + "esModuleInterop": true, + "lib": ["dom", "dom.iterable", "es2015"], + "module": "es2015", + "moduleResolution": "node", + "target": "es2015" + }, + "exclude": ["node_modules", "_unstable"] +} diff --git a/tsconfig.build.cjs.json b/tsconfig.build.cjs.json new file mode 100644 index 000000000..5d8c73de9 --- /dev/null +++ b/tsconfig.build.cjs.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.build.base.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "./.drashland/lib/cjs", + "rootDir": "./.drashland/lib/intermediary" + }, + "include": ["./.drashland/lib/intermediary"] +} diff --git a/tsconfig.build.esm.json b/tsconfig.build.esm.json new file mode 100644 index 000000000..ce81057a2 --- /dev/null +++ b/tsconfig.build.esm.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.build.base.json", + "compilerOptions": { + "outDir": "./.drashland/lib/esm", + "rootDir": "./.drashland/lib/intermediary" + }, + "include": ["./.drashland/lib/intermediary"] +}