open-alex-wrapper 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +437 -0
- package/dist/cli.cjs +1880 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +31 -0
- package/dist/cli.d.ts +31 -0
- package/dist/cli.js +1848 -0
- package/dist/cli.js.map +1 -0
- package/dist/client-CLRZTPFv.d.cts +1266 -0
- package/dist/client-CLRZTPFv.d.ts +1266 -0
- package/dist/index.cjs +1831 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +105 -0
- package/dist/index.d.ts +105 -0
- package/dist/index.js +1750 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,1266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional response caching.
|
|
3
|
+
*
|
|
4
|
+
* A tiny dependency-free LRU with per-entry TTL, plus a {@link CacheStore}
|
|
5
|
+
* interface so callers can plug in Redis, a file cache, etc. Caching is
|
|
6
|
+
* opt-in (`cache: true` or a custom store) and only ever applies to GETs.
|
|
7
|
+
*/
|
|
8
|
+
interface CacheStore {
|
|
9
|
+
get(key: string): unknown | undefined | Promise<unknown | undefined>;
|
|
10
|
+
set(key: string, value: unknown, ttlMs: number): void | Promise<void>;
|
|
11
|
+
delete?(key: string): void | Promise<void>;
|
|
12
|
+
clear?(): void | Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
/** In-memory LRU cache with TTL eviction. */
|
|
15
|
+
declare class LRUCache implements CacheStore {
|
|
16
|
+
private readonly maxEntries;
|
|
17
|
+
private readonly map;
|
|
18
|
+
constructor(maxEntries?: number);
|
|
19
|
+
get(key: string): unknown | undefined;
|
|
20
|
+
set(key: string, value: unknown, ttlMs: number): void;
|
|
21
|
+
delete(key: string): void;
|
|
22
|
+
clear(): void;
|
|
23
|
+
get size(): number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Budget-aware adaptive throttle.
|
|
28
|
+
*
|
|
29
|
+
* Paces requests to a target rate (default 10/s) and *automatically slows down
|
|
30
|
+
* as the daily budget runs low* — so a long extraction glides to the limit
|
|
31
|
+
* instead of slamming into a 429/402. A `minRemainingUsd` floor hard-stops
|
|
32
|
+
* before the budget is fully exhausted.
|
|
33
|
+
*
|
|
34
|
+
* The clock is injectable so pacing is deterministically testable.
|
|
35
|
+
*/
|
|
36
|
+
interface ResolvedThrottle {
|
|
37
|
+
/** Target requests/second (spacing = 1000 / rps). */
|
|
38
|
+
maxRequestsPerSecond: number;
|
|
39
|
+
/** Throw {@link BudgetExceededError} once remaining budget drops below this. */
|
|
40
|
+
minRemainingUsd?: number;
|
|
41
|
+
}
|
|
42
|
+
interface ThrottleState {
|
|
43
|
+
remainingUsd: number;
|
|
44
|
+
dailyLimitUsd: number;
|
|
45
|
+
}
|
|
46
|
+
interface Clock {
|
|
47
|
+
now(): number;
|
|
48
|
+
sleep(ms: number): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
declare const REAL_CLOCK: Clock;
|
|
51
|
+
declare class Throttle {
|
|
52
|
+
private readonly opts;
|
|
53
|
+
private readonly clock;
|
|
54
|
+
private nextAllowedAt;
|
|
55
|
+
constructor(opts: ResolvedThrottle, clock?: Clock);
|
|
56
|
+
/**
|
|
57
|
+
* Wait until the next request is allowed to start. Enforces the target rate
|
|
58
|
+
* and multiplies spacing when the remaining budget fraction is low.
|
|
59
|
+
*/
|
|
60
|
+
gate(state: ThrottleState): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Client configuration and default resolution.
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
interface ThrottleOptions {
|
|
68
|
+
/** Target requests/second. Default 10. */
|
|
69
|
+
maxRequestsPerSecond?: number;
|
|
70
|
+
/** Hard-stop (throws BudgetExceededError) once remaining USD drops below this. */
|
|
71
|
+
minRemainingUsd?: number;
|
|
72
|
+
}
|
|
73
|
+
type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
|
|
74
|
+
interface Logger {
|
|
75
|
+
debug(message: string, ...args: unknown[]): void;
|
|
76
|
+
info(message: string, ...args: unknown[]): void;
|
|
77
|
+
warn(message: string, ...args: unknown[]): void;
|
|
78
|
+
error(message: string, ...args: unknown[]): void;
|
|
79
|
+
}
|
|
80
|
+
interface RetryPolicy {
|
|
81
|
+
/** Max retry attempts after the first try (default 5). */
|
|
82
|
+
maxRetries: number;
|
|
83
|
+
/** Base backoff delay in ms (default 500). */
|
|
84
|
+
baseDelayMs: number;
|
|
85
|
+
/** Ceiling on any single backoff delay (default 20_000). */
|
|
86
|
+
maxDelayMs: number;
|
|
87
|
+
/** Status codes that trigger a retry (default 429, 500, 502, 503, 504). */
|
|
88
|
+
retryOnStatus: number[];
|
|
89
|
+
}
|
|
90
|
+
interface OpenAlexConfig {
|
|
91
|
+
/**
|
|
92
|
+
* OpenAlex API key. Required by OpenAlex for all requests since Feb 2026.
|
|
93
|
+
* Falls back to `process.env.OPENALEX_API_KEY`.
|
|
94
|
+
*/
|
|
95
|
+
apiKey?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Contact email for the "polite pool". Sent as `mailto` and in User-Agent.
|
|
98
|
+
* Falls back to `process.env.OPENALEX_MAILTO`.
|
|
99
|
+
*/
|
|
100
|
+
mailto?: string;
|
|
101
|
+
/** API base URL. Default `https://api.openalex.org`. */
|
|
102
|
+
baseUrl?: string;
|
|
103
|
+
/** Extra User-Agent identifier for your app. */
|
|
104
|
+
userAgent?: string;
|
|
105
|
+
/** Per-request timeout in ms. Default 60_000. */
|
|
106
|
+
timeoutMs?: number;
|
|
107
|
+
/** Retry/backoff behaviour. */
|
|
108
|
+
retry?: Partial<RetryPolicy>;
|
|
109
|
+
/** Max concurrent in-flight requests. Default 8. */
|
|
110
|
+
concurrency?: number;
|
|
111
|
+
/**
|
|
112
|
+
* Soft daily budget in USD. When set, any call (or estimated extraction)
|
|
113
|
+
* that would cross it throws `BudgetExceededError` *before* spending.
|
|
114
|
+
*/
|
|
115
|
+
dailyBudgetUsd?: number;
|
|
116
|
+
/** Called when spend crosses 80% / 100% of `dailyBudgetUsd`. */
|
|
117
|
+
onBudgetWarning?: (info: {
|
|
118
|
+
spentUsd: number;
|
|
119
|
+
limitUsd: number;
|
|
120
|
+
ratio: number;
|
|
121
|
+
}) => void;
|
|
122
|
+
/** `true` for the built-in LRU, or a custom {@link CacheStore}. GET-only. */
|
|
123
|
+
cache?: boolean | CacheStore;
|
|
124
|
+
/** Cache TTL in ms. Default 300_000 (5 min). */
|
|
125
|
+
cacheTtlMs?: number;
|
|
126
|
+
/**
|
|
127
|
+
* Budget-aware adaptive throttle. `true` = 10 req/s with auto-slowdown near
|
|
128
|
+
* the daily budget; or fine-tune with {@link ThrottleOptions}.
|
|
129
|
+
*/
|
|
130
|
+
throttle?: boolean | ThrottleOptions;
|
|
131
|
+
/** Inject a custom fetch (used for testing / proxies). Default global fetch. */
|
|
132
|
+
fetch?: FetchLike;
|
|
133
|
+
/** Optional logger. Default: no-op. */
|
|
134
|
+
logger?: Logger;
|
|
135
|
+
}
|
|
136
|
+
interface ResolvedConfig {
|
|
137
|
+
apiKey?: string;
|
|
138
|
+
mailto?: string;
|
|
139
|
+
baseUrl: string;
|
|
140
|
+
userAgent: string;
|
|
141
|
+
timeoutMs: number;
|
|
142
|
+
retry: RetryPolicy;
|
|
143
|
+
concurrency: number;
|
|
144
|
+
dailyBudgetUsd?: number;
|
|
145
|
+
onBudgetWarning?: (info: {
|
|
146
|
+
spentUsd: number;
|
|
147
|
+
limitUsd: number;
|
|
148
|
+
ratio: number;
|
|
149
|
+
}) => void;
|
|
150
|
+
cache?: CacheStore;
|
|
151
|
+
cacheTtlMs: number;
|
|
152
|
+
throttle?: ResolvedThrottle;
|
|
153
|
+
fetch: FetchLike;
|
|
154
|
+
logger: Logger;
|
|
155
|
+
}
|
|
156
|
+
declare function resolveConfig(config?: OpenAlexConfig): ResolvedConfig;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Cost & budget awareness.
|
|
160
|
+
*
|
|
161
|
+
* OpenAlex moved to usage-based pricing in Feb 2026. This module models that
|
|
162
|
+
* pricing so the SDK can (a) *estimate the dollar cost of an extraction before
|
|
163
|
+
* you run it* and (b) *track live, authoritative spend* from response headers.
|
|
164
|
+
*
|
|
165
|
+
* Prices (USD), per the OpenAlex pricing announcement:
|
|
166
|
+
* - single entity lookup .......... $0 (unlimited)
|
|
167
|
+
* - list / filter ................. $0.0001 / call (1 credit)
|
|
168
|
+
* - search ........................ $0.001 / call
|
|
169
|
+
* - PDF / XML full-text download .. $0.01 / call
|
|
170
|
+
*
|
|
171
|
+
* Free daily allowance: $1.00/day with an API key, $0.10/day without one.
|
|
172
|
+
*
|
|
173
|
+
* OpenAlex reports usage on every response via `x-ratelimit-*` headers
|
|
174
|
+
* (confirmed against a live response):
|
|
175
|
+
* x-ratelimit-cost-usd cost of this request
|
|
176
|
+
* x-ratelimit-remaining-usd USD left in today's budget
|
|
177
|
+
* x-ratelimit-limit-usd today's USD budget
|
|
178
|
+
* x-ratelimit-credits-used credits this request cost
|
|
179
|
+
* x-ratelimit-remaining credits left today
|
|
180
|
+
* x-ratelimit-limit daily credit limit
|
|
181
|
+
* x-ratelimit-prepaid-remaining-usd prepaid balance (beyond the free tier)
|
|
182
|
+
* x-ratelimit-reset seconds until the budget resets
|
|
183
|
+
* {@link UsageTracker} prefers these authoritative values and falls back to
|
|
184
|
+
* the pricing model above when headers are absent.
|
|
185
|
+
*/
|
|
186
|
+
type RequestType = 'get' | 'list' | 'search' | 'download' | 'autocomplete';
|
|
187
|
+
/** USD price per API call, keyed by request type (fallback when no headers). */
|
|
188
|
+
declare const PRICING_USD: Record<RequestType, number>;
|
|
189
|
+
declare const FREE_DAILY_USD_WITH_KEY = 1;
|
|
190
|
+
declare const FREE_DAILY_USD_WITHOUT_KEY = 0.1;
|
|
191
|
+
/** Documented free-tier daily caps (informational). */
|
|
192
|
+
declare const FREE_TIER_LIMITS: {
|
|
193
|
+
readonly singleLookupCalls: number;
|
|
194
|
+
readonly listCalls: 10000;
|
|
195
|
+
readonly listResults: 1000000;
|
|
196
|
+
readonly searchCalls: 1000;
|
|
197
|
+
readonly searchResults: 100000;
|
|
198
|
+
readonly downloadCalls: 100;
|
|
199
|
+
};
|
|
200
|
+
/** Basic (page-number) paging can only reach the first 10,000 results. */
|
|
201
|
+
declare const BASIC_PAGING_MAX_RESULTS = 10000;
|
|
202
|
+
/** Estimated cost of a bulk extraction, produced *before* running it. */
|
|
203
|
+
interface CostEstimate {
|
|
204
|
+
requestType: RequestType;
|
|
205
|
+
/** `meta.count` — total results matching the query. */
|
|
206
|
+
matchingResults: number;
|
|
207
|
+
/** How many of those you can actually page through with this strategy. */
|
|
208
|
+
fetchableResults: number;
|
|
209
|
+
perPage: number;
|
|
210
|
+
/** Page-fetch calls needed to walk `fetchableResults`. */
|
|
211
|
+
pageCalls: number;
|
|
212
|
+
/** USD for the page fetches. */
|
|
213
|
+
pagesCostUsd: number;
|
|
214
|
+
/** The single cheap count probe already spent to produce this estimate. */
|
|
215
|
+
probeCostUsd: number;
|
|
216
|
+
/** `pagesCostUsd + probeCostUsd`. */
|
|
217
|
+
totalCostUsd: number;
|
|
218
|
+
/** True when basic paging clipped the result set at 10k. */
|
|
219
|
+
cappedByBasicPaging: boolean;
|
|
220
|
+
note?: string;
|
|
221
|
+
}
|
|
222
|
+
interface EstimateInput {
|
|
223
|
+
count: number;
|
|
224
|
+
requestType: RequestType;
|
|
225
|
+
perPage?: number;
|
|
226
|
+
strategy?: 'basic' | 'cursor';
|
|
227
|
+
}
|
|
228
|
+
/** Pure cost math: given a result `count`, project the extraction cost. */
|
|
229
|
+
declare function estimateExtractionCost(input: EstimateInput): CostEstimate;
|
|
230
|
+
/** OpenAlex accepts per_page in [1, 200]. */
|
|
231
|
+
declare function clampPerPage(n: number): number;
|
|
232
|
+
interface UsageByType {
|
|
233
|
+
calls: number;
|
|
234
|
+
results: number;
|
|
235
|
+
estimatedCostUsd: number;
|
|
236
|
+
}
|
|
237
|
+
interface UsageSnapshot {
|
|
238
|
+
totalCalls: number;
|
|
239
|
+
/** Spend estimated from the pricing model. */
|
|
240
|
+
estimatedSpentUsd: number;
|
|
241
|
+
/** Authoritative spend today (limit − remaining), from headers, if seen. */
|
|
242
|
+
reportedSpentUsd: number | null;
|
|
243
|
+
/** Authoritative remaining budget today, from headers, if seen. */
|
|
244
|
+
reportedRemainingUsd: number | null;
|
|
245
|
+
/** Prepaid balance beyond the free tier, from headers, if seen. */
|
|
246
|
+
prepaidRemainingUsd: number | null;
|
|
247
|
+
/** Credits remaining today, from headers, if seen. */
|
|
248
|
+
creditsRemaining: number | null;
|
|
249
|
+
/** Seconds until the daily budget resets, from headers, if seen. */
|
|
250
|
+
resetSeconds: number | null;
|
|
251
|
+
/** Cost of the most recent request (USD), from headers, if seen. */
|
|
252
|
+
lastRequestCostUsd: number | null;
|
|
253
|
+
dailyLimitUsd: number;
|
|
254
|
+
/** Best available spend = reported, else estimated. */
|
|
255
|
+
spentUsd: number;
|
|
256
|
+
/** Best available remaining = reported, else limit − spent. */
|
|
257
|
+
remainingUsd: number;
|
|
258
|
+
byType: Record<RequestType, UsageByType>;
|
|
259
|
+
hasApiKey: boolean;
|
|
260
|
+
}
|
|
261
|
+
type Headerish = Headers | Map<string, string> | Record<string, string | string[] | undefined> | undefined;
|
|
262
|
+
interface ParsedUsageHeaders {
|
|
263
|
+
costUsd?: number;
|
|
264
|
+
costRequiredUsd?: number;
|
|
265
|
+
remainingUsd?: number;
|
|
266
|
+
limitUsd?: number;
|
|
267
|
+
prepaidRemainingUsd?: number;
|
|
268
|
+
creditsUsed?: number;
|
|
269
|
+
creditsRemaining?: number;
|
|
270
|
+
creditsLimit?: number;
|
|
271
|
+
resetSeconds?: number;
|
|
272
|
+
raw: Record<string, string>;
|
|
273
|
+
}
|
|
274
|
+
/** Extract OpenAlex `x-ratelimit-*` usage headers into structured numbers. */
|
|
275
|
+
declare function parseUsageHeaders(headers: Headerish): ParsedUsageHeaders;
|
|
276
|
+
/**
|
|
277
|
+
* Accumulates spend across a client session.
|
|
278
|
+
*
|
|
279
|
+
* When OpenAlex sends `x-ratelimit-*` headers, the tracker reports authoritative
|
|
280
|
+
* spend/remaining/limit straight from them; otherwise it falls back to the
|
|
281
|
+
* pricing model.
|
|
282
|
+
*/
|
|
283
|
+
declare class UsageTracker {
|
|
284
|
+
private readonly hasApiKey;
|
|
285
|
+
private readonly dailyBudgetUsd?;
|
|
286
|
+
private byType;
|
|
287
|
+
private estimatedSpentUsd;
|
|
288
|
+
private reportedRemainingUsd;
|
|
289
|
+
private reportedLimitUsd;
|
|
290
|
+
private prepaidRemainingUsd;
|
|
291
|
+
private creditsRemaining;
|
|
292
|
+
private resetSeconds;
|
|
293
|
+
private lastRequestCostUsd;
|
|
294
|
+
private lastRawUsageHeaders;
|
|
295
|
+
constructor(hasApiKey: boolean, dailyBudgetUsd?: number | undefined);
|
|
296
|
+
/** The daily allowance in effect: OpenAlex-reported, else soft cap, else free tier. */
|
|
297
|
+
get dailyLimitUsd(): number;
|
|
298
|
+
/** Authoritative spend today if headers seen (limit − remaining), else estimate. */
|
|
299
|
+
get spentUsd(): number;
|
|
300
|
+
get remainingUsd(): number;
|
|
301
|
+
/** Record one completed request. Called by the HTTP layer. */
|
|
302
|
+
record(type: RequestType, opts?: {
|
|
303
|
+
results?: number;
|
|
304
|
+
headers?: Headerish;
|
|
305
|
+
}): void;
|
|
306
|
+
/**
|
|
307
|
+
* Throw {@link BudgetExceededError} if spending `additionalUsd` more would
|
|
308
|
+
* cross the soft cap. Only enforced when `dailyBudgetUsd` was configured.
|
|
309
|
+
*/
|
|
310
|
+
assertWithinBudget(additionalUsd: number): void;
|
|
311
|
+
/** Immutable point-in-time view of usage. */
|
|
312
|
+
snapshot(): UsageSnapshot;
|
|
313
|
+
/** The last set of raw `x-ratelimit-*` headers OpenAlex sent (for debugging). */
|
|
314
|
+
get lastUsageHeaders(): Record<string, string>;
|
|
315
|
+
/** Human-readable multi-line budget report. */
|
|
316
|
+
report(): string;
|
|
317
|
+
reset(): void;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Low-level HTTP transport.
|
|
322
|
+
*
|
|
323
|
+
* Responsibilities:
|
|
324
|
+
* - build request URLs (api_key + mailto injection, safe encoding)
|
|
325
|
+
* - bound concurrency with a {@link Semaphore}
|
|
326
|
+
* - retry with exponential backoff + jitter, honouring `Retry-After`
|
|
327
|
+
* - per-request timeout via `AbortController`
|
|
328
|
+
* - optional GET response caching
|
|
329
|
+
* - record spend/usage into the {@link UsageTracker}
|
|
330
|
+
* - normalize failures into the typed error hierarchy
|
|
331
|
+
*/
|
|
332
|
+
|
|
333
|
+
interface RequestOptions {
|
|
334
|
+
/** Path after the base URL, e.g. `works` or `works/W123`. Not encoded by caller. */
|
|
335
|
+
path: string;
|
|
336
|
+
/** Already-serialized OpenAlex query params (correct param names). */
|
|
337
|
+
params?: Record<string, string | undefined>;
|
|
338
|
+
/** Cost classification for budgeting. */
|
|
339
|
+
requestType: RequestType;
|
|
340
|
+
/** Bypass the response cache for this call. */
|
|
341
|
+
noCache?: boolean;
|
|
342
|
+
/** Extra fetch signal to compose with the internal timeout signal. */
|
|
343
|
+
signal?: AbortSignal;
|
|
344
|
+
}
|
|
345
|
+
interface HttpResponse<T> {
|
|
346
|
+
data: T;
|
|
347
|
+
status: number;
|
|
348
|
+
headers: Headers;
|
|
349
|
+
url: string;
|
|
350
|
+
cached: boolean;
|
|
351
|
+
}
|
|
352
|
+
declare class HttpClient {
|
|
353
|
+
readonly config: ResolvedConfig;
|
|
354
|
+
readonly usage: UsageTracker;
|
|
355
|
+
private readonly semaphore;
|
|
356
|
+
private readonly firedWarnings;
|
|
357
|
+
private readonly throttle?;
|
|
358
|
+
constructor(config: ResolvedConfig, usage: UsageTracker);
|
|
359
|
+
private gateThrottle;
|
|
360
|
+
/** Build the full request URL plus a redacted variant (api_key hidden). */
|
|
361
|
+
buildUrl(path: string, params?: Record<string, string | undefined>): {
|
|
362
|
+
url: string;
|
|
363
|
+
redacted: string;
|
|
364
|
+
};
|
|
365
|
+
/** Cache key excludes api_key so a shared cache isn't keyed to a secret. */
|
|
366
|
+
private cacheKey;
|
|
367
|
+
request<T>(opts: RequestOptions): Promise<HttpResponse<T>>;
|
|
368
|
+
private fetchWithRetry;
|
|
369
|
+
/** Shared GET-with-retry core: returns the ok `Response` or throws typed. */
|
|
370
|
+
private sendWithRetry;
|
|
371
|
+
/** Append api_key + mailto to any absolute URL (for the content host, etc.). */
|
|
372
|
+
authorizeUrl(rawUrl: string): {
|
|
373
|
+
url: string;
|
|
374
|
+
redacted: string;
|
|
375
|
+
};
|
|
376
|
+
/**
|
|
377
|
+
* Download a binary resource (e.g. a full-text PDF/XML from the content host).
|
|
378
|
+
* Budget-asserted and usage-recorded like any other request.
|
|
379
|
+
*/
|
|
380
|
+
download(opts: {
|
|
381
|
+
url: string;
|
|
382
|
+
redacted?: string;
|
|
383
|
+
requestType: RequestType;
|
|
384
|
+
dest?: string;
|
|
385
|
+
signal?: AbortSignal;
|
|
386
|
+
}): Promise<{
|
|
387
|
+
bytes?: Uint8Array;
|
|
388
|
+
path?: string;
|
|
389
|
+
status: number;
|
|
390
|
+
headers: Headers;
|
|
391
|
+
contentType?: string;
|
|
392
|
+
}>;
|
|
393
|
+
private shouldRetry;
|
|
394
|
+
private backoff;
|
|
395
|
+
private maybeWarnBudget;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Shared response shapes returned by OpenAlex list/group endpoints.
|
|
400
|
+
*
|
|
401
|
+
* Entity interfaces (see `./entities`) are intentionally "open" (they carry an
|
|
402
|
+
* index signature) so that new fields added by OpenAlex never break typing —
|
|
403
|
+
* the raw payload is always reachable.
|
|
404
|
+
*/
|
|
405
|
+
/** The `meta` block attached to every list response. */
|
|
406
|
+
interface Meta {
|
|
407
|
+
count: number;
|
|
408
|
+
db_response_time_ms: number;
|
|
409
|
+
page: number | null;
|
|
410
|
+
per_page: number;
|
|
411
|
+
/** Present only when cursor paging is active. `null` marks the last page. */
|
|
412
|
+
next_cursor?: string | null;
|
|
413
|
+
/** Present only when `groupBy` is active. */
|
|
414
|
+
groups_count?: number | null;
|
|
415
|
+
[key: string]: unknown;
|
|
416
|
+
}
|
|
417
|
+
/** A paged list response: `{ meta, results }` (+ optional `group_by`). */
|
|
418
|
+
interface ListResult<T> {
|
|
419
|
+
meta: Meta;
|
|
420
|
+
results: T[];
|
|
421
|
+
group_by?: GroupResult[];
|
|
422
|
+
}
|
|
423
|
+
/** One bucket from a `group_by` aggregation. */
|
|
424
|
+
interface GroupResult {
|
|
425
|
+
key: string;
|
|
426
|
+
key_display_name: string;
|
|
427
|
+
count: number;
|
|
428
|
+
}
|
|
429
|
+
/** Response from any `group_by` query: `{ meta, group_by }`. */
|
|
430
|
+
interface GroupByResult {
|
|
431
|
+
meta: Meta;
|
|
432
|
+
group_by: GroupResult[];
|
|
433
|
+
}
|
|
434
|
+
/** One suggestion from an autocomplete endpoint. */
|
|
435
|
+
interface AutocompleteResult {
|
|
436
|
+
id: string;
|
|
437
|
+
display_name: string;
|
|
438
|
+
hint?: string;
|
|
439
|
+
cited_by_count?: number;
|
|
440
|
+
works_count?: number;
|
|
441
|
+
entity_type?: string;
|
|
442
|
+
external_id?: string | null;
|
|
443
|
+
[key: string]: unknown;
|
|
444
|
+
}
|
|
445
|
+
/** The eight queryable OpenAlex entity types. */
|
|
446
|
+
type EntityType = 'works' | 'authors' | 'sources' | 'institutions' | 'topics' | 'keywords' | 'publishers' | 'funders';
|
|
447
|
+
/** Anything with an OpenAlex `id`; the common shape every entity shares. */
|
|
448
|
+
interface OpenAlexEntity {
|
|
449
|
+
id: string;
|
|
450
|
+
display_name?: string;
|
|
451
|
+
[key: string]: unknown;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* The narrow interface a QueryBuilder / paginator needs to execute requests,
|
|
456
|
+
* without depending on the concrete client. `EntityEndpoint` implements it.
|
|
457
|
+
*/
|
|
458
|
+
|
|
459
|
+
interface FetchListOptions {
|
|
460
|
+
requestType?: RequestType;
|
|
461
|
+
noCache?: boolean;
|
|
462
|
+
signal?: AbortSignal;
|
|
463
|
+
}
|
|
464
|
+
interface EntityRunner<T> {
|
|
465
|
+
readonly entityType: EntityType;
|
|
466
|
+
readonly logger: Logger;
|
|
467
|
+
/** Default concurrency to use for parallel paging when the caller doesn't specify. */
|
|
468
|
+
readonly defaultConcurrency: number;
|
|
469
|
+
fetchList(params: Record<string, string | undefined>, opts?: FetchListOptions): Promise<ListResult<T>>;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Resumable extraction checkpoints.
|
|
474
|
+
*
|
|
475
|
+
* A long cursor-paged extraction can persist its position after every page, so
|
|
476
|
+
* if it crashes, hits a rate limit, or you Ctrl-C it, the next run resumes from
|
|
477
|
+
* the last saved cursor instead of starting over — which also means you never
|
|
478
|
+
* re-pay for pages you already fetched.
|
|
479
|
+
*/
|
|
480
|
+
interface CheckpointState {
|
|
481
|
+
/** The next cursor to fetch (`null` once the extraction has finished). */
|
|
482
|
+
cursor: string | null;
|
|
483
|
+
/** How many items had been emitted when this state was saved. */
|
|
484
|
+
emitted: number;
|
|
485
|
+
}
|
|
486
|
+
interface Checkpoint {
|
|
487
|
+
load(): CheckpointState | undefined;
|
|
488
|
+
save(state: CheckpointState): void;
|
|
489
|
+
clear(): void;
|
|
490
|
+
}
|
|
491
|
+
/** A file-backed {@link Checkpoint} (JSON on disk, synchronous). */
|
|
492
|
+
declare class FileCheckpoint implements Checkpoint {
|
|
493
|
+
private readonly path;
|
|
494
|
+
constructor(path: string);
|
|
495
|
+
load(): CheckpointState | undefined;
|
|
496
|
+
save(state: CheckpointState): void;
|
|
497
|
+
clear(): void;
|
|
498
|
+
}
|
|
499
|
+
/** An in-memory {@link Checkpoint} (useful for tests or custom persistence). */
|
|
500
|
+
declare class MemoryCheckpoint implements Checkpoint {
|
|
501
|
+
private state;
|
|
502
|
+
load(): CheckpointState | undefined;
|
|
503
|
+
save(state: CheckpointState): void;
|
|
504
|
+
clear(): void;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Pagination strategies as async generators.
|
|
509
|
+
*
|
|
510
|
+
* - cursor paging → unlimited depth, sequential (the safe default for `.all()`)
|
|
511
|
+
* - basic paging → first 10k results, sequential or parallel
|
|
512
|
+
*
|
|
513
|
+
* Parallel basic paging fires every page request up front; the HTTP layer's
|
|
514
|
+
* semaphore caps how many are actually in flight, and pages are yielded in
|
|
515
|
+
* order. It's the fast path for the very common "≤10k results" case.
|
|
516
|
+
*/
|
|
517
|
+
|
|
518
|
+
interface ProgressInfo {
|
|
519
|
+
/** Items emitted so far (cumulative, includes any resumed count). */
|
|
520
|
+
emitted: number;
|
|
521
|
+
/** Pages fetched so far in this run. */
|
|
522
|
+
page: number;
|
|
523
|
+
/** Total matching results (`meta.count`), when known. */
|
|
524
|
+
total: number | null;
|
|
525
|
+
}
|
|
526
|
+
interface StreamOptions {
|
|
527
|
+
/** Results per page (1–200). Default 200 for max throughput. */
|
|
528
|
+
perPage?: number;
|
|
529
|
+
/** Stop after collecting this many items. */
|
|
530
|
+
maxResults?: number;
|
|
531
|
+
/** Stop after this many page requests. */
|
|
532
|
+
maxPages?: number;
|
|
533
|
+
/**
|
|
534
|
+
* 'cursor' (unlimited, sequential), 'basic' (≤10k), or 'auto'.
|
|
535
|
+
* 'auto' picks basic when `parallel` is set, else cursor.
|
|
536
|
+
*/
|
|
537
|
+
strategy?: 'auto' | 'cursor' | 'basic';
|
|
538
|
+
/** Use parallel basic paging (fast; capped at 10k results). */
|
|
539
|
+
parallel?: boolean;
|
|
540
|
+
signal?: AbortSignal;
|
|
541
|
+
/** Cost classification passed through to the tracker. */
|
|
542
|
+
requestType?: RequestType;
|
|
543
|
+
/** Called once per page with cumulative progress. */
|
|
544
|
+
onProgress?: (info: ProgressInfo) => void;
|
|
545
|
+
/**
|
|
546
|
+
* Persist cursor position after every page so the extraction can resume
|
|
547
|
+
* after a crash/interrupt. Forces cursor paging. Cleared on completion.
|
|
548
|
+
*/
|
|
549
|
+
resume?: Checkpoint;
|
|
550
|
+
}
|
|
551
|
+
/** Yield full pages (`ListResult`) according to the chosen strategy. */
|
|
552
|
+
declare function streamPages<T>(runner: EntityRunner<T>, baseParams: Record<string, string | undefined>, opts?: StreamOptions): AsyncGenerator<ListResult<T>>;
|
|
553
|
+
/** Yield individual entities, flattening pages, honouring `maxResults`. */
|
|
554
|
+
declare function streamItems<T>(runner: EntityRunner<T>, baseParams: Record<string, string | undefined>, opts?: StreamOptions): AsyncGenerator<T>;
|
|
555
|
+
declare function cursorPages<T>(runner: EntityRunner<T>, baseParams: Record<string, string | undefined>, opts: StreamOptions): AsyncGenerator<ListResult<T>>;
|
|
556
|
+
declare function sequentialBasicPages<T>(runner: EntityRunner<T>, baseParams: Record<string, string | undefined>, opts: StreamOptions): AsyncGenerator<ListResult<T>>;
|
|
557
|
+
declare function parallelBasicPages<T>(runner: EntityRunner<T>, baseParams: Record<string, string | undefined>, opts: StreamOptions): AsyncGenerator<ListResult<T>>;
|
|
558
|
+
|
|
559
|
+
type FilterScalar = string | number | boolean;
|
|
560
|
+
type FilterValue = FilterScalar | FilterScalar[];
|
|
561
|
+
type Filters = Record<string, FilterValue>;
|
|
562
|
+
/**
|
|
563
|
+
* Filter input with per-entity key autocomplete. Known keys (from the generated
|
|
564
|
+
* `*FilterKey` unions) are suggested in your IDE, while any string is still
|
|
565
|
+
* accepted — so new OpenAlex fields never break compilation.
|
|
566
|
+
*/
|
|
567
|
+
type FilterInput<F extends string = string> = {
|
|
568
|
+
[K in F]?: FilterValue;
|
|
569
|
+
} & {
|
|
570
|
+
[key: string]: FilterValue;
|
|
571
|
+
};
|
|
572
|
+
/** Negate a filter value → `!value`. */
|
|
573
|
+
declare const not: (v: FilterScalar) => string;
|
|
574
|
+
/** Greater-than range filter → `>value`. */
|
|
575
|
+
declare const gt: (v: FilterScalar) => string;
|
|
576
|
+
/** Less-than range filter → `<value`. */
|
|
577
|
+
declare const lt: (v: FilterScalar) => string;
|
|
578
|
+
/** OR several values for one key (also expressible as a plain array). */
|
|
579
|
+
declare const or: (...values: FilterScalar[]) => FilterScalar[];
|
|
580
|
+
type SortSpec = string | string[] | Record<string, 'asc' | 'desc'>;
|
|
581
|
+
interface QueryState {
|
|
582
|
+
filters: Filters;
|
|
583
|
+
search?: string;
|
|
584
|
+
sort?: string;
|
|
585
|
+
select?: string[];
|
|
586
|
+
perPage?: number;
|
|
587
|
+
sampleN?: number;
|
|
588
|
+
seed?: number;
|
|
589
|
+
groupBy?: string;
|
|
590
|
+
extra: Record<string, string>;
|
|
591
|
+
}
|
|
592
|
+
declare class QueryBuilder<T, F extends string = string> {
|
|
593
|
+
private readonly runner;
|
|
594
|
+
private readonly state;
|
|
595
|
+
constructor(runner: EntityRunner<T>, state?: QueryState);
|
|
596
|
+
/** The entity type this query targets (e.g. 'works'). */
|
|
597
|
+
get entityType(): EntityType;
|
|
598
|
+
private clone;
|
|
599
|
+
/** Merge filters. Array values are OR'd; use `not`/`gt`/`lt` helpers. */
|
|
600
|
+
filter(filters: FilterInput<F>): QueryBuilder<T, F>;
|
|
601
|
+
/** Full-text relevance search across the entity's default searchable fields. */
|
|
602
|
+
search(term: string): QueryBuilder<T, F>;
|
|
603
|
+
/** Search within a specific field, e.g. `.searchField('title', 'crispr')`. */
|
|
604
|
+
searchField(field: string, term: string): QueryBuilder<T, F>;
|
|
605
|
+
sort(spec: SortSpec): QueryBuilder<T, F>;
|
|
606
|
+
/** Trim the response to specific fields (cheaper, faster payloads). */
|
|
607
|
+
select(fields: string | string[]): QueryBuilder<T, F>;
|
|
608
|
+
/** Random sample of `n` results (optionally deterministic via `seed`). */
|
|
609
|
+
sample(n: number, seed?: number): QueryBuilder<T, F>;
|
|
610
|
+
seed(seed: number): QueryBuilder<T, F>;
|
|
611
|
+
perPage(n: number): QueryBuilder<T, F>;
|
|
612
|
+
/** Aggregate counts by a field (or two comma-separated fields). */
|
|
613
|
+
groupBy(field: string | string[]): QueryBuilder<T, F>;
|
|
614
|
+
/** Escape hatch: set any raw query parameter (advanced/semantic search, …). */
|
|
615
|
+
param(key: string, value: string | number | boolean): QueryBuilder<T, F>;
|
|
616
|
+
/** The OpenAlex query params this builder will send (no paging keys). */
|
|
617
|
+
toParams(): Record<string, string | undefined>;
|
|
618
|
+
private get requestType();
|
|
619
|
+
/** Fetch a single page. */
|
|
620
|
+
get(opts?: {
|
|
621
|
+
perPage?: number;
|
|
622
|
+
page?: number;
|
|
623
|
+
}): Promise<ListResult<T>>;
|
|
624
|
+
/** Fetch just the first matching entity, or `null`. */
|
|
625
|
+
first(): Promise<T | null>;
|
|
626
|
+
/** Total number of matching results (one cheap probe call). */
|
|
627
|
+
count(): Promise<number>;
|
|
628
|
+
/** Async-iterate every matching page. */
|
|
629
|
+
paginate(opts?: StreamOptions): AsyncGenerator<ListResult<T>>;
|
|
630
|
+
/** Async-iterate every matching entity across all pages. */
|
|
631
|
+
all(opts?: StreamOptions): AsyncGenerator<T>;
|
|
632
|
+
/** Collect all matching entities into an array (respecting `maxResults`). */
|
|
633
|
+
toArray(opts?: StreamOptions): Promise<T[]>;
|
|
634
|
+
/**
|
|
635
|
+
* Stream all results using the cost-optimal strategy: one cheap count, then
|
|
636
|
+
* parallel basic paging when ≤10k results (fastest) or cursor streaming above.
|
|
637
|
+
*/
|
|
638
|
+
streamOptimized(opts?: StreamOptions): AsyncGenerator<T>;
|
|
639
|
+
/** Run a `group_by` aggregation and return the buckets. */
|
|
640
|
+
groups(field?: string | string[]): Promise<GroupResult[]>;
|
|
641
|
+
/** Estimate the dollar cost of extracting *all* matching results. */
|
|
642
|
+
estimateCost(opts?: {
|
|
643
|
+
perPage?: number;
|
|
644
|
+
strategy?: 'basic' | 'cursor';
|
|
645
|
+
}): Promise<CostEstimate>;
|
|
646
|
+
/** Stream all matching results to a file (JSONL / CSV / JSON). */
|
|
647
|
+
export(path: string, opts?: StreamOptions & {
|
|
648
|
+
format?: 'jsonl' | 'csv' | 'json';
|
|
649
|
+
flatten?: boolean;
|
|
650
|
+
}): Promise<{
|
|
651
|
+
path: string;
|
|
652
|
+
count: number;
|
|
653
|
+
}>;
|
|
654
|
+
private streamOpts;
|
|
655
|
+
}
|
|
656
|
+
declare function serializeFilters(filters: Filters): string | undefined;
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Per-entity endpoint. One instance per entity type (`client.works`, …).
|
|
660
|
+
*
|
|
661
|
+
* Implements {@link EntityRunner} so a {@link QueryBuilder} can execute through
|
|
662
|
+
* it, and exposes convenience proxies so simple calls stay terse:
|
|
663
|
+
* await client.works.get('10.7717/peerj.4375')
|
|
664
|
+
* await client.authors.filter({ ... }).sort({ cited_by_count: 'desc' }).toArray()
|
|
665
|
+
*/
|
|
666
|
+
|
|
667
|
+
interface GetOptions {
|
|
668
|
+
select?: string | string[];
|
|
669
|
+
noCache?: boolean;
|
|
670
|
+
}
|
|
671
|
+
declare class EntityEndpoint<T, F extends string = string> implements EntityRunner<T> {
|
|
672
|
+
protected readonly http: HttpClient;
|
|
673
|
+
readonly entityType: EntityType;
|
|
674
|
+
constructor(http: HttpClient, entityType: EntityType);
|
|
675
|
+
get logger(): Logger;
|
|
676
|
+
get defaultConcurrency(): number;
|
|
677
|
+
/** Low-level list fetch used by the query builder & paginators. */
|
|
678
|
+
fetchList(params: Record<string, string | undefined>, opts?: FetchListOptions): Promise<ListResult<T>>;
|
|
679
|
+
/** Start a fresh query. */
|
|
680
|
+
query(): QueryBuilder<T, F>;
|
|
681
|
+
filter(filters: FilterInput<F>): QueryBuilder<T, F>;
|
|
682
|
+
search(term: string): QueryBuilder<T, F>;
|
|
683
|
+
searchField(field: string, term: string): QueryBuilder<T, F>;
|
|
684
|
+
sort(spec: SortSpec): QueryBuilder<T, F>;
|
|
685
|
+
select(fields: string | string[]): QueryBuilder<T, F>;
|
|
686
|
+
sample(n: number, seed?: number): QueryBuilder<T, F>;
|
|
687
|
+
groupBy(field: string | string[]): QueryBuilder<T, F>;
|
|
688
|
+
perPage(n: number): QueryBuilder<T, F>;
|
|
689
|
+
param(key: string, value: string | number | boolean): QueryBuilder<T, F>;
|
|
690
|
+
count(): Promise<number>;
|
|
691
|
+
all(opts?: StreamOptions): AsyncGenerator<T>;
|
|
692
|
+
paginate(opts?: StreamOptions): AsyncGenerator<ListResult<T>>;
|
|
693
|
+
toArray(opts?: StreamOptions): Promise<T[]>;
|
|
694
|
+
groups(field: string | string[]): Promise<GroupResult[]>;
|
|
695
|
+
estimateCost(opts?: {
|
|
696
|
+
perPage?: number;
|
|
697
|
+
strategy?: 'basic' | 'cursor';
|
|
698
|
+
}): Promise<CostEstimate>;
|
|
699
|
+
export(path: string, opts?: StreamOptions & {
|
|
700
|
+
format?: 'jsonl' | 'csv' | 'json';
|
|
701
|
+
flatten?: boolean;
|
|
702
|
+
}): Promise<{
|
|
703
|
+
path: string;
|
|
704
|
+
count: number;
|
|
705
|
+
}>;
|
|
706
|
+
/**
|
|
707
|
+
* Fetch one entity by any supported ID: OpenAlex ID, DOI, ORCID, ROR, ISSN,
|
|
708
|
+
* Wikidata QID, PMID, MAG, or a full URL (all normalized automatically).
|
|
709
|
+
*/
|
|
710
|
+
get(id: string, opts?: GetOptions): Promise<T>;
|
|
711
|
+
/**
|
|
712
|
+
* Fetch many entities by OpenAlex ID in as few calls as possible, batching
|
|
713
|
+
* into `ids.openalex` OR-filters (default 50 per request) run in parallel.
|
|
714
|
+
* Order is not guaranteed. For mixed external IDs, call {@link get} per ID.
|
|
715
|
+
*/
|
|
716
|
+
getMany(ids: string[], opts?: {
|
|
717
|
+
select?: string | string[];
|
|
718
|
+
chunkSize?: number;
|
|
719
|
+
}): Promise<T[]>;
|
|
720
|
+
/** A single random entity (optionally trimmed with `select`). */
|
|
721
|
+
random(opts?: {
|
|
722
|
+
select?: string | string[];
|
|
723
|
+
}): Promise<T>;
|
|
724
|
+
/** Typeahead suggestions for this entity type. */
|
|
725
|
+
autocomplete(q: string): Promise<AutocompleteResult[]>;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Typed interfaces for OpenAlex entities.
|
|
730
|
+
*
|
|
731
|
+
* These cover the fields developers reach for most often. Every interface
|
|
732
|
+
* carries an index signature (`[key: string]: unknown`), so:
|
|
733
|
+
* - fields not modelled here are still accessible (just untyped), and
|
|
734
|
+
* - responses trimmed with `.select(...)` remain assignable.
|
|
735
|
+
* The full untouched payload is always available — nothing is dropped.
|
|
736
|
+
*/
|
|
737
|
+
interface DehydratedEntity {
|
|
738
|
+
id: string;
|
|
739
|
+
display_name: string;
|
|
740
|
+
[key: string]: unknown;
|
|
741
|
+
}
|
|
742
|
+
interface CountsByYear {
|
|
743
|
+
year: number;
|
|
744
|
+
works_count?: number;
|
|
745
|
+
cited_by_count?: number;
|
|
746
|
+
oa_works_count?: number;
|
|
747
|
+
}
|
|
748
|
+
interface Ids {
|
|
749
|
+
openalex?: string;
|
|
750
|
+
doi?: string;
|
|
751
|
+
mag?: string;
|
|
752
|
+
orcid?: string;
|
|
753
|
+
ror?: string;
|
|
754
|
+
issn_l?: string;
|
|
755
|
+
issn?: string[];
|
|
756
|
+
wikidata?: string;
|
|
757
|
+
wikipedia?: string;
|
|
758
|
+
[key: string]: unknown;
|
|
759
|
+
}
|
|
760
|
+
interface Authorship {
|
|
761
|
+
author_position?: 'first' | 'middle' | 'last';
|
|
762
|
+
author?: DehydratedEntity;
|
|
763
|
+
institutions?: DehydratedEntity[];
|
|
764
|
+
countries?: string[];
|
|
765
|
+
is_corresponding?: boolean;
|
|
766
|
+
raw_author_name?: string;
|
|
767
|
+
raw_affiliation_strings?: string[];
|
|
768
|
+
[key: string]: unknown;
|
|
769
|
+
}
|
|
770
|
+
interface Location {
|
|
771
|
+
is_oa?: boolean;
|
|
772
|
+
landing_page_url?: string | null;
|
|
773
|
+
pdf_url?: string | null;
|
|
774
|
+
source?: DehydratedEntity | null;
|
|
775
|
+
license?: string | null;
|
|
776
|
+
version?: string | null;
|
|
777
|
+
[key: string]: unknown;
|
|
778
|
+
}
|
|
779
|
+
interface OpenAccess {
|
|
780
|
+
is_oa?: boolean;
|
|
781
|
+
oa_status?: 'gold' | 'green' | 'hybrid' | 'bronze' | 'closed' | string;
|
|
782
|
+
oa_url?: string | null;
|
|
783
|
+
any_repository_has_fulltext?: boolean;
|
|
784
|
+
[key: string]: unknown;
|
|
785
|
+
}
|
|
786
|
+
interface Work {
|
|
787
|
+
id: string;
|
|
788
|
+
doi?: string | null;
|
|
789
|
+
title?: string | null;
|
|
790
|
+
display_name?: string | null;
|
|
791
|
+
publication_year?: number;
|
|
792
|
+
publication_date?: string;
|
|
793
|
+
ids?: Ids;
|
|
794
|
+
language?: string | null;
|
|
795
|
+
type?: string;
|
|
796
|
+
type_crossref?: string;
|
|
797
|
+
authorships?: Authorship[];
|
|
798
|
+
primary_location?: Location | null;
|
|
799
|
+
best_oa_location?: Location | null;
|
|
800
|
+
locations?: Location[];
|
|
801
|
+
open_access?: OpenAccess;
|
|
802
|
+
cited_by_count?: number;
|
|
803
|
+
cited_by_api_url?: string;
|
|
804
|
+
referenced_works?: string[];
|
|
805
|
+
referenced_works_count?: number;
|
|
806
|
+
related_works?: string[];
|
|
807
|
+
counts_by_year?: CountsByYear[];
|
|
808
|
+
topics?: DehydratedEntity[];
|
|
809
|
+
keywords?: {
|
|
810
|
+
id?: string;
|
|
811
|
+
display_name?: string;
|
|
812
|
+
score?: number;
|
|
813
|
+
}[];
|
|
814
|
+
concepts?: DehydratedEntity[];
|
|
815
|
+
abstract_inverted_index?: Record<string, number[]> | null;
|
|
816
|
+
is_retracted?: boolean;
|
|
817
|
+
is_paratext?: boolean;
|
|
818
|
+
updated_date?: string;
|
|
819
|
+
created_date?: string;
|
|
820
|
+
[key: string]: unknown;
|
|
821
|
+
}
|
|
822
|
+
interface Author {
|
|
823
|
+
id: string;
|
|
824
|
+
orcid?: string | null;
|
|
825
|
+
display_name?: string;
|
|
826
|
+
display_name_alternatives?: string[];
|
|
827
|
+
works_count?: number;
|
|
828
|
+
cited_by_count?: number;
|
|
829
|
+
ids?: Ids;
|
|
830
|
+
affiliations?: {
|
|
831
|
+
institution?: DehydratedEntity;
|
|
832
|
+
years?: number[];
|
|
833
|
+
}[];
|
|
834
|
+
last_known_institutions?: DehydratedEntity[];
|
|
835
|
+
topics?: DehydratedEntity[];
|
|
836
|
+
summary_stats?: SummaryStats;
|
|
837
|
+
counts_by_year?: CountsByYear[];
|
|
838
|
+
works_api_url?: string;
|
|
839
|
+
updated_date?: string;
|
|
840
|
+
created_date?: string;
|
|
841
|
+
[key: string]: unknown;
|
|
842
|
+
}
|
|
843
|
+
interface SummaryStats {
|
|
844
|
+
'2yr_mean_citedness'?: number;
|
|
845
|
+
h_index?: number;
|
|
846
|
+
i10_index?: number;
|
|
847
|
+
[key: string]: unknown;
|
|
848
|
+
}
|
|
849
|
+
interface Source {
|
|
850
|
+
id: string;
|
|
851
|
+
issn_l?: string | null;
|
|
852
|
+
issn?: string[] | null;
|
|
853
|
+
display_name?: string;
|
|
854
|
+
host_organization?: string | null;
|
|
855
|
+
host_organization_name?: string | null;
|
|
856
|
+
type?: string;
|
|
857
|
+
is_oa?: boolean;
|
|
858
|
+
is_in_doaj?: boolean;
|
|
859
|
+
works_count?: number;
|
|
860
|
+
cited_by_count?: number;
|
|
861
|
+
summary_stats?: SummaryStats;
|
|
862
|
+
ids?: Ids;
|
|
863
|
+
counts_by_year?: CountsByYear[];
|
|
864
|
+
works_api_url?: string;
|
|
865
|
+
updated_date?: string;
|
|
866
|
+
created_date?: string;
|
|
867
|
+
[key: string]: unknown;
|
|
868
|
+
}
|
|
869
|
+
interface Institution {
|
|
870
|
+
id: string;
|
|
871
|
+
ror?: string;
|
|
872
|
+
display_name?: string;
|
|
873
|
+
country_code?: string | null;
|
|
874
|
+
type?: string;
|
|
875
|
+
homepage_url?: string | null;
|
|
876
|
+
works_count?: number;
|
|
877
|
+
cited_by_count?: number;
|
|
878
|
+
summary_stats?: SummaryStats;
|
|
879
|
+
ids?: Ids;
|
|
880
|
+
geo?: {
|
|
881
|
+
city?: string;
|
|
882
|
+
region?: string | null;
|
|
883
|
+
country_code?: string;
|
|
884
|
+
country?: string;
|
|
885
|
+
latitude?: number;
|
|
886
|
+
longitude?: number;
|
|
887
|
+
};
|
|
888
|
+
associated_institutions?: DehydratedEntity[];
|
|
889
|
+
counts_by_year?: CountsByYear[];
|
|
890
|
+
works_api_url?: string;
|
|
891
|
+
updated_date?: string;
|
|
892
|
+
created_date?: string;
|
|
893
|
+
[key: string]: unknown;
|
|
894
|
+
}
|
|
895
|
+
interface Topic {
|
|
896
|
+
id: string;
|
|
897
|
+
display_name?: string;
|
|
898
|
+
description?: string;
|
|
899
|
+
keywords?: string[];
|
|
900
|
+
works_count?: number;
|
|
901
|
+
cited_by_count?: number;
|
|
902
|
+
subfield?: DehydratedEntity;
|
|
903
|
+
field?: DehydratedEntity;
|
|
904
|
+
domain?: DehydratedEntity;
|
|
905
|
+
ids?: Ids;
|
|
906
|
+
updated_date?: string;
|
|
907
|
+
created_date?: string;
|
|
908
|
+
[key: string]: unknown;
|
|
909
|
+
}
|
|
910
|
+
interface Keyword {
|
|
911
|
+
id: string;
|
|
912
|
+
display_name?: string;
|
|
913
|
+
works_count?: number;
|
|
914
|
+
cited_by_count?: number;
|
|
915
|
+
updated_date?: string;
|
|
916
|
+
created_date?: string;
|
|
917
|
+
[key: string]: unknown;
|
|
918
|
+
}
|
|
919
|
+
interface Publisher {
|
|
920
|
+
id: string;
|
|
921
|
+
display_name?: string;
|
|
922
|
+
alternate_titles?: string[];
|
|
923
|
+
hierarchy_level?: number;
|
|
924
|
+
parent_publisher?: DehydratedEntity | string | null;
|
|
925
|
+
country_codes?: string[];
|
|
926
|
+
works_count?: number;
|
|
927
|
+
cited_by_count?: number;
|
|
928
|
+
summary_stats?: SummaryStats;
|
|
929
|
+
ids?: Ids;
|
|
930
|
+
counts_by_year?: CountsByYear[];
|
|
931
|
+
sources_api_url?: string;
|
|
932
|
+
updated_date?: string;
|
|
933
|
+
created_date?: string;
|
|
934
|
+
[key: string]: unknown;
|
|
935
|
+
}
|
|
936
|
+
interface Funder {
|
|
937
|
+
id: string;
|
|
938
|
+
display_name?: string;
|
|
939
|
+
alternate_titles?: string[];
|
|
940
|
+
country_code?: string | null;
|
|
941
|
+
description?: string | null;
|
|
942
|
+
homepage_url?: string | null;
|
|
943
|
+
grants_count?: number;
|
|
944
|
+
works_count?: number;
|
|
945
|
+
cited_by_count?: number;
|
|
946
|
+
summary_stats?: SummaryStats;
|
|
947
|
+
ids?: Ids;
|
|
948
|
+
counts_by_year?: CountsByYear[];
|
|
949
|
+
updated_date?: string;
|
|
950
|
+
created_date?: string;
|
|
951
|
+
[key: string]: unknown;
|
|
952
|
+
}
|
|
953
|
+
/** Maps an entity-type string to its interface. */
|
|
954
|
+
interface EntityTypeMap {
|
|
955
|
+
works: Work;
|
|
956
|
+
authors: Author;
|
|
957
|
+
sources: Source;
|
|
958
|
+
institutions: Institution;
|
|
959
|
+
topics: Topic;
|
|
960
|
+
keywords: Keyword;
|
|
961
|
+
publishers: Publisher;
|
|
962
|
+
funders: Funder;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
type WorksFilterKey = "abstract.search" | "abstract.search.exact" | "apc_list.currency" | "apc_list.provenance" | "apc_list.value" | "apc_list.value_usd" | "apc_paid.currency" | "apc_paid.provenance" | "apc_paid.value" | "apc_paid.value_usd" | "author.id" | "author.orcid" | "authors_count" | "authorships.affiliations.institution_ids" | "authorships.author.id" | "authorships.author.orcid" | "authorships.countries" | "authorships.institutions.continent" | "authorships.institutions.country_code" | "authorships.institutions.id" | "authorships.institutions.is_global_south" | "authorships.institutions.lineage" | "authorships.institutions.ror" | "authorships.institutions.type" | "authorships.is_corresponding" | "authorships.raw_orcid" | "awards.doi" | "awards.funder_award_id" | "awards.funder_display_name" | "awards.funder_id" | "awards.id" | "best_oa_location.is_accepted" | "best_oa_location.is_oa" | "best_oa_location.is_published" | "best_oa_location.landing_page_url" | "best_oa_location.license" | "best_oa_location.license_id" | "best_oa_location.raw_type" | "best_oa_location.source.host_organization" | "best_oa_location.source.host_organization_lineage" | "best_oa_location.source.id" | "best_oa_location.source.is_in_doaj" | "best_oa_location.source.is_oa" | "best_oa_location.source.issn" | "best_oa_location.source.type" | "best_oa_location.version" | "best_open_version" | "biblio.first_page" | "biblio.issue" | "biblio.last_page" | "biblio.volume" | "citation_normalized_percentile.is_in_top_10_percent" | "citation_normalized_percentile.is_in_top_1_percent" | "citation_normalized_percentile.value" | "cited_by" | "cited_by_count" | "cited_by_percentile_year.max" | "cited_by_percentile_year.min" | "cites" | "collection" | "concept.id" | "concepts.id" | "concepts.wikidata" | "concepts_count" | "corresponding_author_ids" | "corresponding_institution_ids" | "countries_distinct_count" | "created_date" | "datasets" | "default.search" | "default.search.exact" | "display_name" | "display_name.search" | "display_name.search.exact" | "doi" | "doi_starts_with" | "from_created_date" | "from_publication_date" | "fulltext.search" | "fulltext.search.exact" | "fulltext_origin" | "funders.id" | "fwci" | "has_abstract" | "has_content.grobid_xml" | "has_content.pdf" | "has_doi" | "has_embeddings" | "has_fulltext" | "has_oa_accepted_or_published_version" | "has_oa_submitted_version" | "has_old_authors" | "has_orcid" | "has_pdf_url" | "has_pmcid" | "has_pmid" | "has_raw_affiliation_strings" | "has_references" | "ids.mag" | "ids.openalex" | "ids.pmcid" | "ids.pmid" | "indexed_in" | "institution.id" | "institution_assertions.country_code" | "institution_assertions.id" | "institution_assertions.lineage" | "institution_assertions.ror" | "institution_assertions.type" | "institutions.continent" | "institutions.country_code" | "institutions.id" | "institutions.is_global_south" | "institutions.ror" | "institutions.type" | "institutions_distinct_count" | "is_corresponding" | "is_oa" | "is_paratext" | "is_retracted" | "is_xpac" | "journal" | "keyword.search" | "keywords.id" | "language" | "locations.is_accepted" | "locations.is_oa" | "locations.is_published" | "locations.landing_page_url" | "locations.license" | "locations.license_id" | "locations.raw_type" | "locations.source.has_issn" | "locations.source.host_institution_lineage" | "locations.source.host_organization" | "locations.source.host_organization_lineage" | "locations.source.id" | "locations.source.is_core" | "locations.source.is_in_doaj" | "locations.source.is_oa" | "locations.source.issn" | "locations.source.publisher_lineage" | "locations.source.type" | "locations.version" | "locations_count" | "mag" | "mag_only" | "oa_status" | "open_access.any_repository_has_fulltext" | "open_access.is_oa" | "open_access.oa_status" | "openalex" | "openalex_id" | "pmcid" | "pmid" | "primary_location.is_accepted" | "primary_location.is_oa" | "primary_location.is_published" | "primary_location.landing_page_url" | "primary_location.license" | "primary_location.license_id" | "primary_location.raw_type" | "primary_location.source.has_issn" | "primary_location.source.host_institution_lineage" | "primary_location.source.host_organization" | "primary_location.source.host_organization_lineage" | "primary_location.source.id" | "primary_location.source.is_core" | "primary_location.source.is_in_doaj" | "primary_location.source.is_oa" | "primary_location.source.issn" | "primary_location.source.publisher_lineage" | "primary_location.source.type" | "primary_location.version" | "primary_topic.domain.id" | "primary_topic.field.id" | "primary_topic.id" | "primary_topic.subfield.id" | "publication_date" | "publication_year" | "raw_affiliation_strings" | "raw_affiliation_strings.search" | "raw_author_name.search" | "referenced_works" | "referenced_works_count" | "related_to" | "repository" | "semantic.search" | "sustainable_development_goals.id" | "sustainable_development_goals.score" | "title.search" | "title.search.exact" | "title_and_abstract.search" | "title_and_abstract.search.exact" | "to_created_date" | "to_publication_date" | "to_updated_date" | "topics.domain.id" | "topics.field.id" | "topics.id" | "topics.subfield.id" | "topics_count" | "type" | "type_crossref" | "updated_date" | "version";
|
|
966
|
+
type AuthorsFilterKey = "affiliations.institution.country_code" | "affiliations.institution.id" | "affiliations.institution.lineage" | "affiliations.institution.ror" | "affiliations.institution.type" | "block_key" | "cited_by_count" | "collection" | "concept.id" | "concepts.id" | "default.search" | "display_name" | "display_name.search" | "from_created_date" | "has_orcid" | "id" | "ids.openalex" | "last_known_authorships.institutions.lineage" | "last_known_institutions.continent" | "last_known_institutions.country_code" | "last_known_institutions.id" | "last_known_institutions.is_global_south" | "last_known_institutions.lineage" | "last_known_institutions.ror" | "last_known_institutions.type" | "openalex" | "openalex_id" | "orcid" | "parsed_longest_name.first" | "parsed_longest_name.last" | "parsed_longest_name.middle" | "parsed_longest_name.suffix" | "scopus" | "summary_stats.2yr_mean_citedness" | "summary_stats.h_index" | "summary_stats.i10_index" | "text.search" | "to_created_date" | "to_updated_date" | "topic_share.id" | "topics.id" | "works_count" | "x_concepts.id";
|
|
967
|
+
type SourcesFilterKey = "apc_prices.currency" | "apc_prices.price" | "apc_usd" | "cited_by_count" | "collection" | "concept.id" | "concepts.id" | "continent" | "country_code" | "default.search" | "display_name" | "display_name.search" | "first_publication_year" | "from_created_date" | "has_issn" | "host_organization" | "host_organization.id" | "host_organization_lineage" | "ids.mag" | "ids.openalex" | "is_core" | "is_global_south" | "is_high_oa_rate" | "is_high_oa_rate_since_year" | "is_in_doaj" | "is_in_doaj_since_year" | "is_in_jstage" | "is_in_jstage_since_year" | "is_oa" | "is_ojs" | "is_preprint_repository" | "issn" | "issn_l" | "last_publication_year" | "oa_flip_year" | "openalex" | "openalex_id" | "summary_stats.2yr_mean_citedness" | "summary_stats.h_index" | "summary_stats.i10_index" | "text.search" | "to_created_date" | "to_updated_date" | "topic_share.id" | "topics.id" | "type" | "works_count" | "x_concepts.id";
|
|
968
|
+
type InstitutionsFilterKey = "cited_by_count" | "collection" | "concept.id" | "concepts.id" | "continent" | "country_code" | "default.search" | "display_name" | "display_name.search" | "from_created_date" | "has_ror" | "id" | "ids.openalex" | "is_global_south" | "is_super_system" | "lineage" | "openalex" | "openalex_id" | "repositories.host_organization" | "repositories.host_organization_lineage" | "repositories.id" | "roles.id" | "ror" | "status" | "summary_stats.2yr_mean_citedness" | "summary_stats.h_index" | "summary_stats.i10_index" | "text.search" | "topic_share.id" | "topics.id" | "type" | "works_count" | "x_concepts.id";
|
|
969
|
+
type TopicsFilterKey = "cited_by_count" | "collection" | "default.search" | "description.search" | "display_name" | "display_name.search" | "domain.id" | "field.id" | "from_created_date" | "id" | "ids.openalex" | "keywords.search" | "openalex" | "subfield.id" | "text.search" | "works_count";
|
|
970
|
+
type KeywordsFilterKey = "cited_by_count" | "collection" | "default.search" | "display_name" | "display_name.search" | "from_created_date" | "id" | "text.search" | "works_count";
|
|
971
|
+
type PublishersFilterKey = "cited_by_count" | "collection" | "continent" | "country_codes" | "default.search" | "display_name" | "display_name.search" | "from_created_date" | "hierarchy_level" | "ids.openalex" | "ids.ror" | "ids.wikidata" | "lineage" | "openalex" | "openalex_id" | "parent_publisher" | "roles.id" | "ror" | "summary_stats.2yr_mean_citedness" | "summary_stats.h_index" | "summary_stats.i10_index" | "text.search" | "wikidata" | "works_count";
|
|
972
|
+
type FundersFilterKey = "awards_count" | "cited_by_count" | "collection" | "continent" | "country_code" | "default.search" | "description.search" | "display_name" | "display_name.search" | "from_created_date" | "ids.crossref" | "ids.doi" | "ids.openalex" | "ids.ror" | "ids.wikidata" | "is_global_south" | "openalex" | "openalex_id" | "roles.id" | "ror" | "summary_stats.2yr_mean_citedness" | "summary_stats.h_index" | "summary_stats.i10_index" | "text.search" | "wikidata" | "works_count";
|
|
973
|
+
/** Maps an entity type to its filter-key union. */
|
|
974
|
+
interface EntityFilterKeyMap {
|
|
975
|
+
works: WorksFilterKey;
|
|
976
|
+
authors: AuthorsFilterKey;
|
|
977
|
+
sources: SourcesFilterKey;
|
|
978
|
+
institutions: InstitutionsFilterKey;
|
|
979
|
+
topics: TopicsFilterKey;
|
|
980
|
+
keywords: KeywordsFilterKey;
|
|
981
|
+
publishers: PublishersFilterKey;
|
|
982
|
+
funders: FundersFilterKey;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Works endpoint with citation-graph helpers layered on top of the generic
|
|
987
|
+
* {@link EntityEndpoint}. Each helper returns a composable {@link QueryBuilder},
|
|
988
|
+
* so you can chain `.select()`, `.filter()`, `.all()`, `.export()`, etc.
|
|
989
|
+
*/
|
|
990
|
+
|
|
991
|
+
/** Public host that serves cached full-text (PDF / TEI-XML) for OA works. */
|
|
992
|
+
declare const CONTENT_BASE_URL = "https://content.openalex.org";
|
|
993
|
+
type FulltextFormat = 'pdf' | 'xml';
|
|
994
|
+
declare class WorksEndpoint extends EntityEndpoint<Work, WorksFilterKey> {
|
|
995
|
+
constructor(http: HttpClient);
|
|
996
|
+
/** Works that **cite** the given work (incoming citations). */
|
|
997
|
+
citedBy(workId: string): QueryBuilder<Work, WorksFilterKey>;
|
|
998
|
+
/** Works the given work **cites** — its references (outgoing citations). */
|
|
999
|
+
references(workId: string): QueryBuilder<Work, WorksFilterKey>;
|
|
1000
|
+
/** Works OpenAlex considers related to the given work. */
|
|
1001
|
+
related(workId: string): QueryBuilder<Work, WorksFilterKey>;
|
|
1002
|
+
/** Only works that have a downloadable cached PDF. */
|
|
1003
|
+
withPdf(): QueryBuilder<Work, WorksFilterKey>;
|
|
1004
|
+
/** Only works that have a downloadable TEI-XML (Grobid) parse. */
|
|
1005
|
+
withXml(): QueryBuilder<Work, WorksFilterKey>;
|
|
1006
|
+
/** The content-host URL for a work's full text (api_key redacted). */
|
|
1007
|
+
fulltextUrl(workId: string, format?: FulltextFormat): string;
|
|
1008
|
+
/**
|
|
1009
|
+
* Download a work's cached full text (PDF or TEI-XML). Costs $0.01/call and
|
|
1010
|
+
* is budget-tracked. Pass `dest` to stream to a file, otherwise get bytes.
|
|
1011
|
+
*/
|
|
1012
|
+
downloadFulltext(workId: string, opts?: {
|
|
1013
|
+
format?: FulltextFormat;
|
|
1014
|
+
dest?: string;
|
|
1015
|
+
signal?: AbortSignal;
|
|
1016
|
+
}): Promise<{
|
|
1017
|
+
bytes?: Uint8Array;
|
|
1018
|
+
path?: string;
|
|
1019
|
+
contentType?: string;
|
|
1020
|
+
}>;
|
|
1021
|
+
private rawFulltextUrl;
|
|
1022
|
+
/**
|
|
1023
|
+
* One-hop citation neighbourhood: the work plus its references and citers.
|
|
1024
|
+
* Bounded by `limit` on each side to keep the cost predictable.
|
|
1025
|
+
*/
|
|
1026
|
+
citationGraph(workId: string, opts?: {
|
|
1027
|
+
limit?: number;
|
|
1028
|
+
select?: string[];
|
|
1029
|
+
}): Promise<{
|
|
1030
|
+
seed: Work;
|
|
1031
|
+
references: Work[];
|
|
1032
|
+
citedBy: Work[];
|
|
1033
|
+
}>;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Snapshot / bulk-data bridge — the "$0 extract-all-data" path.
|
|
1038
|
+
*
|
|
1039
|
+
* OpenAlex publishes the *entire* dataset as a free, public S3 snapshot
|
|
1040
|
+
* (gzipped JSON Lines, no credentials needed). For large extractions this is
|
|
1041
|
+
* far cheaper than the metered API: e.g. all Works = 510M records / 665 GB at
|
|
1042
|
+
* $0, versus paying $0.0001/list-call to crawl them.
|
|
1043
|
+
*
|
|
1044
|
+
* This module lets you:
|
|
1045
|
+
* - read the snapshot manifests (per-entity or combined) over HTTPS,
|
|
1046
|
+
* - compare the API cost of an extraction against the free snapshot,
|
|
1047
|
+
* - generate the exact `aws s3 sync` command, and
|
|
1048
|
+
* - stream entities straight from the public bucket in-process (gunzip +
|
|
1049
|
+
* NDJSON), with an optional client-side filter — zero dependencies.
|
|
1050
|
+
*
|
|
1051
|
+
* Paths/schema verified live (2026-07-25) against openalex.s3.amazonaws.com.
|
|
1052
|
+
*/
|
|
1053
|
+
|
|
1054
|
+
type SnapshotFormat = 'jsonl' | 'parquet';
|
|
1055
|
+
/** Default public HTTPS endpoint for the OpenAlex S3 bucket. */
|
|
1056
|
+
declare const SNAPSHOT_BASE_URL = "https://openalex.s3.amazonaws.com";
|
|
1057
|
+
declare const SNAPSHOT_S3_URI = "s3://openalex";
|
|
1058
|
+
interface SnapshotFileEntry {
|
|
1059
|
+
/** Original `s3://openalex/...` URL from the manifest. */
|
|
1060
|
+
s3Url: string;
|
|
1061
|
+
/** HTTPS URL for credential-free download. */
|
|
1062
|
+
httpsUrl: string;
|
|
1063
|
+
contentLength: number;
|
|
1064
|
+
recordCount: number;
|
|
1065
|
+
}
|
|
1066
|
+
interface SnapshotManifest {
|
|
1067
|
+
entity: string;
|
|
1068
|
+
format: string;
|
|
1069
|
+
date?: string;
|
|
1070
|
+
recordCount: number;
|
|
1071
|
+
contentLength: number;
|
|
1072
|
+
files: SnapshotFileEntry[];
|
|
1073
|
+
}
|
|
1074
|
+
interface EntityTotals {
|
|
1075
|
+
entity: string;
|
|
1076
|
+
recordCount: number;
|
|
1077
|
+
contentLength: number;
|
|
1078
|
+
}
|
|
1079
|
+
interface SnapshotPlan {
|
|
1080
|
+
entity: string;
|
|
1081
|
+
/** Results your filtered query would return from the API. */
|
|
1082
|
+
apiResults: number;
|
|
1083
|
+
apiCalls: number;
|
|
1084
|
+
apiCostUsd: number;
|
|
1085
|
+
/** The snapshot is always free. */
|
|
1086
|
+
snapshotCostUsd: 0;
|
|
1087
|
+
/** Total records in the entity's snapshot, when known. */
|
|
1088
|
+
snapshotRecords?: number;
|
|
1089
|
+
/** Compressed bytes of the entity's snapshot, when known. */
|
|
1090
|
+
snapshotBytes?: number;
|
|
1091
|
+
awsCommand: string;
|
|
1092
|
+
recommendation: 'api' | 'snapshot';
|
|
1093
|
+
rationale: string;
|
|
1094
|
+
}
|
|
1095
|
+
interface SnapshotClientOptions {
|
|
1096
|
+
/** Override the HTTPS base (e.g. a mirror or region host). */
|
|
1097
|
+
baseUrl?: string;
|
|
1098
|
+
/** 'jsonl' (default, streamable in-process) or 'parquet'. */
|
|
1099
|
+
format?: SnapshotFormat;
|
|
1100
|
+
/** Inject a fetch (defaults to global fetch / the client's fetch). */
|
|
1101
|
+
fetch?: FetchLike;
|
|
1102
|
+
}
|
|
1103
|
+
interface StreamSnapshotOptions<T> {
|
|
1104
|
+
/** Keep only entities matching this predicate. */
|
|
1105
|
+
filter?: (entity: T) => boolean;
|
|
1106
|
+
/** Only read the first N part files (useful for sampling/testing). */
|
|
1107
|
+
maxParts?: number;
|
|
1108
|
+
/** Stop after emitting this many records. */
|
|
1109
|
+
maxRecords?: number;
|
|
1110
|
+
/** Start from this part index (for manual resume). */
|
|
1111
|
+
startPart?: number;
|
|
1112
|
+
/** Called after each part file completes. */
|
|
1113
|
+
onPart?: (info: {
|
|
1114
|
+
index: number;
|
|
1115
|
+
total: number;
|
|
1116
|
+
url: string;
|
|
1117
|
+
records: number;
|
|
1118
|
+
}) => void;
|
|
1119
|
+
signal?: AbortSignal;
|
|
1120
|
+
}
|
|
1121
|
+
declare class SnapshotClient {
|
|
1122
|
+
readonly baseUrl: string;
|
|
1123
|
+
readonly format: SnapshotFormat;
|
|
1124
|
+
private readonly fetchImpl;
|
|
1125
|
+
constructor(opts?: SnapshotClientOptions);
|
|
1126
|
+
/** Convert an `s3://openalex/KEY` URL to a public HTTPS URL. */
|
|
1127
|
+
s3ToHttps(s3Url: string): string;
|
|
1128
|
+
manifestUrl(entity: string): string;
|
|
1129
|
+
combinedManifestUrl(): string;
|
|
1130
|
+
/** `aws s3 sync` command to download the whole snapshot or one entity. */
|
|
1131
|
+
awsSyncCommand(entity?: string, dest?: string): string;
|
|
1132
|
+
/** Fetch and normalize an entity's manifest. */
|
|
1133
|
+
manifest(entity: string): Promise<SnapshotManifest>;
|
|
1134
|
+
/** Per-entity totals from the combined manifest (no per-file list). */
|
|
1135
|
+
totals(): Promise<EntityTotals[]>;
|
|
1136
|
+
/** Totals for a single entity (cheap-ish: reads combined manifest). */
|
|
1137
|
+
entityTotals(entity: string): Promise<EntityTotals | undefined>;
|
|
1138
|
+
/**
|
|
1139
|
+
* Compare a filtered API extraction against downloading the whole snapshot.
|
|
1140
|
+
* Pass the {@link CostEstimate} from `query.estimateCost()` plus the entity.
|
|
1141
|
+
*/
|
|
1142
|
+
plan(apiEstimate: CostEstimate, entity: string, opts?: {
|
|
1143
|
+
fetchTotals?: boolean;
|
|
1144
|
+
dest?: string;
|
|
1145
|
+
}): Promise<SnapshotPlan>;
|
|
1146
|
+
/**
|
|
1147
|
+
* Stream entities straight from the public snapshot (gunzip + NDJSON), at $0.
|
|
1148
|
+
* Reads the manifest, then each gzipped part over HTTPS.
|
|
1149
|
+
*/
|
|
1150
|
+
stream<T = Record<string, unknown>>(entity: string, opts?: StreamSnapshotOptions<T>): AsyncGenerator<T>;
|
|
1151
|
+
private toFileEntry;
|
|
1152
|
+
private getJson;
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Stream newline-delimited JSON out of a gzipped HTTPS resource, without
|
|
1156
|
+
* buffering the whole file. Uses web streams (`DecompressionStream`) — no deps.
|
|
1157
|
+
*/
|
|
1158
|
+
declare function streamGzippedNdjson<T>(fetchImpl: FetchLike, url: string, signal?: AbortSignal): AsyncGenerator<T>;
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Cost-optimal extraction planner.
|
|
1162
|
+
*
|
|
1163
|
+
* Given a query, it counts once and recommends how to extract cheapest/fastest:
|
|
1164
|
+
* - per-page 200 (fewest calls),
|
|
1165
|
+
* - parallel basic paging when ≤10k results (fastest), else cursor streaming,
|
|
1166
|
+
* - and, when the API bill would be high, the free snapshot instead.
|
|
1167
|
+
*/
|
|
1168
|
+
|
|
1169
|
+
type ExtractionStrategy = 'parallel-basic' | 'cursor' | 'snapshot';
|
|
1170
|
+
interface ExtractionPlan {
|
|
1171
|
+
entity: string;
|
|
1172
|
+
matchingResults: number;
|
|
1173
|
+
requestType: RequestType;
|
|
1174
|
+
recommendedPerPage: number;
|
|
1175
|
+
recommendedStrategy: ExtractionStrategy;
|
|
1176
|
+
estimate: CostEstimate;
|
|
1177
|
+
useSnapshot: boolean;
|
|
1178
|
+
snapshotAwsCommand?: string;
|
|
1179
|
+
rationale: string;
|
|
1180
|
+
}
|
|
1181
|
+
interface PlanOptions {
|
|
1182
|
+
perPage?: number;
|
|
1183
|
+
/** Recommend the snapshot when the API extraction would cost ≥ this (USD). Default 1. */
|
|
1184
|
+
snapshotThresholdUsd?: number;
|
|
1185
|
+
}
|
|
1186
|
+
declare function planExtraction(snapshot: SnapshotClient, query: QueryBuilder<unknown>, opts?: PlanOptions): Promise<ExtractionPlan>;
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* N-grams for a Work — the fulltext n-gram frequencies OpenAlex exposes for
|
|
1190
|
+
* many works (when available).
|
|
1191
|
+
*/
|
|
1192
|
+
|
|
1193
|
+
interface NgramEntry {
|
|
1194
|
+
ngram: string;
|
|
1195
|
+
ngram_count: number;
|
|
1196
|
+
ngram_tokens: number;
|
|
1197
|
+
term_frequency: number;
|
|
1198
|
+
}
|
|
1199
|
+
interface NgramsResponse {
|
|
1200
|
+
meta: {
|
|
1201
|
+
count: number;
|
|
1202
|
+
doi?: string | null;
|
|
1203
|
+
[key: string]: unknown;
|
|
1204
|
+
};
|
|
1205
|
+
ngrams: NgramEntry[];
|
|
1206
|
+
}
|
|
1207
|
+
/** Fetch the n-grams for a single Work (by any supported ID). */
|
|
1208
|
+
declare function fetchNgrams(http: HttpClient, workId: string): Promise<NgramsResponse>;
|
|
1209
|
+
/**
|
|
1210
|
+
* Reconstruct readable abstract text from a Work's `abstract_inverted_index`.
|
|
1211
|
+
* Returns `null` when the index is absent (common for closed works).
|
|
1212
|
+
*/
|
|
1213
|
+
declare function decodeAbstract(invertedIndex: Record<string, number[]> | null | undefined): string | null;
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* The main entry point: `new OpenAlexClient({ apiKey })`.
|
|
1217
|
+
*
|
|
1218
|
+
* Exposes one {@link EntityEndpoint} per entity type plus a shared
|
|
1219
|
+
* {@link UsageTracker} so budget/spend is visible across every call the client
|
|
1220
|
+
* makes.
|
|
1221
|
+
*/
|
|
1222
|
+
|
|
1223
|
+
declare class OpenAlexClient {
|
|
1224
|
+
readonly config: ResolvedConfig;
|
|
1225
|
+
readonly http: HttpClient;
|
|
1226
|
+
/** Live spend/budget tracker, updated from every response. */
|
|
1227
|
+
readonly usage: UsageTracker;
|
|
1228
|
+
/** Free bulk-data snapshot bridge (manifest, cost comparison, streaming). */
|
|
1229
|
+
readonly snapshot: SnapshotClient;
|
|
1230
|
+
readonly works: WorksEndpoint;
|
|
1231
|
+
readonly authors: EntityEndpoint<Author, AuthorsFilterKey>;
|
|
1232
|
+
readonly sources: EntityEndpoint<Source, SourcesFilterKey>;
|
|
1233
|
+
readonly institutions: EntityEndpoint<Institution, InstitutionsFilterKey>;
|
|
1234
|
+
readonly topics: EntityEndpoint<Topic, TopicsFilterKey>;
|
|
1235
|
+
readonly keywords: EntityEndpoint<Keyword, KeywordsFilterKey>;
|
|
1236
|
+
readonly publishers: EntityEndpoint<Publisher, PublishersFilterKey>;
|
|
1237
|
+
readonly funders: EntityEndpoint<Funder, FundersFilterKey>;
|
|
1238
|
+
constructor(config?: OpenAlexConfig);
|
|
1239
|
+
/** Cross-entity typeahead. Optionally scope with `entityType`. */
|
|
1240
|
+
autocomplete(q: string, entityType?: 'works' | 'authors' | 'sources' | 'institutions' | 'topics' | 'keywords' | 'publishers' | 'funders'): Promise<AutocompleteResult[]>;
|
|
1241
|
+
/** N-grams for a Work. */
|
|
1242
|
+
ngrams(workId: string): Promise<NgramsResponse>;
|
|
1243
|
+
/**
|
|
1244
|
+
* Decide API vs free snapshot for a query: estimates the API cost, then
|
|
1245
|
+
* compares against downloading the whole entity's snapshot ($0).
|
|
1246
|
+
*/
|
|
1247
|
+
snapshotPlan(query: QueryBuilder<unknown>): Promise<SnapshotPlan>;
|
|
1248
|
+
/**
|
|
1249
|
+
* Cost-optimal plan for extracting a query: recommends per-page, cursor-vs-
|
|
1250
|
+
* parallel strategy, and whether the free snapshot beats the API.
|
|
1251
|
+
*/
|
|
1252
|
+
planExtraction(query: QueryBuilder<unknown>, opts?: PlanOptions): Promise<ExtractionPlan>;
|
|
1253
|
+
/** Escape hatch: issue a raw GET against any path with any params. */
|
|
1254
|
+
request<T = unknown>(path: string, params?: Record<string, string | undefined>, opts?: {
|
|
1255
|
+
requestType?: RequestType;
|
|
1256
|
+
noCache?: boolean;
|
|
1257
|
+
}): Promise<T>;
|
|
1258
|
+
/** Point-in-time usage snapshot. */
|
|
1259
|
+
usageSnapshot(): UsageSnapshot;
|
|
1260
|
+
/** Human-readable spend/budget report. */
|
|
1261
|
+
usageReport(): string;
|
|
1262
|
+
/** Reset the session usage counters (does not affect OpenAlex's daily total). */
|
|
1263
|
+
resetUsage(): void;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
export { PRICING_USD as $, type Author as A, BASIC_PAGING_MAX_RESULTS as B, CONTENT_BASE_URL as C, type DehydratedEntity as D, EntityEndpoint as E, FREE_DAILY_USD_WITHOUT_KEY as F, type FundersFilterKey as G, type GetOptions as H, type GroupByResult as I, type GroupResult as J, type Ids as K, type Institution as L, type InstitutionsFilterKey as M, type Keyword as N, OpenAlexClient as O, type KeywordsFilterKey as P, LRUCache as Q, type ListResult as R, type Location as S, type Logger as T, MemoryCheckpoint as U, type Meta as V, type NgramEntry as W, type NgramsResponse as X, type OpenAccess as Y, type OpenAlexConfig as Z, type OpenAlexEntity as _, type AuthorsFilterKey as a, type ParsedUsageHeaders as a0, type PlanOptions as a1, type ProgressInfo as a2, type Publisher as a3, type PublishersFilterKey as a4, QueryBuilder as a5, REAL_CLOCK as a6, type RequestType as a7, type ResolvedConfig as a8, type ResolvedThrottle as a9, clampPerPage as aA, cursorPages as aB, decodeAbstract as aC, estimateExtractionCost as aD, fetchNgrams as aE, gt as aF, lt as aG, not as aH, or as aI, parallelBasicPages as aJ, parseUsageHeaders as aK, planExtraction as aL, resolveConfig as aM, sequentialBasicPages as aN, serializeFilters as aO, streamGzippedNdjson as aP, streamItems as aQ, streamPages as aR, type RetryPolicy as aa, SNAPSHOT_BASE_URL as ab, SNAPSHOT_S3_URI as ac, SnapshotClient as ad, type SnapshotClientOptions as ae, type SnapshotFileEntry as af, type SnapshotFormat as ag, type SnapshotManifest as ah, type SnapshotPlan as ai, type SortSpec as aj, type Source as ak, type SourcesFilterKey as al, type StreamOptions as am, type StreamSnapshotOptions as an, type SummaryStats as ao, Throttle as ap, type ThrottleOptions as aq, type ThrottleState as ar, type Topic as as, type TopicsFilterKey as at, type UsageByType as au, type UsageSnapshot as av, UsageTracker as aw, type Work as ax, WorksEndpoint as ay, type WorksFilterKey as az, type Authorship as b, type AutocompleteResult as c, type CacheStore as d, type Checkpoint as e, type CheckpointState as f, type Clock as g, type CostEstimate as h, type CountsByYear as i, type EntityFilterKeyMap as j, type EntityTotals as k, type EntityType as l, type EntityTypeMap as m, type EstimateInput as n, type ExtractionPlan as o, type ExtractionStrategy as p, FREE_DAILY_USD_WITH_KEY as q, FREE_TIER_LIMITS as r, type FetchLike as s, FileCheckpoint as t, type FilterInput as u, type FilterScalar as v, type FilterValue as w, type Filters as x, type FulltextFormat as y, type Funder as z };
|