@cyanheads/pubmed-mcp-server 1.0.12

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.
Files changed (121) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +174 -0
  3. package/dist/config/index.d.ts +91 -0
  4. package/dist/config/index.js +324 -0
  5. package/dist/index.d.ts +24 -0
  6. package/dist/index.js +190 -0
  7. package/dist/mcp-server/resources/echoResource/echoResourceLogic.d.ts +79 -0
  8. package/dist/mcp-server/resources/echoResource/echoResourceLogic.js +82 -0
  9. package/dist/mcp-server/resources/echoResource/index.d.ts +13 -0
  10. package/dist/mcp-server/resources/echoResource/index.js +13 -0
  11. package/dist/mcp-server/resources/echoResource/registration.d.ts +30 -0
  12. package/dist/mcp-server/resources/echoResource/registration.js +168 -0
  13. package/dist/mcp-server/server.d.ts +28 -0
  14. package/dist/mcp-server/server.js +159 -0
  15. package/dist/mcp-server/tools/fetchPubMedContent/index.d.ts +6 -0
  16. package/dist/mcp-server/tools/fetchPubMedContent/index.js +6 -0
  17. package/dist/mcp-server/tools/fetchPubMedContent/logic.d.ts +63 -0
  18. package/dist/mcp-server/tools/fetchPubMedContent/logic.js +518 -0
  19. package/dist/mcp-server/tools/fetchPubMedContent/registration.d.ts +10 -0
  20. package/dist/mcp-server/tools/fetchPubMedContent/registration.js +37 -0
  21. package/dist/mcp-server/tools/getPubMedArticleConnections/index.d.ts +6 -0
  22. package/dist/mcp-server/tools/getPubMedArticleConnections/index.js +6 -0
  23. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.d.ts +9 -0
  24. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +288 -0
  25. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.d.ts +9 -0
  26. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.js +208 -0
  27. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.d.ts +15 -0
  28. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.js +82 -0
  29. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/types.d.ts +27 -0
  30. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/types.js +5 -0
  31. package/dist/mcp-server/tools/getPubMedArticleConnections/logic.d.ts +6 -0
  32. package/dist/mcp-server/tools/getPubMedArticleConnections/logic.js +6 -0
  33. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.d.ts +36 -0
  34. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.js +76 -0
  35. package/dist/mcp-server/tools/pubmedResearchAgent/index.d.ts +6 -0
  36. package/dist/mcp-server/tools/pubmedResearchAgent/index.js +6 -0
  37. package/dist/mcp-server/tools/pubmedResearchAgent/logic/index.d.ts +7 -0
  38. package/dist/mcp-server/tools/pubmedResearchAgent/logic/index.js +9 -0
  39. package/dist/mcp-server/tools/pubmedResearchAgent/logic/inputSchema.d.ts +111 -0
  40. package/dist/mcp-server/tools/pubmedResearchAgent/logic/inputSchema.js +149 -0
  41. package/dist/mcp-server/tools/pubmedResearchAgent/logic/outputTypes.d.ts +106 -0
  42. package/dist/mcp-server/tools/pubmedResearchAgent/logic/outputTypes.js +7 -0
  43. package/dist/mcp-server/tools/pubmedResearchAgent/logic/planOrchestrator.d.ts +10 -0
  44. package/dist/mcp-server/tools/pubmedResearchAgent/logic/planOrchestrator.js +251 -0
  45. package/dist/mcp-server/tools/pubmedResearchAgent/logic.d.ts +12 -0
  46. package/dist/mcp-server/tools/pubmedResearchAgent/logic.js +55 -0
  47. package/dist/mcp-server/tools/pubmedResearchAgent/registration.d.ts +10 -0
  48. package/dist/mcp-server/tools/pubmedResearchAgent/registration.js +36 -0
  49. package/dist/mcp-server/tools/searchPubMedArticles/index.d.ts +6 -0
  50. package/dist/mcp-server/tools/searchPubMedArticles/index.js +6 -0
  51. package/dist/mcp-server/tools/searchPubMedArticles/logic.d.ts +61 -0
  52. package/dist/mcp-server/tools/searchPubMedArticles/logic.js +229 -0
  53. package/dist/mcp-server/tools/searchPubMedArticles/registration.d.ts +10 -0
  54. package/dist/mcp-server/tools/searchPubMedArticles/registration.js +38 -0
  55. package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +29 -0
  56. package/dist/mcp-server/transports/authentication/authMiddleware.js +174 -0
  57. package/dist/mcp-server/transports/httpTransport.d.ts +23 -0
  58. package/dist/mcp-server/transports/httpTransport.js +463 -0
  59. package/dist/mcp-server/transports/stdioTransport.d.ts +42 -0
  60. package/dist/mcp-server/transports/stdioTransport.js +63 -0
  61. package/dist/services/NCBI/ncbiConstants.d.ts +22 -0
  62. package/dist/services/NCBI/ncbiConstants.js +5 -0
  63. package/dist/services/NCBI/ncbiCoreApiClient.d.ts +24 -0
  64. package/dist/services/NCBI/ncbiCoreApiClient.js +118 -0
  65. package/dist/services/NCBI/ncbiRequestQueueManager.d.ts +36 -0
  66. package/dist/services/NCBI/ncbiRequestQueueManager.js +96 -0
  67. package/dist/services/NCBI/ncbiResponseHandler.d.ts +22 -0
  68. package/dist/services/NCBI/ncbiResponseHandler.js +192 -0
  69. package/dist/services/NCBI/ncbiService.d.ts +24 -0
  70. package/dist/services/NCBI/ncbiService.js +57 -0
  71. package/dist/services/index.d.ts +7 -0
  72. package/dist/services/index.js +7 -0
  73. package/dist/services/llm-providers/index.d.ts +7 -0
  74. package/dist/services/llm-providers/index.js +7 -0
  75. package/dist/services/llm-providers/llmFactory.d.ts +69 -0
  76. package/dist/services/llm-providers/llmFactory.js +132 -0
  77. package/dist/services/llm-providers/openRouter/index.d.ts +6 -0
  78. package/dist/services/llm-providers/openRouter/index.js +7 -0
  79. package/dist/services/llm-providers/openRouter/openRouterProvider.d.ts +99 -0
  80. package/dist/services/llm-providers/openRouter/openRouterProvider.js +329 -0
  81. package/dist/types-global/errors.d.ts +119 -0
  82. package/dist/types-global/errors.js +119 -0
  83. package/dist/types-global/pubmedXml.d.ts +350 -0
  84. package/dist/types-global/pubmedXml.js +7 -0
  85. package/dist/utils/index.d.ts +10 -0
  86. package/dist/utils/index.js +18 -0
  87. package/dist/utils/internal/errorHandler.d.ts +176 -0
  88. package/dist/utils/internal/errorHandler.js +335 -0
  89. package/dist/utils/internal/index.d.ts +9 -0
  90. package/dist/utils/internal/index.js +9 -0
  91. package/dist/utils/internal/logger.d.ts +141 -0
  92. package/dist/utils/internal/logger.js +426 -0
  93. package/dist/utils/internal/requestContext.d.ts +83 -0
  94. package/dist/utils/internal/requestContext.js +72 -0
  95. package/dist/utils/metrics/index.d.ts +7 -0
  96. package/dist/utils/metrics/index.js +7 -0
  97. package/dist/utils/metrics/tokenCounter.d.ts +35 -0
  98. package/dist/utils/metrics/tokenCounter.js +110 -0
  99. package/dist/utils/parsing/dateParser.d.ts +73 -0
  100. package/dist/utils/parsing/dateParser.js +107 -0
  101. package/dist/utils/parsing/index.d.ts +8 -0
  102. package/dist/utils/parsing/index.js +8 -0
  103. package/dist/utils/parsing/jsonParser.d.ts +82 -0
  104. package/dist/utils/parsing/jsonParser.js +126 -0
  105. package/dist/utils/parsing/ncbi-parsing/eSummaryResultParser.d.ts +32 -0
  106. package/dist/utils/parsing/ncbi-parsing/eSummaryResultParser.js +295 -0
  107. package/dist/utils/parsing/ncbi-parsing/index.d.ts +8 -0
  108. package/dist/utils/parsing/ncbi-parsing/index.js +8 -0
  109. package/dist/utils/parsing/ncbi-parsing/pubmedArticleStructureParser.d.ts +70 -0
  110. package/dist/utils/parsing/ncbi-parsing/pubmedArticleStructureParser.js +229 -0
  111. package/dist/utils/parsing/ncbi-parsing/xmlGenericHelpers.d.ts +31 -0
  112. package/dist/utils/parsing/ncbi-parsing/xmlGenericHelpers.js +76 -0
  113. package/dist/utils/security/idGenerator.d.ts +134 -0
  114. package/dist/utils/security/idGenerator.js +198 -0
  115. package/dist/utils/security/index.d.ts +9 -0
  116. package/dist/utils/security/index.js +9 -0
  117. package/dist/utils/security/rateLimiter.d.ts +108 -0
  118. package/dist/utils/security/rateLimiter.js +179 -0
  119. package/dist/utils/security/sanitization.d.ts +173 -0
  120. package/dist/utils/security/sanitization.js +439 -0
  121. package/package.json +97 -0
@@ -0,0 +1,295 @@
1
+ /**
2
+ * @fileoverview Helper functions for parsing ESummary results from NCBI.
3
+ * Handles different ESummary XML structures and formats the data into
4
+ * consistent ParsedBriefSummary objects.
5
+ * @module src/utils/parsing/ncbi-parsing/eSummaryResultParser
6
+ */
7
+ import { dateParser, logger, requestContextService, } from "../../../utils/index.js"; // Note: utils/index.js is the barrel file
8
+ import { ensureArray, getAttribute, getText } from "./xmlGenericHelpers.js";
9
+ /**
10
+ * Formats an array of ESummary authors into a string.
11
+ * Limits to the first 3 authors and adds "et al." if more exist.
12
+ * @param authors - Array of ESummary author objects (normalized).
13
+ * @returns A string like "Doe J, Smith A, Brown B, et al." or empty if no authors.
14
+ */
15
+ export function formatESummaryAuthors(authors) {
16
+ if (!authors || authors.length === 0)
17
+ return "";
18
+ return (authors
19
+ .slice(0, 3)
20
+ .map((author) => author.name) // Assumes author.name is the string representation
21
+ .join(", ") + (authors.length > 3 ? ", et al." : ""));
22
+ }
23
+ /**
24
+ * Standardizes date strings from ESummary to "YYYY-MM-DD" format.
25
+ * Uses the dateParser utility.
26
+ * @param dateStr - Date string from ESummary (e.g., "2023/01/15", "2023 Jan 15", "2023").
27
+ * @param parentContext - Optional parent request context for logging.
28
+ * @returns A promise resolving to a standardized date string ("YYYY-MM-DD") or undefined if parsing fails.
29
+ */
30
+ export async function standardizeESummaryDate(dateStr, parentContext) {
31
+ if (dateStr === undefined || dateStr === null)
32
+ return undefined; // Check for null as well
33
+ const dateInputString = String(dateStr); // Ensure it's a string
34
+ const currentContext = parentContext ||
35
+ requestContextService.createRequestContext({
36
+ operation: "standardizeESummaryDateInternal",
37
+ inputDate: dateInputString, // Log the stringified version
38
+ });
39
+ try {
40
+ // Pass the stringified version to the date parser
41
+ const parsedDate = await dateParser.parseDate(dateInputString, currentContext);
42
+ if (parsedDate) {
43
+ return parsedDate.toISOString().split("T")[0]; // Format as YYYY-MM-DD
44
+ }
45
+ logger.debug(`standardizeESummaryDate: dateParser could not parse "${dateInputString}", returning undefined.`, currentContext);
46
+ }
47
+ catch (e) {
48
+ logger.warning(`standardizeESummaryDate: Error during dateParser.parseDate for "${dateInputString}", returning undefined.`, {
49
+ ...currentContext,
50
+ error: e instanceof Error ? e.message : String(e),
51
+ });
52
+ }
53
+ return undefined; // Return undefined if parsing fails
54
+ }
55
+ /**
56
+ * Parses authors from an ESummary DocumentSummary structure.
57
+ * Handles various ways authors might be represented.
58
+ * Returns an array of normalized XmlESummaryAuthor objects.
59
+ * Internal helper function.
60
+ */
61
+ function parseESummaryAuthorsFromDocumentSummary(docSummary) {
62
+ const authorsProp = docSummary.Authors;
63
+ if (!authorsProp)
64
+ return [];
65
+ const parsedAuthors = [];
66
+ const processRawAuthor = (rawAuthInput) => {
67
+ let name = "";
68
+ let authtype;
69
+ let clusterid;
70
+ if (typeof rawAuthInput === "string") {
71
+ name = rawAuthInput;
72
+ }
73
+ else if (rawAuthInput && typeof rawAuthInput === "object") {
74
+ const authorObj = rawAuthInput; // Now typed
75
+ // Try extracting text from the object itself (e.g., if it's { '#text': 'Author Name' })
76
+ name = getText(authorObj, "");
77
+ // If name is still empty, try common property names for author names
78
+ if (!name) {
79
+ name = getText(authorObj.Name || authorObj.name, "");
80
+ }
81
+ authtype = getText(authorObj.AuthType || authorObj.authtype, undefined);
82
+ clusterid = getText(authorObj.ClusterId || authorObj.clusterid, undefined);
83
+ // Fallback for unhandled structures: log and try to stringify
84
+ if (!name) {
85
+ const authInputString = JSON.stringify(authorObj);
86
+ logger.warning(`Unhandled author structure in parseESummaryAuthorsFromDocumentSummary. authInput: ${authInputString.substring(0, 100)}`, requestContextService.createRequestContext({
87
+ operation: "parseESummaryAuthorsFromDocumentSummary",
88
+ detail: "Unhandled author structure",
89
+ }));
90
+ // As a last resort, if it's a simple object with a single value, that might be the name
91
+ const keys = Object.keys(authorObj);
92
+ if (keys.length === 1 &&
93
+ typeof authorObj[keys[0]] === "string") {
94
+ name = authorObj[keys[0]];
95
+ }
96
+ else if (authInputString.length < 100) {
97
+ // Avoid overly long stringified objects
98
+ name = authInputString; // Not ideal, but better than empty for debugging
99
+ }
100
+ }
101
+ }
102
+ if (name.trim()) {
103
+ parsedAuthors.push({
104
+ name: name.trim(),
105
+ authtype,
106
+ clusterid,
107
+ });
108
+ }
109
+ };
110
+ if (Array.isArray(authorsProp)) {
111
+ // authorsProp could be Array<string> or Array<XmlESummaryAuthorRaw>
112
+ authorsProp.forEach(processRawAuthor);
113
+ }
114
+ else if (typeof authorsProp === "object" &&
115
+ "Author" in authorsProp && // authorsProp is { Author: ... }
116
+ authorsProp.Author) {
117
+ const rawAuthors = ensureArray(authorsProp.Author);
118
+ rawAuthors.forEach(processRawAuthor);
119
+ }
120
+ else if (typeof authorsProp === "string") {
121
+ try {
122
+ // Attempt to parse if it looks like a JSON array string
123
+ if (authorsProp.startsWith("[") && authorsProp.endsWith("]")) {
124
+ const parsedJsonAuthors = JSON.parse(authorsProp);
125
+ if (Array.isArray(parsedJsonAuthors)) {
126
+ parsedJsonAuthors.forEach((authItem) => {
127
+ if (typeof authItem === "string") {
128
+ parsedAuthors.push({ name: authItem.trim() });
129
+ }
130
+ else if (typeof authItem === "object" &&
131
+ authItem !== null &&
132
+ (authItem.name || authItem.Name)) {
133
+ // If it's an object with a name property, treat as XmlESummaryAuthorRaw
134
+ processRawAuthor(authItem);
135
+ }
136
+ });
137
+ if (parsedAuthors.length > 0)
138
+ return parsedAuthors; // Return if JSON parsing yielded results
139
+ }
140
+ }
141
+ }
142
+ catch (e) {
143
+ logger.debug(`Failed to parse Authors string as JSON: ${authorsProp.substring(0, 100)}`, requestContextService.createRequestContext({
144
+ operation: "parseESummaryAuthorsFromString",
145
+ input: authorsProp.substring(0, 100),
146
+ error: e instanceof Error ? e.message : String(e),
147
+ }));
148
+ }
149
+ // Fallback: split string by common delimiters
150
+ authorsProp
151
+ .split(/[,;]/)
152
+ .map((namePart) => namePart.trim())
153
+ .filter((namePart) => namePart)
154
+ .forEach((namePart) => parsedAuthors.push({ name: namePart }));
155
+ }
156
+ return parsedAuthors.filter((author) => author.name);
157
+ }
158
+ /**
159
+ * Parses a single ESummary DocumentSummary (newer XML format) into a raw summary object.
160
+ * Internal helper function.
161
+ */
162
+ function parseSingleDocumentSummary(docSummary) {
163
+ const pmid = docSummary["@_uid"];
164
+ const authorsArray = parseESummaryAuthorsFromDocumentSummary(docSummary);
165
+ let doiValue = getText(docSummary.DOI, undefined);
166
+ if (!doiValue) {
167
+ const articleIdsProp = docSummary.ArticleIds;
168
+ if (articleIdsProp) {
169
+ const idsArray = Array.isArray(articleIdsProp)
170
+ ? articleIdsProp
171
+ : ensureArray(articleIdsProp.ArticleId);
172
+ const doiEntry = idsArray.find((id) => id.idtype === "doi");
173
+ if (doiEntry) {
174
+ doiValue = getText(doiEntry.value, undefined);
175
+ }
176
+ }
177
+ }
178
+ return {
179
+ pmid: String(pmid),
180
+ title: getText(docSummary.Title, undefined),
181
+ authors: formatESummaryAuthors(authorsArray),
182
+ source: getText(docSummary.Source, undefined) ||
183
+ getText(docSummary.FullJournalName, undefined) ||
184
+ getText(docSummary.SO, undefined) ||
185
+ undefined,
186
+ doi: doiValue,
187
+ rawPubDate: getText(docSummary.PubDate, undefined),
188
+ rawEPubDate: getText(docSummary.EPubDate, undefined),
189
+ };
190
+ }
191
+ /**
192
+ * Parses a single ESummary DocSum (older XML item-based format) into a raw summary object.
193
+ * Internal helper function.
194
+ */
195
+ function parseSingleDocSumOldXml(docSum) {
196
+ const pmid = docSum.Id;
197
+ const items = ensureArray(docSum.Item);
198
+ const getItemValue = (name, type) => {
199
+ const namesToTry = ensureArray(name);
200
+ for (const n of namesToTry) {
201
+ const item = items.find((i) => i._Name === n &&
202
+ (type ? i._Type === type : true) &&
203
+ i._Type !== "ERROR");
204
+ if (item) {
205
+ const textVal = getText(item);
206
+ if (textVal !== undefined)
207
+ return String(textVal);
208
+ }
209
+ }
210
+ return undefined;
211
+ };
212
+ const getAuthorList = () => {
213
+ const authorListItem = items.find((i) => i._Name === "AuthorList" && i._Type === "List");
214
+ if (authorListItem && authorListItem.Item) {
215
+ return ensureArray(authorListItem.Item)
216
+ .filter((a) => a._Name === "Author" && a._Type === "String")
217
+ .map((a) => ({ name: getText(a, "") }));
218
+ }
219
+ // Fallback for authors directly under DocSum items
220
+ return items
221
+ .filter((i) => i._Name === "Author" && i._Type === "String")
222
+ .map((a) => ({ name: getText(a, "") }));
223
+ };
224
+ const authorsArray = getAuthorList();
225
+ let doiFromItems = getItemValue("DOI", "String");
226
+ if (!doiFromItems) {
227
+ const articleIdsItem = items.find((i) => i._Name === "ArticleIds" && i._Type === "List");
228
+ if (articleIdsItem && articleIdsItem.Item) {
229
+ const ids = ensureArray(articleIdsItem.Item);
230
+ const doiIdItem = ids.find((id) => getAttribute(id, "idtype") === "doi" ||
231
+ id._Name === "doi");
232
+ if (doiIdItem) {
233
+ doiFromItems = getText(doiIdItem);
234
+ }
235
+ }
236
+ }
237
+ return {
238
+ pmid: String(pmid),
239
+ title: getItemValue("Title", "String"),
240
+ authors: formatESummaryAuthors(authorsArray),
241
+ source: getItemValue(["Source", "FullJournalName", "SO"], "String"),
242
+ doi: doiFromItems,
243
+ rawPubDate: getItemValue(["PubDate", "ArticleDate"], "Date"),
244
+ rawEPubDate: getItemValue("EPubDate", "Date"),
245
+ };
246
+ }
247
+ /**
248
+ * Extracts and formats brief summaries from ESummary XML result.
249
+ * Handles both DocumentSummarySet (newer) and older DocSum structures.
250
+ * Asynchronously standardizes dates.
251
+ * @param eSummaryResult - The parsed XML object from ESummary (eSummaryResult part).
252
+ * @param context - Request context for logging and passing to date standardization.
253
+ * @returns A promise resolving to an array of parsed brief summary objects.
254
+ */
255
+ export async function extractBriefSummaries(eSummaryResult, context) {
256
+ if (!eSummaryResult)
257
+ return [];
258
+ const opContext = context ||
259
+ requestContextService.createRequestContext({
260
+ operation: "extractBriefSummariesInternal",
261
+ });
262
+ if (eSummaryResult.ERROR) {
263
+ logger.warning("ESummary result contains an error", {
264
+ ...opContext,
265
+ errorDetails: eSummaryResult.ERROR,
266
+ });
267
+ return [];
268
+ }
269
+ let rawSummaries = [];
270
+ if (eSummaryResult.DocumentSummarySet?.DocumentSummary) {
271
+ const docSummaries = ensureArray(eSummaryResult.DocumentSummarySet.DocumentSummary);
272
+ rawSummaries = docSummaries
273
+ .map(parseSingleDocumentSummary)
274
+ .filter((s) => s.pmid);
275
+ }
276
+ else if (eSummaryResult.DocSum) {
277
+ const docSums = ensureArray(eSummaryResult.DocSum);
278
+ rawSummaries = docSums.map(parseSingleDocSumOldXml).filter((s) => s.pmid);
279
+ }
280
+ const processedSummaries = [];
281
+ for (const rawSummary of rawSummaries) {
282
+ const pubDate = await standardizeESummaryDate(rawSummary.rawPubDate, opContext);
283
+ const epubDate = await standardizeESummaryDate(rawSummary.rawEPubDate, opContext);
284
+ processedSummaries.push({
285
+ pmid: rawSummary.pmid,
286
+ title: rawSummary.title,
287
+ authors: rawSummary.authors,
288
+ source: rawSummary.source,
289
+ doi: rawSummary.doi,
290
+ pubDate,
291
+ epubDate,
292
+ });
293
+ }
294
+ return processedSummaries;
295
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @fileoverview Barrel file for NCBI XML parsing helper utilities.
3
+ * Re-exports functions from more specific parser modules.
4
+ * @module src/utils/parsing/ncbi-parsing/index
5
+ */
6
+ export * from "./xmlGenericHelpers.js";
7
+ export * from "./pubmedArticleStructureParser.js";
8
+ export * from "./eSummaryResultParser.js";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @fileoverview Barrel file for NCBI XML parsing helper utilities.
3
+ * Re-exports functions from more specific parser modules.
4
+ * @module src/utils/parsing/ncbi-parsing/index
5
+ */
6
+ export * from "./xmlGenericHelpers.js";
7
+ export * from "./pubmedArticleStructureParser.js";
8
+ export * from "./eSummaryResultParser.js";
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @fileoverview Helper functions for parsing detailed PubMed Article XML structures,
3
+ * typically from EFetch results.
4
+ * @module src/utils/parsing/ncbi-parsing/pubmedArticleStructureParser
5
+ */
6
+ import { XmlArticle, XmlAuthorList, XmlGrantList, XmlJournal, XmlKeywordList, XmlMedlineCitation, XmlMeshHeadingList, XmlPublicationTypeList, ParsedArticleAuthor, ParsedJournalInfo, ParsedMeshTerm, ParsedGrant, ParsedArticleDate } from "../../../types-global/pubmedXml.js";
7
+ /**
8
+ * Extracts and formats author information from XML.
9
+ * @param authorListXml - The XML AuthorList element.
10
+ * @returns An array of formatted author objects.
11
+ */
12
+ export declare function extractAuthors(authorListXml?: XmlAuthorList): ParsedArticleAuthor[];
13
+ /**
14
+ * Extracts and formats journal information from XML.
15
+ * @param journalXml - The XML Journal element from an Article.
16
+ * @param medlineCitationXml - The XML MedlineCitation element (for MedlinePgn).
17
+ * @returns Formatted journal information.
18
+ */
19
+ export declare function extractJournalInfo(journalXml?: XmlJournal, medlineCitationXml?: XmlMedlineCitation): ParsedJournalInfo | undefined;
20
+ /**
21
+ * Extracts and formats MeSH terms from XML.
22
+ * @param meshHeadingListXml - The XML MeshHeadingList element.
23
+ * @returns An array of formatted MeSH term objects.
24
+ */
25
+ export declare function extractMeshTerms(meshHeadingListXml?: XmlMeshHeadingList): ParsedMeshTerm[];
26
+ /**
27
+ * Extracts and formats grant information from XML.
28
+ * @param grantListXml - The XML GrantList element.
29
+ * @returns An array of formatted grant objects.
30
+ */
31
+ export declare function extractGrants(grantListXml?: XmlGrantList): ParsedGrant[];
32
+ /**
33
+ * Extracts DOI from various possible locations in the XML.
34
+ * Prioritizes ELocationID with ValidYN="Y", then any ELocationID, then ArticleIdList.
35
+ * @param articleXml - The XML Article element.
36
+ * @returns The DOI string or undefined.
37
+ */
38
+ export declare function extractDoi(articleXml?: XmlArticle): string | undefined;
39
+ /**
40
+ * Extracts publication types from XML.
41
+ * @param publicationTypeListXml - The XML PublicationTypeList element.
42
+ * @returns An array of publication type strings.
43
+ */
44
+ export declare function extractPublicationTypes(publicationTypeListXml?: XmlPublicationTypeList): string[];
45
+ /**
46
+ * Extracts keywords from XML. Handles single or multiple KeywordList elements.
47
+ * @param keywordListsXml - The XML KeywordList element or an array of them.
48
+ * @returns An array of keyword strings.
49
+ */
50
+ export declare function extractKeywords(keywordListsXml?: XmlKeywordList[] | XmlKeywordList): string[];
51
+ /**
52
+ * Extracts abstract text from XML. Handles structured abstracts by concatenating sections.
53
+ * If AbstractText is an array, joins them. If it's a single object/string, uses it directly.
54
+ * Prefixes with Label if present.
55
+ * @param abstractXml - The XML Abstract element from an Article.
56
+ * @returns The abstract text string, or undefined if not found or empty.
57
+ */
58
+ export declare function extractAbstractText(abstractXml?: XmlArticle["Abstract"]): string | undefined;
59
+ /**
60
+ * Extracts PMID from MedlineCitation.
61
+ * @param medlineCitationXml - The XML MedlineCitation element.
62
+ * @returns The PMID string or undefined.
63
+ */
64
+ export declare function extractPmid(medlineCitationXml?: XmlMedlineCitation): string | undefined;
65
+ /**
66
+ * Extracts article dates from XML.
67
+ * @param articleXml - The XML Article element.
68
+ * @returns An array of parsed article dates.
69
+ */
70
+ export declare function extractArticleDates(articleXml?: XmlArticle): ParsedArticleDate[];
@@ -0,0 +1,229 @@
1
+ /**
2
+ * @fileoverview Helper functions for parsing detailed PubMed Article XML structures,
3
+ * typically from EFetch results.
4
+ * @module src/utils/parsing/ncbi-parsing/pubmedArticleStructureParser
5
+ */
6
+ import { ensureArray, getText, getAttribute } from "./xmlGenericHelpers.js";
7
+ /**
8
+ * Extracts and formats author information from XML.
9
+ * @param authorListXml - The XML AuthorList element.
10
+ * @returns An array of formatted author objects.
11
+ */
12
+ export function extractAuthors(authorListXml) {
13
+ if (!authorListXml)
14
+ return [];
15
+ const authors = ensureArray(authorListXml.Author);
16
+ return authors.map((auth) => {
17
+ let affiliation = "";
18
+ const affiliations = ensureArray(auth.AffiliationInfo);
19
+ if (affiliations.length > 0) {
20
+ affiliation = getText(affiliations[0]?.Affiliation);
21
+ }
22
+ return {
23
+ lastName: getText(auth.LastName),
24
+ firstName: getText(auth.ForeName), // XML uses ForeName
25
+ initials: getText(auth.Initials),
26
+ affiliation: affiliation || undefined, // Ensure undefined if empty
27
+ };
28
+ });
29
+ }
30
+ /**
31
+ * Extracts and formats journal information from XML.
32
+ * @param journalXml - The XML Journal element from an Article.
33
+ * @param medlineCitationXml - The XML MedlineCitation element (for MedlinePgn).
34
+ * @returns Formatted journal information.
35
+ */
36
+ export function extractJournalInfo(journalXml, medlineCitationXml) {
37
+ if (!journalXml)
38
+ return undefined;
39
+ const pubDate = journalXml.JournalIssue?.PubDate;
40
+ const year = getText(pubDate?.Year, getText(pubDate?.MedlineDate, "").match(/\d{4}/)?.[0]);
41
+ return {
42
+ title: getText(journalXml.Title),
43
+ isoAbbreviation: getText(journalXml.ISOAbbreviation),
44
+ volume: getText(journalXml.JournalIssue?.Volume),
45
+ issue: getText(journalXml.JournalIssue?.Issue),
46
+ pages: getText(medlineCitationXml?.MedlinePgn) ||
47
+ getText(medlineCitationXml?.Article?.Pagination?.MedlinePgn),
48
+ publicationDate: {
49
+ year: year || undefined,
50
+ month: getText(pubDate?.Month) || undefined,
51
+ day: getText(pubDate?.Day) || undefined,
52
+ medlineDate: getText(pubDate?.MedlineDate) || undefined,
53
+ },
54
+ };
55
+ }
56
+ /**
57
+ * Extracts and formats MeSH terms from XML.
58
+ * @param meshHeadingListXml - The XML MeshHeadingList element.
59
+ * @returns An array of formatted MeSH term objects.
60
+ */
61
+ export function extractMeshTerms(meshHeadingListXml) {
62
+ if (!meshHeadingListXml)
63
+ return [];
64
+ const meshHeadings = ensureArray(meshHeadingListXml.MeshHeading);
65
+ return meshHeadings.map((mh) => {
66
+ const qualifiers = ensureArray(mh.QualifierName);
67
+ const firstQualifier = qualifiers[0];
68
+ // Check MajorTopicYN at DescriptorName, QualifierName, and the root MeshHeading element
69
+ const isMajorDescriptor = getAttribute(mh.DescriptorName, "MajorTopicYN") === "Y";
70
+ const isMajorQualifier = firstQualifier
71
+ ? getAttribute(firstQualifier, "MajorTopicYN") === "Y"
72
+ : false;
73
+ // Some schemas might place MajorTopicYN directly on MeshHeading if no qualifiers
74
+ const isMajorRoot = getAttribute(mh, "MajorTopicYN") === "Y";
75
+ return {
76
+ descriptorName: getText(mh.DescriptorName),
77
+ descriptorUi: getAttribute(mh.DescriptorName, "UI"),
78
+ qualifierName: firstQualifier ? getText(firstQualifier) : undefined,
79
+ qualifierUi: firstQualifier
80
+ ? getAttribute(firstQualifier, "UI")
81
+ : undefined,
82
+ isMajorTopic: isMajorRoot || isMajorDescriptor || isMajorQualifier,
83
+ };
84
+ });
85
+ }
86
+ /**
87
+ * Extracts and formats grant information from XML.
88
+ * @param grantListXml - The XML GrantList element.
89
+ * @returns An array of formatted grant objects.
90
+ */
91
+ export function extractGrants(grantListXml) {
92
+ if (!grantListXml)
93
+ return [];
94
+ const grants = ensureArray(grantListXml.Grant);
95
+ return grants.map((g) => ({
96
+ grantId: getText(g.GrantID) || undefined,
97
+ agency: getText(g.Agency) || undefined,
98
+ country: getText(g.Country) || undefined,
99
+ }));
100
+ }
101
+ /**
102
+ * Extracts DOI from various possible locations in the XML.
103
+ * Prioritizes ELocationID with ValidYN="Y", then any ELocationID, then ArticleIdList.
104
+ * @param articleXml - The XML Article element.
105
+ * @returns The DOI string or undefined.
106
+ */
107
+ export function extractDoi(articleXml) {
108
+ if (!articleXml)
109
+ return undefined;
110
+ // Check ELocationID first
111
+ const eLocationIDs = ensureArray(articleXml.ELocationID);
112
+ // Prioritize valid DOI
113
+ for (const eloc of eLocationIDs) {
114
+ if (getAttribute(eloc, "EIdType") === "doi" &&
115
+ getAttribute(eloc, "ValidYN") === "Y") {
116
+ const doi = getText(eloc);
117
+ if (doi)
118
+ return doi;
119
+ }
120
+ }
121
+ // Fallback to any DOI in ELocationID
122
+ for (const eloc of eLocationIDs) {
123
+ if (getAttribute(eloc, "EIdType") === "doi") {
124
+ const doi = getText(eloc);
125
+ if (doi)
126
+ return doi;
127
+ }
128
+ }
129
+ // Check ArticleIdList as a secondary source
130
+ const articleIds = ensureArray(articleXml.ArticleIdList?.ArticleId);
131
+ for (const aid of articleIds) {
132
+ if (getAttribute(aid, "IdType") === "doi") {
133
+ const doi = getText(aid);
134
+ if (doi)
135
+ return doi;
136
+ }
137
+ }
138
+ return undefined;
139
+ }
140
+ /**
141
+ * Extracts publication types from XML.
142
+ * @param publicationTypeListXml - The XML PublicationTypeList element.
143
+ * @returns An array of publication type strings.
144
+ */
145
+ export function extractPublicationTypes(publicationTypeListXml) {
146
+ if (!publicationTypeListXml)
147
+ return [];
148
+ const pubTypes = ensureArray(publicationTypeListXml.PublicationType);
149
+ return pubTypes.map((pt) => getText(pt)).filter(Boolean);
150
+ }
151
+ /**
152
+ * Extracts keywords from XML. Handles single or multiple KeywordList elements.
153
+ * @param keywordListsXml - The XML KeywordList element or an array of them.
154
+ * @returns An array of keyword strings.
155
+ */
156
+ export function extractKeywords(keywordListsXml) {
157
+ if (!keywordListsXml)
158
+ return [];
159
+ const lists = ensureArray(keywordListsXml);
160
+ const allKeywords = [];
161
+ for (const list of lists) {
162
+ const keywords = ensureArray(list.Keyword);
163
+ keywords.forEach((kw) => {
164
+ const keywordText = getText(kw);
165
+ if (keywordText) {
166
+ allKeywords.push(keywordText);
167
+ }
168
+ });
169
+ }
170
+ return allKeywords;
171
+ }
172
+ /**
173
+ * Extracts abstract text from XML. Handles structured abstracts by concatenating sections.
174
+ * If AbstractText is an array, joins them. If it's a single object/string, uses it directly.
175
+ * Prefixes with Label if present.
176
+ * @param abstractXml - The XML Abstract element from an Article.
177
+ * @returns The abstract text string, or undefined if not found or empty.
178
+ */
179
+ export function extractAbstractText(abstractXml) {
180
+ if (!abstractXml || !abstractXml.AbstractText)
181
+ return undefined;
182
+ const abstractTexts = ensureArray(abstractXml.AbstractText);
183
+ if (abstractTexts.length === 0)
184
+ return undefined;
185
+ const processedTexts = abstractTexts
186
+ .map((at) => {
187
+ // AbstractText can be string directly or object
188
+ if (typeof at === "string") {
189
+ return at;
190
+ }
191
+ // If it's an object, it should have #text or Label
192
+ let sectionText = getText(at); // Handles at["#text"]
193
+ const label = getAttribute(at, "Label");
194
+ if (label && sectionText) {
195
+ return `${label.trim()}: ${sectionText.trim()}`;
196
+ }
197
+ return sectionText.trim();
198
+ })
199
+ .filter(Boolean); // Remove any empty strings resulting from empty sections
200
+ if (processedTexts.length === 0)
201
+ return undefined;
202
+ return processedTexts.join("\n\n").trim() || undefined; // Join sections with double newline
203
+ }
204
+ /**
205
+ * Extracts PMID from MedlineCitation.
206
+ * @param medlineCitationXml - The XML MedlineCitation element.
207
+ * @returns The PMID string or undefined.
208
+ */
209
+ export function extractPmid(medlineCitationXml) {
210
+ if (!medlineCitationXml || !medlineCitationXml.PMID)
211
+ return undefined;
212
+ return getText(medlineCitationXml.PMID);
213
+ }
214
+ /**
215
+ * Extracts article dates from XML.
216
+ * @param articleXml - The XML Article element.
217
+ * @returns An array of parsed article dates.
218
+ */
219
+ export function extractArticleDates(articleXml) {
220
+ if (!articleXml || !articleXml.ArticleDate)
221
+ return [];
222
+ const articleDatesXml = ensureArray(articleXml.ArticleDate);
223
+ return articleDatesXml.map((ad) => ({
224
+ dateType: getAttribute(ad, "DateType"),
225
+ year: getText(ad.Year),
226
+ month: getText(ad.Month),
227
+ day: getText(ad.Day),
228
+ }));
229
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @fileoverview Generic helper functions for parsing XML data, particularly
3
+ * structures from fast-xml-parser.
4
+ * @module src/utils/parsing/ncbi-parsing/xmlGenericHelpers
5
+ */
6
+ /**
7
+ * Ensures that the input is an array. If it's not an array, it wraps it in one.
8
+ * Handles undefined or null by returning an empty array.
9
+ * @param item - The item to ensure is an array.
10
+ * @returns An array containing the item, or an empty array if item is null/undefined.
11
+ * @template T - The type of the items in the array.
12
+ */
13
+ export declare function ensureArray<T>(item: T | T[] | undefined | null): T[];
14
+ /**
15
+ * Safely extracts text content from an XML element, which might be a string or an object with a "#text" property.
16
+ * Handles cases where #text might be a number or boolean by converting to string.
17
+ * @param element - The XML element (string, object with #text, or undefined).
18
+ * @param defaultValue - The value to return if text cannot be extracted. Defaults to an empty string.
19
+ * @returns The text content or the default value.
20
+ */
21
+ export declare function getText(element: any, defaultValue?: string): string;
22
+ /**
23
+ * Safely extracts an attribute value from an XML element.
24
+ * Assumes attributes are prefixed with "@_" by fast-xml-parser.
25
+ * @param element - The XML element object.
26
+ * @param attributeName - The name of the attribute (e.g., "_UI", "_MajorTopicYN", without the "@_" prefix).
27
+ * @param defaultValue - The value to return if the attribute is not found. Defaults to an empty string.
28
+ * @returns The attribute value or the default value.
29
+ */
30
+ export declare function getAttribute(element: any, attributeName: string, // e.g., "UI", "MajorTopicYN"
31
+ defaultValue?: string): string;