@ncukondo/search-hub 0.19.0 → 0.20.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.
Files changed (75) hide show
  1. package/dist/cli/commands/related.d.ts +66 -0
  2. package/dist/cli/commands/related.d.ts.map +1 -0
  3. package/dist/cli/commands/related.js +161 -0
  4. package/dist/cli/commands/related.js.map +1 -0
  5. package/dist/cli/commands/review/extract.d.ts.map +1 -1
  6. package/dist/cli/commands/review/extract.js +15 -5
  7. package/dist/cli/commands/review/extract.js.map +1 -1
  8. package/dist/cli/commands/review/init.d.ts +2 -3
  9. package/dist/cli/commands/review/init.d.ts.map +1 -1
  10. package/dist/cli/commands/review/init.js +1 -0
  11. package/dist/cli/commands/review/init.js.map +1 -1
  12. package/dist/cli/commands/review/next-steps.d.ts +3 -0
  13. package/dist/cli/commands/review/next-steps.d.ts.map +1 -1
  14. package/dist/cli/commands/review/next-steps.js +53 -19
  15. package/dist/cli/commands/review/next-steps.js.map +1 -1
  16. package/dist/cli/commands/review/schema.d.ts +8 -0
  17. package/dist/cli/commands/review/schema.d.ts.map +1 -1
  18. package/dist/cli/commands/review/schema.js +3 -0
  19. package/dist/cli/commands/review/schema.js.map +1 -1
  20. package/dist/cli/commands/review/status.d.ts +3 -1
  21. package/dist/cli/commands/review/status.d.ts.map +1 -1
  22. package/dist/cli/commands/review/status.js +3 -1
  23. package/dist/cli/commands/review/status.js.map +1 -1
  24. package/dist/cli/commands/review/types.d.ts +2 -1
  25. package/dist/cli/commands/review/types.d.ts.map +1 -1
  26. package/dist/cli/commands/review/types.js.map +1 -1
  27. package/dist/cli/commands/search-executor.d.ts.map +1 -1
  28. package/dist/cli/commands/search-executor.js +3 -2
  29. package/dist/cli/commands/search-executor.js.map +1 -1
  30. package/dist/cli/commands/search.d.ts +2 -0
  31. package/dist/cli/commands/search.d.ts.map +1 -1
  32. package/dist/cli/commands/search.js +3 -0
  33. package/dist/cli/commands/search.js.map +1 -1
  34. package/dist/cli/index.d.ts.map +1 -1
  35. package/dist/cli/index.js +128 -3
  36. package/dist/cli/index.js.map +1 -1
  37. package/dist/cli/suggestions/rules.d.ts.map +1 -1
  38. package/dist/cli/suggestions/rules.js +19 -3
  39. package/dist/cli/suggestions/rules.js.map +1 -1
  40. package/dist/providers/arxiv/provider.d.ts.map +1 -1
  41. package/dist/providers/arxiv/provider.js +7 -4
  42. package/dist/providers/arxiv/provider.js.map +1 -1
  43. package/dist/providers/base/types.d.ts +8 -0
  44. package/dist/providers/base/types.d.ts.map +1 -1
  45. package/dist/providers/base/types.js.map +1 -1
  46. package/dist/providers/eric/provider.d.ts +3 -0
  47. package/dist/providers/eric/provider.d.ts.map +1 -1
  48. package/dist/providers/eric/provider.js +11 -0
  49. package/dist/providers/eric/provider.js.map +1 -1
  50. package/dist/providers/pubmed/client.d.ts +15 -1
  51. package/dist/providers/pubmed/client.d.ts.map +1 -1
  52. package/dist/providers/pubmed/client.js +64 -1
  53. package/dist/providers/pubmed/client.js.map +1 -1
  54. package/dist/providers/pubmed/index.d.ts +2 -2
  55. package/dist/providers/pubmed/index.d.ts.map +1 -1
  56. package/dist/providers/pubmed/parser.d.ts +8 -1
  57. package/dist/providers/pubmed/parser.d.ts.map +1 -1
  58. package/dist/providers/pubmed/parser.js +23 -1
  59. package/dist/providers/pubmed/parser.js.map +1 -1
  60. package/dist/providers/pubmed/provider.d.ts.map +1 -1
  61. package/dist/providers/pubmed/provider.js +8 -2
  62. package/dist/providers/pubmed/provider.js.map +1 -1
  63. package/dist/providers/pubmed/types.d.ts +29 -0
  64. package/dist/providers/pubmed/types.d.ts.map +1 -1
  65. package/dist/providers/scopus/client.d.ts +2 -0
  66. package/dist/providers/scopus/client.d.ts.map +1 -1
  67. package/dist/providers/scopus/client.js +3 -0
  68. package/dist/providers/scopus/client.js.map +1 -1
  69. package/dist/providers/scopus/provider.d.ts.map +1 -1
  70. package/dist/providers/scopus/provider.js +7 -1
  71. package/dist/providers/scopus/provider.js.map +1 -1
  72. package/dist/session/types.d.ts +13 -1
  73. package/dist/session/types.d.ts.map +1 -1
  74. package/dist/session/types.js.map +1 -1
  75. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sources":["../../../src/providers/pubmed/client.ts"],"sourcesContent":["/**\n * PubMed HTTP client for E-utilities API.\n *\n * Handles communication with NCBI's PubMed database including:\n * - esearch: Search and get PMIDs\n * - efetch: Fetch full records by PMID\n */\n\nimport { RateLimiter, createProviderError } from '../base/index.js';\nimport type { ProviderError, ProviderErrorCode } from '../base/types.js';\nimport { parseESearchResponse, parseEFetchResponse } from './parser.js';\nimport type { ESearchResponse, PubMedArticle, PubMedConfig } from './types.js';\n\nconst BASE_URL = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils';\n\n/**\n * Options for esearch API call.\n */\nexport interface SearchOptions {\n /** Starting offset for pagination (default: 0) */\n retstart?: number;\n /** Maximum number of results to return (default: 20, max: 10000) */\n retmax?: number;\n /** Use history server for large result sets */\n useHistory?: boolean;\n}\n\n/**\n * Options for history-based fetch.\n */\nexport interface HistoryFetchOptions {\n /** Web environment from esearch */\n webenv: string;\n /** Query key from esearch */\n querykey: string;\n /** Starting offset */\n retstart: number;\n /** Maximum number of results */\n retmax: number;\n}\n\n/**\n * HTTP client for PubMed E-utilities API.\n */\nexport class PubMedClient {\n private readonly config: PubMedConfig;\n private readonly rateLimiter: RateLimiter;\n\n constructor(config: PubMedConfig, rateLimiter: RateLimiter) {\n this.config = config;\n this.rateLimiter = rateLimiter;\n }\n\n /**\n * Search PubMed using esearch API.\n */\n async search(query: string, options: SearchOptions = {}): Promise<ESearchResponse> {\n await this.rateLimiter.acquire();\n\n const params = new URLSearchParams({\n db: 'pubmed',\n term: query,\n email: this.config.email,\n retmode: 'xml',\n });\n\n if (this.config.apiKey) {\n params.set('api_key', this.config.apiKey);\n }\n\n if (options.retstart !== undefined) {\n params.set('retstart', String(options.retstart));\n }\n\n if (options.retmax !== undefined) {\n params.set('retmax', String(options.retmax));\n }\n\n if (options.useHistory) {\n params.set('usehistory', 'y');\n }\n\n const url = `${BASE_URL}/esearch.fcgi?${params.toString()}`;\n const response = await this.fetchWithErrorHandling(url);\n const xml = await response.text();\n\n this.rateLimiter.resetBackoff();\n return parseESearchResponse(xml);\n }\n\n /**\n * Get total hit count for a query using ESearch with rettype=count.\n * Does not return IDs or download any results.\n */\n async searchCount(query: string): Promise<number> {\n await this.rateLimiter.acquire();\n\n const params = new URLSearchParams({\n db: 'pubmed',\n term: query,\n email: this.config.email,\n rettype: 'count',\n retmode: 'xml',\n });\n\n if (this.config.apiKey) {\n params.set('api_key', this.config.apiKey);\n }\n\n const url = `${BASE_URL}/esearch.fcgi?${params.toString()}`;\n const response = await this.fetchWithErrorHandling(url);\n const xml = await response.text();\n\n this.rateLimiter.resetBackoff();\n const parsed = parseESearchResponse(xml);\n return parsed.count;\n }\n\n /**\n * Fetch articles by PMID list using efetch API.\n */\n async fetch(pmids: string[]): Promise<PubMedArticle[]> {\n if (pmids.length === 0) {\n return [];\n }\n\n await this.rateLimiter.acquire();\n\n const params = new URLSearchParams({\n db: 'pubmed',\n id: pmids.join(','),\n rettype: 'xml',\n retmode: 'xml',\n email: this.config.email,\n });\n\n if (this.config.apiKey) {\n params.set('api_key', this.config.apiKey);\n }\n\n const url = `${BASE_URL}/efetch.fcgi?${params.toString()}`;\n const response = await this.fetchWithErrorHandling(url);\n const xml = await response.text();\n\n this.rateLimiter.resetBackoff();\n return parseEFetchResponse(xml).articles;\n }\n\n /**\n * Fetch articles using history server (webenv/querykey).\n */\n async fetchFromHistory(options: HistoryFetchOptions): Promise<PubMedArticle[]> {\n await this.rateLimiter.acquire();\n\n const params = new URLSearchParams({\n db: 'pubmed',\n WebEnv: options.webenv,\n query_key: options.querykey,\n retstart: String(options.retstart),\n retmax: String(options.retmax),\n rettype: 'xml',\n retmode: 'xml',\n email: this.config.email,\n });\n\n if (this.config.apiKey) {\n params.set('api_key', this.config.apiKey);\n }\n\n const url = `${BASE_URL}/efetch.fcgi?${params.toString()}`;\n const response = await this.fetchWithErrorHandling(url);\n const xml = await response.text();\n\n this.rateLimiter.resetBackoff();\n return parseEFetchResponse(xml).articles;\n }\n\n /**\n * Fetch with error handling for HTTP responses.\n */\n private async fetchWithErrorHandling(url: string): Promise<Response> {\n let response: Response;\n\n try {\n response = await fetch(url);\n } catch (error) {\n throw this.createError('NETWORK_ERROR', 'Network request failed', true, error);\n }\n\n if (response.ok) {\n return response;\n }\n\n // Handle error responses\n if (response.status === 400) {\n throw this.createError('PARSE_ERROR', 'Invalid query syntax', false);\n }\n\n if (response.status === 429) {\n const retryAfter = response.headers.get('Retry-After');\n const retryAfterMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : undefined;\n const error = this.createError(\n 'RATE_LIMIT_EXCEEDED',\n 'Too many requests',\n true\n ) as ProviderError & { retryAfter?: number };\n if (retryAfterMs !== undefined) {\n error.retryAfter = retryAfterMs;\n }\n throw error;\n }\n\n if (response.status >= 500) {\n throw this.createError('SERVER_ERROR', `Server error: ${response.status}`, true);\n }\n\n throw this.createError(\n 'NETWORK_ERROR',\n `HTTP ${response.status}: ${response.statusText}`,\n true\n );\n }\n\n /**\n * Create a ProviderError.\n */\n private createError(\n code: ProviderErrorCode,\n message: string,\n retryable: boolean,\n cause?: unknown\n ): ProviderError {\n return createProviderError(code, message, 'pubmed', { retryable, cause });\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.js","sources":["../../../src/providers/pubmed/client.ts"],"sourcesContent":["/**\n * PubMed HTTP client for E-utilities API.\n *\n * Handles communication with NCBI's PubMed database including:\n * - esearch: Search and get PMIDs\n * - efetch: Fetch full records by PMID\n */\n\nimport { RateLimiter, createProviderError } from '../base/index.js';\nimport type { ProviderError, ProviderErrorCode } from '../base/types.js';\nimport { parseESearchResponse, parseEFetchResponse, parseELinkResponse } from './parser.js';\nimport type { ELinkOptions, ELinkResponse, RelatedArticle, ESearchResponse, PubMedArticle, PubMedConfig } from './types.js';\n\nconst BASE_URL = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils';\n\n/**\n * Options for esearch API call.\n */\nexport interface SearchOptions {\n /** Starting offset for pagination (default: 0) */\n retstart?: number;\n /** Maximum number of results to return (default: 20, max: 10000) */\n retmax?: number;\n /** Use history server for large result sets */\n useHistory?: boolean;\n /** Sort parameter for esearch (e.g. 'relevance', 'pub_date') */\n sort?: string;\n}\n\n/**\n * Options for history-based fetch.\n */\nexport interface HistoryFetchOptions {\n /** Web environment from esearch */\n webenv: string;\n /** Query key from esearch */\n querykey: string;\n /** Starting offset */\n retstart: number;\n /** Maximum number of results */\n retmax: number;\n}\n\n/**\n * HTTP client for PubMed E-utilities API.\n */\nexport class PubMedClient {\n private readonly config: PubMedConfig;\n private readonly rateLimiter: RateLimiter;\n\n constructor(config: PubMedConfig, rateLimiter: RateLimiter) {\n this.config = config;\n this.rateLimiter = rateLimiter;\n }\n\n /**\n * Search PubMed using esearch API.\n */\n async search(query: string, options: SearchOptions = {}): Promise<ESearchResponse> {\n await this.rateLimiter.acquire();\n\n const params = new URLSearchParams({\n db: 'pubmed',\n term: query,\n email: this.config.email,\n retmode: 'xml',\n });\n\n if (this.config.apiKey) {\n params.set('api_key', this.config.apiKey);\n }\n\n if (options.retstart !== undefined) {\n params.set('retstart', String(options.retstart));\n }\n\n if (options.retmax !== undefined) {\n params.set('retmax', String(options.retmax));\n }\n\n if (options.useHistory) {\n params.set('usehistory', 'y');\n }\n\n if (options.sort) {\n params.set('sort', options.sort);\n }\n\n const url = `${BASE_URL}/esearch.fcgi?${params.toString()}`;\n const response = await this.fetchWithErrorHandling(url);\n const xml = await response.text();\n\n this.rateLimiter.resetBackoff();\n return parseESearchResponse(xml);\n }\n\n /**\n * Get total hit count for a query using ESearch with rettype=count.\n * Does not return IDs or download any results.\n */\n async searchCount(query: string): Promise<number> {\n await this.rateLimiter.acquire();\n\n const params = new URLSearchParams({\n db: 'pubmed',\n term: query,\n email: this.config.email,\n rettype: 'count',\n retmode: 'xml',\n });\n\n if (this.config.apiKey) {\n params.set('api_key', this.config.apiKey);\n }\n\n const url = `${BASE_URL}/esearch.fcgi?${params.toString()}`;\n const response = await this.fetchWithErrorHandling(url);\n const xml = await response.text();\n\n this.rateLimiter.resetBackoff();\n const parsed = parseESearchResponse(xml);\n return parsed.count;\n }\n\n /**\n * Fetch articles by PMID list using efetch API.\n */\n async fetch(pmids: string[]): Promise<PubMedArticle[]> {\n if (pmids.length === 0) {\n return [];\n }\n\n await this.rateLimiter.acquire();\n\n const params = new URLSearchParams({\n db: 'pubmed',\n id: pmids.join(','),\n rettype: 'xml',\n retmode: 'xml',\n email: this.config.email,\n });\n\n if (this.config.apiKey) {\n params.set('api_key', this.config.apiKey);\n }\n\n const url = `${BASE_URL}/efetch.fcgi?${params.toString()}`;\n const response = await this.fetchWithErrorHandling(url);\n const xml = await response.text();\n\n this.rateLimiter.resetBackoff();\n return parseEFetchResponse(xml).articles;\n }\n\n /**\n * Fetch articles using history server (webenv/querykey).\n */\n async fetchFromHistory(options: HistoryFetchOptions): Promise<PubMedArticle[]> {\n await this.rateLimiter.acquire();\n\n const params = new URLSearchParams({\n db: 'pubmed',\n WebEnv: options.webenv,\n query_key: options.querykey,\n retstart: String(options.retstart),\n retmax: String(options.retmax),\n rettype: 'xml',\n retmode: 'xml',\n email: this.config.email,\n });\n\n if (this.config.apiKey) {\n params.set('api_key', this.config.apiKey);\n }\n\n const url = `${BASE_URL}/efetch.fcgi?${params.toString()}`;\n const response = await this.fetchWithErrorHandling(url);\n const xml = await response.text();\n\n this.rateLimiter.resetBackoff();\n return parseEFetchResponse(xml).articles;\n }\n\n /**\n * Find related articles using ELink API with neighbor_score.\n */\n async findRelated(options: ELinkOptions): Promise<ELinkResponse[]> {\n await this.rateLimiter.acquire();\n\n const params = new URLSearchParams({\n dbfrom: 'pubmed',\n db: 'pubmed',\n cmd: 'neighbor_score',\n retmode: 'xml',\n email: this.config.email,\n });\n\n for (const id of options.ids) {\n params.append('id', id);\n }\n\n if (this.config.apiKey) {\n params.set('api_key', this.config.apiKey);\n }\n\n if (options.term) {\n params.set('term', options.term);\n }\n\n const url = `${BASE_URL}/elink.fcgi?${params.toString()}`;\n const response = await this.fetchWithErrorHandling(url);\n const xml = await response.text();\n\n this.rateLimiter.resetBackoff();\n const results = parseELinkResponse(xml);\n\n // Apply maxResults truncation per seed\n if (options.maxResults !== undefined) {\n for (const result of results) {\n result.relatedIds = result.relatedIds.slice(0, options.maxResults);\n }\n }\n\n return results;\n }\n\n /**\n * Find related articles with deduplication across multiple seeds.\n *\n * Merges related articles from all seeds, keeps highest score for duplicates,\n * excludes seed PMIDs from results, sorts by score descending, and truncates\n * to maxResults.\n */\n async findRelatedMerged(options: ELinkOptions): Promise<RelatedArticle[]> {\n // Pass options without maxResults to findRelated() to avoid double-truncation:\n // each seed should return all results so the merge sees the full picture.\n const { maxResults, ...findRelatedOptions } = options;\n const responses = await this.findRelated(findRelatedOptions);\n\n const seedSet = new Set(options.ids);\n const scoreMap = new Map<string, number>();\n\n for (const response of responses) {\n for (const related of response.relatedIds) {\n if (seedSet.has(related.id)) continue;\n const existing = scoreMap.get(related.id);\n if (existing === undefined || related.score > existing) {\n scoreMap.set(related.id, related.score);\n }\n }\n }\n\n const merged: RelatedArticle[] = Array.from(scoreMap.entries())\n .map(([id, score]) => ({ id, score }))\n .sort((a, b) => b.score - a.score);\n\n if (maxResults !== undefined) {\n return merged.slice(0, maxResults);\n }\n\n return merged;\n }\n\n /**\n * Fetch with error handling for HTTP responses.\n */\n private async fetchWithErrorHandling(url: string): Promise<Response> {\n let response: Response;\n\n try {\n response = await fetch(url);\n } catch (error) {\n throw this.createError('NETWORK_ERROR', 'Network request failed', true, error);\n }\n\n if (response.ok) {\n return response;\n }\n\n // Handle error responses\n if (response.status === 400) {\n throw this.createError('PARSE_ERROR', 'Invalid query syntax', false);\n }\n\n if (response.status === 429) {\n const retryAfter = response.headers.get('Retry-After');\n const retryAfterMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : undefined;\n const error = this.createError(\n 'RATE_LIMIT_EXCEEDED',\n 'Too many requests',\n true\n ) as ProviderError & { retryAfter?: number };\n if (retryAfterMs !== undefined) {\n error.retryAfter = retryAfterMs;\n }\n throw error;\n }\n\n if (response.status >= 500) {\n throw this.createError('SERVER_ERROR', `Server error: ${response.status}`, true);\n }\n\n throw this.createError(\n 'NETWORK_ERROR',\n `HTTP ${response.status}: ${response.statusText}`,\n true\n );\n }\n\n /**\n * Create a ProviderError.\n */\n private createError(\n code: ProviderErrorCode,\n message: string,\n retryable: boolean,\n cause?: unknown\n ): ProviderError {\n return createProviderError(code, message, 'pubmed', { retryable, cause });\n }\n}\n"],"names":[],"mappings":"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"}
@@ -16,9 +16,9 @@
16
16
  * ```
17
17
  */
18
18
  export { PubMedProvider } from './provider.js';
19
- export type { PubMedArticle, PubMedConfig, ESearchResponse, EFetchResponse, PubMedProviderState, } from './types.js';
19
+ export type { PubMedArticle, PubMedConfig, ESearchResponse, EFetchResponse, ELinkOptions, ELinkResponse, RelatedArticle, PubMedProviderState, } from './types.js';
20
20
  export { translateQuery } from './translator.js';
21
- export { parseESearchResponse, parseEFetchResponse } from './parser.js';
21
+ export { parseESearchResponse, parseEFetchResponse, parseELinkResponse } from './parser.js';
22
22
  export type { EFetchResult } from './parser.js';
23
23
  export { PubMedClient } from './client.js';
24
24
  export type { SearchOptions, HistoryFetchOptions } from './client.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/providers/pubmed/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG/C,YAAY,EACV,aAAa,EACb,YAAY,EACZ,eAAe,EACf,cAAc,EACd,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGjD,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACxE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAGtE,OAAO,EACL,sBAAsB,EACtB,cAAc,EACd,YAAY,EACZ,WAAW,EACX,mBAAmB,EACnB,eAAe,EACf,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAE1B,YAAY,EACV,YAAY,EACZ,MAAM,EACN,OAAO,EACP,eAAe,EACf,aAAa,IAAI,iBAAiB,EAClC,QAAQ,EACR,aAAa,EACb,WAAW,EACX,kBAAkB,EAClB,QAAQ,GACT,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/providers/pubmed/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG/C,YAAY,EACV,aAAa,EACb,YAAY,EACZ,eAAe,EACf,cAAc,EACd,YAAY,EACZ,aAAa,EACb,cAAc,EACd,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGjD,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAC5F,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAGtE,OAAO,EACL,sBAAsB,EACtB,cAAc,EACd,YAAY,EACZ,WAAW,EACX,mBAAmB,EACnB,eAAe,EACf,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAE1B,YAAY,EACV,YAAY,EACZ,MAAM,EACN,OAAO,EACP,eAAe,EACf,aAAa,IAAI,iBAAiB,EAClC,QAAQ,EACR,aAAa,EACb,WAAW,EACX,kBAAkB,EAClB,QAAQ,GACT,MAAM,kBAAkB,CAAC"}
@@ -1,4 +1,4 @@
1
- import { ESearchResponse, PubMedArticle } from './types.js';
1
+ import { ELinkResponse, ESearchResponse, PubMedArticle } from './types.js';
2
2
  /**
3
3
  * Response structure for efetch parsing.
4
4
  */
@@ -29,4 +29,11 @@ export declare function parseESearchResponse(xml: string): ESearchResponse;
29
29
  * @returns Parsed result containing PubMedArticle array
30
30
  */
31
31
  export declare function parseEFetchResponse(xml: string): EFetchResult;
32
+ /**
33
+ * Parse ELink XML response into structured ELinkResponse array.
34
+ *
35
+ * Extracts seed-to-related article mappings with similarity scores
36
+ * from the ELink `cmd=neighbor_score` response.
37
+ */
38
+ export declare function parseELinkResponse(xml: string): ELinkResponse[];
32
39
  //# sourceMappingURL=parser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../src/providers/pubmed/parser.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEjE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B;AAaD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMlD;AAgCD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAqCjE;AAkPD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAc7D"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../src/providers/pubmed/parser.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhF;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B;AAaD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMlD;AAmCD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAqCjE;AAkPD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAc7D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,EAAE,CAyB/D"}
@@ -25,7 +25,10 @@ const parser = new XMLParser({
25
25
  "ArticleId",
26
26
  "AffiliationInfo",
27
27
  "OutputMessage",
28
- "QuotedPhraseNotFound"
28
+ "QuotedPhraseNotFound",
29
+ "Link",
30
+ "LinkSet",
31
+ "LinkSetDb"
29
32
  ];
30
33
  return arrayElements.includes(name);
31
34
  }
@@ -195,9 +198,28 @@ function parseEFetchResponse(xml) {
195
198
  );
196
199
  return { articles };
197
200
  }
201
+ function parseELinkResponse(xml) {
202
+ const parsed = parser.parse(xml);
203
+ const linkSets = parsed.eLinkResult?.LinkSet ?? [];
204
+ return linkSets.map((linkSet) => {
205
+ const idList = linkSet["IdList"];
206
+ const seedId = String(idList?.Id?.[0] ?? "");
207
+ const linkSetDbs = linkSet["LinkSetDb"] ?? [];
208
+ const pubmedLinks = linkSetDbs.find(
209
+ (db) => db["LinkName"] === "pubmed_pubmed"
210
+ );
211
+ const links = pubmedLinks?.["Link"] ?? [];
212
+ const relatedIds = links.map((link) => ({
213
+ id: String(link.Id),
214
+ score: Number(link.Score)
215
+ })).sort((a, b) => b.score - a.score);
216
+ return { seedId, relatedIds };
217
+ });
218
+ }
198
219
  export {
199
220
  cleanXmlText,
200
221
  parseEFetchResponse,
222
+ parseELinkResponse,
201
223
  parseESearchResponse
202
224
  };
203
225
  //# sourceMappingURL=parser.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"parser.js","sources":["../../../src/providers/pubmed/parser.ts"],"sourcesContent":["/**\n * PubMed XML response parser using fast-xml-parser.\n *\n * Parses esearch and efetch XML responses from PubMed E-utilities API.\n */\n\nimport { XMLParser } from 'fast-xml-parser';\nimport type { Author } from '../base/types.js';\nimport type { ESearchResponse, PubMedArticle } from './types.js';\n\n/**\n * Response structure for efetch parsing.\n */\nexport interface EFetchResult {\n articles: PubMedArticle[];\n}\n\n/**\n * Named XML entity map for the five predefined XML entities.\n */\nconst XML_ENTITIES: Record<string, string> = {\n amp: '&',\n lt: '<',\n gt: '>',\n quot: '\"',\n apos: \"'\",\n};\n\n/**\n * Decode XML/HTML entities and strip inline markup tags from a string.\n *\n * Handles:\n * - Hex numeric entities: `&#x2264;` → `≤`\n * - Decimal numeric entities: `&#8804;` → `≤`\n * - Named XML entities: `&amp;` → `&`, `&lt;` → `<`, etc.\n * - Inline markup tags: `<i>`, `<sub>`, `<sup>`, `<b>`, `<u>` → stripped\n */\nexport function cleanXmlText(value: string): string {\n return value\n .replace(/<[^>]+>/g, '')\n .replace(/&#x([0-9a-fA-F]+);/g, (_, hex: string) => String.fromCodePoint(parseInt(hex, 16)))\n .replace(/&#(\\d+);/g, (_, dec: string) => String.fromCodePoint(parseInt(dec, 10)))\n .replace(/&([a-zA-Z]+);/g, (match, name: string) => XML_ENTITIES[name] ?? match);\n}\n\n/**\n * XML Parser configured for PubMed responses.\n *\n * `stopNodes` prevents the parser from interpreting inline markup within\n * `ArticleTitle` and `AbstractText`, preserving them as raw strings\n * that can be cleaned via `stripXmlTags`.\n */\nconst parser = new XMLParser({\n ignoreAttributes: false,\n attributeNamePrefix: '@_',\n textNodeName: '#text',\n stopNodes: ['*.ArticleTitle', '*.AbstractText'],\n isArray: (name) => {\n // These elements should always be arrays\n const arrayElements = [\n 'Id',\n 'PubmedArticle',\n 'Author',\n 'MeshHeading',\n 'PublicationType',\n 'AbstractText',\n 'ArticleId',\n 'AffiliationInfo',\n 'OutputMessage',\n 'QuotedPhraseNotFound',\n ];\n return arrayElements.includes(name);\n },\n});\n\n/**\n * Parse esearch XML response from PubMed.\n *\n * @param xml - Raw XML string from esearch endpoint\n * @returns Parsed ESearchResponse with PMID list and metadata\n */\nexport function parseESearchResponse(xml: string): ESearchResponse {\n const parsed = parser.parse(xml);\n const result = parsed.eSearchResult;\n\n const idList = result.IdList?.Id ?? [];\n\n const response: ESearchResponse = {\n count: Number(result.Count) || 0,\n retmax: Number(result.RetMax) || 0,\n retstart: Number(result.RetStart) || 0,\n idlist: idList.map((id: string | number) => String(id)),\n };\n\n if (result.WebEnv) {\n response.webenv = String(result.WebEnv);\n }\n if (result.QueryKey) {\n response.querykey = String(result.QueryKey);\n }\n\n const warningList = result.WarningList;\n if (warningList) {\n const warnings: string[] = [];\n const outputMessages = warningList.OutputMessage ?? [];\n for (const msg of outputMessages) {\n warnings.push(`PubMed warning: ${String(msg)}`);\n }\n const notFoundPhrases = warningList.QuotedPhraseNotFound ?? [];\n for (const phrase of notFoundPhrases) {\n warnings.push(`Quoted phrase not found: ${String(phrase)}`);\n }\n if (warnings.length > 0) {\n response.warnings = warnings;\n }\n }\n\n return response;\n}\n\n/**\n * Month name to number mapping.\n */\nconst MONTH_MAP: Record<string, string> = {\n Jan: '01',\n Feb: '02',\n Mar: '03',\n Apr: '04',\n May: '05',\n Jun: '06',\n Jul: '07',\n Aug: '08',\n Sep: '09',\n Oct: '10',\n Nov: '11',\n Dec: '12',\n};\n\n/**\n * Parse a PubMed date element into ISO format.\n */\nfunction parseDate(pubDate: {\n Year?: string;\n Month?: string;\n Day?: string;\n}): string | undefined {\n if (!pubDate?.Year) return undefined;\n\n const year = String(pubDate.Year);\n if (!pubDate.Month) return year;\n\n // Month can be numeric or name (Jan, Feb, etc.)\n let month = String(pubDate.Month);\n if (MONTH_MAP[month]) {\n month = MONTH_MAP[month]!;\n } else if (month.length === 1) {\n month = `0${month}`;\n }\n\n if (!pubDate.Day) return `${year}-${month}`;\n\n const dayStr = String(pubDate.Day);\n const day = dayStr.length === 1 ? `0${dayStr}` : dayStr;\n return `${year}-${month}-${day}`;\n}\n\n/**\n * Parse author information from PubMed Author element.\n */\nfunction parseAuthor(authorData: {\n LastName?: string;\n ForeName?: string;\n CollectiveName?: string;\n AffiliationInfo?: Array<{ Affiliation?: string }>;\n Identifier?: { '#text'?: string; '@_Source'?: string };\n}): Author {\n const author: Author = {\n family: authorData.LastName ?? authorData.CollectiveName ?? '',\n };\n\n if (authorData.ForeName) {\n author.given = authorData.ForeName;\n }\n\n if (authorData.AffiliationInfo?.[0]?.Affiliation) {\n author.affiliation = authorData.AffiliationInfo[0].Affiliation;\n }\n\n // Extract ORCID from Identifier\n if (\n authorData.Identifier &&\n authorData.Identifier['@_Source'] === 'ORCID' &&\n authorData.Identifier['#text']\n ) {\n author.orcid = authorData.Identifier['#text'];\n }\n\n return author;\n}\n\n/**\n * Parse abstract text which may be structured or simple.\n */\nfunction parseAbstract(\n abstractData: { AbstractText?: Array<{ '#text'?: string; '@_Label'?: string } | string> } | undefined\n): string | undefined {\n if (!abstractData?.AbstractText) return undefined;\n\n const texts = abstractData.AbstractText;\n if (!Array.isArray(texts)) {\n return cleanXmlText(String(texts));\n }\n\n // Check if structured abstract (has labels)\n if (texts.length > 0 && typeof texts[0] === 'object' && texts[0]['@_Label']) {\n return texts\n .map((section) => {\n if (typeof section === 'string') return cleanXmlText(section);\n const label = section['@_Label'];\n const text = cleanXmlText(section['#text'] ?? '');\n return `${label}: ${text}`;\n })\n .join('\\n\\n');\n }\n\n // Simple abstract\n return texts\n .map((t) => cleanXmlText(typeof t === 'string' ? t : t['#text'] ?? ''))\n .join(' ');\n}\n\n/**\n * Extract article ID by type from ArticleIdList.\n */\nfunction getArticleId(\n idList: Array<{ '#text'?: string; '@_IdType'?: string }> | undefined,\n idType: string\n): string | undefined {\n if (!idList) return undefined;\n const found = idList.find((id) => id['@_IdType'] === idType);\n return found?.['#text'];\n}\n\n/**\n * Parse a single PubMed article from efetch response.\n * Returns null if required fields are missing.\n */\nfunction parsePubMedArticle(articleData: {\n MedlineCitation?: {\n PMID?: { '#text'?: string } | string;\n Article?: {\n Journal?: {\n Title?: string;\n ISSN?: { '#text'?: string };\n JournalIssue?: {\n Volume?: string;\n Issue?: string;\n PubDate?: { Year?: string; Month?: string; Day?: string };\n };\n };\n ArticleTitle?: string;\n Pagination?: { MedlinePgn?: string };\n Abstract?: { AbstractText?: unknown[] };\n AuthorList?: { Author?: unknown[] };\n PublicationTypeList?: { PublicationType?: unknown[] };\n };\n MeshHeadingList?: { MeshHeading?: unknown[] };\n };\n PubmedData?: {\n ArticleIdList?: { ArticleId?: unknown[] };\n };\n}): PubMedArticle | null {\n const citation = articleData.MedlineCitation;\n if (!citation?.Article) {\n // Skip malformed article data missing required fields\n return null;\n }\n const articleContent = citation.Article;\n const journalIssue = articleContent.Journal?.JournalIssue;\n\n // Extract PMID (can be object with #text or direct value)\n const pmidData = citation.PMID;\n const pmid =\n typeof pmidData === 'object'\n ? String(pmidData['#text'] ?? '')\n : String(pmidData ?? '');\n\n // Parse authors\n const authorList = articleContent.AuthorList?.Author ?? [];\n const authors: Author[] = authorList.map((a: unknown) => parseAuthor(a as Parameters<typeof parseAuthor>[0]));\n\n // Parse MeSH terms\n const meshList = citation.MeshHeadingList?.MeshHeading ?? [];\n const meshTerms =\n meshList.length > 0\n ? meshList.map((mh: unknown) => {\n const meshHeading = mh as { DescriptorName?: { '#text'?: string } | string };\n if (typeof meshHeading.DescriptorName === 'object') {\n return meshHeading.DescriptorName['#text'] ?? '';\n }\n return String(meshHeading.DescriptorName ?? '');\n })\n : undefined;\n\n // Parse publication types\n const pubTypeList = articleContent.PublicationTypeList?.PublicationType ?? [];\n const pubTypes =\n pubTypeList.length > 0\n ? pubTypeList.map((pt: unknown) => {\n if (typeof pt === 'object' && pt !== null) {\n return (pt as { '#text'?: string })['#text'] ?? '';\n }\n return String(pt);\n })\n : undefined;\n\n // Get article IDs\n const articleIdList = articleData.PubmedData?.ArticleIdList?.ArticleId as Array<{ '#text'?: string; '@_IdType'?: string }> | undefined;\n const doi = getArticleId(articleIdList, 'doi');\n const pmc = getArticleId(articleIdList, 'pmc');\n\n const article: PubMedArticle = {\n pmid,\n source: 'pubmed',\n title: cleanXmlText(String(articleContent.ArticleTitle ?? '')),\n authors,\n retrievedAt: new Date().toISOString(),\n };\n\n // Optional fields\n const abstract = parseAbstract(articleContent.Abstract as Parameters<typeof parseAbstract>[0]);\n if (abstract) article.abstract = abstract;\n\n if (doi) article.doi = doi;\n if (pmc) article.pmc = pmc;\n\n if (articleContent.Journal?.Title) {\n article.journal = articleContent.Journal.Title;\n }\n\n if (journalIssue?.Volume) article.volume = String(journalIssue.Volume);\n if (journalIssue?.Issue) article.issue = String(journalIssue.Issue);\n\n if (articleContent.Pagination?.MedlinePgn) {\n article.pages = articleContent.Pagination.MedlinePgn;\n }\n\n const pubDate = parseDate(journalIssue?.PubDate ?? {});\n if (pubDate) article.publicationDate = pubDate;\n\n if (meshTerms) article.meshTerms = meshTerms;\n if (pubTypes) article.pubTypes = pubTypes;\n\n if (articleContent.Journal?.ISSN?.['#text']) {\n article.journalIssn = articleContent.Journal.ISSN['#text'];\n }\n\n return article;\n}\n\n/**\n * Parse efetch XML response from PubMed.\n *\n * @param xml - Raw XML string from efetch endpoint\n * @returns Parsed result containing PubMedArticle array\n */\nexport function parseEFetchResponse(xml: string): EFetchResult {\n const parsed = parser.parse(xml);\n const articleSet = parsed.PubmedArticleSet?.PubmedArticle ?? [];\n\n const articles = articleSet\n .map((article: unknown) =>\n parsePubMedArticle(article as Parameters<typeof parsePubMedArticle>[0])\n )\n .filter(\n (article: PubMedArticle | null): article is PubMedArticle =>\n article !== null\n );\n\n return { articles };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"parser.js","sources":["../../../src/providers/pubmed/parser.ts"],"sourcesContent":["/**\n * PubMed XML response parser using fast-xml-parser.\n *\n * Parses esearch and efetch XML responses from PubMed E-utilities API.\n */\n\nimport { XMLParser } from 'fast-xml-parser';\nimport type { Author } from '../base/types.js';\nimport type { ELinkResponse, ESearchResponse, PubMedArticle } from './types.js';\n\n/**\n * Response structure for efetch parsing.\n */\nexport interface EFetchResult {\n articles: PubMedArticle[];\n}\n\n/**\n * Named XML entity map for the five predefined XML entities.\n */\nconst XML_ENTITIES: Record<string, string> = {\n amp: '&',\n lt: '<',\n gt: '>',\n quot: '\"',\n apos: \"'\",\n};\n\n/**\n * Decode XML/HTML entities and strip inline markup tags from a string.\n *\n * Handles:\n * - Hex numeric entities: `&#x2264;` → `≤`\n * - Decimal numeric entities: `&#8804;` → `≤`\n * - Named XML entities: `&amp;` → `&`, `&lt;` → `<`, etc.\n * - Inline markup tags: `<i>`, `<sub>`, `<sup>`, `<b>`, `<u>` → stripped\n */\nexport function cleanXmlText(value: string): string {\n return value\n .replace(/<[^>]+>/g, '')\n .replace(/&#x([0-9a-fA-F]+);/g, (_, hex: string) => String.fromCodePoint(parseInt(hex, 16)))\n .replace(/&#(\\d+);/g, (_, dec: string) => String.fromCodePoint(parseInt(dec, 10)))\n .replace(/&([a-zA-Z]+);/g, (match, name: string) => XML_ENTITIES[name] ?? match);\n}\n\n/**\n * XML Parser configured for PubMed responses.\n *\n * `stopNodes` prevents the parser from interpreting inline markup within\n * `ArticleTitle` and `AbstractText`, preserving them as raw strings\n * that can be cleaned via `stripXmlTags`.\n */\nconst parser = new XMLParser({\n ignoreAttributes: false,\n attributeNamePrefix: '@_',\n textNodeName: '#text',\n stopNodes: ['*.ArticleTitle', '*.AbstractText'],\n isArray: (name) => {\n // These elements should always be arrays\n const arrayElements = [\n 'Id',\n 'PubmedArticle',\n 'Author',\n 'MeshHeading',\n 'PublicationType',\n 'AbstractText',\n 'ArticleId',\n 'AffiliationInfo',\n 'OutputMessage',\n 'QuotedPhraseNotFound',\n 'Link',\n 'LinkSet',\n 'LinkSetDb',\n ];\n return arrayElements.includes(name);\n },\n});\n\n/**\n * Parse esearch XML response from PubMed.\n *\n * @param xml - Raw XML string from esearch endpoint\n * @returns Parsed ESearchResponse with PMID list and metadata\n */\nexport function parseESearchResponse(xml: string): ESearchResponse {\n const parsed = parser.parse(xml);\n const result = parsed.eSearchResult;\n\n const idList = result.IdList?.Id ?? [];\n\n const response: ESearchResponse = {\n count: Number(result.Count) || 0,\n retmax: Number(result.RetMax) || 0,\n retstart: Number(result.RetStart) || 0,\n idlist: idList.map((id: string | number) => String(id)),\n };\n\n if (result.WebEnv) {\n response.webenv = String(result.WebEnv);\n }\n if (result.QueryKey) {\n response.querykey = String(result.QueryKey);\n }\n\n const warningList = result.WarningList;\n if (warningList) {\n const warnings: string[] = [];\n const outputMessages = warningList.OutputMessage ?? [];\n for (const msg of outputMessages) {\n warnings.push(`PubMed warning: ${String(msg)}`);\n }\n const notFoundPhrases = warningList.QuotedPhraseNotFound ?? [];\n for (const phrase of notFoundPhrases) {\n warnings.push(`Quoted phrase not found: ${String(phrase)}`);\n }\n if (warnings.length > 0) {\n response.warnings = warnings;\n }\n }\n\n return response;\n}\n\n/**\n * Month name to number mapping.\n */\nconst MONTH_MAP: Record<string, string> = {\n Jan: '01',\n Feb: '02',\n Mar: '03',\n Apr: '04',\n May: '05',\n Jun: '06',\n Jul: '07',\n Aug: '08',\n Sep: '09',\n Oct: '10',\n Nov: '11',\n Dec: '12',\n};\n\n/**\n * Parse a PubMed date element into ISO format.\n */\nfunction parseDate(pubDate: {\n Year?: string;\n Month?: string;\n Day?: string;\n}): string | undefined {\n if (!pubDate?.Year) return undefined;\n\n const year = String(pubDate.Year);\n if (!pubDate.Month) return year;\n\n // Month can be numeric or name (Jan, Feb, etc.)\n let month = String(pubDate.Month);\n if (MONTH_MAP[month]) {\n month = MONTH_MAP[month]!;\n } else if (month.length === 1) {\n month = `0${month}`;\n }\n\n if (!pubDate.Day) return `${year}-${month}`;\n\n const dayStr = String(pubDate.Day);\n const day = dayStr.length === 1 ? `0${dayStr}` : dayStr;\n return `${year}-${month}-${day}`;\n}\n\n/**\n * Parse author information from PubMed Author element.\n */\nfunction parseAuthor(authorData: {\n LastName?: string;\n ForeName?: string;\n CollectiveName?: string;\n AffiliationInfo?: Array<{ Affiliation?: string }>;\n Identifier?: { '#text'?: string; '@_Source'?: string };\n}): Author {\n const author: Author = {\n family: authorData.LastName ?? authorData.CollectiveName ?? '',\n };\n\n if (authorData.ForeName) {\n author.given = authorData.ForeName;\n }\n\n if (authorData.AffiliationInfo?.[0]?.Affiliation) {\n author.affiliation = authorData.AffiliationInfo[0].Affiliation;\n }\n\n // Extract ORCID from Identifier\n if (\n authorData.Identifier &&\n authorData.Identifier['@_Source'] === 'ORCID' &&\n authorData.Identifier['#text']\n ) {\n author.orcid = authorData.Identifier['#text'];\n }\n\n return author;\n}\n\n/**\n * Parse abstract text which may be structured or simple.\n */\nfunction parseAbstract(\n abstractData: { AbstractText?: Array<{ '#text'?: string; '@_Label'?: string } | string> } | undefined\n): string | undefined {\n if (!abstractData?.AbstractText) return undefined;\n\n const texts = abstractData.AbstractText;\n if (!Array.isArray(texts)) {\n return cleanXmlText(String(texts));\n }\n\n // Check if structured abstract (has labels)\n if (texts.length > 0 && typeof texts[0] === 'object' && texts[0]['@_Label']) {\n return texts\n .map((section) => {\n if (typeof section === 'string') return cleanXmlText(section);\n const label = section['@_Label'];\n const text = cleanXmlText(section['#text'] ?? '');\n return `${label}: ${text}`;\n })\n .join('\\n\\n');\n }\n\n // Simple abstract\n return texts\n .map((t) => cleanXmlText(typeof t === 'string' ? t : t['#text'] ?? ''))\n .join(' ');\n}\n\n/**\n * Extract article ID by type from ArticleIdList.\n */\nfunction getArticleId(\n idList: Array<{ '#text'?: string; '@_IdType'?: string }> | undefined,\n idType: string\n): string | undefined {\n if (!idList) return undefined;\n const found = idList.find((id) => id['@_IdType'] === idType);\n return found?.['#text'];\n}\n\n/**\n * Parse a single PubMed article from efetch response.\n * Returns null if required fields are missing.\n */\nfunction parsePubMedArticle(articleData: {\n MedlineCitation?: {\n PMID?: { '#text'?: string } | string;\n Article?: {\n Journal?: {\n Title?: string;\n ISSN?: { '#text'?: string };\n JournalIssue?: {\n Volume?: string;\n Issue?: string;\n PubDate?: { Year?: string; Month?: string; Day?: string };\n };\n };\n ArticleTitle?: string;\n Pagination?: { MedlinePgn?: string };\n Abstract?: { AbstractText?: unknown[] };\n AuthorList?: { Author?: unknown[] };\n PublicationTypeList?: { PublicationType?: unknown[] };\n };\n MeshHeadingList?: { MeshHeading?: unknown[] };\n };\n PubmedData?: {\n ArticleIdList?: { ArticleId?: unknown[] };\n };\n}): PubMedArticle | null {\n const citation = articleData.MedlineCitation;\n if (!citation?.Article) {\n // Skip malformed article data missing required fields\n return null;\n }\n const articleContent = citation.Article;\n const journalIssue = articleContent.Journal?.JournalIssue;\n\n // Extract PMID (can be object with #text or direct value)\n const pmidData = citation.PMID;\n const pmid =\n typeof pmidData === 'object'\n ? String(pmidData['#text'] ?? '')\n : String(pmidData ?? '');\n\n // Parse authors\n const authorList = articleContent.AuthorList?.Author ?? [];\n const authors: Author[] = authorList.map((a: unknown) => parseAuthor(a as Parameters<typeof parseAuthor>[0]));\n\n // Parse MeSH terms\n const meshList = citation.MeshHeadingList?.MeshHeading ?? [];\n const meshTerms =\n meshList.length > 0\n ? meshList.map((mh: unknown) => {\n const meshHeading = mh as { DescriptorName?: { '#text'?: string } | string };\n if (typeof meshHeading.DescriptorName === 'object') {\n return meshHeading.DescriptorName['#text'] ?? '';\n }\n return String(meshHeading.DescriptorName ?? '');\n })\n : undefined;\n\n // Parse publication types\n const pubTypeList = articleContent.PublicationTypeList?.PublicationType ?? [];\n const pubTypes =\n pubTypeList.length > 0\n ? pubTypeList.map((pt: unknown) => {\n if (typeof pt === 'object' && pt !== null) {\n return (pt as { '#text'?: string })['#text'] ?? '';\n }\n return String(pt);\n })\n : undefined;\n\n // Get article IDs\n const articleIdList = articleData.PubmedData?.ArticleIdList?.ArticleId as Array<{ '#text'?: string; '@_IdType'?: string }> | undefined;\n const doi = getArticleId(articleIdList, 'doi');\n const pmc = getArticleId(articleIdList, 'pmc');\n\n const article: PubMedArticle = {\n pmid,\n source: 'pubmed',\n title: cleanXmlText(String(articleContent.ArticleTitle ?? '')),\n authors,\n retrievedAt: new Date().toISOString(),\n };\n\n // Optional fields\n const abstract = parseAbstract(articleContent.Abstract as Parameters<typeof parseAbstract>[0]);\n if (abstract) article.abstract = abstract;\n\n if (doi) article.doi = doi;\n if (pmc) article.pmc = pmc;\n\n if (articleContent.Journal?.Title) {\n article.journal = articleContent.Journal.Title;\n }\n\n if (journalIssue?.Volume) article.volume = String(journalIssue.Volume);\n if (journalIssue?.Issue) article.issue = String(journalIssue.Issue);\n\n if (articleContent.Pagination?.MedlinePgn) {\n article.pages = articleContent.Pagination.MedlinePgn;\n }\n\n const pubDate = parseDate(journalIssue?.PubDate ?? {});\n if (pubDate) article.publicationDate = pubDate;\n\n if (meshTerms) article.meshTerms = meshTerms;\n if (pubTypes) article.pubTypes = pubTypes;\n\n if (articleContent.Journal?.ISSN?.['#text']) {\n article.journalIssn = articleContent.Journal.ISSN['#text'];\n }\n\n return article;\n}\n\n/**\n * Parse efetch XML response from PubMed.\n *\n * @param xml - Raw XML string from efetch endpoint\n * @returns Parsed result containing PubMedArticle array\n */\nexport function parseEFetchResponse(xml: string): EFetchResult {\n const parsed = parser.parse(xml);\n const articleSet = parsed.PubmedArticleSet?.PubmedArticle ?? [];\n\n const articles = articleSet\n .map((article: unknown) =>\n parsePubMedArticle(article as Parameters<typeof parsePubMedArticle>[0])\n )\n .filter(\n (article: PubMedArticle | null): article is PubMedArticle =>\n article !== null\n );\n\n return { articles };\n}\n\n/**\n * Parse ELink XML response into structured ELinkResponse array.\n *\n * Extracts seed-to-related article mappings with similarity scores\n * from the ELink `cmd=neighbor_score` response.\n */\nexport function parseELinkResponse(xml: string): ELinkResponse[] {\n const parsed = parser.parse(xml);\n const linkSets = parsed.eLinkResult?.LinkSet ?? [];\n\n return linkSets.map((linkSet: Record<string, unknown>) => {\n const idList = linkSet['IdList'] as { Id?: (string | number)[] } | undefined;\n const seedId = String(idList?.Id?.[0] ?? '');\n\n // Find the pubmed_pubmed LinkSetDb (primary related articles)\n const linkSetDbs = (linkSet['LinkSetDb'] ?? []) as Record<string, unknown>[];\n const pubmedLinks = linkSetDbs.find(\n (db) => db['LinkName'] === 'pubmed_pubmed'\n );\n\n const links = (pubmedLinks?.['Link'] ?? []) as { Id: string | number; Score: string | number }[];\n\n const relatedIds = links\n .map((link) => ({\n id: String(link.Id),\n score: Number(link.Score),\n }))\n .sort((a, b) => b.score - a.score);\n\n return { seedId, relatedIds };\n });\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/providers/pubmed/provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,KAAK,EACV,OAAO,EACP,WAAW,EACX,aAAa,EACb,WAAW,EACX,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EACrB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,YAAY,CAAC;AAKpE;;GAEG;AACH,qBAAa,cAAe,SAAQ,YAAY;IAC9C,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAU;IAElC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAE5C,mDAAmD;IACnD,OAAO,CAAC,YAAY,CAA4B;IAEhD,2CAA2C;IAC3C,OAAO,CAAC,cAAc,CAAgB;gBAE1B,MAAM,EAAE,YAAY;IAShC;;;OAGG;IACI,MAAM,CACX,KAAK,EAAE,eAAe,EACtB,OAAO,CAAC,EAAE,aAAa,GACtB,aAAa,CAAC,OAAO,CAAC;IAsGzB;;OAEG;IACH,OAAO,CAAC,WAAW;IAenB;;;OAGG;IACG,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC;IAIpD;;OAEG;IACH,cAAc,CAAC,QAAQ,EAAE,WAAW,GAAG,eAAe;IAItD;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,oBAAoB,CAAC;IAUrD;;OAEG;IACH,cAAc,IAAI,WAAW,GAAG,IAAI;IAIpC;;OAEG;IACH,WAAW,IAAI,MAAM,EAAE;IAIvB;;OAEG;IACI,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,aAAa,CAAC,OAAO,CAAC;IA2F/D;;OAEG;IACG,aAAa,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,kBAAkB,CAAC;CA6BrE"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/providers/pubmed/provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,KAAK,EACV,OAAO,EACP,WAAW,EACX,aAAa,EAEb,WAAW,EACX,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EACrB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,YAAY,CAAC;AAUpE;;GAEG;AACH,qBAAa,cAAe,SAAQ,YAAY;IAC9C,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAU;IAElC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAE5C,mDAAmD;IACnD,OAAO,CAAC,YAAY,CAA4B;IAEhD,2CAA2C;IAC3C,OAAO,CAAC,cAAc,CAAgB;gBAE1B,MAAM,EAAE,YAAY;IAShC;;;OAGG;IACI,MAAM,CACX,KAAK,EAAE,eAAe,EACtB,OAAO,CAAC,EAAE,aAAa,GACtB,aAAa,CAAC,OAAO,CAAC;IA2GzB;;OAEG;IACH,OAAO,CAAC,WAAW;IAenB;;;OAGG;IACG,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC;IAIpD;;OAEG;IACH,cAAc,CAAC,QAAQ,EAAE,WAAW,GAAG,eAAe;IAItD;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,oBAAoB,CAAC;IAUrD;;OAEG;IACH,cAAc,IAAI,WAAW,GAAG,IAAI;IAIpC;;OAEG;IACH,WAAW,IAAI,MAAM,EAAE;IAIvB;;OAEG;IACI,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,aAAa,CAAC,OAAO,CAAC;IA2F/D;;OAEG;IACG,aAAa,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,kBAAkB,CAAC;CA6BrE"}
@@ -2,6 +2,9 @@ import { BaseProvider } from "../base/provider.js";
2
2
  import { PubMedClient } from "./client.js";
3
3
  import { translateQuery } from "./translator.js";
4
4
  const DEFAULT_PAGE_SIZE = 20;
5
+ function mapSortField(sort) {
6
+ return sort === "date" ? "pub_date" : sort;
7
+ }
5
8
  class PubMedProvider extends BaseProvider {
6
9
  name = "pubmed";
7
10
  client;
@@ -27,10 +30,12 @@ class PubMedProvider extends BaseProvider {
27
30
  const pageSize = options?.pageSize ?? DEFAULT_PAGE_SIZE;
28
31
  let retstart = 0;
29
32
  let totalRetrieved = 0;
33
+ const pubmedSort = options?.sort ? mapSortField(options.sort) : void 0;
30
34
  const initialResult = await this.withRetry(() => this.client.search(query.native, {
31
35
  retstart: 0,
32
36
  retmax: pageSize,
33
- useHistory: true
37
+ useHistory: true,
38
+ ...pubmedSort && { sort: pubmedSort }
34
39
  }));
35
40
  const totalCount = initialResult.count;
36
41
  const webenv = initialResult.webenv;
@@ -75,7 +80,8 @@ class PubMedProvider extends BaseProvider {
75
80
  } else {
76
81
  const result = await this.withRetry(() => this.client.search(query.native, {
77
82
  retstart,
78
- retmax: remainingToFetch
83
+ retmax: remainingToFetch,
84
+ ...pubmedSort && { sort: pubmedSort }
79
85
  }));
80
86
  articles = await this.withRetry(() => this.client.fetch(result.idlist));
81
87
  }
@@ -1 +1 @@
1
- {"version":3,"file":"provider.js","sources":["../../../src/providers/pubmed/provider.ts"],"sourcesContent":["/**\n * PubMed Provider implementation.\n *\n * Provides access to NCBI's PubMed database for biomedical literature searches.\n */\n\nimport { BaseProvider } from '../base/provider.js';\nimport type {\n Article,\n ResolvedAST,\n SearchOptions,\n SearchState,\n TranslatedQuery,\n SearchResumeResult,\n ConnectionTestResult,\n} from '../base/types.js';\nimport { PubMedClient } from './client.js';\nimport { translateQuery } from './translator.js';\nimport type { PubMedConfig, PubMedProviderState } from './types.js';\n\n/** Default page size for fetching results */\nconst DEFAULT_PAGE_SIZE = 20;\n\n/**\n * PubMed provider for searching biomedical literature.\n */\nexport class PubMedProvider extends BaseProvider {\n readonly name = 'pubmed' as const;\n\n private readonly client: PubMedClient;\n private readonly pubmedConfig: PubMedConfig;\n\n /** Current search state for session persistence */\n private currentState: SearchState | null = null;\n\n /** Warnings from the most recent search */\n private searchWarnings: string[] = [];\n\n constructor(config: PubMedConfig) {\n super({\n ...config,\n rateLimit: config.rateLimit ?? (config.apiKey ? 10 : 3),\n });\n this.pubmedConfig = config;\n this.client = new PubMedClient(config, this.rateLimiter);\n }\n\n /**\n * Search PubMed and stream results.\n * Uses NCBI history server for efficient pagination of large result sets.\n */\n async *search(\n query: TranslatedQuery,\n options?: SearchOptions\n ): AsyncIterable<Article> {\n const maxResults = options?.maxResults;\n const pageSize = options?.pageSize ?? DEFAULT_PAGE_SIZE;\n let retstart = 0;\n let totalRetrieved = 0;\n\n // Initial search with history server enabled\n const initialResult = await this.withRetry(() => this.client.search(query.native, {\n retstart: 0,\n retmax: pageSize,\n useHistory: true,\n }));\n\n const totalCount = initialResult.count;\n const webenv = initialResult.webenv;\n const querykey = initialResult.querykey;\n\n // Store any warnings from the search response\n this.searchWarnings = initialResult.warnings ?? [];\n\n // Initialize state with provider-specific history server info\n const providerState: PubMedProviderState = {\n retstart: 0,\n useHistory: !!(webenv && querykey),\n ...(webenv && { webenv }),\n ...(querykey && { querykey }),\n };\n\n this.currentState = {\n ...this.createBaseState(query, totalCount, 0),\n providerState,\n };\n\n // If no results, return early\n if (totalCount === 0 || initialResult.idlist.length === 0) {\n return;\n }\n\n // Fetch first page of articles using PMIDs from initial search\n const firstPageArticles = await this.withRetry(() => this.client.fetch(initialResult.idlist));\n\n for (const article of firstPageArticles) {\n totalRetrieved++;\n retstart++;\n\n this.updateState(totalRetrieved, retstart, providerState);\n\n yield article;\n\n if (maxResults !== undefined && totalRetrieved >= maxResults) {\n return;\n }\n }\n\n // Continue with subsequent pages using history server if available\n while (retstart < totalCount) {\n if (maxResults !== undefined && totalRetrieved >= maxResults) {\n break;\n }\n\n const remainingToFetch = maxResults !== undefined\n ? Math.min(pageSize, maxResults - totalRetrieved)\n : pageSize;\n\n let articles: Article[];\n\n if (webenv && querykey) {\n // Use history server for efficient pagination\n articles = await this.withRetry(() => this.client.fetchFromHistory({\n webenv,\n querykey,\n retstart,\n retmax: remainingToFetch,\n }));\n } else {\n // Fallback to offset-based pagination\n const result = await this.withRetry(() => this.client.search(query.native, {\n retstart,\n retmax: remainingToFetch,\n }));\n articles = await this.withRetry(() => this.client.fetch(result.idlist));\n }\n\n if (articles.length === 0) {\n break;\n }\n\n for (const article of articles) {\n totalRetrieved++;\n retstart++;\n\n this.updateState(totalRetrieved, retstart, providerState);\n\n yield article;\n\n if (maxResults !== undefined && totalRetrieved >= maxResults) {\n return;\n }\n }\n }\n }\n\n /**\n * Update current state with progress information.\n */\n private updateState(\n retrievedCount: number,\n retstart: number,\n providerState: PubMedProviderState\n ): void {\n if (this.currentState) {\n this.currentState.retrievedCount = retrievedCount;\n this.currentState.lastUpdated = new Date();\n this.currentState.providerState = {\n ...providerState,\n retstart,\n };\n }\n }\n\n /**\n * Get total hit count for a query without downloading results.\n * Uses ESearch with rettype=count for efficiency.\n */\n async count(query: TranslatedQuery): Promise<number> {\n return this.withRetry(() => this.client.searchCount(query.native));\n }\n\n /**\n * Convert ResolvedAST to PubMed native syntax.\n */\n translateQuery(resolved: ResolvedAST): TranslatedQuery {\n return translateQuery(resolved);\n }\n\n /**\n * Test connection to PubMed API.\n */\n async testConnection(): Promise<ConnectionTestResult> {\n try {\n await this.client.search('test', { retmax: 1 });\n return { ok: true };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return { ok: false, error: message };\n }\n }\n\n /**\n * Get current search state for persistence.\n */\n getSearchState(): SearchState | null {\n return this.currentState;\n }\n\n /**\n * Get warnings from the most recent search.\n */\n getWarnings(): string[] {\n return this.searchWarnings;\n }\n\n /**\n * Resume search from saved state.\n */\n async *resumeSearch(state: SearchState): AsyncIterable<Article> {\n const providerState = state.providerState as PubMedProviderState | undefined;\n\n if (!providerState) {\n // No provider-specific state, start fresh from offset\n const query = state.query;\n const retstart = state.retrievedCount;\n\n // Continue from where we left off\n const result = await this.withRetry(() => this.client.search(query.native, {\n retstart,\n retmax: DEFAULT_PAGE_SIZE,\n }));\n\n // Update current state\n this.currentState = {\n ...state,\n lastUpdated: new Date(),\n };\n\n let currentPmids = result.idlist;\n let totalRetrieved = state.retrievedCount;\n\n while (currentPmids.length > 0) {\n const articles = await this.withRetry(() => this.client.fetch(currentPmids));\n\n for (const article of articles) {\n yield article;\n totalRetrieved++;\n\n if (this.currentState) {\n this.currentState.retrievedCount = totalRetrieved;\n this.currentState.lastUpdated = new Date();\n }\n }\n\n // Check if done\n if (totalRetrieved >= state.totalResults) {\n break;\n }\n\n // Fetch next page\n const nextResult = await this.withRetry(() => this.client.search(query.native, {\n retstart: totalRetrieved,\n retmax: DEFAULT_PAGE_SIZE,\n }));\n\n currentPmids = nextResult.idlist;\n }\n\n return;\n }\n\n // Use history server for resume\n if (providerState.webenv && providerState.querykey) {\n const webenv = providerState.webenv;\n const querykey = providerState.querykey;\n this.currentState = {\n ...state,\n lastUpdated: new Date(),\n };\n\n let retstart = providerState.retstart;\n let totalRetrieved = state.retrievedCount;\n\n while (totalRetrieved < state.totalResults) {\n const articles = await this.withRetry(() => this.client.fetchFromHistory({\n webenv,\n querykey,\n retstart,\n retmax: DEFAULT_PAGE_SIZE,\n }));\n\n if (articles.length === 0) {\n break;\n }\n\n for (const article of articles) {\n yield article;\n totalRetrieved++;\n retstart++;\n\n if (this.currentState) {\n this.currentState.retrievedCount = totalRetrieved;\n this.currentState.lastUpdated = new Date();\n }\n }\n }\n }\n }\n\n /**\n * Validate if saved state is still usable.\n */\n async validateState(state: SearchState): Promise<SearchResumeResult> {\n const providerState = state.providerState as PubMedProviderState | undefined;\n\n // If no provider state, we can resume with offset pagination\n if (!providerState) {\n return { valid: true };\n }\n\n // If using history server, validate webenv is still valid\n if (providerState.webenv && providerState.querykey) {\n try {\n // Try to fetch one record to verify history is still valid\n await this.client.fetchFromHistory({\n webenv: providerState.webenv,\n querykey: providerState.querykey,\n retstart: 0,\n retmax: 1,\n });\n return { valid: true };\n } catch {\n return {\n valid: false,\n reason: 'Server-side history expired',\n };\n }\n }\n\n return { valid: true };\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"provider.js","sources":["../../../src/providers/pubmed/provider.ts"],"sourcesContent":["/**\n * PubMed Provider implementation.\n *\n * Provides access to NCBI's PubMed database for biomedical literature searches.\n */\n\nimport { BaseProvider } from '../base/provider.js';\nimport type {\n Article,\n ResolvedAST,\n SearchOptions,\n SortField,\n SearchState,\n TranslatedQuery,\n SearchResumeResult,\n ConnectionTestResult,\n} from '../base/types.js';\nimport { PubMedClient } from './client.js';\nimport { translateQuery } from './translator.js';\nimport type { PubMedConfig, PubMedProviderState } from './types.js';\n\n/** Default page size for fetching results */\nconst DEFAULT_PAGE_SIZE = 20;\n\n/** Map base SortField to PubMed esearch sort parameter */\nfunction mapSortField(sort: SortField): string {\n return sort === 'date' ? 'pub_date' : sort;\n}\n\n/**\n * PubMed provider for searching biomedical literature.\n */\nexport class PubMedProvider extends BaseProvider {\n readonly name = 'pubmed' as const;\n\n private readonly client: PubMedClient;\n private readonly pubmedConfig: PubMedConfig;\n\n /** Current search state for session persistence */\n private currentState: SearchState | null = null;\n\n /** Warnings from the most recent search */\n private searchWarnings: string[] = [];\n\n constructor(config: PubMedConfig) {\n super({\n ...config,\n rateLimit: config.rateLimit ?? (config.apiKey ? 10 : 3),\n });\n this.pubmedConfig = config;\n this.client = new PubMedClient(config, this.rateLimiter);\n }\n\n /**\n * Search PubMed and stream results.\n * Uses NCBI history server for efficient pagination of large result sets.\n */\n async *search(\n query: TranslatedQuery,\n options?: SearchOptions\n ): AsyncIterable<Article> {\n const maxResults = options?.maxResults;\n const pageSize = options?.pageSize ?? DEFAULT_PAGE_SIZE;\n let retstart = 0;\n let totalRetrieved = 0;\n\n // Map sort field to PubMed-specific parameter\n const pubmedSort = options?.sort ? mapSortField(options.sort) : undefined;\n\n // Initial search with history server enabled\n const initialResult = await this.withRetry(() => this.client.search(query.native, {\n retstart: 0,\n retmax: pageSize,\n useHistory: true,\n ...(pubmedSort && { sort: pubmedSort }),\n }));\n\n const totalCount = initialResult.count;\n const webenv = initialResult.webenv;\n const querykey = initialResult.querykey;\n\n // Store any warnings from the search response\n this.searchWarnings = initialResult.warnings ?? [];\n\n // Initialize state with provider-specific history server info\n const providerState: PubMedProviderState = {\n retstart: 0,\n useHistory: !!(webenv && querykey),\n ...(webenv && { webenv }),\n ...(querykey && { querykey }),\n };\n\n this.currentState = {\n ...this.createBaseState(query, totalCount, 0),\n providerState,\n };\n\n // If no results, return early\n if (totalCount === 0 || initialResult.idlist.length === 0) {\n return;\n }\n\n // Fetch first page of articles using PMIDs from initial search\n const firstPageArticles = await this.withRetry(() => this.client.fetch(initialResult.idlist));\n\n for (const article of firstPageArticles) {\n totalRetrieved++;\n retstart++;\n\n this.updateState(totalRetrieved, retstart, providerState);\n\n yield article;\n\n if (maxResults !== undefined && totalRetrieved >= maxResults) {\n return;\n }\n }\n\n // Continue with subsequent pages using history server if available\n while (retstart < totalCount) {\n if (maxResults !== undefined && totalRetrieved >= maxResults) {\n break;\n }\n\n const remainingToFetch = maxResults !== undefined\n ? Math.min(pageSize, maxResults - totalRetrieved)\n : pageSize;\n\n let articles: Article[];\n\n if (webenv && querykey) {\n // Use history server for efficient pagination\n articles = await this.withRetry(() => this.client.fetchFromHistory({\n webenv,\n querykey,\n retstart,\n retmax: remainingToFetch,\n }));\n } else {\n // Fallback to offset-based pagination\n const result = await this.withRetry(() => this.client.search(query.native, {\n retstart,\n retmax: remainingToFetch,\n ...(pubmedSort && { sort: pubmedSort }),\n }));\n articles = await this.withRetry(() => this.client.fetch(result.idlist));\n }\n\n if (articles.length === 0) {\n break;\n }\n\n for (const article of articles) {\n totalRetrieved++;\n retstart++;\n\n this.updateState(totalRetrieved, retstart, providerState);\n\n yield article;\n\n if (maxResults !== undefined && totalRetrieved >= maxResults) {\n return;\n }\n }\n }\n }\n\n /**\n * Update current state with progress information.\n */\n private updateState(\n retrievedCount: number,\n retstart: number,\n providerState: PubMedProviderState\n ): void {\n if (this.currentState) {\n this.currentState.retrievedCount = retrievedCount;\n this.currentState.lastUpdated = new Date();\n this.currentState.providerState = {\n ...providerState,\n retstart,\n };\n }\n }\n\n /**\n * Get total hit count for a query without downloading results.\n * Uses ESearch with rettype=count for efficiency.\n */\n async count(query: TranslatedQuery): Promise<number> {\n return this.withRetry(() => this.client.searchCount(query.native));\n }\n\n /**\n * Convert ResolvedAST to PubMed native syntax.\n */\n translateQuery(resolved: ResolvedAST): TranslatedQuery {\n return translateQuery(resolved);\n }\n\n /**\n * Test connection to PubMed API.\n */\n async testConnection(): Promise<ConnectionTestResult> {\n try {\n await this.client.search('test', { retmax: 1 });\n return { ok: true };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return { ok: false, error: message };\n }\n }\n\n /**\n * Get current search state for persistence.\n */\n getSearchState(): SearchState | null {\n return this.currentState;\n }\n\n /**\n * Get warnings from the most recent search.\n */\n getWarnings(): string[] {\n return this.searchWarnings;\n }\n\n /**\n * Resume search from saved state.\n */\n async *resumeSearch(state: SearchState): AsyncIterable<Article> {\n const providerState = state.providerState as PubMedProviderState | undefined;\n\n if (!providerState) {\n // No provider-specific state, start fresh from offset\n const query = state.query;\n const retstart = state.retrievedCount;\n\n // Continue from where we left off\n const result = await this.withRetry(() => this.client.search(query.native, {\n retstart,\n retmax: DEFAULT_PAGE_SIZE,\n }));\n\n // Update current state\n this.currentState = {\n ...state,\n lastUpdated: new Date(),\n };\n\n let currentPmids = result.idlist;\n let totalRetrieved = state.retrievedCount;\n\n while (currentPmids.length > 0) {\n const articles = await this.withRetry(() => this.client.fetch(currentPmids));\n\n for (const article of articles) {\n yield article;\n totalRetrieved++;\n\n if (this.currentState) {\n this.currentState.retrievedCount = totalRetrieved;\n this.currentState.lastUpdated = new Date();\n }\n }\n\n // Check if done\n if (totalRetrieved >= state.totalResults) {\n break;\n }\n\n // Fetch next page\n const nextResult = await this.withRetry(() => this.client.search(query.native, {\n retstart: totalRetrieved,\n retmax: DEFAULT_PAGE_SIZE,\n }));\n\n currentPmids = nextResult.idlist;\n }\n\n return;\n }\n\n // Use history server for resume\n if (providerState.webenv && providerState.querykey) {\n const webenv = providerState.webenv;\n const querykey = providerState.querykey;\n this.currentState = {\n ...state,\n lastUpdated: new Date(),\n };\n\n let retstart = providerState.retstart;\n let totalRetrieved = state.retrievedCount;\n\n while (totalRetrieved < state.totalResults) {\n const articles = await this.withRetry(() => this.client.fetchFromHistory({\n webenv,\n querykey,\n retstart,\n retmax: DEFAULT_PAGE_SIZE,\n }));\n\n if (articles.length === 0) {\n break;\n }\n\n for (const article of articles) {\n yield article;\n totalRetrieved++;\n retstart++;\n\n if (this.currentState) {\n this.currentState.retrievedCount = totalRetrieved;\n this.currentState.lastUpdated = new Date();\n }\n }\n }\n }\n }\n\n /**\n * Validate if saved state is still usable.\n */\n async validateState(state: SearchState): Promise<SearchResumeResult> {\n const providerState = state.providerState as PubMedProviderState | undefined;\n\n // If no provider state, we can resume with offset pagination\n if (!providerState) {\n return { valid: true };\n }\n\n // If using history server, validate webenv is still valid\n if (providerState.webenv && providerState.querykey) {\n try {\n // Try to fetch one record to verify history is still valid\n await this.client.fetchFromHistory({\n webenv: providerState.webenv,\n querykey: providerState.querykey,\n retstart: 0,\n retmax: 1,\n });\n return { valid: true };\n } catch {\n return {\n valid: false,\n reason: 'Server-side history expired',\n };\n }\n }\n\n return { valid: true };\n }\n}\n"],"names":[],"mappings":";;;AAsBA,MAAM,oBAAoB;AAG1B,SAAS,aAAa,MAAyB;AAC7C,SAAO,SAAS,SAAS,aAAa;AACxC;AAKO,MAAM,uBAAuB,aAAa;AAAA,EACtC,OAAO;AAAA,EAEC;AAAA,EACA;AAAA;AAAA,EAGT,eAAmC;AAAA;AAAA,EAGnC,iBAA2B,CAAA;AAAA,EAEnC,YAAY,QAAsB;AAChC,UAAM;AAAA,MACJ,GAAG;AAAA,MACH,WAAW,OAAO,cAAc,OAAO,SAAS,KAAK;AAAA,IAAA,CACtD;AACD,SAAK,eAAe;AACpB,SAAK,SAAS,IAAI,aAAa,QAAQ,KAAK,WAAW;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,OACL,OACA,SACwB;AACxB,UAAM,aAAa,SAAS;AAC5B,UAAM,WAAW,SAAS,YAAY;AACtC,QAAI,WAAW;AACf,QAAI,iBAAiB;AAGrB,UAAM,aAAa,SAAS,OAAO,aAAa,QAAQ,IAAI,IAAI;AAGhE,UAAM,gBAAgB,MAAM,KAAK,UAAU,MAAM,KAAK,OAAO,OAAO,MAAM,QAAQ;AAAA,MAChF,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,YAAY;AAAA,MACZ,GAAI,cAAc,EAAE,MAAM,WAAA;AAAA,IAAW,CACtC,CAAC;AAEF,UAAM,aAAa,cAAc;AACjC,UAAM,SAAS,cAAc;AAC7B,UAAM,WAAW,cAAc;AAG/B,SAAK,iBAAiB,cAAc,YAAY,CAAA;AAGhD,UAAM,gBAAqC;AAAA,MACzC,UAAU;AAAA,MACV,YAAY,CAAC,EAAE,UAAU;AAAA,MACzB,GAAI,UAAU,EAAE,OAAA;AAAA,MAChB,GAAI,YAAY,EAAE,SAAA;AAAA,IAAS;AAG7B,SAAK,eAAe;AAAA,MAClB,GAAG,KAAK,gBAAgB,OAAO,YAAY,CAAC;AAAA,MAC5C;AAAA,IAAA;AAIF,QAAI,eAAe,KAAK,cAAc,OAAO,WAAW,GAAG;AACzD;AAAA,IACF;AAGA,UAAM,oBAAoB,MAAM,KAAK,UAAU,MAAM,KAAK,OAAO,MAAM,cAAc,MAAM,CAAC;AAE5F,eAAW,WAAW,mBAAmB;AACvC;AACA;AAEA,WAAK,YAAY,gBAAgB,UAAU,aAAa;AAExD,YAAM;AAEN,UAAI,eAAe,UAAa,kBAAkB,YAAY;AAC5D;AAAA,MACF;AAAA,IACF;AAGA,WAAO,WAAW,YAAY;AAC5B,UAAI,eAAe,UAAa,kBAAkB,YAAY;AAC5D;AAAA,MACF;AAEA,YAAM,mBAAmB,eAAe,SACpC,KAAK,IAAI,UAAU,aAAa,cAAc,IAC9C;AAEJ,UAAI;AAEJ,UAAI,UAAU,UAAU;AAEtB,mBAAW,MAAM,KAAK,UAAU,MAAM,KAAK,OAAO,iBAAiB;AAAA,UACjE;AAAA,UACA;AAAA,UACA;AAAA,UACA,QAAQ;AAAA,QAAA,CACT,CAAC;AAAA,MACJ,OAAO;AAEL,cAAM,SAAS,MAAM,KAAK,UAAU,MAAM,KAAK,OAAO,OAAO,MAAM,QAAQ;AAAA,UACzE;AAAA,UACA,QAAQ;AAAA,UACR,GAAI,cAAc,EAAE,MAAM,WAAA;AAAA,QAAW,CACtC,CAAC;AACF,mBAAW,MAAM,KAAK,UAAU,MAAM,KAAK,OAAO,MAAM,OAAO,MAAM,CAAC;AAAA,MACxE;AAEA,UAAI,SAAS,WAAW,GAAG;AACzB;AAAA,MACF;AAEA,iBAAW,WAAW,UAAU;AAC9B;AACA;AAEA,aAAK,YAAY,gBAAgB,UAAU,aAAa;AAExD,cAAM;AAEN,YAAI,eAAe,UAAa,kBAAkB,YAAY;AAC5D;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,YACN,gBACA,UACA,eACM;AACN,QAAI,KAAK,cAAc;AACrB,WAAK,aAAa,iBAAiB;AACnC,WAAK,aAAa,cAAc,oBAAI,KAAA;AACpC,WAAK,aAAa,gBAAgB;AAAA,QAChC,GAAG;AAAA,QACH;AAAA,MAAA;AAAA,IAEJ;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,MAAM,OAAyC;AACnD,WAAO,KAAK,UAAU,MAAM,KAAK,OAAO,YAAY,MAAM,MAAM,CAAC;AAAA,EACnE;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe,UAAwC;AACrD,WAAO,eAAe,QAAQ;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,iBAAgD;AACpD,QAAI;AACF,YAAM,KAAK,OAAO,OAAO,QAAQ,EAAE,QAAQ,GAAG;AAC9C,aAAO,EAAE,IAAI,KAAA;AAAA,IACf,SAAS,OAAO;AACd,YAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,aAAO,EAAE,IAAI,OAAO,OAAO,QAAA;AAAA,IAC7B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAqC;AACnC,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,cAAwB;AACtB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,aAAa,OAA4C;AAC9D,UAAM,gBAAgB,MAAM;AAE5B,QAAI,CAAC,eAAe;AAElB,YAAM,QAAQ,MAAM;AACpB,YAAM,WAAW,MAAM;AAGvB,YAAM,SAAS,MAAM,KAAK,UAAU,MAAM,KAAK,OAAO,OAAO,MAAM,QAAQ;AAAA,QACzE;AAAA,QACA,QAAQ;AAAA,MAAA,CACT,CAAC;AAGF,WAAK,eAAe;AAAA,QAClB,GAAG;AAAA,QACH,iCAAiB,KAAA;AAAA,MAAK;AAGxB,UAAI,eAAe,OAAO;AAC1B,UAAI,iBAAiB,MAAM;AAE3B,aAAO,aAAa,SAAS,GAAG;AAC9B,cAAM,WAAW,MAAM,KAAK,UAAU,MAAM,KAAK,OAAO,MAAM,YAAY,CAAC;AAE3E,mBAAW,WAAW,UAAU;AAC9B,gBAAM;AACN;AAEA,cAAI,KAAK,cAAc;AACrB,iBAAK,aAAa,iBAAiB;AACnC,iBAAK,aAAa,cAAc,oBAAI,KAAA;AAAA,UACtC;AAAA,QACF;AAGA,YAAI,kBAAkB,MAAM,cAAc;AACxC;AAAA,QACF;AAGA,cAAM,aAAa,MAAM,KAAK,UAAU,MAAM,KAAK,OAAO,OAAO,MAAM,QAAQ;AAAA,UAC7E,UAAU;AAAA,UACV,QAAQ;AAAA,QAAA,CACT,CAAC;AAEF,uBAAe,WAAW;AAAA,MAC5B;AAEA;AAAA,IACF;AAGA,QAAI,cAAc,UAAU,cAAc,UAAU;AAClD,YAAM,SAAS,cAAc;AAC7B,YAAM,WAAW,cAAc;AAC/B,WAAK,eAAe;AAAA,QAClB,GAAG;AAAA,QACH,iCAAiB,KAAA;AAAA,MAAK;AAGxB,UAAI,WAAW,cAAc;AAC7B,UAAI,iBAAiB,MAAM;AAE3B,aAAO,iBAAiB,MAAM,cAAc;AAC1C,cAAM,WAAW,MAAM,KAAK,UAAU,MAAM,KAAK,OAAO,iBAAiB;AAAA,UACvE;AAAA,UACA;AAAA,UACA;AAAA,UACA,QAAQ;AAAA,QAAA,CACT,CAAC;AAEF,YAAI,SAAS,WAAW,GAAG;AACzB;AAAA,QACF;AAEA,mBAAW,WAAW,UAAU;AAC9B,gBAAM;AACN;AACA;AAEA,cAAI,KAAK,cAAc;AACrB,iBAAK,aAAa,iBAAiB;AACnC,iBAAK,aAAa,cAAc,oBAAI,KAAA;AAAA,UACtC;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,cAAc,OAAiD;AACnE,UAAM,gBAAgB,MAAM;AAG5B,QAAI,CAAC,eAAe;AAClB,aAAO,EAAE,OAAO,KAAA;AAAA,IAClB;AAGA,QAAI,cAAc,UAAU,cAAc,UAAU;AAClD,UAAI;AAEF,cAAM,KAAK,OAAO,iBAAiB;AAAA,UACjC,QAAQ,cAAc;AAAA,UACtB,UAAU,cAAc;AAAA,UACxB,UAAU;AAAA,UACV,QAAQ;AAAA,QAAA,CACT;AACD,eAAO,EAAE,OAAO,KAAA;AAAA,MAClB,QAAQ;AACN,eAAO;AAAA,UACL,OAAO;AAAA,UACP,QAAQ;AAAA,QAAA;AAAA,MAEZ;AAAA,IACF;AAEA,WAAO,EAAE,OAAO,KAAA;AAAA,EAClB;AACF;"}
@@ -71,4 +71,33 @@ export interface PubMedProviderState {
71
71
  /** Whether the search uses history server */
72
72
  useHistory: boolean;
73
73
  }
74
+ /**
75
+ * Options for PubMed ELink API call (related articles).
76
+ */
77
+ export interface ELinkOptions {
78
+ /** Seed PMIDs to find related articles for */
79
+ ids: string[];
80
+ /** Optional term filter applied server-side (e.g., 'review[filter]+AND+2024[pdat]') */
81
+ term?: string;
82
+ /** Limit returned related IDs (post-fetch truncation by score) */
83
+ maxResults?: number;
84
+ }
85
+ /**
86
+ * A related article with similarity score.
87
+ */
88
+ export interface RelatedArticle {
89
+ /** PubMed ID of the related article */
90
+ id: string;
91
+ /** Computed similarity score (relative, not normalized) */
92
+ score: number;
93
+ }
94
+ /**
95
+ * ELink response for a single seed ID.
96
+ */
97
+ export interface ELinkResponse {
98
+ /** The seed PMID that was used to find related articles */
99
+ seedId: string;
100
+ /** Related articles with scores, sorted by score descending */
101
+ relatedIds: RelatedArticle[];
102
+ }
74
103
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/providers/pubmed/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,OAAO;IAC5C,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,MAAM,EAAE,QAAQ,CAAC;IACjB,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mBAAmB;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mCAAmC;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,4CAA4C;IAC5C,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,YAAa,SAAQ,kBAAkB;IACtD,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uCAAuC;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,UAAU,EAAE,OAAO,CAAC;CACrB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/providers/pubmed/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,OAAO;IAC5C,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,MAAM,EAAE,QAAQ,CAAC;IACjB,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mBAAmB;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mCAAmC;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,4CAA4C;IAC5C,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,YAAa,SAAQ,kBAAkB;IACtD,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uCAAuC;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,8CAA8C;IAC9C,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,uFAAuF;IACvF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,uCAAuC;IACvC,EAAE,EAAE,MAAM,CAAC;IACX,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,UAAU,EAAE,cAAc,EAAE,CAAC;CAC9B"}
@@ -23,6 +23,8 @@ export interface ScopusSearchOptions {
23
23
  view?: 'STANDARD' | 'COMPLETE';
24
24
  /** Fields to return */
25
25
  fields?: string;
26
+ /** Sort parameter (e.g. '-relevancy' for relevance sort) */
27
+ sort?: string;
26
28
  }
27
29
  /**
28
30
  * Extended search response with rate limit info.
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/providers/scopus/client.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAElE,OAAO,EAAuB,KAAK,oBAAoB,EAA2C,MAAM,eAAe,CAAC;AAMxH;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,2CAA2C;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,IAAI,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;IAC/B,uBAAuB;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAqB,SAAQ,oBAAoB;IAChE,mDAAmD;IACnD,SAAS,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC;CAC7C;AAED;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;gBAE1B,MAAM,EAAE,YAAY;IAIhC;;OAEG;IACG,MAAM,CACV,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,oBAAoB,CAAC;IAyChC;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,oBAAoB,CAAC;IAerD;;OAEG;IACH,OAAO,CAAC,cAAc;IAmBtB;;OAEG;IACH,OAAO,CAAC,YAAY;IAapB;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAgB7B;;OAEG;IACH,OAAO,CAAC,mBAAmB;CAmD5B"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/providers/scopus/client.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAElE,OAAO,EAAuB,KAAK,oBAAoB,EAA2C,MAAM,eAAe,CAAC;AAMxH;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,2CAA2C;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,IAAI,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;IAC/B,uBAAuB;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,oBAAqB,SAAQ,oBAAoB;IAChE,mDAAmD;IACnD,SAAS,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC;CAC7C;AAED;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;gBAE1B,MAAM,EAAE,YAAY;IAIhC;;OAEG;IACG,MAAM,CACV,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,oBAAoB,CAAC;IAyChC;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,oBAAoB,CAAC;IAerD;;OAEG;IACH,OAAO,CAAC,cAAc;IAsBtB;;OAEG;IACH,OAAO,CAAC,YAAY;IAapB;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAgB7B;;OAEG;IACH,OAAO,CAAC,mBAAmB;CAmD5B"}
@@ -74,6 +74,9 @@ class ScopusClient {
74
74
  if (options.fields !== void 0) {
75
75
  url.searchParams.set("field", options.fields);
76
76
  }
77
+ if (options.sort !== void 0) {
78
+ url.searchParams.set("sort", options.sort);
79
+ }
77
80
  return url;
78
81
  }
79
82
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sources":["../../../src/providers/scopus/client.ts"],"sourcesContent":["/**\n * Scopus HTTP Client\n *\n * Handles HTTP communication with the Scopus Search API.\n */\n\nimport type { ScopusConfig, ScopusSearchResponse } from './types';\nimport { parseSearchResponse } from './parser';\nimport { createProviderError, type ConnectionTestResult, type ProviderError, type RateLimitError } from '../base/types';\n\nconst SCOPUS_API_BASE = 'https://api.elsevier.com';\nconst SCOPUS_SEARCH_ENDPOINT = '/content/search/scopus';\nconst DEFAULT_TIMEOUT_MS = 30000;\n\n/**\n * Rate limit information from response headers.\n */\nexport interface ScopusRateLimitInfo {\n /** Maximum requests per time window */\n limit: number;\n /** Remaining requests in current window */\n remaining: number;\n /** Unix timestamp when the window resets */\n reset: number;\n}\n\n/**\n * Search options for the Scopus client.\n */\nexport interface ScopusSearchOptions {\n /** Start index for pagination (0-based) */\n start?: number;\n /** Number of results per page (max 25 for COMPLETE view) */\n count?: number;\n /** View type (STANDARD or COMPLETE) */\n view?: 'STANDARD' | 'COMPLETE';\n /** Fields to return */\n fields?: string;\n}\n\n/**\n * Extended search response with rate limit info.\n */\nexport interface ScopusClientResponse extends ScopusSearchResponse {\n /** Rate limit information from response headers */\n rateLimit?: ScopusRateLimitInfo | undefined;\n}\n\n/**\n * HTTP client for Scopus API.\n */\nexport class ScopusClient {\n private readonly config: ScopusConfig;\n\n constructor(config: ScopusConfig) {\n this.config = config;\n }\n\n /**\n * Execute a search query against Scopus API.\n */\n async search(\n query: string,\n options: ScopusSearchOptions = {}\n ): Promise<ScopusClientResponse> {\n const url = this.buildSearchUrl(query, options);\n const headers = this.buildHeaders();\n const timeoutMs = this.config.timeout ?? DEFAULT_TIMEOUT_MS;\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, {\n method: 'GET',\n headers,\n signal: controller.signal,\n });\n\n if (!response.ok) {\n throw this.handleErrorResponse(response);\n }\n\n const json = await response.json();\n const parsed = parseSearchResponse(json);\n\n return {\n ...parsed,\n rateLimit: this.parseRateLimitHeaders(response.headers),\n };\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') {\n throw createProviderError(\n 'TIMEOUT',\n `Scopus API request timed out after ${timeoutMs}ms`,\n 'scopus',\n { retryable: true }\n );\n }\n throw error;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /**\n * Test the API connection by making a minimal search request.\n */\n async testConnection(): Promise<ConnectionTestResult> {\n try {\n // Use a simple valid query instead of '*' which Scopus may reject\n await this.search('ALL(test)', { count: 1, view: 'STANDARD' });\n return { ok: true };\n } catch (error) {\n const message = error instanceof Error\n ? error.message\n : typeof error === 'object' && error !== null && 'message' in error\n ? String((error as { message: unknown }).message)\n : String(error);\n return { ok: false, error: message };\n }\n }\n\n /**\n * Build the search URL with query parameters.\n */\n private buildSearchUrl(query: string, options: ScopusSearchOptions): URL {\n const url = new URL(SCOPUS_SEARCH_ENDPOINT, SCOPUS_API_BASE);\n\n url.searchParams.set('query', query);\n url.searchParams.set('view', options.view ?? 'COMPLETE');\n\n if (options.start !== undefined) {\n url.searchParams.set('start', String(options.start));\n }\n if (options.count !== undefined) {\n url.searchParams.set('count', String(options.count));\n }\n if (options.fields !== undefined) {\n url.searchParams.set('field', options.fields);\n }\n\n return url;\n }\n\n /**\n * Build request headers including authentication.\n */\n private buildHeaders(): Record<string, string> {\n const headers: Record<string, string> = {\n Accept: 'application/json',\n 'X-ELS-APIKey': this.config.apiKey,\n };\n\n if (this.config.instToken) {\n headers['X-ELS-Insttoken'] = this.config.instToken;\n }\n\n return headers;\n }\n\n /**\n * Parse rate limit information from response headers.\n */\n private parseRateLimitHeaders(headers: Headers): ScopusRateLimitInfo | undefined {\n const limit = headers.get('X-RateLimit-Limit');\n const remaining = headers.get('X-RateLimit-Remaining');\n const reset = headers.get('X-RateLimit-Reset');\n\n if (limit && remaining && reset) {\n return {\n limit: parseInt(limit, 10),\n remaining: parseInt(remaining, 10),\n reset: parseInt(reset, 10),\n };\n }\n\n return undefined;\n }\n\n /**\n * Handle error responses and convert to ProviderError.\n */\n private handleErrorResponse(response: Response): ProviderError | RateLimitError {\n const { status } = response;\n\n switch (status) {\n case 401:\n return createProviderError(\n 'API_KEY_INVALID',\n `Scopus API key is invalid or expired (HTTP 401). Verify your key at https://dev.elsevier.com/`,\n 'scopus',\n { retryable: false }\n );\n\n case 403:\n return createProviderError(\n 'ACCESS_DENIED',\n `Scopus API access denied (HTTP 403). Your key may lack permissions for this resource.`,\n 'scopus',\n { retryable: false }\n );\n\n case 429: {\n const retryAfter = response.headers.get('Retry-After');\n const retryMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : undefined;\n return {\n ...createProviderError(\n 'RATE_LIMIT_EXCEEDED',\n 'Scopus API rate limit exceeded',\n 'scopus',\n { retryable: true }\n ),\n retryAfter: retryMs,\n } as RateLimitError;\n }\n\n default:\n if (status >= 500) {\n return createProviderError(\n 'SERVER_ERROR',\n `Scopus API server error: ${status} ${response.statusText}`,\n 'scopus',\n { retryable: true }\n );\n }\n return createProviderError(\n 'NETWORK_ERROR',\n `Scopus API error: ${status} ${response.statusText}`,\n 'scopus',\n { retryable: false }\n );\n }\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.js","sources":["../../../src/providers/scopus/client.ts"],"sourcesContent":["/**\n * Scopus HTTP Client\n *\n * Handles HTTP communication with the Scopus Search API.\n */\n\nimport type { ScopusConfig, ScopusSearchResponse } from './types';\nimport { parseSearchResponse } from './parser';\nimport { createProviderError, type ConnectionTestResult, type ProviderError, type RateLimitError } from '../base/types';\n\nconst SCOPUS_API_BASE = 'https://api.elsevier.com';\nconst SCOPUS_SEARCH_ENDPOINT = '/content/search/scopus';\nconst DEFAULT_TIMEOUT_MS = 30000;\n\n/**\n * Rate limit information from response headers.\n */\nexport interface ScopusRateLimitInfo {\n /** Maximum requests per time window */\n limit: number;\n /** Remaining requests in current window */\n remaining: number;\n /** Unix timestamp when the window resets */\n reset: number;\n}\n\n/**\n * Search options for the Scopus client.\n */\nexport interface ScopusSearchOptions {\n /** Start index for pagination (0-based) */\n start?: number;\n /** Number of results per page (max 25 for COMPLETE view) */\n count?: number;\n /** View type (STANDARD or COMPLETE) */\n view?: 'STANDARD' | 'COMPLETE';\n /** Fields to return */\n fields?: string;\n /** Sort parameter (e.g. '-relevancy' for relevance sort) */\n sort?: string;\n}\n\n/**\n * Extended search response with rate limit info.\n */\nexport interface ScopusClientResponse extends ScopusSearchResponse {\n /** Rate limit information from response headers */\n rateLimit?: ScopusRateLimitInfo | undefined;\n}\n\n/**\n * HTTP client for Scopus API.\n */\nexport class ScopusClient {\n private readonly config: ScopusConfig;\n\n constructor(config: ScopusConfig) {\n this.config = config;\n }\n\n /**\n * Execute a search query against Scopus API.\n */\n async search(\n query: string,\n options: ScopusSearchOptions = {}\n ): Promise<ScopusClientResponse> {\n const url = this.buildSearchUrl(query, options);\n const headers = this.buildHeaders();\n const timeoutMs = this.config.timeout ?? DEFAULT_TIMEOUT_MS;\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, {\n method: 'GET',\n headers,\n signal: controller.signal,\n });\n\n if (!response.ok) {\n throw this.handleErrorResponse(response);\n }\n\n const json = await response.json();\n const parsed = parseSearchResponse(json);\n\n return {\n ...parsed,\n rateLimit: this.parseRateLimitHeaders(response.headers),\n };\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') {\n throw createProviderError(\n 'TIMEOUT',\n `Scopus API request timed out after ${timeoutMs}ms`,\n 'scopus',\n { retryable: true }\n );\n }\n throw error;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /**\n * Test the API connection by making a minimal search request.\n */\n async testConnection(): Promise<ConnectionTestResult> {\n try {\n // Use a simple valid query instead of '*' which Scopus may reject\n await this.search('ALL(test)', { count: 1, view: 'STANDARD' });\n return { ok: true };\n } catch (error) {\n const message = error instanceof Error\n ? error.message\n : typeof error === 'object' && error !== null && 'message' in error\n ? String((error as { message: unknown }).message)\n : String(error);\n return { ok: false, error: message };\n }\n }\n\n /**\n * Build the search URL with query parameters.\n */\n private buildSearchUrl(query: string, options: ScopusSearchOptions): URL {\n const url = new URL(SCOPUS_SEARCH_ENDPOINT, SCOPUS_API_BASE);\n\n url.searchParams.set('query', query);\n url.searchParams.set('view', options.view ?? 'COMPLETE');\n\n if (options.start !== undefined) {\n url.searchParams.set('start', String(options.start));\n }\n if (options.count !== undefined) {\n url.searchParams.set('count', String(options.count));\n }\n if (options.fields !== undefined) {\n url.searchParams.set('field', options.fields);\n }\n if (options.sort !== undefined) {\n url.searchParams.set('sort', options.sort);\n }\n\n return url;\n }\n\n /**\n * Build request headers including authentication.\n */\n private buildHeaders(): Record<string, string> {\n const headers: Record<string, string> = {\n Accept: 'application/json',\n 'X-ELS-APIKey': this.config.apiKey,\n };\n\n if (this.config.instToken) {\n headers['X-ELS-Insttoken'] = this.config.instToken;\n }\n\n return headers;\n }\n\n /**\n * Parse rate limit information from response headers.\n */\n private parseRateLimitHeaders(headers: Headers): ScopusRateLimitInfo | undefined {\n const limit = headers.get('X-RateLimit-Limit');\n const remaining = headers.get('X-RateLimit-Remaining');\n const reset = headers.get('X-RateLimit-Reset');\n\n if (limit && remaining && reset) {\n return {\n limit: parseInt(limit, 10),\n remaining: parseInt(remaining, 10),\n reset: parseInt(reset, 10),\n };\n }\n\n return undefined;\n }\n\n /**\n * Handle error responses and convert to ProviderError.\n */\n private handleErrorResponse(response: Response): ProviderError | RateLimitError {\n const { status } = response;\n\n switch (status) {\n case 401:\n return createProviderError(\n 'API_KEY_INVALID',\n `Scopus API key is invalid or expired (HTTP 401). Verify your key at https://dev.elsevier.com/`,\n 'scopus',\n { retryable: false }\n );\n\n case 403:\n return createProviderError(\n 'ACCESS_DENIED',\n `Scopus API access denied (HTTP 403). Your key may lack permissions for this resource.`,\n 'scopus',\n { retryable: false }\n );\n\n case 429: {\n const retryAfter = response.headers.get('Retry-After');\n const retryMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : undefined;\n return {\n ...createProviderError(\n 'RATE_LIMIT_EXCEEDED',\n 'Scopus API rate limit exceeded',\n 'scopus',\n { retryable: true }\n ),\n retryAfter: retryMs,\n } as RateLimitError;\n }\n\n default:\n if (status >= 500) {\n return createProviderError(\n 'SERVER_ERROR',\n `Scopus API server error: ${status} ${response.statusText}`,\n 'scopus',\n { retryable: true }\n );\n }\n return createProviderError(\n 'NETWORK_ERROR',\n `Scopus API error: ${status} ${response.statusText}`,\n 'scopus',\n { retryable: false }\n );\n }\n }\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/providers/scopus/provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,YAAY,EAA2B,MAAM,kBAAkB,CAAC;AACzE,OAAO,KAAK,EACV,OAAO,EACP,eAAe,EACf,aAAa,EACb,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,oBAAoB,EACrB,MAAM,eAAe,CAAC;AAIvB,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,SAAS,CAAC;AAKjE;;GAEG;AACH,qBAAa,cAAe,SAAQ,YAAY;IAC9C,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAU;IAElC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAE5C,8CAA8C;IAC9C,OAAO,CAAC,YAAY,CAA4B;gBAEpC,MAAM,EAAE,YAAY;IAuBhC;;OAEG;IACH,cAAc,CAAC,QAAQ,EAAE,WAAW,GAAG,eAAe;IAItD;;;OAGG;IACG,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC;IAWpD;;OAEG;IACI,MAAM,CACX,KAAK,EAAE,eAAe,EACtB,OAAO,GAAE,aAAkB,GAC1B,aAAa,CAAC,OAAO,CAAC;IA6EzB;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,oBAAoB,CAAC;IAIrD;;OAEG;IACH,cAAc,IAAI,WAAW,GAAG,IAAI;IAIpC;;OAEG;IACI,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,aAAa,CAAC,OAAO,CAAC;IAqE/D;;OAEG;IACG,aAAa,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAsBrE"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/providers/scopus/provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,YAAY,EAA2B,MAAM,kBAAkB,CAAC;AACzE,OAAO,KAAK,EACV,OAAO,EACP,eAAe,EACf,aAAa,EAEb,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,oBAAoB,EACrB,MAAM,eAAe,CAAC;AAIvB,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,SAAS,CAAC;AAYjE;;GAEG;AACH,qBAAa,cAAe,SAAQ,YAAY;IAC9C,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAU;IAElC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAE5C,8CAA8C;IAC9C,OAAO,CAAC,YAAY,CAA4B;gBAEpC,MAAM,EAAE,YAAY;IAuBhC;;OAEG;IACH,cAAc,CAAC,QAAQ,EAAE,WAAW,GAAG,eAAe;IAItD;;;OAGG;IACG,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC;IAWpD;;OAEG;IACI,MAAM,CACX,KAAK,EAAE,eAAe,EACtB,OAAO,GAAE,aAAkB,GAC1B,aAAa,CAAC,OAAO,CAAC;IAiFzB;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,oBAAoB,CAAC;IAIrD;;OAEG;IACH,cAAc,IAAI,WAAW,GAAG,IAAI;IAIpC;;OAEG;IACI,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,aAAa,CAAC,OAAO,CAAC;IAqE/D;;OAEG;IACG,aAAa,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAsBrE"}
@@ -3,6 +3,10 @@ import { ScopusClient } from "./client.js";
3
3
  import { parseDocument } from "./parser.js";
4
4
  import { translateQuery } from "./translator.js";
5
5
  const DEFAULT_PAGE_SIZE = 25;
6
+ function mapSortField(sort) {
7
+ if (sort === "relevance") return "-relevancy";
8
+ return void 0;
9
+ }
6
10
  class ScopusProvider extends BaseProvider {
7
11
  name = "scopus";
8
12
  client;
@@ -60,12 +64,14 @@ class ScopusProvider extends BaseProvider {
60
64
  let totalResults = 0;
61
65
  let retrievedCount = 0;
62
66
  this.currentState = this.createBaseState(query, 0, 0);
67
+ const scopusSort = options.sort ? mapSortField(options.sort) : void 0;
63
68
  while (retrievedCount < maxResults) {
64
69
  await this.rateLimiter.acquire();
65
70
  const response = await this.withRetry(async () => {
66
71
  return await this.client.search(query.native, {
67
72
  start: offset,
68
- count: Math.min(pageSize, maxResults - retrievedCount)
73
+ count: Math.min(pageSize, maxResults - retrievedCount),
74
+ ...scopusSort && { sort: scopusSort }
69
75
  });
70
76
  });
71
77
  if (offset === 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"provider.js","sources":["../../../src/providers/scopus/provider.ts"],"sourcesContent":["/**\n * Scopus Provider\n *\n * Implements the Provider interface for Scopus database searches.\n */\n\nimport { BaseProvider, type BaseProviderConfig } from '../base/provider';\nimport type {\n Article,\n TranslatedQuery,\n SearchOptions,\n ResolvedAST,\n SearchState,\n SearchResumeResult,\n ConnectionTestResult,\n} from '../base/types';\nimport { ScopusClient } from './client';\nimport { parseDocument } from './parser';\nimport { translateQuery } from './translator';\nimport type { ScopusConfig, ScopusProviderState } from './types';\n\n/** Default page size for Scopus (max 25 for COMPLETE view) */\nconst DEFAULT_PAGE_SIZE = 25;\n\n/**\n * Scopus database provider.\n */\nexport class ScopusProvider extends BaseProvider {\n readonly name = 'scopus' as const;\n\n private readonly client: ScopusClient;\n private readonly scopusConfig: ScopusConfig;\n\n /** Current search state for resume support */\n private currentState: SearchState | null = null;\n\n constructor(config: ScopusConfig) {\n const baseConfig: BaseProviderConfig = {};\n if (config.rateLimit !== undefined) {\n baseConfig.rateLimit = config.rateLimit;\n }\n if (config.timeout !== undefined) {\n baseConfig.timeout = config.timeout;\n }\n if (config.retries !== undefined) {\n baseConfig.retries = config.retries;\n }\n if (config.initialBackoff !== undefined) {\n baseConfig.initialBackoff = config.initialBackoff;\n }\n if (config.maxBackoff !== undefined) {\n baseConfig.maxBackoff = config.maxBackoff;\n }\n super(baseConfig);\n\n this.scopusConfig = config;\n this.client = new ScopusClient(config);\n }\n\n /**\n * Translate ResolvedAST to Scopus search syntax.\n */\n translateQuery(resolved: ResolvedAST): TranslatedQuery {\n return translateQuery(resolved);\n }\n\n /**\n * Get total hit count for a query without downloading results.\n * Uses a minimal search with count=1 to get the total from response metadata.\n */\n async count(query: TranslatedQuery): Promise<number> {\n await this.rateLimiter.acquire();\n const response = await this.withRetry(async () => {\n return await this.client.search(query.native, { start: 0, count: 1 });\n });\n if (response.parseWarning) {\n console.error(`Warning: ${response.parseWarning}`);\n }\n return response.totalResults;\n }\n\n /**\n * Execute search and return results as async iterable.\n */\n async *search(\n query: TranslatedQuery,\n options: SearchOptions = {}\n ): AsyncIterable<Article> {\n const maxResults = options.maxResults ?? this.scopusConfig.maxResults ?? 10000;\n const pageSize = Math.min(options.pageSize ?? DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE);\n\n let offset = 0;\n let totalResults = 0;\n let retrievedCount = 0;\n\n // Initialize state\n this.currentState = this.createBaseState(query, 0, 0);\n\n while (retrievedCount < maxResults) {\n // Wait for rate limiter\n await this.rateLimiter.acquire();\n\n // Fetch page with retry\n const response = await this.withRetry(async () => {\n return await this.client.search(query.native, {\n start: offset,\n count: Math.min(pageSize, maxResults - retrievedCount),\n });\n });\n\n // Update total on first page and check for parse warnings\n if (offset === 0) {\n totalResults = response.totalResults;\n if (response.parseWarning) {\n console.error(`Warning: ${response.parseWarning}`);\n }\n }\n\n // Update state\n this.currentState = {\n ...this.createBaseState(query, totalResults, retrievedCount),\n providerState: {\n offset,\n totalResults,\n query: query.native,\n } as ScopusProviderState,\n };\n\n // Yield articles\n for (const entry of response.entries) {\n if (retrievedCount >= maxResults) {\n break;\n }\n\n const doc = parseDocument(entry);\n retrievedCount++;\n\n // Update state before yield so it's available when consumer breaks\n if (this.currentState) {\n this.currentState.retrievedCount = retrievedCount;\n this.currentState.lastUpdated = new Date();\n }\n\n yield doc;\n }\n\n // Move to next page\n offset += response.entries.length;\n\n // Check if we've retrieved all results\n if (offset >= totalResults || response.entries.length === 0) {\n break;\n }\n\n // Check abort signal\n if (options.signal?.aborted) {\n break;\n }\n }\n\n // Clear state when search completes\n this.currentState = null;\n }\n\n /**\n * Verify API access and credentials.\n */\n async testConnection(): Promise<ConnectionTestResult> {\n return this.client.testConnection();\n }\n\n /**\n * Get current search state for session persistence.\n */\n getSearchState(): SearchState | null {\n return this.currentState;\n }\n\n /**\n * Resume search from saved state.\n */\n async *resumeSearch(state: SearchState): AsyncIterable<Article> {\n const providerState = state.providerState as ScopusProviderState;\n if (!providerState) {\n throw new Error('Invalid state: missing provider state');\n }\n\n const maxResults = state.totalResults;\n const pageSize = DEFAULT_PAGE_SIZE;\n\n let offset = state.retrievedCount;\n let retrievedCount = state.retrievedCount;\n\n // Restore state\n this.currentState = { ...state };\n\n while (retrievedCount < maxResults) {\n // Wait for rate limiter\n await this.rateLimiter.acquire();\n\n // Fetch page with retry\n const response = await this.withRetry(async () => {\n return await this.client.search(providerState.query, {\n start: offset,\n count: Math.min(pageSize, maxResults - retrievedCount),\n });\n });\n\n // Update state\n this.currentState = {\n ...this.currentState!,\n retrievedCount,\n lastUpdated: new Date(),\n providerState: {\n ...providerState,\n offset,\n },\n };\n\n // Yield articles\n for (const entry of response.entries) {\n if (retrievedCount >= maxResults) {\n break;\n }\n\n const doc = parseDocument(entry);\n retrievedCount++;\n\n // Update state before yield so it's available when consumer breaks\n if (this.currentState) {\n this.currentState.retrievedCount = retrievedCount;\n this.currentState.lastUpdated = new Date();\n }\n\n yield doc;\n }\n\n // Move to next page\n offset += response.entries.length;\n\n // Check if we've retrieved all results\n if (offset >= maxResults || response.entries.length === 0) {\n break;\n }\n }\n\n // Clear state when search completes\n this.currentState = null;\n }\n\n /**\n * Validate if a saved state is still valid for resuming.\n */\n async validateState(state: SearchState): Promise<SearchResumeResult> {\n // Check if the API key is still valid\n const connectionValid = await this.testConnection();\n if (!connectionValid) {\n return {\n valid: false,\n reason: 'API key is invalid or connection failed',\n };\n }\n\n // For Scopus, offset-based pagination is always valid if the API key works\n // We don't need to check server-side state like PubMed's WebEnv\n const providerState = state.providerState as ScopusProviderState;\n if (!providerState) {\n return {\n valid: false,\n reason: 'Missing provider state',\n };\n }\n\n return { valid: true };\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"provider.js","sources":["../../../src/providers/scopus/provider.ts"],"sourcesContent":["/**\n * Scopus Provider\n *\n * Implements the Provider interface for Scopus database searches.\n */\n\nimport { BaseProvider, type BaseProviderConfig } from '../base/provider';\nimport type {\n Article,\n TranslatedQuery,\n SearchOptions,\n SortField,\n ResolvedAST,\n SearchState,\n SearchResumeResult,\n ConnectionTestResult,\n} from '../base/types';\nimport { ScopusClient } from './client';\nimport { parseDocument } from './parser';\nimport { translateQuery } from './translator';\nimport type { ScopusConfig, ScopusProviderState } from './types';\n\n/** Default page size for Scopus (max 25 for COMPLETE view) */\nconst DEFAULT_PAGE_SIZE = 25;\n\n/** Map base SortField to Scopus sort parameter */\nfunction mapSortField(sort: SortField): string | undefined {\n if (sort === 'relevance') return '-relevancy';\n // 'date' is the default Scopus sort order, no parameter needed\n return undefined;\n}\n\n/**\n * Scopus database provider.\n */\nexport class ScopusProvider extends BaseProvider {\n readonly name = 'scopus' as const;\n\n private readonly client: ScopusClient;\n private readonly scopusConfig: ScopusConfig;\n\n /** Current search state for resume support */\n private currentState: SearchState | null = null;\n\n constructor(config: ScopusConfig) {\n const baseConfig: BaseProviderConfig = {};\n if (config.rateLimit !== undefined) {\n baseConfig.rateLimit = config.rateLimit;\n }\n if (config.timeout !== undefined) {\n baseConfig.timeout = config.timeout;\n }\n if (config.retries !== undefined) {\n baseConfig.retries = config.retries;\n }\n if (config.initialBackoff !== undefined) {\n baseConfig.initialBackoff = config.initialBackoff;\n }\n if (config.maxBackoff !== undefined) {\n baseConfig.maxBackoff = config.maxBackoff;\n }\n super(baseConfig);\n\n this.scopusConfig = config;\n this.client = new ScopusClient(config);\n }\n\n /**\n * Translate ResolvedAST to Scopus search syntax.\n */\n translateQuery(resolved: ResolvedAST): TranslatedQuery {\n return translateQuery(resolved);\n }\n\n /**\n * Get total hit count for a query without downloading results.\n * Uses a minimal search with count=1 to get the total from response metadata.\n */\n async count(query: TranslatedQuery): Promise<number> {\n await this.rateLimiter.acquire();\n const response = await this.withRetry(async () => {\n return await this.client.search(query.native, { start: 0, count: 1 });\n });\n if (response.parseWarning) {\n console.error(`Warning: ${response.parseWarning}`);\n }\n return response.totalResults;\n }\n\n /**\n * Execute search and return results as async iterable.\n */\n async *search(\n query: TranslatedQuery,\n options: SearchOptions = {}\n ): AsyncIterable<Article> {\n const maxResults = options.maxResults ?? this.scopusConfig.maxResults ?? 10000;\n const pageSize = Math.min(options.pageSize ?? DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE);\n\n let offset = 0;\n let totalResults = 0;\n let retrievedCount = 0;\n\n // Initialize state\n this.currentState = this.createBaseState(query, 0, 0);\n\n // Map sort field to Scopus-specific parameter\n const scopusSort = options.sort ? mapSortField(options.sort) : undefined;\n\n while (retrievedCount < maxResults) {\n // Wait for rate limiter\n await this.rateLimiter.acquire();\n\n // Fetch page with retry\n const response = await this.withRetry(async () => {\n return await this.client.search(query.native, {\n start: offset,\n count: Math.min(pageSize, maxResults - retrievedCount),\n ...(scopusSort && { sort: scopusSort }),\n });\n });\n\n // Update total on first page and check for parse warnings\n if (offset === 0) {\n totalResults = response.totalResults;\n if (response.parseWarning) {\n console.error(`Warning: ${response.parseWarning}`);\n }\n }\n\n // Update state\n this.currentState = {\n ...this.createBaseState(query, totalResults, retrievedCount),\n providerState: {\n offset,\n totalResults,\n query: query.native,\n } as ScopusProviderState,\n };\n\n // Yield articles\n for (const entry of response.entries) {\n if (retrievedCount >= maxResults) {\n break;\n }\n\n const doc = parseDocument(entry);\n retrievedCount++;\n\n // Update state before yield so it's available when consumer breaks\n if (this.currentState) {\n this.currentState.retrievedCount = retrievedCount;\n this.currentState.lastUpdated = new Date();\n }\n\n yield doc;\n }\n\n // Move to next page\n offset += response.entries.length;\n\n // Check if we've retrieved all results\n if (offset >= totalResults || response.entries.length === 0) {\n break;\n }\n\n // Check abort signal\n if (options.signal?.aborted) {\n break;\n }\n }\n\n // Clear state when search completes\n this.currentState = null;\n }\n\n /**\n * Verify API access and credentials.\n */\n async testConnection(): Promise<ConnectionTestResult> {\n return this.client.testConnection();\n }\n\n /**\n * Get current search state for session persistence.\n */\n getSearchState(): SearchState | null {\n return this.currentState;\n }\n\n /**\n * Resume search from saved state.\n */\n async *resumeSearch(state: SearchState): AsyncIterable<Article> {\n const providerState = state.providerState as ScopusProviderState;\n if (!providerState) {\n throw new Error('Invalid state: missing provider state');\n }\n\n const maxResults = state.totalResults;\n const pageSize = DEFAULT_PAGE_SIZE;\n\n let offset = state.retrievedCount;\n let retrievedCount = state.retrievedCount;\n\n // Restore state\n this.currentState = { ...state };\n\n while (retrievedCount < maxResults) {\n // Wait for rate limiter\n await this.rateLimiter.acquire();\n\n // Fetch page with retry\n const response = await this.withRetry(async () => {\n return await this.client.search(providerState.query, {\n start: offset,\n count: Math.min(pageSize, maxResults - retrievedCount),\n });\n });\n\n // Update state\n this.currentState = {\n ...this.currentState!,\n retrievedCount,\n lastUpdated: new Date(),\n providerState: {\n ...providerState,\n offset,\n },\n };\n\n // Yield articles\n for (const entry of response.entries) {\n if (retrievedCount >= maxResults) {\n break;\n }\n\n const doc = parseDocument(entry);\n retrievedCount++;\n\n // Update state before yield so it's available when consumer breaks\n if (this.currentState) {\n this.currentState.retrievedCount = retrievedCount;\n this.currentState.lastUpdated = new Date();\n }\n\n yield doc;\n }\n\n // Move to next page\n offset += response.entries.length;\n\n // Check if we've retrieved all results\n if (offset >= maxResults || response.entries.length === 0) {\n break;\n }\n }\n\n // Clear state when search completes\n this.currentState = null;\n }\n\n /**\n * Validate if a saved state is still valid for resuming.\n */\n async validateState(state: SearchState): Promise<SearchResumeResult> {\n // Check if the API key is still valid\n const connectionValid = await this.testConnection();\n if (!connectionValid) {\n return {\n valid: false,\n reason: 'API key is invalid or connection failed',\n };\n }\n\n // For Scopus, offset-based pagination is always valid if the API key works\n // We don't need to check server-side state like PubMed's WebEnv\n const providerState = state.providerState as ScopusProviderState;\n if (!providerState) {\n return {\n valid: false,\n reason: 'Missing provider state',\n };\n }\n\n return { valid: true };\n }\n}\n"],"names":[],"mappings":"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"}