diff --git a/src/resources/HttpResource.ts b/src/resources/HttpResource.ts index 32304c71..4bf5f3dc 100644 --- a/src/resources/HttpResource.ts +++ b/src/resources/HttpResource.ts @@ -9,6 +9,9 @@ export type Endpoint = string | ((id?: string) => string); export type HttpContext = Context & { readonly httpApi: TApi; + readonly options?: { + throwOnEmptyStringPaths?: boolean; + }; }; export type HttpResourceOptions = { @@ -56,6 +59,15 @@ export default class HttpResource< getPath(id?: string) { const endpoint = this._endpoint; + + // this protects against GraphQL inputs of empty string hitting list endpoints + // example: GET /users/ with an empty string user_id will request /users + if (this.context.options?.throwOnEmptyStringPaths && id === '') { + throw new TypeError( + "id can't be an empty string; pass a value or undefined", + ); + } + if (typeof endpoint === 'function') { return endpoint(id); } diff --git a/test/HttpResource.test.ts b/test/HttpResource.test.ts index 2770d3f0..78b7fa31 100644 --- a/test/HttpResource.test.ts +++ b/test/HttpResource.test.ts @@ -135,4 +135,23 @@ describe('HttpResource', () => { expect(await resource.delete('5')).toEqual(null); }); + + it('should throw an error if an empty string is provided and the option is enabled', async () => { + const data = [{ spicy: true }, { spicy: true }, { spicy: true }]; + mockedFetch.get('https://gateway/v1/salads', { + status: 200, + body: { data }, + }); + + const resource = new HttpResource(mockContext, { endpoint: 'salads' }); + expect(await resource.get('')).toEqual(data); + + mockContext.options = { throwOnEmptyStringPaths: true }; + expect(() => { + resource.get(''); + }).toThrowErrorMatchingInlineSnapshot( + `"id can't be an empty string; pass a value or undefined"`, + ); + expect(await resource.get()).toEqual(data); + }); });