From e288526abe5bb91f08873f66dcfda9d8fc5cab9a Mon Sep 17 00:00:00 2001 From: Tdanks2000 <40835340+TDanks2000@users.noreply.github.com> Date: Mon, 10 Aug 2026 03:45:43 +0100 Subject: [PATCH 1/7] fix: narrow searchable endpoint types --- src/endpoints/registry.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/endpoints/registry.ts b/src/endpoints/registry.ts index cc1a08a..14c1691 100644 --- a/src/endpoints/registry.ts +++ b/src/endpoints/registry.ts @@ -95,6 +95,10 @@ export const IGDB_ENDPOINT_METADATA = [ ] as const; type EndpointMetadataEntry = (typeof IGDB_ENDPOINT_METADATA)[number]; +type SearchableEndpointMetadataEntry = Extract< + EndpointMetadataEntry, + { searchable: true } +>; export const IGDB_ENDPOINTS = Object.fromEntries( IGDB_ENDPOINT_METADATA.map(({ key, path }) => [key, path]), @@ -104,10 +108,9 @@ export const IGDB_ENDPOINTS = Object.fromEntries( export type IGDBEndpointKey = keyof typeof IGDB_ENDPOINTS; export type IGDBEndpointPath = (typeof IGDB_ENDPOINTS)[IGDBEndpointKey]; +export type IGDBSearchableEndpointPath = + SearchableEndpointMetadataEntry["path"]; export const IGDB_SEARCHABLE_ENDPOINTS = IGDB_ENDPOINT_METADATA.filter( (endpoint) => endpoint.searchable === true, -).map((endpoint) => endpoint.path) as readonly IGDBEndpointPath[]; - -export type IGDBSearchableEndpointPath = - (typeof IGDB_SEARCHABLE_ENDPOINTS)[number]; +).map((endpoint) => endpoint.path) as readonly IGDBSearchableEndpointPath[]; From 6b2b08360fa88fd05dfb10aa7f61472a2cc1ea92 Mon Sep 17 00:00:00 2001 From: Tdanks2000 <40835340+TDanks2000@users.noreply.github.com> Date: Mon, 10 Aug 2026 03:45:55 +0100 Subject: [PATCH 2/7] fix: reject unsupported endpoint search helpers --- src/endpoints/Endpoint.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/endpoints/Endpoint.ts b/src/endpoints/Endpoint.ts index 17f8b1d..084ff56 100644 --- a/src/endpoints/Endpoint.ts +++ b/src/endpoints/Endpoint.ts @@ -1,7 +1,10 @@ -import { IGDBNotFoundError } from "../errors"; +import { IGDBNotFoundError, IGDBValidationError } from "../errors"; import type { HttpClient } from "../http/HttpClient"; import { QueryBuilder } from "../query/QueryBuilder"; import type { IGDBEntity, MetaField } from "../types/models"; +import { IGDB_SEARCHABLE_ENDPOINTS } from "./registry"; + +const SEARCHABLE_ENDPOINTS = new Set(IGDB_SEARCHABLE_ENDPOINTS); export class IGDBEndpoint { readonly #http: HttpClient; @@ -37,6 +40,11 @@ export class IGDBEndpoint { } search(term: string): QueryBuilder { + if (!SEARCHABLE_ENDPOINTS.has(this.#path)) { + throw new IGDBValidationError( + `search is not supported on the IGDB /${this.#path} endpoint`, + ); + } return this.query().search(term).limit(50); } From 31194fdbeda3fe99ca8d7dd5b45dab57864183d7 Mon Sep 17 00:00:00 2001 From: Tdanks2000 <40835340+TDanks2000@users.noreply.github.com> Date: Mon, 10 Aug 2026 03:46:07 +0100 Subject: [PATCH 3/7] test: cover searchable endpoint guard --- src/__tests__/search-capabilities.test.ts | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/__tests__/search-capabilities.test.ts diff --git a/src/__tests__/search-capabilities.test.ts b/src/__tests__/search-capabilities.test.ts new file mode 100644 index 0000000..6527ccc --- /dev/null +++ b/src/__tests__/search-capabilities.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from "bun:test"; +import { IGDBClient } from "../client/IGDBClient"; +import type { IGDBSearchableEndpointPath } from "../endpoints/registry"; +import { IGDBValidationError } from "../errors"; + +const testConfig = { + clientId: "client-id", + clientSecret: "client-secret", + fetch: (async () => new Response()) as unknown as typeof fetch, +}; + +describe("endpoint search capabilities", () => { + test("allows the high-level search helper only on searchable IGDB endpoints", async () => { + const client = new IGDBClient(testConfig); + + expect(client.games.search("zelda").raw()).toContain('search "zelda";'); + expect(() => client.companies.search("Nintendo")).toThrow( + IGDBValidationError, + ); + + await client.dispose(); + }); + + test("narrows IGDBSearchableEndpointPath to documented searchable paths", () => { + const path: IGDBSearchableEndpointPath = "games"; + expect(path).toBe("games"); + + // @ts-expect-error genres is not a searchable IGDB endpoint + const unsupported: IGDBSearchableEndpointPath = "genres"; + expect(unsupported).toBe("genres"); + }); +}); From 64f49cf46be12a4ce8c5a43f0a5deb178fe5cfa8 Mon Sep 17 00:00:00 2001 From: Tdanks2000 <40835340+TDanks2000@users.noreply.github.com> Date: Mon, 10 Aug 2026 03:46:15 +0100 Subject: [PATCH 4/7] chore: add search capability changeset --- .changeset/guard-unsupported-search.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/guard-unsupported-search.md diff --git a/.changeset/guard-unsupported-search.md b/.changeset/guard-unsupported-search.md new file mode 100644 index 0000000..4285f59 --- /dev/null +++ b/.changeset/guard-unsupported-search.md @@ -0,0 +1,5 @@ +--- +"@api-wrappers/igdb-wrapper": patch +--- + +Reject the high-level search helper on endpoints that IGDB does not document as searchable, and narrow the exported searchable endpoint path type accordingly. From 8067447aa7d5d5a15fe4ebdc09bbf180fcf48063 Mon Sep 17 00:00:00 2001 From: Tdanks2000 <40835340+TDanks2000@users.noreply.github.com> Date: Mon, 10 Aug 2026 03:46:42 +0100 Subject: [PATCH 5/7] docs: clarify searchable endpoint support --- docs/endpoints.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/endpoints.md b/docs/endpoints.md index 2767708..17ce457 100644 --- a/docs/endpoints.md +++ b/docs/endpoints.md @@ -7,16 +7,16 @@ property on `IGDBClient`. Each property is an `IGDBEndpoint` with: endpoint.query(); // QueryBuilder endpoint.findMany(); // query().limit(50) endpoint.findById(id); // first result where id = id -endpoint.search("zelda"); // query().search("zelda").limit(50) +endpoint.search("zelda"); // searchable IGDB endpoints only endpoint.request("fields *;"); // raw APICalypse body endpoint.count("where id > 0;"); endpoint.meta(); // GET /{endpoint}/meta endpoint.requestProtobuf("fields id;"); ``` -IGDB only supports `search` on selected endpoints. The wrapper exposes the -method uniformly so new searchable endpoints do not require a wrapper release; -the current documented searchable endpoints are exported as +IGDB only supports `search` on selected endpoints. The high-level `search()` +helper validates the endpoint and throws `IGDBValidationError` for unsupported +paths. The current documented searchable endpoints are exported as `IGDB_SEARCHABLE_ENDPOINTS`. Endpoint properties and path exports are derived from @@ -28,7 +28,7 @@ Use endpoint properties for known IGDB resources: ```ts await client.games.query().limit(10).execute(); await client.platforms.findById(48); -await client.companies.search("Nintendo").execute(); +await client.platforms.search("PlayStation").execute(); ``` Use `client.endpoint(path)` when IGDB adds a new endpoint before the wrapper publishes typed model support: From 56339ba1fd3a46640ee449f3effa244004faab05 Mon Sep 17 00:00:00 2001 From: Tdanks2000 <40835340+TDanks2000@users.noreply.github.com> Date: Mon, 10 Aug 2026 03:49:45 +0100 Subject: [PATCH 6/7] fix: preserve searchable endpoint literals --- src/endpoints/registry.ts | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/endpoints/registry.ts b/src/endpoints/registry.ts index 14c1691..f579d3b 100644 --- a/src/endpoints/registry.ts +++ b/src/endpoints/registry.ts @@ -1,14 +1,29 @@ -interface EndpointMetadata { +interface EndpointMetadata< + TKey extends string, + TPath extends string, + TSearchable extends boolean, +> { key: TKey; path: TPath; - searchable?: boolean; + searchable: TSearchable; } -const endpoint = ( +function endpoint( key: TKey, path: TPath, - options: { searchable?: boolean } = {}, -): EndpointMetadata => ({ key, path, ...options }); +): EndpointMetadata; +function endpoint( + key: TKey, + path: TPath, + options: { searchable: true }, +): EndpointMetadata; +function endpoint( + key: TKey, + path: TPath, + options?: { searchable?: boolean }, +): EndpointMetadata { + return { key, path, searchable: options?.searchable === true }; +} export const IGDB_ENDPOINT_METADATA = [ endpoint("ageRatings", "age_ratings"), From 24413fb8ff67d3b305c5e8fe01004b1abf1d57d9 Mon Sep 17 00:00:00 2001 From: Tdanks2000 <40835340+TDanks2000@users.noreply.github.com> Date: Mon, 10 Aug 2026 03:50:22 +0100 Subject: [PATCH 7/7] test: keep unsupported search assertion compile-only --- src/__tests__/search-capabilities.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/search-capabilities.test.ts b/src/__tests__/search-capabilities.test.ts index 6527ccc..d408f24 100644 --- a/src/__tests__/search-capabilities.test.ts +++ b/src/__tests__/search-capabilities.test.ts @@ -27,6 +27,6 @@ describe("endpoint search capabilities", () => { // @ts-expect-error genres is not a searchable IGDB endpoint const unsupported: IGDBSearchableEndpointPath = "genres"; - expect(unsupported).toBe("genres"); + void unsupported; }); });