diff --git a/src/rest/ErrorCode.ts b/src/rest/ErrorCode.ts index f7f1b95a2..e69de29bb 100644 --- a/src/rest/ErrorCode.ts +++ b/src/rest/ErrorCode.ts @@ -1,20 +0,0 @@ -export enum ErrorCode { - NOT_FOUND = 'NOT_FOUND', - VALIDATION_ERROR = 'VALIDATION_ERROR', - UNKNOWN = 'UNKNOWN' -} - -export const parseErrorCode = (body: string) => { - let json - try { - json = JSON.parse(body) - } catch (err) { - return ErrorCode.UNKNOWN - } - const code = json.code - const keys = Object.keys(ErrorCode) - if (keys.includes(code)) { - return code as ErrorCode - } - return ErrorCode.UNKNOWN -} diff --git a/src/rest/StreamEndpoints.ts b/src/rest/StreamEndpoints.ts index 3960bdfaa..3e4c1d3b3 100644 --- a/src/rest/StreamEndpoints.ts +++ b/src/rest/StreamEndpoints.ts @@ -10,10 +10,9 @@ import Stream, { StreamOperation, StreamProperties } from '../stream' import StreamPart from '../stream/StreamPart' import { isKeyExchangeStream } from '../stream/KeyExchange' -import authFetch, { AuthFetchError } from './authFetch' +import authFetch, { ErrorCode, NotFoundError } from './authFetch' import { Todo } from '../types' import StreamrClient from '../StreamrClient' -import { ErrorCode } from './ErrorCode' // TODO change this import when streamr-client-protocol exports StreamMessage type or the enums types directly import { ContentType, EncryptionType, SignatureType, StreamMessageType } from 'streamr-client-protocol/dist/src/protocol/message_layer/StreamMessage' @@ -120,7 +119,7 @@ export class StreamEndpoints { // @ts-expect-error public: false, }) - return json[0] ? new Stream(this.client, json[0]) : Promise.reject(new AuthFetchError('', undefined, undefined, ErrorCode.NOT_FOUND)) + return json[0] ? new Stream(this.client, json[0]) : Promise.reject(new NotFoundError('Stream: name=' + name)) } async createStream(props?: StreamProperties) { @@ -139,7 +138,7 @@ export class StreamEndpoints { return new Stream(this.client, json) } - async getOrCreateStream(props: { id?: string, name?: string }) { + async getOrCreateStream(props: { id: string, name?: never } | { id?: never, name: string }) { this.client.debug('getOrCreateStream %o', { props, }) @@ -151,9 +150,8 @@ export class StreamEndpoints { } const stream = await this.getStreamByName(props.name!) return stream - } catch (err) { - const isNotFoundError = (err instanceof AuthFetchError) && (err.errorCode === ErrorCode.NOT_FOUND) - if (!isNotFoundError) { + } catch (err: any) { + if (err.errorCode !== ErrorCode.NOT_FOUND) { throw err } } diff --git a/src/rest/authFetch.ts b/src/rest/authFetch.ts index 16a299bad..62f9f99f0 100644 --- a/src/rest/authFetch.ts +++ b/src/rest/authFetch.ts @@ -2,9 +2,14 @@ import fetch, { Response } from 'node-fetch' import Debug from 'debug' import { getVersionString } from '../utils' -import { ErrorCode, parseErrorCode } from './ErrorCode' import Session from '../Session' +export enum ErrorCode { + NOT_FOUND = 'NOT_FOUND', + VALIDATION_ERROR = 'VALIDATION_ERROR', + UNKNOWN = 'UNKNOWN' +} + export const DEFAULT_HEADERS = { 'Streamr-Client': `streamr-client-javascript/${getVersionString()}`, } @@ -12,15 +17,16 @@ export const DEFAULT_HEADERS = { export class AuthFetchError extends Error { response?: Response body?: any - errorCode?: ErrorCode + errorCode: ErrorCode constructor(message: string, response?: Response, body?: any, errorCode?: ErrorCode) { + const typePrefix = errorCode ? errorCode + ': ' : '' // add leading space if there is a body set const bodyMessage = body ? ` ${(typeof body === 'string' ? body : JSON.stringify(body).slice(0, 1024))}...` : '' - super(message + bodyMessage) + super(typePrefix + message + bodyMessage) this.response = response this.body = body - this.errorCode = errorCode + this.errorCode = errorCode || ErrorCode.UNKNOWN if (Error.captureStackTrace) { Error.captureStackTrace(this, this.constructor) @@ -28,6 +34,34 @@ export class AuthFetchError extends Error { } } +export class ValidationError extends AuthFetchError { + constructor(message: string, response?: Response, body?: any) { + super(message, response, body, ErrorCode.VALIDATION_ERROR) + } +} + +export class NotFoundError extends AuthFetchError { + constructor(message: string, response?: Response, body?: any) { + super(message, response, body, ErrorCode.NOT_FOUND) + } +} + +const ERROR_TYPES = new Map() +ERROR_TYPES.set(ErrorCode.VALIDATION_ERROR, ValidationError) +ERROR_TYPES.set(ErrorCode.NOT_FOUND, NotFoundError) +ERROR_TYPES.set(ErrorCode.UNKNOWN, AuthFetchError) + +const parseErrorCode = (body: string) => { + let json + try { + json = JSON.parse(body) + } catch (err) { + return ErrorCode.UNKNOWN + } + const { code } = json + return code in ErrorCode ? code : ErrorCode.UNKNOWN +} + const debug = Debug('StreamrClient:utils:authfetch') // TODO: could use the debug instance from the client? (e.g. client.debug.extend('authFetch')) let ID = 0 @@ -78,6 +112,8 @@ export default async function authFetch(url: string, session?: return authFetch(url, session, options, true) } else { debug('%d %s – failed', id, url) - throw new AuthFetchError(`Request ${id} to ${url} returned with error code ${response.status}.`, response, body, parseErrorCode(body)) + const errorCode = parseErrorCode(body) + const ErrorClass = ERROR_TYPES.get(errorCode)! + throw new ErrorClass(`Request ${id} to ${url} returned with error code ${response.status}.`, response, body, errorCode) } } diff --git a/src/stream/index.ts b/src/stream/index.ts index b0b6a0595..3be723063 100644 --- a/src/stream/index.ts +++ b/src/stream/index.ts @@ -6,6 +6,7 @@ import StreamrClient from '../StreamrClient' import { Todo } from '../types' interface StreamPermisionBase { + id: number operation: StreamOperation } @@ -72,6 +73,8 @@ export default class Stream { // TODO add field definitions for all fields // @ts-expect-error id: string + // @ts-expect-error + name: string config: { fields: Field[]; } = { fields: [] } diff --git a/test/integration/StreamEndpoints.test.js b/test/integration/StreamEndpoints.test.ts similarity index 89% rename from test/integration/StreamEndpoints.test.js rename to test/integration/StreamEndpoints.test.ts index 0fd2ebf9e..ccd617f6f 100644 --- a/test/integration/StreamEndpoints.test.js +++ b/test/integration/StreamEndpoints.test.ts @@ -1,4 +1,6 @@ -import { ethers } from 'ethers' +import { ethers, Wallet } from 'ethers' +import { NotFoundError, ValidationError } from '../../src/rest/authFetch' +import Stream, { StreamOperation } from '../../src/stream' import StreamrClient from '../../src/StreamrClient' import { uid } from '../utils' @@ -9,17 +11,17 @@ import config from './config' * These tests should be run in sequential order! */ -function TestStreamEndpoints(getName) { - let client - let wallet - let createdStream +function TestStreamEndpoints(getName: () => string) { + let client: StreamrClient + let wallet: Wallet + let createdStream: Stream const createClient = (opts = {}) => new StreamrClient({ ...config.clientOptions, autoConnect: false, autoDisconnect: false, ...opts, - }) + } as any) beforeAll(() => { wallet = ethers.Wallet.createRandom() @@ -53,7 +55,7 @@ function TestStreamEndpoints(getName) { }) it('invalid id', () => { - return expect(() => client.createStream({ id: 'invalid.eth/foobar' })).rejects.toThrow() + return expect(() => client.createStream({ id: 'invalid.eth/foobar' })).rejects.toThrow(ValidationError) }) }) @@ -66,7 +68,7 @@ function TestStreamEndpoints(getName) { it('get a non-existing Stream', async () => { const id = `${wallet.address}/StreamEndpoints-integration-nonexisting-${Date.now()}` - return expect(() => client.getStream(id)).rejects.toThrow() + return expect(() => client.getStream(id)).rejects.toThrow(NotFoundError) }) }) @@ -79,7 +81,7 @@ function TestStreamEndpoints(getName) { it('get a non-existing Stream', async () => { const name = `${wallet.address}/StreamEndpoints-integration-nonexisting-${Date.now()}` - return expect(() => client.getStreamByName(name)).rejects.toThrow() + return expect(() => client.getStreamByName(name)).rejects.toThrow(NotFoundError) }) }) @@ -205,25 +207,25 @@ function TestStreamEndpoints(getName) { }) it('Stream.hasPermission', async () => { - expect(await createdStream.hasPermission('stream_share', wallet.address)).toBeTruthy() + expect(await createdStream.hasPermission(StreamOperation.STREAM_SHARE, wallet.address)).toBeTruthy() }) it('Stream.grantPermission', async () => { - await createdStream.grantPermission('stream_subscribe', null) // public read - expect(await createdStream.hasPermission('stream_subscribe', null)).toBeTruthy() + await createdStream.grantPermission(StreamOperation.STREAM_SUBSCRIBE, undefined) // public read + expect(await createdStream.hasPermission(StreamOperation.STREAM_SUBSCRIBE, undefined)).toBeTruthy() }) it('Stream.revokePermission', async () => { - const publicRead = await createdStream.hasPermission('stream_subscribe', null) - await createdStream.revokePermission(publicRead.id) - expect(!(await createdStream.hasPermission('stream_subscribe', null))).toBeTruthy() + const publicRead = await createdStream.hasPermission(StreamOperation.STREAM_SUBSCRIBE, undefined) + await createdStream.revokePermission(publicRead!.id) + expect(!(await createdStream.hasPermission(StreamOperation.STREAM_SUBSCRIBE, undefined))).toBeTruthy() }) }) describe('Stream deletion', () => { it('Stream.delete', async () => { await createdStream.delete() - return expect(() => client.getStream(createdStream.id)).rejects.toThrow() + return expect(() => client.getStream(createdStream.id)).rejects.toThrow(NotFoundError) }) })