@everdeep/pubmed 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 +207 -0
- package/dist/index.cjs +2560 -0
- package/dist/index.d.cts +432 -0
- package/dist/index.d.ts +432 -0
- package/dist/index.js +2515 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 everdeep
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# @everdeep/pubmed
|
|
2
|
+
|
|
3
|
+
A strict TypeScript client for PubMed search and record retrieval through the NCBI E-utilities API. It supports Node.js 18+, ESM and CommonJS.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @everdeep/pubmed
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configure
|
|
12
|
+
|
|
13
|
+
NCBI asks API clients to identify themselves. `email` and `tool` are therefore required explicitly:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { PubMedClient } from "@everdeep/pubmed";
|
|
17
|
+
|
|
18
|
+
const client = new PubMedClient({
|
|
19
|
+
email: "research@example.org",
|
|
20
|
+
tool: "literature-review-service",
|
|
21
|
+
apiKey: "optional-ncbi-api-key",
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The package does **not** read environment variables. It always uses the fixed NCBI endpoint at `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/`; there is no custom base URL or generic E-utilities request method. A Fetch-compatible implementation can be passed as `fetch` for testing.
|
|
26
|
+
|
|
27
|
+
## Retrieve records
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
const record = await client.get("38601234"); // PubMedRecord | null
|
|
31
|
+
|
|
32
|
+
const batch = await client.getMany(["38601234", "38300001", "38601234"]);
|
|
33
|
+
console.log(batch.records); // follows caller order and retains duplicates
|
|
34
|
+
console.log(batch.missingPmids); // missing IDs in caller order
|
|
35
|
+
console.log(batch.warnings);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
PMIDs must be non-zero numeric strings. `getMany()` deduplicates network retrieval, uses batches of at most 200 IDs, and reconstructs caller order. Cancellation rejects the whole operation with `AbortedError`; it never returns a normal-looking partial result.
|
|
39
|
+
|
|
40
|
+
## Retrieve summaries
|
|
41
|
+
|
|
42
|
+
Use ESummary when you need lightweight metadata without full XML records:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const summary = await client.getSummary("38601234"); // PubMedSummary | null
|
|
46
|
+
|
|
47
|
+
const batch = await client.getManySummaries([
|
|
48
|
+
"38601234",
|
|
49
|
+
"38300001",
|
|
50
|
+
"38601234",
|
|
51
|
+
]);
|
|
52
|
+
console.log(batch.summaries); // follows caller order and retains duplicates
|
|
53
|
+
console.log(batch.missingPmids); // missing IDs in caller order
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
A summary includes normalized authors, journal or book metadata, publication dates, languages, publication types, identifiers, and DOI/PMCID conveniences when available. Its `source` property retains the validated JSON object returned for that UID so newer ESummary fields remain accessible. Optional malformed metadata is ignored, while invalid response envelopes or mismatched UIDs are rejected.
|
|
57
|
+
|
|
58
|
+
Summary retrieval uses the same validation, batching, cancellation, response limits, caching, and in-flight coalescing policy as record retrieval.
|
|
59
|
+
|
|
60
|
+
## Search
|
|
61
|
+
|
|
62
|
+
Native PubMed query syntax and sort values are passed to ESearch:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const first = await client.search({
|
|
66
|
+
query: "CRISPR[Title] AND 2024[Date - Publication]",
|
|
67
|
+
pageSize: 50,
|
|
68
|
+
sort: "relevance",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (first.nextCursor) {
|
|
72
|
+
const second = await client.search({ cursor: first.nextCursor });
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Cursors are versioned, base64url-encoded, unsigned, implementation-specific continuation state backed by NCBI search history. Treat them as untrusted values: they are validated when consumed but are not encrypted or authenticated, and their decoded shape is not a public API. They contain no client credentials. Cursors are temporary and can produce `CursorExpiredError`; malformed values produce `CursorInvalidError`.
|
|
77
|
+
|
|
78
|
+
For progressive consumption, use `searchAll()`. `maxResults` is required so a caller must make the retrieval bound explicit:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
for await (const batch of client.searchAll({
|
|
82
|
+
query: "single cell[Title/Abstract]",
|
|
83
|
+
maxResults: 1_000,
|
|
84
|
+
pageSize: 100,
|
|
85
|
+
includeLinkOuts: true,
|
|
86
|
+
})) {
|
|
87
|
+
for (const record of batch.records) {
|
|
88
|
+
console.log(record.pmid, record.title);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
PubMed ranking is retained. PubMed history retrieval has an approximately 10,000-record window. The client checks ESearch metadata and raises `SearchLimitError` before fetching any records when the requested result target exceeds that window, instead of silently truncating. Automatic date partitioning is intentionally not part of v1.
|
|
94
|
+
|
|
95
|
+
## Records
|
|
96
|
+
|
|
97
|
+
`PubMedRecord` is a readonly discriminated union:
|
|
98
|
+
|
|
99
|
+
- `kind: "article"` — `PubmedArticle`
|
|
100
|
+
- `kind: "book"` — `PubmedBookArticle`
|
|
101
|
+
- `kind: "unknown"` — a forward-compatible direct record type, retained with a warning
|
|
102
|
+
|
|
103
|
+
Records are plain JSON-safe values. They expose ordered identifiers and `pmid`, `doi`, and `pmcid` conveniences; safe plain-text titles; structured abstracts and container-level `abstractCopyright`; structured authors; affiliations and author identifiers; journal/book citation fields; partial calendar dates (never JavaScript `Date`); history; publication types; keywords; MeSH headings; and languages. The deprecated `AbstractSection.copyright` field is retained for source compatibility but is not populated.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
if (record?.kind === "article") {
|
|
107
|
+
console.log(record.journal?.title);
|
|
108
|
+
console.log(record.dates.electronic?.year);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Every record includes `rawXml`, which is the exact direct-child XML fragment received from PubMed, without serialization or normalization. `source` retains parsed source data for fields not represented in the normalized surface.
|
|
113
|
+
|
|
114
|
+
## Citation export
|
|
115
|
+
|
|
116
|
+
Full article/book records and lightweight summaries can be serialized as RIS or BibTeX:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { formatCitation, formatCitations } from "@everdeep/pubmed";
|
|
120
|
+
|
|
121
|
+
const ris = formatCitation(record, "ris");
|
|
122
|
+
const bibtex = formatCitation(summary, "bibtex");
|
|
123
|
+
const bibliography = formatCitations([record, summary], "bibtex");
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`formatCitation()` accepts `PubMedArticleRecord`, `PubMedBookRecord`, or `PubMedSummary`. `formatCitations()` preserves caller order and duplicates, separates entries with one blank line, gives repeated BibTeX keys deterministic occurrence suffixes, and returns an empty string for an empty input. Article sources produce RIS `JOUR` / BibTeX `article` entries; book chapters produce RIS `CHAP` / BibTeX `incollection` entries; whole books produce RIS `BOOK` / BibTeX `book` entries.
|
|
127
|
+
|
|
128
|
+
Serialization is pure and deterministic. Fields use a fixed order, line breaks and control characters cannot inject RIS tags, and BibTeX-sensitive characters are escaped. Only available normalized metadata is emitted; this is a safe interchange export, not a citation-style or bibliography-rendering engine.
|
|
129
|
+
|
|
130
|
+
## Links and LinkOut
|
|
131
|
+
|
|
132
|
+
Canonical HTTPS links for PubMed, DOI, and PMC are generated from source identifiers. Per-call LinkOut enrichment is opt-in:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
const record = await client.get("38601234", { includeLinkOuts: true });
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
LinkOut URLs are returned with provider/provenance metadata. The client never dereferences them and discards schemes other than HTTP and HTTPS.
|
|
139
|
+
|
|
140
|
+
## Caching
|
|
141
|
+
|
|
142
|
+
No persistent or memory cache is enabled implicitly. Supply an async adapter, or opt into the bounded helper:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { MemoryCache, PubMedClient } from "@everdeep/pubmed";
|
|
146
|
+
|
|
147
|
+
const cache = new MemoryCache({
|
|
148
|
+
maxEntries: 500,
|
|
149
|
+
maxBytes: 25 * 1024 * 1024,
|
|
150
|
+
ttlMs: 5 * 60_000,
|
|
151
|
+
});
|
|
152
|
+
const client = new PubMedClient({ email, tool, cache });
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Eligible successful EFetch, ESummary, and ELink response bodies are cached. History-bearing ESearch requests bypass cache reads and writes because their continuation metadata can become stale; equivalent in-flight searches are still coalesced. Successful cache writes are bounded, asynchronous best effort and never delay API responses. `MemoryCache` defaults to 500 entries and 25 MiB; its byte limit counts the UTF-8 bytes of both keys and values, and entries larger than the limit are skipped. Cache and in-flight coalescing keys are hashed and credential-free. Equivalent in-flight requests are always coalesced; canceling one subscriber does not cancel another subscriber.
|
|
156
|
+
|
|
157
|
+
Custom cache adapters must be objects with async `get` and `set` functions; `delete`, when provided, must also be a function. Custom rate-limit coordinators must provide an async `acquire` function; `cooldown`, when provided, must be a function. Invalid adapter shapes and non-function `onEvent` values throw `ValidationError` synchronously when `PubMedClient` is constructed. Constructor validation checks method shapes only and does not invoke adapters.
|
|
158
|
+
|
|
159
|
+
## Rate and transport policy
|
|
160
|
+
|
|
161
|
+
Conservative defaults:
|
|
162
|
+
|
|
163
|
+
| Setting | Default |
|
|
164
|
+
| --- | ---: |
|
|
165
|
+
| Search page size | 20 |
|
|
166
|
+
| Maximum search/fetch batch | 200 |
|
|
167
|
+
| Response body cap | 25 MiB |
|
|
168
|
+
| Limiter queue | 1,000 |
|
|
169
|
+
| Timeout per attempt | 30 seconds |
|
|
170
|
+
| Attempts, including the first | 4 |
|
|
171
|
+
|
|
172
|
+
A process-shared FIFO limiter stays below NCBI ceilings: approximately 2.8 requests/second without a key and 9 requests/second with a key (below NCBI's 3/10 limits). These rate ceilings cannot be raised. `RateLimitCoordinator` can add distributed coordination and receives only a non-reversible credential fingerprint, never the API key. HTTP 429 `Retry-After` pauses the shared and distributed bucket. Server-directed cooldowns are conservatively capped at five minutes; larger values publish that bounded cooldown and stop automatic retry rather than scheduling an excessive timer.
|
|
173
|
+
|
|
174
|
+
The client retries network failures, timeouts, HTTP 408, 429, and 5xx responses with exponential full jitter. Other 4xx responses and XML/JSON parse failures are not retried. Every request uses an `application/x-www-form-urlencoded` POST to a fixed NCBI endpoint; parameters and credentials are never placed in the URL. Response bodies are capped while streaming.
|
|
175
|
+
|
|
176
|
+
There is no default logging. An optional `onEvent` callback receives sanitized events for correlation IDs, cache hits/misses, in-flight coalescing, requests, response byte counts, retries, queue delays, cooldowns, parse warnings, and terminal failures. Each logical transport request gets a generated opaque correlation ID. A `request-coalesced` event links a joining request's ID to the shared operation's ID; IDs are not derived from request content. Request and retry events also carry correlation IDs when they belong to an HTTP operation. Terminal failures expose only a stable error code, not an error message.
|
|
177
|
+
|
|
178
|
+
Callback exceptions are ignored. Events and typed errors never include API keys, email addresses, queries, request bodies, raw responses, or internal cache keys. Event payloads contain only bounded operational metadata such as endpoint, status, attempt, timing, byte count, error code, and opaque correlation IDs.
|
|
179
|
+
|
|
180
|
+
## Errors
|
|
181
|
+
|
|
182
|
+
All library failures extend `PubMedError` and have stable `code` and `retryable` properties. Exported subclasses include:
|
|
183
|
+
|
|
184
|
+
- `ValidationError`
|
|
185
|
+
- `HttpError` and `RateLimitError`
|
|
186
|
+
- `TimeoutError` and `NetworkError`
|
|
187
|
+
- `ResponseTooLargeError` and `QueueFullError`
|
|
188
|
+
- `ParseError` and `InvalidResponseError`
|
|
189
|
+
- `CursorExpiredError` and `CursorInvalidError`
|
|
190
|
+
- `AbortedError`
|
|
191
|
+
- `SearchLimitError`
|
|
192
|
+
|
|
193
|
+
Missing PMIDs are data (`null` or `missingPmids`), not exceptions.
|
|
194
|
+
|
|
195
|
+
## Development
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
npm test
|
|
199
|
+
npm run typecheck
|
|
200
|
+
npm run build
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
All normal tests mock Fetch and perform no live requests. To run the opt-in integration test, provide an identity explicitly:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
PUBMED_LIVE=1 PUBMED_EMAIL=research@example.org npm run test:live
|
|
207
|
+
```
|