@everdeep/pubmed 0.1.1 → 0.1.3
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/README.md +17 -5
- package/dist/index.cjs +42 -14
- package/dist/index.d.cts +14 -5
- package/dist/index.d.ts +14 -5
- package/dist/index.js +42 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,12 +77,14 @@ Cursors are versioned, base64url-encoded, unsigned, implementation-specific cont
|
|
|
77
77
|
|
|
78
78
|
### Count changes during pagination
|
|
79
79
|
|
|
80
|
-
NCBI can return HTTP 200 with valid IDs but a different total on a later page. By default,
|
|
80
|
+
NCBI can return HTTP 200 with valid IDs but a different total on a later page. By default, `search()` and `searchAll()` **continue retrieving valid pages**, returning count-drift diagnostics and emitting a `search-total-drift` event. The default `totalDriftPolicy` is `"warn"` (version 0.1.1 defaulted to `"error"`); no explicit opt-in or event handler is required. This is best-effort pagination, not a snapshot guarantee.
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
To stop on count changes, explicitly configure `totalDriftPolicy: "error"`. Strict mode throws `PaginationConsistencyError` (`code: "PAGINATION_INCONSISTENT"`, `retryable: false`) before fetching the changed page's records. This is a consistency signal, **not provider unavailability**. The client does not automatically retry or restart such searches.
|
|
83
|
+
|
|
84
|
+
With the default policy:
|
|
83
85
|
|
|
84
86
|
```ts
|
|
85
|
-
const client = new PubMedClient({ email, tool
|
|
87
|
+
const client = new PubMedClient({ email, tool }); // totalDriftPolicy: "warn" by default
|
|
86
88
|
const first = await client.search({ query: "cancer", pageSize: 2 });
|
|
87
89
|
if (first.nextCursor) {
|
|
88
90
|
const second = await client.search({ cursor: first.nextCursor });
|
|
@@ -94,7 +96,7 @@ In both modes, `SearchBatch.total` and the pagination bound remain the **initial
|
|
|
94
96
|
|
|
95
97
|
Warn mode returns a `diagnostics` entry on each drifted page and emits a `search-total-drift` event. A consistency error exposes the same metadata through `.diagnostic` and `toJSON()`: `reason`, `originalTotal`, `observedTotal`, `offset`, `requestedIds`, and `returnedIds`. No queries, PMIDs, cursors, or history tokens are included. Record parse `warnings` remain separate.
|
|
96
98
|
|
|
97
|
-
Neither mode guarantees snapshot enumeration: equal counts do not prove stable membership or ordering, and page-local uniqueness does not detect cross-page duplicates or omissions. Warn mode
|
|
99
|
+
Neither mode guarantees snapshot enumeration: equal counts do not prove stable membership or ordering, and page-local uniqueness does not detect cross-page duplicates or omissions. Warn mode accepts that risk; it does not silently deduplicate or claim complete results. Consumers should persist by PMID idempotently and inspect `missingPmids` and `diagnostics`. Consumers requiring a stop on count drift should select `"error"`, but that alone does not establish exact enumeration.
|
|
98
100
|
|
|
99
101
|
For progressive consumption, use `searchAll()`. `maxResults` is required so a caller must make the retrieval bound explicit:
|
|
100
102
|
|
|
@@ -130,7 +132,17 @@ if (record?.kind === "article") {
|
|
|
130
132
|
}
|
|
131
133
|
```
|
|
132
134
|
|
|
133
|
-
|
|
135
|
+
`rawXml` is omitted by default, including on unknown records. Enable it for a client or individual full-record request to retain the exact direct-child XML fragment received from PubMed, without serialization or normalization:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const client = new PubMedClient({ email, tool, includeRawXml: true });
|
|
139
|
+
const record = await client.get("123"); // includes rawXml
|
|
140
|
+
const compact = await client.get("123", { includeRawXml: false }); // omits rawXml
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The per-request option also applies to `getMany`, `search` (including cursor requests), and every page of `searchAll`. Each cursor request uses its own option or the client's default, not the previous page's option. The standalone parser similarly supports `parsePubMedXml(xml, { includeRawXml: true })` and omits XML by default.
|
|
144
|
+
|
|
145
|
+
**Compatibility change:** `rawXml` is now optional (`string | undefined`); consumers that previously relied on its presence must explicitly opt in. This controls returned fields only: XML is still fetched and parsed, and any configured response cache still stores XML bodies. `source` continues to retain parsed source data for fields not represented in the normalized surface. Summary methods are unchanged and do not support this option.
|
|
134
146
|
|
|
135
147
|
## Citation export
|
|
136
148
|
|
package/dist/index.cjs
CHANGED
|
@@ -715,8 +715,19 @@ function parseFragment(fragment) {
|
|
|
715
715
|
}
|
|
716
716
|
|
|
717
717
|
// src/parser.ts
|
|
718
|
-
function parsePubMedXml(xml) {
|
|
719
|
-
|
|
718
|
+
function parsePubMedXml(xml, options = {}) {
|
|
719
|
+
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
|
720
|
+
throw new ValidationError("parser options must be an object");
|
|
721
|
+
}
|
|
722
|
+
if (options.includeRawXml !== void 0 && typeof options.includeRawXml !== "boolean") {
|
|
723
|
+
throw new ValidationError("includeRawXml must be a boolean");
|
|
724
|
+
}
|
|
725
|
+
const records = extractFragments(xml).map((fragment) => {
|
|
726
|
+
const record = parseFragment(fragment);
|
|
727
|
+
if (options.includeRawXml === true) return record;
|
|
728
|
+
const { rawXml, ...withoutRawXml } = record;
|
|
729
|
+
return withoutRawXml;
|
|
730
|
+
});
|
|
720
731
|
const warnings = records.flatMap((record) => record.kind === "unknown" ? [{ code: "UNKNOWN_RECORD", message: `Unknown PubMed record type: ${record.recordType}`, recordType: record.recordType }] : []);
|
|
721
732
|
return { records, warnings };
|
|
722
733
|
}
|
|
@@ -1567,7 +1578,9 @@ var Transport = class {
|
|
|
1567
1578
|
const correlationId = (0, import_node_crypto.randomUUID)();
|
|
1568
1579
|
safeEvent(this.#onEvent, { type: "correlation-id", endpoint, correlationId });
|
|
1569
1580
|
try {
|
|
1570
|
-
|
|
1581
|
+
const value = await this.#coordinate(endpoint, parameters, decode, options, correlationId);
|
|
1582
|
+
if (options.signal?.aborted) throw new AbortedError();
|
|
1583
|
+
return value;
|
|
1571
1584
|
} catch (error) {
|
|
1572
1585
|
safeEvent(this.#onEvent, {
|
|
1573
1586
|
type: "terminal-failure",
|
|
@@ -1591,6 +1604,7 @@ ${semantic.toString()}`);
|
|
|
1591
1604
|
const cached = await this.#cache.read(key, signal);
|
|
1592
1605
|
if (cached.status === "hit") {
|
|
1593
1606
|
safeEvent(this.#onEvent, { type: "cache-hit", endpoint, correlationId });
|
|
1607
|
+
if (signal?.aborted) throw new AbortedError();
|
|
1594
1608
|
if (encoder.encode(cached.value).byteLength > this.#maxResponseBytes) {
|
|
1595
1609
|
await this.#cache.invalidate(key);
|
|
1596
1610
|
} else {
|
|
@@ -1671,6 +1685,9 @@ function isAbortSignal(value) {
|
|
|
1671
1685
|
function validateRequestOptions(options, name) {
|
|
1672
1686
|
const candidate = object3(options);
|
|
1673
1687
|
if (candidate === void 0) throw new ValidationError(`${name} must be an object`);
|
|
1688
|
+
if (candidate.includeRawXml !== void 0 && typeof candidate.includeRawXml !== "boolean") {
|
|
1689
|
+
throw new ValidationError("includeRawXml must be a boolean");
|
|
1690
|
+
}
|
|
1674
1691
|
if (candidate.includeLinkOuts !== void 0 && typeof candidate.includeLinkOuts !== "boolean") {
|
|
1675
1692
|
throw new ValidationError("includeLinkOuts must be a boolean");
|
|
1676
1693
|
}
|
|
@@ -1681,6 +1698,9 @@ function validateRequestOptions(options, name) {
|
|
|
1681
1698
|
function validateSummaryRequestOptions(options) {
|
|
1682
1699
|
const candidate = object3(options);
|
|
1683
1700
|
if (candidate === void 0) throw new ValidationError("summary request options must be an object");
|
|
1701
|
+
if (candidate.includeRawXml !== void 0) {
|
|
1702
|
+
throw new ValidationError("includeRawXml is not supported for summary requests");
|
|
1703
|
+
}
|
|
1684
1704
|
if (candidate.includeLinkOuts !== void 0) {
|
|
1685
1705
|
throw new ValidationError("includeLinkOuts is not supported for summary requests");
|
|
1686
1706
|
}
|
|
@@ -1813,7 +1833,7 @@ function validateSearchState(state, expectedIds, offset = 0) {
|
|
|
1813
1833
|
}
|
|
1814
1834
|
}
|
|
1815
1835
|
function parseExpectedFetch(body, expectedPmids) {
|
|
1816
|
-
const parsed = parsePubMedXml(body);
|
|
1836
|
+
const parsed = parsePubMedXml(body, { includeRawXml: true });
|
|
1817
1837
|
const expected = new Set(expectedPmids);
|
|
1818
1838
|
const seen = /* @__PURE__ */ new Set();
|
|
1819
1839
|
for (const record of parsed.records) {
|
|
@@ -1894,15 +1914,20 @@ function withLinks(record, extra) {
|
|
|
1894
1914
|
var PubMedClient = class {
|
|
1895
1915
|
#transport;
|
|
1896
1916
|
#maxBatchSize;
|
|
1917
|
+
#includeRawXml;
|
|
1897
1918
|
#totalDriftPolicy;
|
|
1898
1919
|
#onEvent;
|
|
1899
1920
|
constructor(options) {
|
|
1900
1921
|
if (typeof options !== "object" || options === null) throw new ValidationError("PubMedClient options are required");
|
|
1901
1922
|
validateAdapterShapes(options);
|
|
1923
|
+
if (options.includeRawXml !== void 0 && typeof options.includeRawXml !== "boolean") {
|
|
1924
|
+
throw new ValidationError("includeRawXml must be a boolean");
|
|
1925
|
+
}
|
|
1926
|
+
this.#includeRawXml = options.includeRawXml ?? false;
|
|
1902
1927
|
if (options.totalDriftPolicy !== void 0 && options.totalDriftPolicy !== "error" && options.totalDriftPolicy !== "warn") {
|
|
1903
1928
|
throw new ValidationError('totalDriftPolicy must be "error" or "warn"');
|
|
1904
1929
|
}
|
|
1905
|
-
this.#totalDriftPolicy = options.totalDriftPolicy ?? "
|
|
1930
|
+
this.#totalDriftPolicy = options.totalDriftPolicy ?? "warn";
|
|
1906
1931
|
if (typeof options.email !== "string" || options.email.trim() === "") throw new ValidationError("email is required");
|
|
1907
1932
|
if (typeof options.tool !== "string" || options.tool.trim() === "") throw new ValidationError("tool is required");
|
|
1908
1933
|
if (options.apiKey !== void 0 && (typeof options.apiKey !== "string" || options.apiKey.trim() === "")) {
|
|
@@ -1965,7 +1990,11 @@ var PubMedClient = class {
|
|
|
1965
1990
|
}
|
|
1966
1991
|
let ordered = input.flatMap((pmid) => byPmid.get(pmid) ?? []);
|
|
1967
1992
|
if (unknown.length > 0) ordered = [...ordered, ...unknown];
|
|
1993
|
+
if (!(options.includeRawXml ?? this.#includeRawXml)) {
|
|
1994
|
+
ordered = ordered.map(({ rawXml, ...record }) => record);
|
|
1995
|
+
}
|
|
1968
1996
|
if (options.includeLinkOuts === true && ordered.length > 0) ordered = await this.#enrich(ordered, options.signal);
|
|
1997
|
+
if (options.signal?.aborted) throw new AbortedError();
|
|
1969
1998
|
return {
|
|
1970
1999
|
records: ordered,
|
|
1971
2000
|
missingPmids: input.filter((pmid) => !byPmid.has(pmid)),
|
|
@@ -1994,6 +2023,7 @@ var PubMedClient = class {
|
|
|
1994
2023
|
);
|
|
1995
2024
|
received.push(...summaries);
|
|
1996
2025
|
}
|
|
2026
|
+
if (options.signal?.aborted) throw new AbortedError();
|
|
1997
2027
|
const byPmid = new Map(received.map((summary) => [summary.pmid, summary]));
|
|
1998
2028
|
return {
|
|
1999
2029
|
summaries: input.flatMap((pmid) => byPmid.get(pmid) ?? []),
|
|
@@ -2004,7 +2034,7 @@ var PubMedClient = class {
|
|
|
2004
2034
|
validateRequestOptions(options, "search options");
|
|
2005
2035
|
if ("cursor" in options) {
|
|
2006
2036
|
if (typeof options.cursor !== "string" || options.cursor === "") throw new ValidationError("cursor must be a non-empty string");
|
|
2007
|
-
return (await this.#searchCursorPage(options.cursor, options
|
|
2037
|
+
return (await this.#searchCursorPage(options.cursor, options)).batch;
|
|
2008
2038
|
}
|
|
2009
2039
|
validateQueryOptions(options);
|
|
2010
2040
|
return (await this.#searchQueryPage(options)).batch;
|
|
@@ -2020,6 +2050,7 @@ var PubMedClient = class {
|
|
|
2020
2050
|
query: options.query,
|
|
2021
2051
|
pageSize,
|
|
2022
2052
|
...options.sort === void 0 ? {} : { sort: options.sort },
|
|
2053
|
+
...options.includeRawXml === void 0 ? {} : { includeRawXml: options.includeRawXml },
|
|
2023
2054
|
...options.includeLinkOuts === void 0 ? {} : { includeLinkOuts: options.includeLinkOuts },
|
|
2024
2055
|
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
2025
2056
|
};
|
|
@@ -2036,8 +2067,7 @@ var PubMedClient = class {
|
|
|
2036
2067
|
if (page.batch.nextCursor === null) throw new InvalidResponseError("PubMed search ended before the requested result count");
|
|
2037
2068
|
page = await this.#searchCursorPage(
|
|
2038
2069
|
page.batch.nextCursor,
|
|
2039
|
-
options
|
|
2040
|
-
options.signal,
|
|
2070
|
+
options,
|
|
2041
2071
|
target - processed
|
|
2042
2072
|
);
|
|
2043
2073
|
}
|
|
@@ -2077,10 +2107,7 @@ var PubMedClient = class {
|
|
|
2077
2107
|
expectedPmids: []
|
|
2078
2108
|
};
|
|
2079
2109
|
}
|
|
2080
|
-
const batch = await this.getMany(state.ids,
|
|
2081
|
-
...options.includeLinkOuts === void 0 ? {} : { includeLinkOuts: options.includeLinkOuts },
|
|
2082
|
-
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
2083
|
-
});
|
|
2110
|
+
const batch = await this.getMany(state.ids, options);
|
|
2084
2111
|
const offset = state.ids.length;
|
|
2085
2112
|
return {
|
|
2086
2113
|
batch: {
|
|
@@ -2091,7 +2118,8 @@ var PubMedClient = class {
|
|
|
2091
2118
|
expectedPmids: state.ids
|
|
2092
2119
|
};
|
|
2093
2120
|
}
|
|
2094
|
-
async #searchCursorPage(cursorValue,
|
|
2121
|
+
async #searchCursorPage(cursorValue, options, maxExpected) {
|
|
2122
|
+
const { signal } = options;
|
|
2095
2123
|
const cursor = decodeCursor(cursorValue);
|
|
2096
2124
|
if (cursor.offset >= SEARCH_WINDOW) throw new SearchLimitError();
|
|
2097
2125
|
const remainingWindow = SEARCH_WINDOW - cursor.offset;
|
|
@@ -2130,7 +2158,7 @@ var PubMedClient = class {
|
|
|
2130
2158
|
{ cache: false, ...signal === void 0 ? {} : { signal } }
|
|
2131
2159
|
);
|
|
2132
2160
|
if (diagnostic !== void 0) safeEvent(this.#onEvent, { type: "search-total-drift", ...diagnostic });
|
|
2133
|
-
const batch = await this.getMany(state.ids,
|
|
2161
|
+
const batch = await this.getMany(state.ids, options);
|
|
2134
2162
|
const offset = cursor.offset + state.ids.length;
|
|
2135
2163
|
return {
|
|
2136
2164
|
batch: {
|
package/dist/index.d.cts
CHANGED
|
@@ -169,7 +169,8 @@ interface PubMedWarning {
|
|
|
169
169
|
interface BasePubMedRecord {
|
|
170
170
|
readonly kind: "article" | "book" | "unknown";
|
|
171
171
|
readonly recordType: string;
|
|
172
|
-
|
|
172
|
+
/** Exact source fragment, present only when includeRawXml is enabled. */
|
|
173
|
+
readonly rawXml?: string;
|
|
173
174
|
readonly source: JsonObject;
|
|
174
175
|
readonly identifiers: readonly PubMedIdentifier[];
|
|
175
176
|
readonly links: readonly PubMedLink[];
|
|
@@ -305,6 +306,8 @@ interface SearchBatch extends BatchResult {
|
|
|
305
306
|
readonly diagnostics?: readonly SearchTotalDriftDiagnostic[];
|
|
306
307
|
}
|
|
307
308
|
interface RequestOptions {
|
|
309
|
+
/** Override the client's raw XML inclusion default for this request. */
|
|
310
|
+
readonly includeRawXml?: boolean;
|
|
308
311
|
readonly includeLinkOuts?: boolean;
|
|
309
312
|
readonly signal?: AbortSignal;
|
|
310
313
|
}
|
|
@@ -396,6 +399,8 @@ type PubMedEvent = Readonly<{
|
|
|
396
399
|
recordType?: string;
|
|
397
400
|
}>;
|
|
398
401
|
interface PubMedClientOptions {
|
|
402
|
+
/** Include exact source XML fragments in full records. Default false. */
|
|
403
|
+
readonly includeRawXml?: boolean;
|
|
399
404
|
readonly email: string;
|
|
400
405
|
readonly tool: string;
|
|
401
406
|
readonly apiKey?: string;
|
|
@@ -408,7 +413,7 @@ interface PubMedClientOptions {
|
|
|
408
413
|
readonly maxResponseBytes?: number;
|
|
409
414
|
readonly maxQueuedRequests?: number;
|
|
410
415
|
readonly maxBatchSize?: number;
|
|
411
|
-
/** Default "
|
|
416
|
+
/** Default "warn": continue valid pages with drift diagnostics. "error" opts into stopping on count changes. Neither guarantees a snapshot. */
|
|
412
417
|
readonly totalDriftPolicy?: "error" | "warn";
|
|
413
418
|
}
|
|
414
419
|
|
|
@@ -449,7 +454,11 @@ interface ParsedPubMedXml {
|
|
|
449
454
|
readonly records: readonly PubMedRecord[];
|
|
450
455
|
readonly warnings: readonly PubMedWarning[];
|
|
451
456
|
}
|
|
452
|
-
|
|
453
|
-
|
|
457
|
+
interface ParsePubMedXmlOptions {
|
|
458
|
+
/** Include each direct child's exact source XML fragment. Default false. */
|
|
459
|
+
readonly includeRawXml?: boolean;
|
|
460
|
+
}
|
|
461
|
+
/** Parse an entire PubmedArticleSet, optionally retaining exact source fragments. */
|
|
462
|
+
declare function parsePubMedXml(xml: string, options?: ParsePubMedXmlOptions): ParsedPubMedXml;
|
|
454
463
|
|
|
455
|
-
export { AbortedError, type AbstractSection, type Affiliation, type AuthorIdentifier, type BasePubMedRecord, type BatchResult, type CacheAdapter, type CitationFormat, type CitationSource, type CollectiveAuthor, CursorExpiredError, CursorInvalidError, HttpError, InvalidResponseError, type JournalCitation, type JsonObject, type JsonPrimitive, type JsonValue, type Keyword, MemoryCache, type MemoryCacheOptions, type MeshHeading, NetworkError, PaginationConsistencyError, ParseError, type ParsedPubMedXml, type PartialDate, type PersonalAuthor, type PubMedArticleRecord, type PubMedAuthor, type PubMedBookRecord, PubMedClient, type PubMedClientOptions, type PubMedEndpoint, PubMedError, type PubMedErrorCode, type PubMedEvent, type PubMedIdentifier, type PubMedLink, type PubMedRecord, type PubMedSummary, type PubMedSummaryAuthor, type PubMedSummaryBook, type PubMedSummaryHistoryEntry, type PubMedSummaryIdentifier, type PubMedSummaryJournal, type PubMedWarning, type PublicationDates, type PublicationHistoryEntry, QueueFullError, type RateLimitBucket, type RateLimitCoordinator, RateLimitError, type RequestOptions, ResponseTooLargeError, type SearchAllOptions, type SearchBatch, type SearchCursorOptions, SearchLimitError, type SearchOptions, type SearchQueryOptions, type SearchTotalDriftDiagnostic, type SummaryBatchResult, type SummaryRequestOptions, TimeoutError, type UnknownPubMedRecord, ValidationError, formatCitation, formatCitations, parsePubMedXml };
|
|
464
|
+
export { AbortedError, type AbstractSection, type Affiliation, type AuthorIdentifier, type BasePubMedRecord, type BatchResult, type CacheAdapter, type CitationFormat, type CitationSource, type CollectiveAuthor, CursorExpiredError, CursorInvalidError, HttpError, InvalidResponseError, type JournalCitation, type JsonObject, type JsonPrimitive, type JsonValue, type Keyword, MemoryCache, type MemoryCacheOptions, type MeshHeading, NetworkError, PaginationConsistencyError, ParseError, type ParsePubMedXmlOptions, type ParsedPubMedXml, type PartialDate, type PersonalAuthor, type PubMedArticleRecord, type PubMedAuthor, type PubMedBookRecord, PubMedClient, type PubMedClientOptions, type PubMedEndpoint, PubMedError, type PubMedErrorCode, type PubMedEvent, type PubMedIdentifier, type PubMedLink, type PubMedRecord, type PubMedSummary, type PubMedSummaryAuthor, type PubMedSummaryBook, type PubMedSummaryHistoryEntry, type PubMedSummaryIdentifier, type PubMedSummaryJournal, type PubMedWarning, type PublicationDates, type PublicationHistoryEntry, QueueFullError, type RateLimitBucket, type RateLimitCoordinator, RateLimitError, type RequestOptions, ResponseTooLargeError, type SearchAllOptions, type SearchBatch, type SearchCursorOptions, SearchLimitError, type SearchOptions, type SearchQueryOptions, type SearchTotalDriftDiagnostic, type SummaryBatchResult, type SummaryRequestOptions, TimeoutError, type UnknownPubMedRecord, ValidationError, formatCitation, formatCitations, parsePubMedXml };
|
package/dist/index.d.ts
CHANGED
|
@@ -169,7 +169,8 @@ interface PubMedWarning {
|
|
|
169
169
|
interface BasePubMedRecord {
|
|
170
170
|
readonly kind: "article" | "book" | "unknown";
|
|
171
171
|
readonly recordType: string;
|
|
172
|
-
|
|
172
|
+
/** Exact source fragment, present only when includeRawXml is enabled. */
|
|
173
|
+
readonly rawXml?: string;
|
|
173
174
|
readonly source: JsonObject;
|
|
174
175
|
readonly identifiers: readonly PubMedIdentifier[];
|
|
175
176
|
readonly links: readonly PubMedLink[];
|
|
@@ -305,6 +306,8 @@ interface SearchBatch extends BatchResult {
|
|
|
305
306
|
readonly diagnostics?: readonly SearchTotalDriftDiagnostic[];
|
|
306
307
|
}
|
|
307
308
|
interface RequestOptions {
|
|
309
|
+
/** Override the client's raw XML inclusion default for this request. */
|
|
310
|
+
readonly includeRawXml?: boolean;
|
|
308
311
|
readonly includeLinkOuts?: boolean;
|
|
309
312
|
readonly signal?: AbortSignal;
|
|
310
313
|
}
|
|
@@ -396,6 +399,8 @@ type PubMedEvent = Readonly<{
|
|
|
396
399
|
recordType?: string;
|
|
397
400
|
}>;
|
|
398
401
|
interface PubMedClientOptions {
|
|
402
|
+
/** Include exact source XML fragments in full records. Default false. */
|
|
403
|
+
readonly includeRawXml?: boolean;
|
|
399
404
|
readonly email: string;
|
|
400
405
|
readonly tool: string;
|
|
401
406
|
readonly apiKey?: string;
|
|
@@ -408,7 +413,7 @@ interface PubMedClientOptions {
|
|
|
408
413
|
readonly maxResponseBytes?: number;
|
|
409
414
|
readonly maxQueuedRequests?: number;
|
|
410
415
|
readonly maxBatchSize?: number;
|
|
411
|
-
/** Default "
|
|
416
|
+
/** Default "warn": continue valid pages with drift diagnostics. "error" opts into stopping on count changes. Neither guarantees a snapshot. */
|
|
412
417
|
readonly totalDriftPolicy?: "error" | "warn";
|
|
413
418
|
}
|
|
414
419
|
|
|
@@ -449,7 +454,11 @@ interface ParsedPubMedXml {
|
|
|
449
454
|
readonly records: readonly PubMedRecord[];
|
|
450
455
|
readonly warnings: readonly PubMedWarning[];
|
|
451
456
|
}
|
|
452
|
-
|
|
453
|
-
|
|
457
|
+
interface ParsePubMedXmlOptions {
|
|
458
|
+
/** Include each direct child's exact source XML fragment. Default false. */
|
|
459
|
+
readonly includeRawXml?: boolean;
|
|
460
|
+
}
|
|
461
|
+
/** Parse an entire PubmedArticleSet, optionally retaining exact source fragments. */
|
|
462
|
+
declare function parsePubMedXml(xml: string, options?: ParsePubMedXmlOptions): ParsedPubMedXml;
|
|
454
463
|
|
|
455
|
-
export { AbortedError, type AbstractSection, type Affiliation, type AuthorIdentifier, type BasePubMedRecord, type BatchResult, type CacheAdapter, type CitationFormat, type CitationSource, type CollectiveAuthor, CursorExpiredError, CursorInvalidError, HttpError, InvalidResponseError, type JournalCitation, type JsonObject, type JsonPrimitive, type JsonValue, type Keyword, MemoryCache, type MemoryCacheOptions, type MeshHeading, NetworkError, PaginationConsistencyError, ParseError, type ParsedPubMedXml, type PartialDate, type PersonalAuthor, type PubMedArticleRecord, type PubMedAuthor, type PubMedBookRecord, PubMedClient, type PubMedClientOptions, type PubMedEndpoint, PubMedError, type PubMedErrorCode, type PubMedEvent, type PubMedIdentifier, type PubMedLink, type PubMedRecord, type PubMedSummary, type PubMedSummaryAuthor, type PubMedSummaryBook, type PubMedSummaryHistoryEntry, type PubMedSummaryIdentifier, type PubMedSummaryJournal, type PubMedWarning, type PublicationDates, type PublicationHistoryEntry, QueueFullError, type RateLimitBucket, type RateLimitCoordinator, RateLimitError, type RequestOptions, ResponseTooLargeError, type SearchAllOptions, type SearchBatch, type SearchCursorOptions, SearchLimitError, type SearchOptions, type SearchQueryOptions, type SearchTotalDriftDiagnostic, type SummaryBatchResult, type SummaryRequestOptions, TimeoutError, type UnknownPubMedRecord, ValidationError, formatCitation, formatCitations, parsePubMedXml };
|
|
464
|
+
export { AbortedError, type AbstractSection, type Affiliation, type AuthorIdentifier, type BasePubMedRecord, type BatchResult, type CacheAdapter, type CitationFormat, type CitationSource, type CollectiveAuthor, CursorExpiredError, CursorInvalidError, HttpError, InvalidResponseError, type JournalCitation, type JsonObject, type JsonPrimitive, type JsonValue, type Keyword, MemoryCache, type MemoryCacheOptions, type MeshHeading, NetworkError, PaginationConsistencyError, ParseError, type ParsePubMedXmlOptions, type ParsedPubMedXml, type PartialDate, type PersonalAuthor, type PubMedArticleRecord, type PubMedAuthor, type PubMedBookRecord, PubMedClient, type PubMedClientOptions, type PubMedEndpoint, PubMedError, type PubMedErrorCode, type PubMedEvent, type PubMedIdentifier, type PubMedLink, type PubMedRecord, type PubMedSummary, type PubMedSummaryAuthor, type PubMedSummaryBook, type PubMedSummaryHistoryEntry, type PubMedSummaryIdentifier, type PubMedSummaryJournal, type PubMedWarning, type PublicationDates, type PublicationHistoryEntry, QueueFullError, type RateLimitBucket, type RateLimitCoordinator, RateLimitError, type RequestOptions, ResponseTooLargeError, type SearchAllOptions, type SearchBatch, type SearchCursorOptions, SearchLimitError, type SearchOptions, type SearchQueryOptions, type SearchTotalDriftDiagnostic, type SummaryBatchResult, type SummaryRequestOptions, TimeoutError, type UnknownPubMedRecord, ValidationError, formatCitation, formatCitations, parsePubMedXml };
|
package/dist/index.js
CHANGED
|
@@ -670,8 +670,19 @@ function parseFragment(fragment) {
|
|
|
670
670
|
}
|
|
671
671
|
|
|
672
672
|
// src/parser.ts
|
|
673
|
-
function parsePubMedXml(xml) {
|
|
674
|
-
|
|
673
|
+
function parsePubMedXml(xml, options = {}) {
|
|
674
|
+
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
|
675
|
+
throw new ValidationError("parser options must be an object");
|
|
676
|
+
}
|
|
677
|
+
if (options.includeRawXml !== void 0 && typeof options.includeRawXml !== "boolean") {
|
|
678
|
+
throw new ValidationError("includeRawXml must be a boolean");
|
|
679
|
+
}
|
|
680
|
+
const records = extractFragments(xml).map((fragment) => {
|
|
681
|
+
const record = parseFragment(fragment);
|
|
682
|
+
if (options.includeRawXml === true) return record;
|
|
683
|
+
const { rawXml, ...withoutRawXml } = record;
|
|
684
|
+
return withoutRawXml;
|
|
685
|
+
});
|
|
675
686
|
const warnings = records.flatMap((record) => record.kind === "unknown" ? [{ code: "UNKNOWN_RECORD", message: `Unknown PubMed record type: ${record.recordType}`, recordType: record.recordType }] : []);
|
|
676
687
|
return { records, warnings };
|
|
677
688
|
}
|
|
@@ -1522,7 +1533,9 @@ var Transport = class {
|
|
|
1522
1533
|
const correlationId = randomUUID();
|
|
1523
1534
|
safeEvent(this.#onEvent, { type: "correlation-id", endpoint, correlationId });
|
|
1524
1535
|
try {
|
|
1525
|
-
|
|
1536
|
+
const value = await this.#coordinate(endpoint, parameters, decode, options, correlationId);
|
|
1537
|
+
if (options.signal?.aborted) throw new AbortedError();
|
|
1538
|
+
return value;
|
|
1526
1539
|
} catch (error) {
|
|
1527
1540
|
safeEvent(this.#onEvent, {
|
|
1528
1541
|
type: "terminal-failure",
|
|
@@ -1546,6 +1559,7 @@ ${semantic.toString()}`);
|
|
|
1546
1559
|
const cached = await this.#cache.read(key, signal);
|
|
1547
1560
|
if (cached.status === "hit") {
|
|
1548
1561
|
safeEvent(this.#onEvent, { type: "cache-hit", endpoint, correlationId });
|
|
1562
|
+
if (signal?.aborted) throw new AbortedError();
|
|
1549
1563
|
if (encoder.encode(cached.value).byteLength > this.#maxResponseBytes) {
|
|
1550
1564
|
await this.#cache.invalidate(key);
|
|
1551
1565
|
} else {
|
|
@@ -1626,6 +1640,9 @@ function isAbortSignal(value) {
|
|
|
1626
1640
|
function validateRequestOptions(options, name) {
|
|
1627
1641
|
const candidate = object3(options);
|
|
1628
1642
|
if (candidate === void 0) throw new ValidationError(`${name} must be an object`);
|
|
1643
|
+
if (candidate.includeRawXml !== void 0 && typeof candidate.includeRawXml !== "boolean") {
|
|
1644
|
+
throw new ValidationError("includeRawXml must be a boolean");
|
|
1645
|
+
}
|
|
1629
1646
|
if (candidate.includeLinkOuts !== void 0 && typeof candidate.includeLinkOuts !== "boolean") {
|
|
1630
1647
|
throw new ValidationError("includeLinkOuts must be a boolean");
|
|
1631
1648
|
}
|
|
@@ -1636,6 +1653,9 @@ function validateRequestOptions(options, name) {
|
|
|
1636
1653
|
function validateSummaryRequestOptions(options) {
|
|
1637
1654
|
const candidate = object3(options);
|
|
1638
1655
|
if (candidate === void 0) throw new ValidationError("summary request options must be an object");
|
|
1656
|
+
if (candidate.includeRawXml !== void 0) {
|
|
1657
|
+
throw new ValidationError("includeRawXml is not supported for summary requests");
|
|
1658
|
+
}
|
|
1639
1659
|
if (candidate.includeLinkOuts !== void 0) {
|
|
1640
1660
|
throw new ValidationError("includeLinkOuts is not supported for summary requests");
|
|
1641
1661
|
}
|
|
@@ -1768,7 +1788,7 @@ function validateSearchState(state, expectedIds, offset = 0) {
|
|
|
1768
1788
|
}
|
|
1769
1789
|
}
|
|
1770
1790
|
function parseExpectedFetch(body, expectedPmids) {
|
|
1771
|
-
const parsed = parsePubMedXml(body);
|
|
1791
|
+
const parsed = parsePubMedXml(body, { includeRawXml: true });
|
|
1772
1792
|
const expected = new Set(expectedPmids);
|
|
1773
1793
|
const seen = /* @__PURE__ */ new Set();
|
|
1774
1794
|
for (const record of parsed.records) {
|
|
@@ -1849,15 +1869,20 @@ function withLinks(record, extra) {
|
|
|
1849
1869
|
var PubMedClient = class {
|
|
1850
1870
|
#transport;
|
|
1851
1871
|
#maxBatchSize;
|
|
1872
|
+
#includeRawXml;
|
|
1852
1873
|
#totalDriftPolicy;
|
|
1853
1874
|
#onEvent;
|
|
1854
1875
|
constructor(options) {
|
|
1855
1876
|
if (typeof options !== "object" || options === null) throw new ValidationError("PubMedClient options are required");
|
|
1856
1877
|
validateAdapterShapes(options);
|
|
1878
|
+
if (options.includeRawXml !== void 0 && typeof options.includeRawXml !== "boolean") {
|
|
1879
|
+
throw new ValidationError("includeRawXml must be a boolean");
|
|
1880
|
+
}
|
|
1881
|
+
this.#includeRawXml = options.includeRawXml ?? false;
|
|
1857
1882
|
if (options.totalDriftPolicy !== void 0 && options.totalDriftPolicy !== "error" && options.totalDriftPolicy !== "warn") {
|
|
1858
1883
|
throw new ValidationError('totalDriftPolicy must be "error" or "warn"');
|
|
1859
1884
|
}
|
|
1860
|
-
this.#totalDriftPolicy = options.totalDriftPolicy ?? "
|
|
1885
|
+
this.#totalDriftPolicy = options.totalDriftPolicy ?? "warn";
|
|
1861
1886
|
if (typeof options.email !== "string" || options.email.trim() === "") throw new ValidationError("email is required");
|
|
1862
1887
|
if (typeof options.tool !== "string" || options.tool.trim() === "") throw new ValidationError("tool is required");
|
|
1863
1888
|
if (options.apiKey !== void 0 && (typeof options.apiKey !== "string" || options.apiKey.trim() === "")) {
|
|
@@ -1920,7 +1945,11 @@ var PubMedClient = class {
|
|
|
1920
1945
|
}
|
|
1921
1946
|
let ordered = input.flatMap((pmid) => byPmid.get(pmid) ?? []);
|
|
1922
1947
|
if (unknown.length > 0) ordered = [...ordered, ...unknown];
|
|
1948
|
+
if (!(options.includeRawXml ?? this.#includeRawXml)) {
|
|
1949
|
+
ordered = ordered.map(({ rawXml, ...record }) => record);
|
|
1950
|
+
}
|
|
1923
1951
|
if (options.includeLinkOuts === true && ordered.length > 0) ordered = await this.#enrich(ordered, options.signal);
|
|
1952
|
+
if (options.signal?.aborted) throw new AbortedError();
|
|
1924
1953
|
return {
|
|
1925
1954
|
records: ordered,
|
|
1926
1955
|
missingPmids: input.filter((pmid) => !byPmid.has(pmid)),
|
|
@@ -1949,6 +1978,7 @@ var PubMedClient = class {
|
|
|
1949
1978
|
);
|
|
1950
1979
|
received.push(...summaries);
|
|
1951
1980
|
}
|
|
1981
|
+
if (options.signal?.aborted) throw new AbortedError();
|
|
1952
1982
|
const byPmid = new Map(received.map((summary) => [summary.pmid, summary]));
|
|
1953
1983
|
return {
|
|
1954
1984
|
summaries: input.flatMap((pmid) => byPmid.get(pmid) ?? []),
|
|
@@ -1959,7 +1989,7 @@ var PubMedClient = class {
|
|
|
1959
1989
|
validateRequestOptions(options, "search options");
|
|
1960
1990
|
if ("cursor" in options) {
|
|
1961
1991
|
if (typeof options.cursor !== "string" || options.cursor === "") throw new ValidationError("cursor must be a non-empty string");
|
|
1962
|
-
return (await this.#searchCursorPage(options.cursor, options
|
|
1992
|
+
return (await this.#searchCursorPage(options.cursor, options)).batch;
|
|
1963
1993
|
}
|
|
1964
1994
|
validateQueryOptions(options);
|
|
1965
1995
|
return (await this.#searchQueryPage(options)).batch;
|
|
@@ -1975,6 +2005,7 @@ var PubMedClient = class {
|
|
|
1975
2005
|
query: options.query,
|
|
1976
2006
|
pageSize,
|
|
1977
2007
|
...options.sort === void 0 ? {} : { sort: options.sort },
|
|
2008
|
+
...options.includeRawXml === void 0 ? {} : { includeRawXml: options.includeRawXml },
|
|
1978
2009
|
...options.includeLinkOuts === void 0 ? {} : { includeLinkOuts: options.includeLinkOuts },
|
|
1979
2010
|
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
1980
2011
|
};
|
|
@@ -1991,8 +2022,7 @@ var PubMedClient = class {
|
|
|
1991
2022
|
if (page.batch.nextCursor === null) throw new InvalidResponseError("PubMed search ended before the requested result count");
|
|
1992
2023
|
page = await this.#searchCursorPage(
|
|
1993
2024
|
page.batch.nextCursor,
|
|
1994
|
-
options
|
|
1995
|
-
options.signal,
|
|
2025
|
+
options,
|
|
1996
2026
|
target - processed
|
|
1997
2027
|
);
|
|
1998
2028
|
}
|
|
@@ -2032,10 +2062,7 @@ var PubMedClient = class {
|
|
|
2032
2062
|
expectedPmids: []
|
|
2033
2063
|
};
|
|
2034
2064
|
}
|
|
2035
|
-
const batch = await this.getMany(state.ids,
|
|
2036
|
-
...options.includeLinkOuts === void 0 ? {} : { includeLinkOuts: options.includeLinkOuts },
|
|
2037
|
-
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
2038
|
-
});
|
|
2065
|
+
const batch = await this.getMany(state.ids, options);
|
|
2039
2066
|
const offset = state.ids.length;
|
|
2040
2067
|
return {
|
|
2041
2068
|
batch: {
|
|
@@ -2046,7 +2073,8 @@ var PubMedClient = class {
|
|
|
2046
2073
|
expectedPmids: state.ids
|
|
2047
2074
|
};
|
|
2048
2075
|
}
|
|
2049
|
-
async #searchCursorPage(cursorValue,
|
|
2076
|
+
async #searchCursorPage(cursorValue, options, maxExpected) {
|
|
2077
|
+
const { signal } = options;
|
|
2050
2078
|
const cursor = decodeCursor(cursorValue);
|
|
2051
2079
|
if (cursor.offset >= SEARCH_WINDOW) throw new SearchLimitError();
|
|
2052
2080
|
const remainingWindow = SEARCH_WINDOW - cursor.offset;
|
|
@@ -2085,7 +2113,7 @@ var PubMedClient = class {
|
|
|
2085
2113
|
{ cache: false, ...signal === void 0 ? {} : { signal } }
|
|
2086
2114
|
);
|
|
2087
2115
|
if (diagnostic !== void 0) safeEvent(this.#onEvent, { type: "search-total-drift", ...diagnostic });
|
|
2088
|
-
const batch = await this.getMany(state.ids,
|
|
2116
|
+
const batch = await this.getMany(state.ids, options);
|
|
2089
2117
|
const offset = cursor.offset + state.ids.length;
|
|
2090
2118
|
return {
|
|
2091
2119
|
batch: {
|