Skip to content

Commit 4df3e97

Browse files
committed
Add typed multi-query builder, single-flight auth, endpoint metadata
- Single-flight Twitch token refresh prevents stampede on cold start - Typed MultiQueryBuilder compiles QueryBuilder blocks into multi-query syntax - QueryBuilder gains inspect()/toJSON() for silent introspection - IGDB_ENDPOINT_METADATA is the single source of truth for paths - Package smoke script validates ESM, CJS, and declaration files - Expanded tests cover auth concurrency, error mapping, validation, drift
1 parent 4bed72c commit 4df3e97

19 files changed

Lines changed: 1131 additions & 335 deletions

File tree

.ai/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# .ai
2+
3+
Machine-readable planning artifacts written by the **strategist** skill.
4+
Each entry is a self-contained plan, audit, or investigation an agent can execute.
5+
6+
## How to consume
7+
1. Read this manifest to find the relevant artifact.
8+
2. Open its folder; read `plan.html` / `audit.html` / `investigation.html` for full context.
9+
3. Hand `executor-prompt.md` to the implementing agent.
10+
11+
## Conventions
12+
- Folders: `<type>/<YYYY-MM-DD>-<slug>/`
13+
- Primary artifacts default to HTML; Markdown versions are optional.
14+
- Every artifact has metadata (`type`, `status`, `target`).
15+
- `status: superseded` means a newer artifact replaces it.
16+
17+
## Artifacts
18+
<!-- newest first; one line per run -->
19+
- 2026-06-25 - [API wrapper improvements audit](audits/2026-06-25-api-wrapper-improvements/audit.html) - status: ready - target: TypeScript IGDB wrapper reliability, ergonomics, endpoint metadata, docs, and release validation.

.ai/audits/2026-06-25-api-wrapper-improvements/audit.html

Lines changed: 194 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
---
2+
type: executor-prompt
3+
title: Improve IGDB Wrapper Reliability and Ergonomics
4+
slug: api-wrapper-improvements
5+
created: 2026-06-25
6+
status: ready
7+
target: TypeScript IGDB wrapper reliability, ergonomics, endpoint metadata, docs, and release validation
8+
related:
9+
- audit.html
10+
---
11+
12+
# Task
13+
14+
Implement the following plan exactly. Do not redesign the approach unless you find a real blocker.
15+
16+
# Context
17+
18+
This repository is `@api-wrappers/igdb-wrapper`, a TypeScript package for IGDB v4. It uses Bun tests, tsdown, strict TypeScript, and `@api-wrappers/api-core` for HTTP transport, retry, rate limiting, auth plugin wiring, timeout, custom fetch, plugins, and logging.
19+
20+
The current baseline is healthy: `bun run typecheck`, `bun test`, and `bun run build` passed during the strategist audit. The package should be improved incrementally, with no broad rewrite and no removal of raw APICalypse escape hatches.
21+
22+
Relevant files:
23+
- `src/auth/AuthManager.ts`
24+
- `src/http/HttpClient.ts`
25+
- `src/client/IGDBClient.ts`
26+
- `src/client/config.ts`
27+
- `src/endpoints/registry.ts`
28+
- `src/endpoints/Endpoint.ts`
29+
- `src/query/QueryBuilder.ts`
30+
- `src/query/compiler.ts`
31+
- `src/query/ast.ts`
32+
- `src/query/helpers.ts`
33+
- `src/query/operators.ts`
34+
- `src/query/fieldProxy.ts`
35+
- `src/types/models.ts`
36+
- `src/types/query.types.ts`
37+
- `src/types/response.types.ts`
38+
- `src/__tests__/igdb-client.test.ts`
39+
- `README.md`, `ROADMAP.md`, and `docs/`
40+
- `package.json`, `tsdown.config.mjs`, and `tsconfig.json`
41+
42+
# Implementation Plan
43+
44+
1. Add single-flight token refresh.
45+
- In `AuthManager`, add a private in-flight refresh promise, for example `#refreshing: Promise<string> | null`.
46+
- Keep the current cache check.
47+
- When the cache is missing or stale, reuse the pending refresh promise if present.
48+
- When starting a new refresh, store the promise and clear it in `finally`.
49+
- Preserve current Twitch token request shape and current `IGDBAuthError` mapping.
50+
- Add a test that fires multiple concurrent IGDB requests against an empty cache and asserts exactly one Twitch token request.
51+
52+
2. Expand error and validation tests.
53+
- Add focused tests for 401 IGDB responses mapping to `IGDBAuthError`.
54+
- Add a failed Twitch token fetch test that maps to `IGDBAuthError`.
55+
- Add tests for non-JSON or string-ish error body formatting if api-core exposes it through `ApiError.responseBody`.
56+
- Add validation tests for invalid `limit()`, `offset()`, `paginate()`, empty `fields()`, and unsupported `count()` on a manually created `QueryBuilder`.
57+
- Add a not-found test for `findById()` or endpoint `firstOrThrow()`.
58+
59+
3. Create endpoint metadata as the single source of truth.
60+
- Introduce structured metadata that binds public key, endpoint path, model type name, and searchability.
61+
- Derive `IGDB_ENDPOINTS` and `IGDB_SEARCHABLE_ENDPOINTS` from that metadata.
62+
- Preserve the existing exported type names: `IGDBEndpointKey`, `IGDBEndpointPath`, and `IGDBSearchableEndpointPath`.
63+
- Preserve all existing endpoint paths and client property names.
64+
- If code generation is used, commit generated output and add a check that fails when generation output is stale.
65+
66+
4. Reduce manual endpoint-client drift.
67+
- Keep existing named `IGDBClient` properties for backward compatibility.
68+
- Either generate the property declarations/assignments from endpoint metadata or add a type-level/runtime drift test that verifies every metadata row has a client property and docs entry.
69+
- Do not replace the named properties with only a generic map.
70+
71+
5. Add a minimal typed multi-query builder.
72+
- Keep `client.multiQuery(rawString)` unchanged.
73+
- Add a small API that supports named list queries and named count queries.
74+
- Reuse existing `QueryBuilder.raw()` output for query bodies instead of reimplementing APICalypse compilation.
75+
- Ensure output matches IGDB multi-query syntax.
76+
- Add tests for compiled body and response typing.
77+
- Document the API in README/docs with one concise example.
78+
79+
6. Improve query introspection behavior.
80+
- Prefer non-printing introspection as the stable path. `raw()` already exists; add `inspect()` or `toJSON()` only if useful.
81+
- If `debug()` and `explain()` remain, document that they write to console and are debug-only.
82+
- Do not introduce unconditional extra console output elsewhere.
83+
84+
7. Add package smoke validation.
85+
- Add a script or test that runs after build, packs the package locally, verifies expected files, imports ESM, requires CJS, and typechecks a minimal consumer snippet.
86+
- Wire it into `verify` if it is fast and deterministic.
87+
- Avoid new dependencies unless the smoke test cannot be done cleanly with Bun/Node built-ins.
88+
89+
8. Update docs and roadmap.
90+
- Update README/docs for typed multi-query usage, debug guidance, and package verification.
91+
- Keep browser credential guidance server-side; do not encourage exposing Twitch client secrets.
92+
- Mark completed roadmap items rather than duplicating them.
93+
94+
# Constraints
95+
96+
- Follow existing project patterns.
97+
- Keep the change focused.
98+
- Do not perform unrelated refactors.
99+
- Preserve existing behavior unless explicitly changed by the plan.
100+
- Reuse existing types, schemas, utilities, services, and components before creating new ones.
101+
- Avoid adding dependencies unless explicitly allowed.
102+
- Do not remove raw APICalypse methods or raw `multiQuery(string)`.
103+
- Do not replace `@api-wrappers/api-core`.
104+
- Keep named endpoint properties source-compatible.
105+
106+
# TypeScript Rules
107+
108+
- Do not use `any`.
109+
- Avoid weakening public types.
110+
- Prefer clear exported types for new public APIs.
111+
- Keep generic types understandable; do not over-engineer typed multi-query.
112+
- Maintain strict TypeScript compatibility.
113+
114+
# Validation
115+
116+
Before finishing:
117+
118+
- Run `bun run typecheck`.
119+
- Run `bun test`.
120+
- Run `bun run build`.
121+
- Run `npm pack --dry-run`.
122+
- Run the new package smoke validation if added.
123+
- Confirm README examples affected by public API changes still typecheck or are otherwise covered.
124+
125+
# Acceptance Criteria
126+
127+
- Concurrent cold-start requests share exactly one Twitch token refresh.
128+
- Existing public API remains source-compatible for current README examples.
129+
- Endpoint metadata has one clear source of truth and tests catch registry/client/searchability drift.
130+
- Typed multi-query supports at least named list queries and named count queries while raw multi-query remains available.
131+
- Error mapping and validation behavior are covered by focused tests.
132+
- Package smoke validation proves ESM, CJS, and declarations work from the built package.
133+
- `bun run typecheck`, `bun test`, `bun run build`, and package verification pass.
134+
135+
# Final Response
136+
137+
When done, respond with:
138+
139+
1. Summary of changes
140+
2. Files changed
141+
3. Commands run
142+
4. Tests/checks completed
143+
5. Any blockers, assumptions, or follow-up recommendations

README.md

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,14 @@ const games = await client.games
104104
## Why use it?
105105

106106
- **Typed query builder:** select fields, compose filters, sort, limit, offset,
107-
inspect the generated APICalypse with `.raw()`, and keep the returned shape
108-
typed.
107+
inspect the generated APICalypse with `.raw()` / `.inspect()`, and keep the
108+
returned shape typed.
109109
- **Endpoint helpers:** every registered IGDB v4 endpoint is exposed as a
110110
camel-cased `IGDBClient` property with `.query()`, `.findById()`,
111111
`.search()`, `.request()`, `.count()`, `.meta()`, and protobuf helpers.
112-
- **Auth:** Twitch client credentials are exchanged and cached for IGDB
113-
requests.
112+
- **Auth:** Twitch client credentials are exchanged, cached, and refreshed with
113+
single-flight concurrency so parallel cold-start requests share one token
114+
fetch.
114115
- **Retries:** transient failures are retried through `@api-wrappers/api-core`.
115116
- **Rate limiting:** the default limiter matches IGDB's documented request and
116117
concurrency limits.
@@ -133,7 +134,8 @@ const games = await client.games
133134
`.query()`, `.findMany()`, `.findById()`, `.search()`, `.request()`,
134135
`.count()`, `.meta()`, and `.requestProtobuf()`.
135136
- Client-level helpers cover custom endpoint access, raw requests, counts,
136-
metadata, protobuf responses, raw multi-query bodies, and webhooks.
137+
metadata, protobuf responses, raw or builder-based multi-query bodies, and
138+
webhooks.
137139
- Utility exports include IGDB image URL building and tag-number creation.
138140

139141
See [Endpoints](./docs/endpoints.md) and [API Reference](./docs/api-reference.md)
@@ -236,8 +238,21 @@ const coverUrl = game.cover?.imageId
236238

237239
### Send a multi-query request
238240

239-
`multiQuery()` currently accepts IGDB's raw multi-query body. A typed
240-
multi-query builder is tracked in the roadmap.
241+
Use `multiQueryBuilder()` when each block can be expressed with the normal
242+
query builder. It compiles through the same `.raw()` path as regular queries:
243+
244+
```ts
245+
const query = client
246+
.multiQueryBuilder()
247+
.query(client.games, "Top Games", (games) =>
248+
games.fields("name", "rating").sort((game) => game.rating, "desc").limit(5),
249+
)
250+
.count(client.platforms, "Platform Count");
251+
252+
const response = await client.multiQuery(query);
253+
```
254+
255+
Raw IGDB multi-query bodies remain supported for full APICalypse compatibility:
241256

242257
```ts
243258
const response = await client.multiQuery(`

ROADMAP.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ raw IGDB access when needed.
99
- Typed query builder for field selection, filters, sorting, limits, offsets,
1010
counts, and async pagination.
1111
- Endpoint helpers for registered IGDB v4 endpoints.
12-
- Twitch credential auth with token caching.
12+
- Metadata-driven endpoint helpers for registered IGDB v4 endpoints.
13+
- Twitch credential auth with token caching and single-flight refresh.
1314
- Retry, timeout, plugin, and rate-limit behavior through
1415
`@api-wrappers/api-core`.
1516
- Structured errors for auth, rate limit, not found, and validation cases.
16-
- Image URL, tag number, meta, protobuf, webhook, and raw multi-query helpers.
17+
- Image URL, tag number, meta, protobuf, webhook, raw multi-query, and typed
18+
multi-query builder helpers.
1719

1820
## Near Term
1921

@@ -22,28 +24,26 @@ raw IGDB access when needed.
2224
- Improve generated model docs so users can discover IGDB fields without
2325
opening source files.
2426
- Add tests that assert README and docs snippets compile where practical.
25-
- Add package-content smoke tests for the published tarball.
2627
- Expand endpoint docs with the searchable endpoints and common field sets.
2728

2829
## Query Builder Ergonomics
2930

30-
- Explore a typed multi-query builder. `client.multiQuery()` currently accepts
31-
IGDB's raw multi-query body, which is accurate but still string-based.
31+
- Expand the typed multi-query builder with more result-shape examples while
32+
keeping raw `client.multiQuery()` bodies supported.
3233
- Improve relation-array result typing for `select()` examples that fetch nested
3334
arrays such as `platforms.name` or `genres.name`.
3435
- Consider clearer helpers for IGDB array/set expressions so fewer examples need
3536
`whereRaw()`.
3637
- Add date helper recipes for converting JavaScript `Date` values to IGDB Unix
3738
timestamps.
38-
- Add optional query snippet tooling that prints the generated APICalypse for
39-
docs and debugging.
39+
- Add optional query snippet tooling that captures generated APICalypse for
40+
docs from `.inspect()` payloads.
4041

4142
## Reliability and Trust
4243

4344
- Keep CI running typecheck, tests, build, and package smoke checks.
4445
- Maintain a release-readiness checklist for every release.
45-
- Add more structured error tests around HTTP status handling and retry
46-
exhaustion.
46+
- Add more structured error tests around retry exhaustion.
4747
- Document supported runtimes and compatibility expectations.
4848

4949
## Not Planned Right Now

docs/api-reference.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ class IGDBClient {
2020
count(endpoint: string, query?: string): Promise<number>;
2121
meta(endpoint: string): Promise<MetaField[]>;
2222
requestProtobuf(endpoint: string, query: string): Promise<ArrayBuffer>;
23-
multiQuery<T = IGDBAnyEntity>(query: string): Promise<Array<MultiQueryResult<T>>>;
23+
multiQuery<T = IGDBAnyEntity>(query: string | MultiQueryBuilder): Promise<Array<MultiQueryResult<T>>>;
24+
multiQueryBuilder(): MultiQueryBuilder;
2425
createWebhook(endpoint: string, options: CreateWebhookOptions): Promise<Webhook>;
2526
listWebhooks(): Promise<Webhook[]>;
2627
getWebhook(id: number | string): Promise<Webhook>;
@@ -94,11 +95,57 @@ class QueryBuilder<TModel, TShape = TModel> {
9495

9596
// Debug
9697
raw(): string;
98+
inspect(): QueryDebugInfo;
99+
toJSON(): QueryDebugInfo;
97100
debug(): this;
98101
explain(): this;
99102
}
100103
```
101104

105+
```ts
106+
interface QueryDebugInfo {
107+
ast: QueryAST;
108+
query: string;
109+
}
110+
```
111+
112+
---
113+
114+
## MultiQueryBuilder
115+
116+
```ts
117+
class MultiQueryBuilder {
118+
query<TModel extends IGDBEntity>(
119+
endpoint: IGDBEndpoint<TModel>,
120+
name: string,
121+
build: (query: QueryBuilder<TModel>) => QueryBuilder<TModel, unknown>
122+
): MultiQueryBuilder;
123+
query<TModel extends IGDBEntity = IGDBEntity>(
124+
endpoint: string,
125+
name: string,
126+
build: (query: QueryBuilder<TModel>) => QueryBuilder<TModel, unknown>
127+
): MultiQueryBuilder;
128+
count<TModel extends IGDBEntity>(
129+
endpoint: IGDBEndpoint<TModel>,
130+
name: string,
131+
build?: (query: QueryBuilder<TModel>) => QueryBuilder<TModel, unknown>
132+
): MultiQueryBuilder;
133+
count<TModel extends IGDBEntity = IGDBEntity>(
134+
endpoint: string,
135+
name: string,
136+
build?: (query: QueryBuilder<TModel>) => QueryBuilder<TModel, unknown>
137+
): MultiQueryBuilder;
138+
raw(): string;
139+
inspect(): MultiQueryDebugInfo;
140+
toJSON(): MultiQueryDebugInfo;
141+
}
142+
143+
interface MultiQueryDebugInfo {
144+
blocks: MultiQueryBlock[];
145+
query: string;
146+
}
147+
```
148+
102149
---
103150

104151
## WhereHelpers

docs/endpoints.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ method uniformly so new searchable endpoints do not require a wrapper release;
1919
the current documented searchable endpoints are exported as
2020
`IGDB_SEARCHABLE_ENDPOINTS`.
2121

22+
Endpoint properties and path exports are derived from
23+
`IGDB_ENDPOINT_METADATA`, which is exported for tooling that needs the wrapper's
24+
registered endpoint list without constructing a client.
25+
2226
Use endpoint properties for known IGDB resources:
2327

2428
```ts

0 commit comments

Comments
 (0)