diff --git a/.eslintrc.js b/.eslintrc.js index ed1509d3f..34e935ece 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -44,6 +44,7 @@ module.exports = { // Problems with optional parameters '@typescript-eslint/no-unnecessary-condition': 'off', '@typescript-eslint/prefer-optional-chain': 'error', + '@typescript-eslint/promise-function-async': [ 'error', { checkArrowFunctions: false } ], '@typescript-eslint/space-before-function-paren': [ 'error', 'never' ], '@typescript-eslint/unbound-method': 'off', '@typescript-eslint/unified-signatures': 'off', diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 000000000..34415882a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,16 @@ +--- +name: "\U0001F41B Bug report" +about: If something is not working as expected or crashes +title: '' +labels: '' +assignees: '' + +--- + +#### Environment +- Server version: *Output of `community-solid-server --version` for a global or `npx community-solid-server --version` for a local installation* +- Node.js version: *Output of `node -v`* +- npm version: *Output of `npm -v`* + +#### Description + diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 000000000..fca4c3fec --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,13 @@ +--- +name: "➕ Feature request" +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +#### Feature description: + + + diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 000000000..7add40a8a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,13 @@ +--- +name: "❓ Question" +about: A general question +title: '' +labels: '' +assignees: '' + +--- + +#### Question + + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..7ef89572c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ + + +#### Related issues + + +#### Description + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e7bae486..921d7ac1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,6 @@ jobs: - '14.x' - '16.0' - '16.x' - - '17.0' - '17.x' timeout-minutes: 10 steps: @@ -74,11 +73,9 @@ jobs: run: npm run test:deploy test-integration: - runs-on: ${{ matrix.operating-system }} + runs-on: ubuntu-latest strategy: matrix: - operating-system: - - ubuntu-latest node-version: - '12.x' - '14.x' @@ -109,6 +106,29 @@ jobs: - name: Run integration tests run: npm run test:integration + test-integration-windows: + runs-on: windows-latest + strategy: + matrix: + node-version: + - '12.x' + - '14.x' + - '16.x' + timeout-minutes: 10 + steps: + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + - name: Ensure line endings are consistent + run: git config --global core.autocrlf input + - name: Check out repository + uses: actions/checkout@v2 + - name: Install dependencies and run build scripts + run: npm ci + - name: Run integration tests + run: npm run test:integration + coveralls: needs: test-unit runs-on: ubuntu-latest diff --git a/.github/workflows/schedule.yml b/.github/workflows/schedule.yml index b61d7a04b..42060327e 100644 --- a/.github/workflows/schedule.yml +++ b/.github/workflows/schedule.yml @@ -14,7 +14,7 @@ jobs: matrix: branch: - 'main' - - 'versions/2.1.0' + - 'versions/3.0.0' timeout-minutes: 10 steps: - name: Use Node.js 16.x diff --git a/package-lock.json b/package-lock.json index 307c415cc..14c2c49a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "@types/sparqljs": "^3.1.2", "@types/url-join": "^4.0.0", "@types/uuid": "^8.3.0", - "@types/ws": "^7.4.5", + "@types/ws": "^8.2.0", "@types/yargs": "^17.0.0", "arrayify-stream": "^1.0.0", "async-lock": "^1.3.0", @@ -56,13 +56,13 @@ "rdf-serialize": "^1.1.0", "redis": "^3.1.2", "redlock": "^4.2.0", - "sparqlalgebrajs": "^3.0.0", + "sparqlalgebrajs": "^4.0.1", "sparqljs": "^3.4.2", "url-join": "^4.0.1", "uuid": "^8.3.2", "winston": "^3.3.3", "winston-transport": "^4.4.0", - "ws": "^8.0.0", + "ws": "^8.2.3", "yargs": "^17.0.1" }, "bin": { @@ -3780,6 +3780,24 @@ "sparqlalgebrajs": "^3.0.1" } }, + "node_modules/@comunica/types/node_modules/sparqlalgebrajs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparqlalgebrajs/-/sparqlalgebrajs-3.0.3.tgz", + "integrity": "sha512-XFNhsO55bprayrM35h/jY0kzzuGc3oZ1On3kc+s7Un0BFQBXa046aLcMZFp4MYSvn7GtMe9eZ08ONFnBH5kEsQ==", + "dependencies": { + "@rdfjs/types": "*", + "@types/sparqljs": "^3.1.2", + "fast-deep-equal": "^3.1.3", + "minimist": "^1.2.5", + "rdf-data-factory": "^1.1.0", + "rdf-isomorphic": "^1.3.0", + "rdf-string": "^1.6.0", + "sparqljs": "^3.4.2" + }, + "bin": { + "sparqlalgebrajs": "bin/sparqlalgebrajs.js" + } + }, "node_modules/@comunica/utils-datasource": { "version": "1.21.1", "resolved": "https://registry.npmjs.org/@comunica/utils-datasource/-/utils-datasource-1.21.1.tgz", @@ -4886,9 +4904,9 @@ "integrity": "sha512-82E/lVRaqelV9qmRzzJ1PKTpyrpnT7mwdneKNJB9hUtypZDMggloDfFUCIqRRx3lYRxteCwXSq9c+W71Vf0QnQ==" }, "node_modules/@types/sparqljs": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@types/sparqljs/-/sparqljs-3.1.2.tgz", - "integrity": "sha512-tLfrnBuK37P2Bn8Fo7Qik95sBXYHw5D+gq3MMq1HVyoTpCWivwPnP0Mmd7Apamdc9eH3mLJwIZIETHCQ6HxMUw==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/sparqljs/-/sparqljs-3.1.3.tgz", + "integrity": "sha512-nmFgmR6ns4i8sg9fYu+293H+PMLKmDOZy34sgwgAeUEEiIqSs4guj5aCZRt3gq1g0yuKXkqrxLDq/684g7pGtQ==", "dependencies": { "rdf-js": "^4.0.2" } @@ -4934,9 +4952,9 @@ "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==" }, "node_modules/@types/ws": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.5.tgz", - "integrity": "sha512-8mbDgtc8xpxDDem5Gwj76stBDJX35KQ3YBoayxlqUQcL5BZUthiqP/VQ4PQnLHqM4PmlbyO74t98eJpURO+gPA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.0.tgz", + "integrity": "sha512-cyeefcUCgJlEk+hk2h3N+MqKKsPViQgF5boi9TTHSK+PoR9KWBb/C5ccPcDyAqgsbAYHTwulch725DV84+pSpg==", "dependencies": { "@types/node": "*" } @@ -14164,18 +14182,18 @@ "integrity": "sha512-0tF3AGSD1ppQeuffsLDIOWlKUd3lS92tFxcsrh5Pe3ZphhnoK+oXIBTzOAThZCiuINZLvpiLH/1VS1/ANEJVig==" }, "node_modules/sparqlalgebrajs": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparqlalgebrajs/-/sparqlalgebrajs-3.0.3.tgz", - "integrity": "sha512-XFNhsO55bprayrM35h/jY0kzzuGc3oZ1On3kc+s7Un0BFQBXa046aLcMZFp4MYSvn7GtMe9eZ08ONFnBH5kEsQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/sparqlalgebrajs/-/sparqlalgebrajs-4.0.1.tgz", + "integrity": "sha512-/YOn4+5JEawI29h9eFYb9rScCtiLIK4TG+MrXpbJlHDjL8W34+V/RQoXqeau4SFXtA60Vzc7LRokefguh+I37Q==", "dependencies": { "@rdfjs/types": "*", - "@types/sparqljs": "^3.1.2", + "@types/sparqljs": "^3.1.3", "fast-deep-equal": "^3.1.3", "minimist": "^1.2.5", "rdf-data-factory": "^1.1.0", "rdf-isomorphic": "^1.3.0", "rdf-string": "^1.6.0", - "sparqljs": "^3.4.2" + "sparqljs": "^3.5.1" }, "bin": { "sparqlalgebrajs": "bin/sparqlalgebrajs.js" @@ -14220,9 +14238,9 @@ } }, "node_modules/sparqljs": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/sparqljs/-/sparqljs-3.4.2.tgz", - "integrity": "sha512-MmmZ6cMuvhf4Eh2FXX21dalgADUiZ9WN8XKMedwhTFg0r7W09/o8wvoZ8C4yA6FptnjjAjm+mGnxAEpkSRY3QQ==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/sparqljs/-/sparqljs-3.5.1.tgz", + "integrity": "sha512-sHc6z7hNF3ACvXurKe8hT1sD52Fc0fN3uPLS6SQnXRV9CJl33GNAS4w5Dd3X3GgykUt9SlnjhI1QRKhLzun4qQ==", "dependencies": { "rdf-data-factory": "^1.0.4" }, @@ -15577,9 +15595,9 @@ } }, "node_modules/ws": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.0.0.tgz", - "integrity": "sha512-6AcSIXpBlS0QvCVKk+3cWnWElLsA6SzC0lkQ43ciEglgXJXiCWK3/CGFEJ+Ybgp006CMibamAsqOlxE9s4AvYA==", + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", "engines": { "node": ">=10.0.0" }, @@ -18574,6 +18592,23 @@ "asynciterator": "^3.2.0", "immutable": "^3.8.2", "sparqlalgebrajs": "^3.0.1" + }, + "dependencies": { + "sparqlalgebrajs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparqlalgebrajs/-/sparqlalgebrajs-3.0.3.tgz", + "integrity": "sha512-XFNhsO55bprayrM35h/jY0kzzuGc3oZ1On3kc+s7Un0BFQBXa046aLcMZFp4MYSvn7GtMe9eZ08ONFnBH5kEsQ==", + "requires": { + "@rdfjs/types": "*", + "@types/sparqljs": "^3.1.2", + "fast-deep-equal": "^3.1.3", + "minimist": "^1.2.5", + "rdf-data-factory": "^1.1.0", + "rdf-isomorphic": "^1.3.0", + "rdf-string": "^1.6.0", + "sparqljs": "^3.4.2" + } + } } }, "@comunica/utils-datasource": { @@ -19578,9 +19613,9 @@ "integrity": "sha512-82E/lVRaqelV9qmRzzJ1PKTpyrpnT7mwdneKNJB9hUtypZDMggloDfFUCIqRRx3lYRxteCwXSq9c+W71Vf0QnQ==" }, "@types/sparqljs": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@types/sparqljs/-/sparqljs-3.1.2.tgz", - "integrity": "sha512-tLfrnBuK37P2Bn8Fo7Qik95sBXYHw5D+gq3MMq1HVyoTpCWivwPnP0Mmd7Apamdc9eH3mLJwIZIETHCQ6HxMUw==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/sparqljs/-/sparqljs-3.1.3.tgz", + "integrity": "sha512-nmFgmR6ns4i8sg9fYu+293H+PMLKmDOZy34sgwgAeUEEiIqSs4guj5aCZRt3gq1g0yuKXkqrxLDq/684g7pGtQ==", "requires": { "rdf-js": "^4.0.2" } @@ -19626,9 +19661,9 @@ "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==" }, "@types/ws": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.5.tgz", - "integrity": "sha512-8mbDgtc8xpxDDem5Gwj76stBDJX35KQ3YBoayxlqUQcL5BZUthiqP/VQ4PQnLHqM4PmlbyO74t98eJpURO+gPA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.0.tgz", + "integrity": "sha512-cyeefcUCgJlEk+hk2h3N+MqKKsPViQgF5boi9TTHSK+PoR9KWBb/C5ccPcDyAqgsbAYHTwulch725DV84+pSpg==", "requires": { "@types/node": "*" } @@ -26661,18 +26696,18 @@ "integrity": "sha512-0tF3AGSD1ppQeuffsLDIOWlKUd3lS92tFxcsrh5Pe3ZphhnoK+oXIBTzOAThZCiuINZLvpiLH/1VS1/ANEJVig==" }, "sparqlalgebrajs": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparqlalgebrajs/-/sparqlalgebrajs-3.0.3.tgz", - "integrity": "sha512-XFNhsO55bprayrM35h/jY0kzzuGc3oZ1On3kc+s7Un0BFQBXa046aLcMZFp4MYSvn7GtMe9eZ08ONFnBH5kEsQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/sparqlalgebrajs/-/sparqlalgebrajs-4.0.1.tgz", + "integrity": "sha512-/YOn4+5JEawI29h9eFYb9rScCtiLIK4TG+MrXpbJlHDjL8W34+V/RQoXqeau4SFXtA60Vzc7LRokefguh+I37Q==", "requires": { "@rdfjs/types": "*", - "@types/sparqljs": "^3.1.2", + "@types/sparqljs": "^3.1.3", "fast-deep-equal": "^3.1.3", "minimist": "^1.2.5", "rdf-data-factory": "^1.1.0", "rdf-isomorphic": "^1.3.0", "rdf-string": "^1.6.0", - "sparqljs": "^3.4.2" + "sparqljs": "^3.5.1" } }, "sparqlee": { @@ -26710,9 +26745,9 @@ } }, "sparqljs": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/sparqljs/-/sparqljs-3.4.2.tgz", - "integrity": "sha512-MmmZ6cMuvhf4Eh2FXX21dalgADUiZ9WN8XKMedwhTFg0r7W09/o8wvoZ8C4yA6FptnjjAjm+mGnxAEpkSRY3QQ==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/sparqljs/-/sparqljs-3.5.1.tgz", + "integrity": "sha512-sHc6z7hNF3ACvXurKe8hT1sD52Fc0fN3uPLS6SQnXRV9CJl33GNAS4w5Dd3X3GgykUt9SlnjhI1QRKhLzun4qQ==", "requires": { "rdf-data-factory": "^1.0.4" } @@ -27816,9 +27851,9 @@ } }, "ws": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.0.0.tgz", - "integrity": "sha512-6AcSIXpBlS0QvCVKk+3cWnWElLsA6SzC0lkQ43ciEglgXJXiCWK3/CGFEJ+Ybgp006CMibamAsqOlxE9s4AvYA==", + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", "requires": {} }, "xdg-basedir": { diff --git a/package.json b/package.json index 0b2684840..4580f267b 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "@types/sparqljs": "^3.1.2", "@types/url-join": "^4.0.0", "@types/uuid": "^8.3.0", - "@types/ws": "^7.4.5", + "@types/ws": "^8.2.0", "@types/yargs": "^17.0.0", "arrayify-stream": "^1.0.0", "async-lock": "^1.3.0", @@ -122,13 +122,13 @@ "rdf-serialize": "^1.1.0", "redis": "^3.1.2", "redlock": "^4.2.0", - "sparqlalgebrajs": "^3.0.0", + "sparqlalgebrajs": "^4.0.1", "sparqljs": "^3.4.2", "url-join": "^4.0.1", "uuid": "^8.3.2", "winston": "^3.3.3", "winston-transport": "^4.4.0", - "ws": "^8.0.0", + "ws": "^8.2.3", "yargs": "^17.0.1" }, "devDependencies": { diff --git a/src/authorization/permissions/SparqlPatchModesExtractor.ts b/src/authorization/permissions/SparqlPatchModesExtractor.ts index cad78bc52..cae2798d6 100644 --- a/src/authorization/permissions/SparqlPatchModesExtractor.ts +++ b/src/authorization/permissions/SparqlPatchModesExtractor.ts @@ -40,25 +40,25 @@ export class SparqlPatchModesExtractor extends ModesExtractor { return Boolean((data as SparqlUpdatePatch).algebra); } - private isSupported(op: Algebra.Operation): boolean { + private isSupported(op: Algebra.Update): boolean { if (this.isDeleteInsert(op) || this.isNop(op)) { return true; } if (op.type === Algebra.types.COMPOSITE_UPDATE) { - return (op as Algebra.CompositeUpdate).updates.every((update): boolean => this.isSupported(update)); + return op.updates.every((update): boolean => this.isSupported(update)); } return false; } - private isDeleteInsert(op: Algebra.Operation): op is Algebra.DeleteInsert { + private isDeleteInsert(op: Algebra.Update): op is Algebra.DeleteInsert { return op.type === Algebra.types.DELETE_INSERT; } - private isNop(op: Algebra.Operation): op is Algebra.Nop { + private isNop(op: Algebra.Update): op is Algebra.Nop { return op.type === Algebra.types.NOP; } - private needsAppend(update: Algebra.Operation): boolean { + private needsAppend(update: Algebra.Update): boolean { if (this.isNop(update)) { return false; } @@ -69,7 +69,7 @@ export class SparqlPatchModesExtractor extends ModesExtractor { return (update as Algebra.CompositeUpdate).updates.some((op): boolean => this.needsAppend(op)); } - private needsWrite(update: Algebra.Operation): boolean { + private needsWrite(update: Algebra.Update): boolean { if (this.isNop(update)) { return false; } diff --git a/src/http/UnsecureWebSocketsProtocol.ts b/src/http/UnsecureWebSocketsProtocol.ts index 5bfe5040b..bafc520c3 100644 --- a/src/http/UnsecureWebSocketsProtocol.ts +++ b/src/http/UnsecureWebSocketsProtocol.ts @@ -1,5 +1,5 @@ import { EventEmitter } from 'events'; -import type WebSocket from 'ws'; +import type { WebSocket } from 'ws'; import { getLoggerFor } from '../logging/LogUtil'; import type { HttpRequest } from '../server/HttpRequest'; import { WebSocketHandler } from '../server/WebSocketHandler'; diff --git a/src/http/input/body/SparqlUpdateBodyParser.ts b/src/http/input/body/SparqlUpdateBodyParser.ts index b9600769a..8b0d871e8 100644 --- a/src/http/input/body/SparqlUpdateBodyParser.ts +++ b/src/http/input/body/SparqlUpdateBodyParser.ts @@ -26,7 +26,7 @@ export class SparqlUpdateBodyParser extends BodyParser { const sparql = await readableToString(request); let algebra: Algebra.Operation; try { - algebra = translate(sparql, { quads: true, baseIRI: metadata.identifier.value }); + algebra = translate(sparql, { quads: true, baseIRI: metadata.identifier.value }) as Algebra.Update; } catch (error: unknown) { this.logger.warn('Could not translate SPARQL query to SPARQL algebra', { error }); throw new BadRequestHttpError(createErrorMessage(error), { cause: error }); diff --git a/src/http/output/error/ConvertingErrorHandler.ts b/src/http/output/error/ConvertingErrorHandler.ts index 278c7e45b..7bd6faecd 100644 --- a/src/http/output/error/ConvertingErrorHandler.ts +++ b/src/http/output/error/ConvertingErrorHandler.ts @@ -3,7 +3,7 @@ import type { RepresentationConverterArgs, } from '../../../storage/conversion/RepresentationConverter'; import { INTERNAL_ERROR } from '../../../util/ContentTypes'; -import { getStatusCode } from '../../../util/errors/ErrorUtil'; +import { getStatusCode } from '../../../util/errors/HttpErrorUtil'; import { toLiteral } from '../../../util/TermUtil'; import { HTTP, XSD } from '../../../util/Vocabularies'; import { BasicRepresentation } from '../../representation/BasicRepresentation'; diff --git a/src/http/output/error/SafeErrorHandler.ts b/src/http/output/error/SafeErrorHandler.ts index 7071c773d..9da9b095d 100644 --- a/src/http/output/error/SafeErrorHandler.ts +++ b/src/http/output/error/SafeErrorHandler.ts @@ -1,5 +1,6 @@ import { getLoggerFor } from '../../../logging/LogUtil'; -import { createErrorMessage, getStatusCode } from '../../../util/errors/ErrorUtil'; +import { createErrorMessage } from '../../../util/errors/ErrorUtil'; +import { getStatusCode } from '../../../util/errors/HttpErrorUtil'; import { guardedStreamFrom } from '../../../util/StreamUtil'; import { toLiteral } from '../../../util/TermUtil'; import { HTTP, XSD } from '../../../util/Vocabularies'; diff --git a/src/index.ts b/src/index.ts index 5352a0bf6..fd5342005 100644 --- a/src/index.ts +++ b/src/index.ts @@ -320,6 +320,7 @@ export * from './util/errors/ConflictHttpError'; export * from './util/errors/ErrorUtil'; export * from './util/errors/ForbiddenHttpError'; export * from './util/errors/HttpError'; +export * from './util/errors/HttpErrorUtil'; export * from './util/errors/InternalServerError'; export * from './util/errors/MethodNotAllowedHttpError'; export * from './util/errors/NotFoundHttpError'; diff --git a/src/server/WebSocketHandler.ts b/src/server/WebSocketHandler.ts index ec7c40f5a..d968edabb 100644 --- a/src/server/WebSocketHandler.ts +++ b/src/server/WebSocketHandler.ts @@ -1,4 +1,4 @@ -import type WebSocket from 'ws'; +import type { WebSocket } from 'ws'; import { AsyncHandler } from '../util/handlers/AsyncHandler'; import type { HttpRequest } from './HttpRequest'; diff --git a/src/server/WebSocketServerFactory.ts b/src/server/WebSocketServerFactory.ts index a154ff4cc..5826e1501 100644 --- a/src/server/WebSocketServerFactory.ts +++ b/src/server/WebSocketServerFactory.ts @@ -1,6 +1,6 @@ import type { Server } from 'http'; import type { Socket } from 'net'; -import type WebSocket from 'ws'; +import type { WebSocket } from 'ws'; import { Server as WebSocketServer } from 'ws'; import type { HttpRequest } from './HttpRequest'; import type { HttpServerFactory } from './HttpServerFactory'; diff --git a/src/util/PromiseUtil.ts b/src/util/PromiseUtil.ts index 7b72f82ee..8d38b3ae9 100644 --- a/src/util/PromiseUtil.ts +++ b/src/util/PromiseUtil.ts @@ -1,5 +1,7 @@ +import { createAggregateError } from './errors/HttpErrorUtil'; + // eslint-disable-next-line @typescript-eslint/no-empty-function -const infinitePromise = new Promise((): void => {}); +function noop(): void {} /** * A function that simulates the Array.some behaviour but on an array of Promises. @@ -14,17 +16,36 @@ const infinitePromise = new Promise((): void => {}); * 2. throwing an error should be logically equivalent to returning false. */ export async function promiseSome(predicates: Promise[]): Promise { - // These promises will only finish when their predicate returns true - const infinitePredicates = predicates.map(async(predicate): Promise => predicate.then( - async(value): Promise => value ? true : infinitePromise, - async(): Promise => infinitePromise, - )); - - // Returns after all predicates are resolved - const finalPromise = Promise.allSettled(predicates).then((results): boolean => - results.some((result): boolean => result.status === 'fulfilled' && result.value)); - - // Either one of the infinitePredicates will return true, - // or finalPromise will return the result if none of them did or finalPromise was faster - return Promise.race([ ...infinitePredicates, finalPromise ]); + return new Promise((resolve): void => { + function resolveIfTrue(value: boolean): void { + if (value) { + resolve(true); + } + } + Promise.all(predicates.map((predicate): Promise => predicate.then(resolveIfTrue, noop))) + .then((): void => resolve(false), noop); + }); +} + +/** + * Obtains the values of all fulfilled promises. + * If there are rejections (and `ignoreErrors` is false), throws a combined error of all rejected promises. + */ +export async function allFulfilled(promises: Promise [], ignoreErrors = false): Promise { + // Collect values and errors + const values: T[] = []; + const errors: Error[] = []; + for (const result of await Promise.allSettled(promises)) { + if (result.status === 'fulfilled') { + values.push(result.value); + } else if (!ignoreErrors) { + errors.push(result.reason); + } + } + + // Either throw or return + if (errors.length > 0) { + throw createAggregateError(errors); + } + return values; } diff --git a/src/util/errors/ErrorUtil.ts b/src/util/errors/ErrorUtil.ts index 84e94716e..405b29c80 100644 --- a/src/util/errors/ErrorUtil.ts +++ b/src/util/errors/ErrorUtil.ts @@ -1,5 +1,4 @@ import { types } from 'util'; -import { HttpError } from './HttpError'; /** * Checks if the input is an {@link Error}. @@ -25,10 +24,3 @@ export function assertError(error: unknown): asserts error is Error { export function createErrorMessage(error: unknown): string { return isError(error) ? error.message : `Unknown error: ${error}`; } - -/** - * Returns the HTTP status code corresponding to the error. - */ -export function getStatusCode(error: Error): number { - return HttpError.isInstance(error) ? error.statusCode : 500; -} diff --git a/src/util/errors/HttpErrorUtil.ts b/src/util/errors/HttpErrorUtil.ts new file mode 100644 index 000000000..a30bf64b1 --- /dev/null +++ b/src/util/errors/HttpErrorUtil.ts @@ -0,0 +1,38 @@ +import { BadRequestHttpError } from './BadRequestHttpError'; +import { createErrorMessage } from './ErrorUtil'; +import { HttpError } from './HttpError'; +import { InternalServerError } from './InternalServerError'; + +/** + * Returns the HTTP status code corresponding to the error. + */ +export function getStatusCode(error: Error): number { + return HttpError.isInstance(error) ? error.statusCode : 500; +} + +/** + * Combines a list of errors into a single HttpErrors. + * Status code depends on the input errors. If they all share the same status code that code will be re-used. + * If they are all within the 4xx range, 400 will be used, otherwise 500. + * + * @param errors - Errors to combine. + * @param messagePrefix - Prefix for the aggregate error message. Will be followed with an array of all the messages. + */ +export function createAggregateError(errors: Error[], messagePrefix = 'No handler supports the given input:'): +HttpError { + const httpErrors = errors.map((error): HttpError => + HttpError.isInstance(error) ? error : new InternalServerError(createErrorMessage(error))); + const joined = httpErrors.map((error: Error): string => error.message).join(', '); + const message = `${messagePrefix} [${joined}]`; + + // Check if all errors have the same status code + if (httpErrors.length > 0 && httpErrors.every((error): boolean => error.statusCode === httpErrors[0].statusCode)) { + return new HttpError(httpErrors[0].statusCode, httpErrors[0].name, message); + } + + // Find the error range (4xx or 5xx) + if (httpErrors.some((error): boolean => error.statusCode >= 500)) { + return new InternalServerError(message); + } + return new BadRequestHttpError(message); +} diff --git a/src/util/handlers/HandlerUtil.ts b/src/util/handlers/HandlerUtil.ts index efb61ea52..dfc436cf8 100644 --- a/src/util/handlers/HandlerUtil.ts +++ b/src/util/handlers/HandlerUtil.ts @@ -1,36 +1,7 @@ -import { BadRequestHttpError } from '../errors/BadRequestHttpError'; import { createErrorMessage, isError } from '../errors/ErrorUtil'; -import { HttpError } from '../errors/HttpError'; -import { InternalServerError } from '../errors/InternalServerError'; +import { createAggregateError } from '../errors/HttpErrorUtil'; import type { AsyncHandler } from './AsyncHandler'; -/** - * Combines a list of errors into a single HttpErrors. - * Status code depends on the input errors. If they all share the same status code that code will be re-used. - * If they are all within the 4xx range, 400 will be used, otherwise 500. - * - * @param errors - Errors to combine. - * @param messagePrefix - Prefix for the aggregate error message. Will be followed with an array of all the messages. - */ -export function createAggregateError(errors: Error[], messagePrefix = 'No handler supports the given input:'): -HttpError { - const httpErrors = errors.map((error): HttpError => - HttpError.isInstance(error) ? error : new InternalServerError(createErrorMessage(error))); - const joined = httpErrors.map((error: Error): string => error.message).join(', '); - const message = `${messagePrefix} [${joined}]`; - - // Check if all errors have the same status code - if (httpErrors.length > 0 && httpErrors.every((error): boolean => error.statusCode === httpErrors[0].statusCode)) { - return new HttpError(httpErrors[0].statusCode, httpErrors[0].name, message); - } - - // Find the error range (4xx or 5xx) - if (httpErrors.some((error): boolean => error.statusCode >= 500)) { - return new InternalServerError(message); - } - return new BadRequestHttpError(message); -} - /** * Finds a handler that can handle the given input data. * Otherwise an error gets thrown. diff --git a/src/util/handlers/ParallelHandler.ts b/src/util/handlers/ParallelHandler.ts index 3d78fb88b..b517257bb 100644 --- a/src/util/handlers/ParallelHandler.ts +++ b/src/util/handlers/ParallelHandler.ts @@ -12,12 +12,10 @@ export class ParallelHandler extends AsyncHandler { - // eslint-disable-next-line @typescript-eslint/promise-function-async await Promise.all(this.handlers.map((handler): Promise => handler.canHandle(input))); } public async handle(input: TIn): Promise { - // eslint-disable-next-line @typescript-eslint/promise-function-async return Promise.all(this.handlers.map((handler): Promise => handler.handle(input))); } } diff --git a/src/util/handlers/UnionHandler.ts b/src/util/handlers/UnionHandler.ts index c165686fb..2a63656ca 100644 --- a/src/util/handlers/UnionHandler.ts +++ b/src/util/handlers/UnionHandler.ts @@ -1,31 +1,41 @@ +import { allFulfilled } from '../PromiseUtil'; import { AsyncHandler } from './AsyncHandler'; -import { createAggregateError, filterHandlers, findHandler } from './HandlerUtil'; +import { filterHandlers, findHandler } from './HandlerUtil'; // Helper types to make sure the UnionHandler has the same in/out types as the AsyncHandler type it wraps -type ThenArg = T extends PromiseLike ? U : T; +type Awaited = T extends PromiseLike ? U : T; type InType> = Parameters[0]; -type OutType> = ThenArg>; -type HandlerType = AsyncHandler, OutType>; +type OutType> = Awaited>; /** * Utility handler that allows combining the results of multiple handlers into one. - * Will run all the handlers and then call the abstract `combine` function with the results, - * which should return the output of the class. - * - * If `requireAll` is true, the handler will fail if any of the handlers do not support the input. - * If `requireAll` is false, only the handlers that support the input will be called, - * only if all handlers reject the input will this handler reject as well. - * With `requireAll` set to false, the length of the input array - * for the `combine` function is variable (but always at least 1). + * Will run the handlers and then call the abstract `combine` function with the results, + * which then generates the handler's output. */ export abstract class UnionHandler> extends AsyncHandler, OutType> { protected readonly handlers: T[]; private readonly requireAll: boolean; + private readonly ignoreErrors: boolean; - protected constructor(handlers: T[], requireAll = false) { + /** + * Creates a new `UnionHandler`. + * + * When `requireAll` is false or `ignoreErrors` is true, + * the length of the input to `combine` can vary; + * otherwise, it is exactly the number of handlers. + * + * @param handlers - The handlers whose output is to be combined. + * @param requireAll - If true, will fail if any of the handlers do not support the input. + If false, only the handlers that support the input will be called; + * will fail only if none of the handlers can handle the input. + * @param ignoreErrors - If true, ignores handlers that fail by omitting their output; + * if false, fails when any handlers fail. + */ + public constructor(handlers: T[], requireAll = false, ignoreErrors = !requireAll) { super(); this.handlers = handlers; this.requireAll = requireAll; + this.ignoreErrors = ignoreErrors; } public async canHandle(input: InType): Promise { @@ -38,57 +48,21 @@ export abstract class UnionHandler> extends Asy } public async handle(input: InType): Promise> { - let handlers: HandlerType[]; - if (this.requireAll) { - // Handlers were already checked in canHandle - // eslint-disable-next-line prefer-destructuring - handlers = this.handlers; - } else { - handlers = await filterHandlers(this.handlers, input); - } - - const results = await Promise.all( - handlers.map(async(handler): Promise> => handler.handle(input)), - ); - - return this.combine(results); - } - - public async handleSafe(input: InType): Promise> { - let handlers: HandlerType[]; - if (this.requireAll) { - await this.allCanHandle(input); - // eslint-disable-next-line prefer-destructuring - handlers = this.handlers; - } else { - // This will error if no handler supports the input - handlers = await filterHandlers(this.handlers, input); - } - - const results = await Promise.all( - handlers.map(async(handler): Promise> => handler.handle(input)), - ); - - return this.combine(results); + const handlers = this.requireAll ? this.handlers : await filterHandlers(this.handlers, input); + const results = handlers.map((handler): Promise> => handler.handle(input)); + return this.combine(await allFulfilled(results, this.ignoreErrors)); } /** * Checks if all handlers can handle the input. * If not, throw an error based on the errors of the failed handlers. */ - private async allCanHandle(input: InType): Promise { - const results = await Promise.allSettled(this.handlers.map(async(handler): Promise> => { - await handler.canHandle(input); - return handler; - })); - if (results.some(({ status }): boolean => status === 'rejected')) { - const errors = results.map((result): Error => (result as PromiseRejectedResult).reason); - throw createAggregateError(errors); - } + protected async allCanHandle(input: InType): Promise { + await allFulfilled(this.handlers.map((handler): Promise => handler.canHandle(input))); } /** - * Combine the results of the handlers into a single output. + * Combines the results of the handlers into a single output. */ protected abstract combine(results: OutType[]): Promise>; } diff --git a/test/integration/Setup.test.ts b/test/integration/Setup.test.ts index 8cec739c4..b4be03ef3 100644 --- a/test/integration/Setup.test.ts +++ b/test/integration/Setup.test.ts @@ -51,7 +51,7 @@ describe('A Solid server with setup', (): void => { // Root access disabled res = await fetch(baseUrl); - expect(res.status).toBe(403); + expect(res.status).toBe(401); // Registration still possible const registerParams = { email, podName, password, confirmPassword: password, createWebId: true }; diff --git a/test/integration/WebSocketsProtocol.test.ts b/test/integration/WebSocketsProtocol.test.ts index 7fa961cf3..2c678af4b 100644 --- a/test/integration/WebSocketsProtocol.test.ts +++ b/test/integration/WebSocketsProtocol.test.ts @@ -1,5 +1,5 @@ import fetch from 'cross-fetch'; -import WebSocket from 'ws'; +import { WebSocket } from 'ws'; import type { App } from '../../src/init/App'; import { getPort } from '../util/Util'; import { getDefaultVariables, getTestConfigPath, instantiateFromConfig } from './Config'; diff --git a/test/unit/authentication/UnionCredentialsExtractor.test.ts b/test/unit/authentication/UnionCredentialsExtractor.test.ts index 1b69cb886..427f6ba40 100644 --- a/test/unit/authentication/UnionCredentialsExtractor.test.ts +++ b/test/unit/authentication/UnionCredentialsExtractor.test.ts @@ -43,4 +43,11 @@ describe('A UnionCredentialsExtractor', (): void => { [CredentialGroup.public]: {}, }); }); + + it('skips erroring handlers.', async(): Promise => { + extractors[0].handle.mockRejectedValueOnce(new Error('error')); + await expect(extractor.handle(request)).resolves.toEqual({ + [CredentialGroup.public]: {}, + }); + }); }); diff --git a/test/unit/server/WebSocketServerFactory.test.ts b/test/unit/server/WebSocketServerFactory.test.ts index 4e422aa0a..7605cf4cd 100644 --- a/test/unit/server/WebSocketServerFactory.test.ts +++ b/test/unit/server/WebSocketServerFactory.test.ts @@ -1,6 +1,6 @@ import type { Server } from 'http'; import request from 'supertest'; -import WebSocket from 'ws'; +import { WebSocket } from 'ws'; import { BaseHttpServerFactory } from '../../../src/server/BaseHttpServerFactory'; import type { HttpHandlerInput } from '../../../src/server/HttpHandler'; import { HttpHandler } from '../../../src/server/HttpHandler'; diff --git a/test/unit/storage/patch/SparqlUpdatePatcher.test.ts b/test/unit/storage/patch/SparqlUpdatePatcher.test.ts index 01386217a..6bb035a1f 100644 --- a/test/unit/storage/patch/SparqlUpdatePatcher.test.ts +++ b/test/unit/storage/patch/SparqlUpdatePatcher.test.ts @@ -2,6 +2,7 @@ import 'jest-rdf'; import { namedNode, quad } from '@rdfjs/data-model'; import arrayifyStream from 'arrayify-stream'; import type { Quad } from 'rdf-js'; +import type { Algebra } from 'sparqlalgebrajs'; import { translate } from 'sparqlalgebrajs'; import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation'; import type { Representation } from '../../../../src/http/representation/Representation'; @@ -15,7 +16,7 @@ import { guardedStreamFrom } from '../../../../src/util/StreamUtil'; function getPatch(query: string): SparqlUpdatePatch { const prefixedQuery = `prefix : \n${query}`; return { - algebra: translate(prefixedQuery, { quads: true }), + algebra: translate(prefixedQuery, { quads: true }) as Algebra.Update, data: guardedStreamFrom(prefixedQuery), metadata: new RepresentationMetadata(), binary: true, diff --git a/test/unit/util/errors/ErrorUtil.test.ts b/test/unit/util/errors/ErrorUtil.test.ts index be242152a..49eaf7c36 100644 --- a/test/unit/util/errors/ErrorUtil.test.ts +++ b/test/unit/util/errors/ErrorUtil.test.ts @@ -1,5 +1,4 @@ -import { assertError, createErrorMessage, getStatusCode, isError } from '../../../../src/util/errors/ErrorUtil'; -import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError'; +import { assertError, createErrorMessage, isError } from '../../../../src/util/errors/ErrorUtil'; describe('ErrorUtil', (): void => { describe('#isError', (): void => { @@ -39,14 +38,4 @@ describe('ErrorUtil', (): void => { expect(createErrorMessage('apple')).toBe('Unknown error: apple'); }); }); - - describe('#getStatusCode', (): void => { - it('returns the corresponding status code for HttpErrors.', async(): Promise => { - expect(getStatusCode(new NotFoundHttpError())).toBe(404); - }); - - it('returns 500 for other errors.', async(): Promise => { - expect(getStatusCode(new Error('404'))).toBe(500); - }); - }); }); diff --git a/test/unit/util/errors/HttpErrorUtil.test.ts b/test/unit/util/errors/HttpErrorUtil.test.ts new file mode 100644 index 000000000..d257bf9fb --- /dev/null +++ b/test/unit/util/errors/HttpErrorUtil.test.ts @@ -0,0 +1,50 @@ +import { HttpError } from '../../../../src/util/errors/HttpError'; +import { createAggregateError, getStatusCode } from '../../../../src/util/errors/HttpErrorUtil'; +import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError'; + +describe('ErrorUtil', (): void => { + describe('createAggregateError', (): void => { + const error401 = new HttpError(401, 'UnauthorizedHttpError'); + const error415 = new HttpError(415, 'UnsupportedMediaTypeHttpError'); + const error501 = new HttpError(501, 'NotImplementedHttpError'); + const error = new Error('noStatusCode'); + + it('throws an error with matching status code if all errors have the same.', async(): Promise => { + expect(createAggregateError([ error401, error401 ])).toMatchObject({ + statusCode: 401, + name: 'UnauthorizedHttpError', + }); + }); + + it('throws an InternalServerError if one of the errors has status code 5xx.', async(): Promise => { + expect(createAggregateError([ error401, error501 ])).toMatchObject({ + statusCode: 500, + name: 'InternalServerError', + }); + }); + + it('throws an BadRequestHttpError if all handlers have 4xx status codes.', async(): Promise => { + expect(createAggregateError([ error401, error415 ])).toMatchObject({ + statusCode: 400, + name: 'BadRequestHttpError', + }); + }); + + it('interprets non-HTTP errors as internal errors.', async(): Promise => { + expect(createAggregateError([ error ])).toMatchObject({ + statusCode: 500, + name: 'InternalServerError', + }); + }); + }); + + describe('#getStatusCode', (): void => { + it('returns the corresponding status code for HttpErrors.', async(): Promise => { + expect(getStatusCode(new NotFoundHttpError())).toBe(404); + }); + + it('returns 500 for other errors.', async(): Promise => { + expect(getStatusCode(new Error('404'))).toBe(500); + }); + }); +}); diff --git a/test/unit/util/handlers/HandlerUtil.test.ts b/test/unit/util/handlers/HandlerUtil.test.ts index 9cef18305..718d03900 100644 --- a/test/unit/util/handlers/HandlerUtil.test.ts +++ b/test/unit/util/handlers/HandlerUtil.test.ts @@ -1,44 +1,8 @@ -import { HttpError } from '../../../../src/util/errors/HttpError'; import type { AsyncHandler } from '../../../../src/util/handlers/AsyncHandler'; -import { createAggregateError, filterHandlers, findHandler } from '../../../../src/util/handlers/HandlerUtil'; +import { filterHandlers, findHandler } from '../../../../src/util/handlers/HandlerUtil'; import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler'; describe('HandlerUtil', (): void => { - describe('createAggregateError', (): void => { - const error401 = new HttpError(401, 'UnauthorizedHttpError'); - const error415 = new HttpError(415, 'UnsupportedMediaTypeHttpError'); - const error501 = new HttpError(501, 'NotImplementedHttpError'); - const error = new Error('noStatusCode'); - - it('throws an error with matching status code if all errors have the same.', async(): Promise => { - expect(createAggregateError([ error401, error401 ])).toMatchObject({ - statusCode: 401, - name: 'UnauthorizedHttpError', - }); - }); - - it('throws an InternalServerError if one of the errors has status code 5xx.', async(): Promise => { - expect(createAggregateError([ error401, error501 ])).toMatchObject({ - statusCode: 500, - name: 'InternalServerError', - }); - }); - - it('throws an BadRequestHttpError if all handlers have 4xx status codes.', async(): Promise => { - expect(createAggregateError([ error401, error415 ])).toMatchObject({ - statusCode: 400, - name: 'BadRequestHttpError', - }); - }); - - it('interprets non-HTTP errors as internal errors.', async(): Promise => { - expect(createAggregateError([ error ])).toMatchObject({ - statusCode: 500, - name: 'InternalServerError', - }); - }); - }); - describe('findHandler', (): void => { let handlerTrue: AsyncHandler; let handlerFalse: AsyncHandler; diff --git a/test/unit/util/handlers/UnionHandler.test.ts b/test/unit/util/handlers/UnionHandler.test.ts index 27a803054..fc409e3d6 100644 --- a/test/unit/util/handlers/UnionHandler.test.ts +++ b/test/unit/util/handlers/UnionHandler.test.ts @@ -2,10 +2,6 @@ import type { AsyncHandler } from '../../../../src/util/handlers/AsyncHandler'; import { UnionHandler } from '../../../../src/util/handlers/UnionHandler'; class SimpleUnionHandler extends UnionHandler> { - public constructor(handlers: AsyncHandler[], requireAll?: boolean) { - super(handlers, requireAll); - } - protected async combine(results: string[]): Promise { return results.join(''); } @@ -61,4 +57,25 @@ describe('A UnionHandler', (): void => { handlers[0].canHandle.mockRejectedValue(new Error('bad request')); await expect(handler.handle(input)).resolves.toBe('ab'); }); + + it('requires all handlers to succeed if requireAll is true.', async(): Promise => { + handler = new SimpleUnionHandler(handlers, true); + + handlers[0].handle.mockRejectedValue(new Error('bad request')); + await expect(handler.handleSafe(input)).rejects.toThrow('bad request'); + }); + + it('does not require all handlers to succeed if ignoreErrors is true.', async(): Promise => { + handler = new SimpleUnionHandler(handlers, true, true); + + handlers[0].handle.mockRejectedValueOnce(new Error('bad request')); + await expect(handler.handleSafe(input)).resolves.toBe('b'); + + handlers[1].handle.mockRejectedValueOnce(new Error('bad request')); + await expect(handler.handleSafe(input)).resolves.toBe('a'); + + handlers[0].handle.mockRejectedValueOnce(new Error('bad request')); + handlers[1].handle.mockRejectedValueOnce(new Error('bad request')); + await expect(handler.handleSafe(input)).resolves.toBe(''); + }); });