@ncukondo/search-hub 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -9
- package/dist/cli/commands/check.d.ts +34 -0
- package/dist/cli/commands/check.d.ts.map +1 -0
- package/dist/cli/commands/check.js +126 -0
- package/dist/cli/commands/check.js.map +1 -0
- package/dist/cli/commands/export.d.ts +5 -3
- package/dist/cli/commands/export.d.ts.map +1 -1
- package/dist/cli/commands/export.js +0 -4
- package/dist/cli/commands/export.js.map +1 -1
- package/dist/cli/commands/query/init.d.ts.map +1 -1
- package/dist/cli/commands/query/init.js +17 -7
- package/dist/cli/commands/query/init.js.map +1 -1
- package/dist/cli/commands/query/inspect.d.ts +36 -0
- package/dist/cli/commands/query/inspect.d.ts.map +1 -0
- package/dist/cli/commands/query/inspect.js +155 -0
- package/dist/cli/commands/query/inspect.js.map +1 -0
- package/dist/cli/commands/query/translate.d.ts.map +1 -1
- package/dist/cli/commands/query/translate.js +3 -1
- package/dist/cli/commands/query/translate.js.map +1 -1
- package/dist/cli/commands/query-filter.d.ts +13 -0
- package/dist/cli/commands/query-filter.d.ts.map +1 -0
- package/dist/cli/commands/query-filter.js +149 -0
- package/dist/cli/commands/query-filter.js.map +1 -0
- package/dist/cli/commands/results.d.ts +3 -3
- package/dist/cli/commands/results.d.ts.map +1 -1
- package/dist/cli/commands/results.js +12 -3
- package/dist/cli/commands/results.js.map +1 -1
- package/dist/cli/commands/search-executor.d.ts.map +1 -1
- package/dist/cli/commands/search-executor.js +12 -7
- package/dist/cli/commands/search-executor.js.map +1 -1
- package/dist/cli/e2e-helpers.d.ts +5 -2
- package/dist/cli/e2e-helpers.d.ts.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +228 -45
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/providers/arxiv/provider.d.ts +3 -3
- package/dist/providers/arxiv/provider.d.ts.map +1 -1
- package/dist/providers/arxiv/provider.js +3 -3
- package/dist/providers/arxiv/provider.js.map +1 -1
- package/dist/providers/arxiv/translator.d.ts +3 -3
- package/dist/providers/arxiv/translator.d.ts.map +1 -1
- package/dist/providers/arxiv/translator.js +6 -8
- package/dist/providers/arxiv/translator.js.map +1 -1
- package/dist/providers/base/index.d.ts +1 -1
- package/dist/providers/base/index.d.ts.map +1 -1
- package/dist/providers/base/mock-provider.d.ts +2 -2
- package/dist/providers/base/mock-provider.d.ts.map +1 -1
- package/dist/providers/base/mock-provider.js +2 -9
- package/dist/providers/base/mock-provider.js.map +1 -1
- package/dist/providers/base/provider.d.ts +3 -3
- package/dist/providers/base/provider.d.ts.map +1 -1
- package/dist/providers/base/provider.js.map +1 -1
- package/dist/providers/base/types.d.ts +4 -6
- package/dist/providers/base/types.d.ts.map +1 -1
- package/dist/providers/base/types.js.map +1 -1
- package/dist/providers/eric/provider.d.ts +3 -3
- package/dist/providers/eric/provider.d.ts.map +1 -1
- package/dist/providers/eric/provider.js +3 -3
- package/dist/providers/eric/provider.js.map +1 -1
- package/dist/providers/eric/translator.d.ts +5 -5
- package/dist/providers/eric/translator.d.ts.map +1 -1
- package/dist/providers/eric/translator.js +6 -7
- package/dist/providers/eric/translator.js.map +1 -1
- package/dist/providers/pubmed/provider.d.ts +3 -3
- package/dist/providers/pubmed/provider.d.ts.map +1 -1
- package/dist/providers/pubmed/provider.js +3 -3
- package/dist/providers/pubmed/provider.js.map +1 -1
- package/dist/providers/pubmed/translator.d.ts +3 -3
- package/dist/providers/pubmed/translator.d.ts.map +1 -1
- package/dist/providers/pubmed/translator.js +4 -23
- package/dist/providers/pubmed/translator.js.map +1 -1
- package/dist/providers/scopus/provider.d.ts +3 -3
- package/dist/providers/scopus/provider.d.ts.map +1 -1
- package/dist/providers/scopus/provider.js +3 -3
- package/dist/providers/scopus/provider.js.map +1 -1
- package/dist/providers/scopus/translator.d.ts +3 -3
- package/dist/providers/scopus/translator.d.ts.map +1 -1
- package/dist/providers/scopus/translator.js +7 -9
- package/dist/providers/scopus/translator.js.map +1 -1
- package/dist/query/index.d.ts +3 -2
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/json-schema.d.ts.map +1 -1
- package/dist/query/json-schema.js +20 -11
- package/dist/query/json-schema.js.map +1 -1
- package/dist/query/mesh-lookup.d.ts.map +1 -1
- package/dist/query/mesh-lookup.js +66 -3
- package/dist/query/mesh-lookup.js.map +1 -1
- package/dist/query/resolver.d.ts +14 -0
- package/dist/query/resolver.d.ts.map +1 -0
- package/dist/query/resolver.js +61 -0
- package/dist/query/resolver.js.map +1 -0
- package/dist/query/types.d.ts +31 -11
- package/dist/query/types.d.ts.map +1 -1
- package/dist/query/validator.d.ts +659 -348
- package/dist/query/validator.d.ts.map +1 -1
- package/dist/query/validator.js +70 -30
- package/dist/query/validator.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.js","sources":["../../../src/providers/eric/provider.ts"],"sourcesContent":["/**\n * ERIC Provider implementation.\n * Provides search functionality for the ERIC education database.\n */\n\nimport { BaseProvider, type BaseProviderConfig } from '../base/provider';\nimport type {\n Article,\n TranslatedQuery,\n SearchOptions,\n QueryAST,\n SearchState,\n SearchResumeResult,\n ConnectionTestResult,\n} from '../base/types';\nimport { ERICClient, type ERICSearchOptions } from './client';\nimport type { ERICSearchResult } from './parser';\nimport { translateQuery } from './translator';\nimport type { ERICConfig, ERICProviderState } from './types';\n\n/** Default page size for ERIC searches */\nconst DEFAULT_PAGE_SIZE = 100;\n\n/** ERIC API base URL for connection test */\nconst ERIC_API_BASE_URL = 'https://api.ies.ed.gov/eric/';\n\n/**\n * Interface for ERIC client (for dependency injection in tests).\n */\nexport interface IERICClient {\n search(query: string, options?: ERICSearchOptions): Promise<ERICSearchResult>;\n}\n\n/**\n * Extended configuration for ERIC provider with optional client injection.\n */\nexport interface ERICProviderOptions extends ERICConfig {\n /** Optional client for dependency injection (testing) */\n client?: IERICClient;\n}\n\n/**\n * ERIC database provider.\n * Implements the Provider interface for searching ERIC.\n */\nexport class ERICProvider extends BaseProvider {\n readonly name = 'eric' as const;\n\n private readonly client: IERICClient;\n private readonly pageSize: number;\n\n // Current search state for session persistence\n private currentQuery: TranslatedQuery | null = null;\n private currentOffset = 0;\n private currentTotalResults = 0;\n private currentRetrievedCount = 0;\n\n constructor(config: ERICProviderOptions = {}) {\n // Set default rate limit for ERIC (5 req/s recommended)\n const baseConfig: BaseProviderConfig = {\n rateLimit: config.rateLimit ?? 5,\n timeout: config.timeout ?? 30000,\n retries: config.retries ?? 3,\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.pageSize = config.maxResultsPerPage ?? DEFAULT_PAGE_SIZE;\n // Allow client injection for testing\n this.client = config.client ?? new ERICClient({\n timeout: this.config.timeout,\n });\n }\n\n /**\n * Translate a QueryAST to ERIC-native query syntax.\n */\n translateQuery(ast: QueryAST): TranslatedQuery {\n return translateQuery(ast);\n }\n\n /**\n * Get total hit count for a query without downloading results.\n * Uses a minimal search with rows=0 to get only the total count.\n */\n async count(query: TranslatedQuery): Promise<number> {\n await this.rateLimiter.acquire();\n const result = await this.withRetry(() =>\n this.client.search(query.native, { start: 0, rows: 0 })\n );\n return result.totalResults;\n }\n\n /**\n * Execute search and return results as async iterable (streaming).\n */\n async *search(\n query: TranslatedQuery,\n options: SearchOptions = {}\n ): AsyncIterable<Article> {\n const maxResults = options.maxResults ?? Number.MAX_SAFE_INTEGER;\n const pageSize = options.pageSize ?? this.pageSize;\n\n // Initialize search state\n this.currentQuery = query;\n this.currentOffset = 0;\n this.currentTotalResults = 0;\n this.currentRetrievedCount = 0;\n\n let retrieved = 0;\n\n while (retrieved < maxResults) {\n // Wait for rate limiter\n await this.rateLimiter.acquire();\n\n // Execute search with retry\n const searchOptions: ERICSearchOptions = {\n start: this.currentOffset,\n rows: Math.min(pageSize, maxResults - retrieved),\n };\n if (options.signal) {\n searchOptions.signal = options.signal;\n }\n const result = await this.withRetry(() =>\n this.client.search(query.native, searchOptions)\n );\n\n // Update total on first page\n if (this.currentOffset === 0) {\n this.currentTotalResults = result.totalResults;\n }\n\n // No more results\n if (result.documents.length === 0) {\n break;\n }\n\n // Yield documents\n for (const doc of result.documents) {\n if (retrieved >= maxResults) {\n break;\n }\n yield doc;\n retrieved++;\n this.currentRetrievedCount = retrieved;\n }\n\n // Update offset for next page\n this.currentOffset += result.documents.length;\n\n // Check if we've retrieved all results\n if (this.currentOffset >= this.currentTotalResults) {\n break;\n }\n }\n }\n\n /**\n * Verify API access.\n * Returns false on failure (doesn't throw).\n */\n async testConnection(): Promise<ConnectionTestResult> {\n try {\n const response = await fetch(`${ERIC_API_BASE_URL}?search=test&format=json&rows=1`);\n if (!response.ok) {\n return { ok: false, error: `ERIC API returned HTTP ${response.status}` };\n }\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 the current search state for session persistence.\n */\n getSearchState(): SearchState | null {\n if (!this.currentQuery) {\n return null;\n }\n\n const providerState: ERICProviderState = {\n offset: this.currentOffset,\n pageSize: this.pageSize,\n };\n\n return {\n ...this.createBaseState(\n this.currentQuery,\n this.currentTotalResults,\n this.currentRetrievedCount\n ),\n providerState,\n };\n }\n\n /**\n * Resume a search from a saved state.\n */\n async *resumeSearch(state: SearchState): AsyncIterable<Article> {\n const providerState = state.providerState as ERICProviderState | undefined;\n if (!providerState) {\n throw new Error('Invalid state: missing providerState');\n }\n\n // Restore state\n this.currentQuery = state.query;\n this.currentOffset = providerState.offset;\n this.currentTotalResults = state.totalResults;\n this.currentRetrievedCount = state.retrievedCount;\n\n const maxResults = this.currentTotalResults - this.currentRetrievedCount;\n const pageSize = providerState.pageSize ?? this.pageSize;\n let retrieved = 0;\n\n // Continue from saved offset\n while (retrieved < maxResults) {\n await this.rateLimiter.acquire();\n\n const searchOptions: ERICSearchOptions = {\n start: this.currentOffset,\n rows: Math.min(pageSize, maxResults - retrieved),\n };\n\n const result = await this.withRetry(() =>\n this.client.search(state.query.native, searchOptions)\n );\n\n if (result.documents.length === 0) {\n break;\n }\n\n for (const doc of result.documents) {\n if (retrieved >= maxResults) {\n break;\n }\n yield doc;\n retrieved++;\n this.currentRetrievedCount++;\n }\n\n this.currentOffset += result.documents.length;\n\n if (this.currentOffset >= this.currentTotalResults) {\n break;\n }\n }\n }\n\n /**\n * Validate if a saved state is still valid for resuming.\n * ERIC uses offset-based pagination, so state is always valid.\n */\n async validateState(_state: SearchState): Promise<SearchResumeResult> {\n // ERIC uses offset-based pagination with no server-side state\n // The state is always valid for resuming\n return { valid: true };\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"provider.js","sources":["../../../src/providers/eric/provider.ts"],"sourcesContent":["/**\n * ERIC Provider implementation.\n * Provides search functionality for the ERIC education database.\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 { ERICClient, type ERICSearchOptions } from './client';\nimport type { ERICSearchResult } from './parser';\nimport { translateQuery } from './translator';\nimport type { ERICConfig, ERICProviderState } from './types';\n\n/** Default page size for ERIC searches */\nconst DEFAULT_PAGE_SIZE = 100;\n\n/** ERIC API base URL for connection test */\nconst ERIC_API_BASE_URL = 'https://api.ies.ed.gov/eric/';\n\n/**\n * Interface for ERIC client (for dependency injection in tests).\n */\nexport interface IERICClient {\n search(query: string, options?: ERICSearchOptions): Promise<ERICSearchResult>;\n}\n\n/**\n * Extended configuration for ERIC provider with optional client injection.\n */\nexport interface ERICProviderOptions extends ERICConfig {\n /** Optional client for dependency injection (testing) */\n client?: IERICClient;\n}\n\n/**\n * ERIC database provider.\n * Implements the Provider interface for searching ERIC.\n */\nexport class ERICProvider extends BaseProvider {\n readonly name = 'eric' as const;\n\n private readonly client: IERICClient;\n private readonly pageSize: number;\n\n // Current search state for session persistence\n private currentQuery: TranslatedQuery | null = null;\n private currentOffset = 0;\n private currentTotalResults = 0;\n private currentRetrievedCount = 0;\n\n constructor(config: ERICProviderOptions = {}) {\n // Set default rate limit for ERIC (5 req/s recommended)\n const baseConfig: BaseProviderConfig = {\n rateLimit: config.rateLimit ?? 5,\n timeout: config.timeout ?? 30000,\n retries: config.retries ?? 3,\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.pageSize = config.maxResultsPerPage ?? DEFAULT_PAGE_SIZE;\n // Allow client injection for testing\n this.client = config.client ?? new ERICClient({\n timeout: this.config.timeout,\n });\n }\n\n /**\n * Translate a ResolvedAST to ERIC-native query 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 rows=0 to get only the total count.\n */\n async count(query: TranslatedQuery): Promise<number> {\n await this.rateLimiter.acquire();\n const result = await this.withRetry(() =>\n this.client.search(query.native, { start: 0, rows: 0 })\n );\n return result.totalResults;\n }\n\n /**\n * Execute search and return results as async iterable (streaming).\n */\n async *search(\n query: TranslatedQuery,\n options: SearchOptions = {}\n ): AsyncIterable<Article> {\n const maxResults = options.maxResults ?? Number.MAX_SAFE_INTEGER;\n const pageSize = options.pageSize ?? this.pageSize;\n\n // Initialize search state\n this.currentQuery = query;\n this.currentOffset = 0;\n this.currentTotalResults = 0;\n this.currentRetrievedCount = 0;\n\n let retrieved = 0;\n\n while (retrieved < maxResults) {\n // Wait for rate limiter\n await this.rateLimiter.acquire();\n\n // Execute search with retry\n const searchOptions: ERICSearchOptions = {\n start: this.currentOffset,\n rows: Math.min(pageSize, maxResults - retrieved),\n };\n if (options.signal) {\n searchOptions.signal = options.signal;\n }\n const result = await this.withRetry(() =>\n this.client.search(query.native, searchOptions)\n );\n\n // Update total on first page\n if (this.currentOffset === 0) {\n this.currentTotalResults = result.totalResults;\n }\n\n // No more results\n if (result.documents.length === 0) {\n break;\n }\n\n // Yield documents\n for (const doc of result.documents) {\n if (retrieved >= maxResults) {\n break;\n }\n yield doc;\n retrieved++;\n this.currentRetrievedCount = retrieved;\n }\n\n // Update offset for next page\n this.currentOffset += result.documents.length;\n\n // Check if we've retrieved all results\n if (this.currentOffset >= this.currentTotalResults) {\n break;\n }\n }\n }\n\n /**\n * Verify API access.\n * Returns false on failure (doesn't throw).\n */\n async testConnection(): Promise<ConnectionTestResult> {\n try {\n const response = await fetch(`${ERIC_API_BASE_URL}?search=test&format=json&rows=1`);\n if (!response.ok) {\n return { ok: false, error: `ERIC API returned HTTP ${response.status}` };\n }\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 the current search state for session persistence.\n */\n getSearchState(): SearchState | null {\n if (!this.currentQuery) {\n return null;\n }\n\n const providerState: ERICProviderState = {\n offset: this.currentOffset,\n pageSize: this.pageSize,\n };\n\n return {\n ...this.createBaseState(\n this.currentQuery,\n this.currentTotalResults,\n this.currentRetrievedCount\n ),\n providerState,\n };\n }\n\n /**\n * Resume a search from a saved state.\n */\n async *resumeSearch(state: SearchState): AsyncIterable<Article> {\n const providerState = state.providerState as ERICProviderState | undefined;\n if (!providerState) {\n throw new Error('Invalid state: missing providerState');\n }\n\n // Restore state\n this.currentQuery = state.query;\n this.currentOffset = providerState.offset;\n this.currentTotalResults = state.totalResults;\n this.currentRetrievedCount = state.retrievedCount;\n\n const maxResults = this.currentTotalResults - this.currentRetrievedCount;\n const pageSize = providerState.pageSize ?? this.pageSize;\n let retrieved = 0;\n\n // Continue from saved offset\n while (retrieved < maxResults) {\n await this.rateLimiter.acquire();\n\n const searchOptions: ERICSearchOptions = {\n start: this.currentOffset,\n rows: Math.min(pageSize, maxResults - retrieved),\n };\n\n const result = await this.withRetry(() =>\n this.client.search(state.query.native, searchOptions)\n );\n\n if (result.documents.length === 0) {\n break;\n }\n\n for (const doc of result.documents) {\n if (retrieved >= maxResults) {\n break;\n }\n yield doc;\n retrieved++;\n this.currentRetrievedCount++;\n }\n\n this.currentOffset += result.documents.length;\n\n if (this.currentOffset >= this.currentTotalResults) {\n break;\n }\n }\n }\n\n /**\n * Validate if a saved state is still valid for resuming.\n * ERIC uses offset-based pagination, so state is always valid.\n */\n async validateState(_state: SearchState): Promise<SearchResumeResult> {\n // ERIC uses offset-based pagination with no server-side state\n // The state is always valid for resuming\n return { valid: true };\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ResolvedAST } from '../../query/types';
|
|
2
2
|
import { TranslatedQuery } from '../base/types';
|
|
3
3
|
/**
|
|
4
|
-
* Translate a
|
|
4
|
+
* Translate a ResolvedAST to ERIC-native query syntax.
|
|
5
5
|
*/
|
|
6
|
-
export declare function translateQueryAST(
|
|
6
|
+
export declare function translateQueryAST(resolved: ResolvedAST): TranslatedQuery;
|
|
7
7
|
/**
|
|
8
|
-
* Translate a
|
|
8
|
+
* Translate a ResolvedAST to ERIC query.
|
|
9
9
|
* This is the Provider interface method signature.
|
|
10
10
|
*/
|
|
11
|
-
export declare function translateQuery(
|
|
11
|
+
export declare function translateQuery(resolved: ResolvedAST): TranslatedQuery;
|
|
12
12
|
//# sourceMappingURL=translator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translator.d.ts","sourceRoot":"","sources":["../../../src/providers/eric/translator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"translator.d.ts","sourceRoot":"","sources":["../../../src/providers/eric/translator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAuB,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC1E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AA8IrD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,WAAW,GAAG,eAAe,CA8CxE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,WAAW,GAAG,eAAe,CAErE"}
|
|
@@ -78,10 +78,10 @@ function translateDateFilters(filters) {
|
|
|
78
78
|
const to = yearTo !== void 0 ? yearTo.toString() : "*";
|
|
79
79
|
return `publicationdateyear:[${from} TO ${to}]`;
|
|
80
80
|
}
|
|
81
|
-
function translateQueryAST(
|
|
81
|
+
function translateQueryAST(resolved) {
|
|
82
82
|
const blockQueries = [];
|
|
83
83
|
const notClauses = [];
|
|
84
|
-
for (const block of
|
|
84
|
+
for (const block of resolved.blocks) {
|
|
85
85
|
const { query, notClause } = translateBlock(block);
|
|
86
86
|
if (query) {
|
|
87
87
|
blockQueries.push(query);
|
|
@@ -98,7 +98,7 @@ function translateQueryAST(ast) {
|
|
|
98
98
|
native = notClause;
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
-
const dateFilter = translateDateFilters(
|
|
101
|
+
const dateFilter = translateDateFilters(resolved.filters);
|
|
102
102
|
if (dateFilter) {
|
|
103
103
|
if (native) {
|
|
104
104
|
native = `${native} AND ${dateFilter}`;
|
|
@@ -106,16 +106,15 @@ function translateQueryAST(ast) {
|
|
|
106
106
|
native = dateFilter;
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
-
const warnings = collectUnsupportedVocabWarnings(
|
|
109
|
+
const warnings = collectUnsupportedVocabWarnings(resolved.blocks, "ERIC", /* @__PURE__ */ new Set(["eric"]));
|
|
110
110
|
return {
|
|
111
111
|
native,
|
|
112
|
-
originalAst: ast,
|
|
113
112
|
provider: "eric",
|
|
114
113
|
...warnings.length > 0 ? { warnings } : {}
|
|
115
114
|
};
|
|
116
115
|
}
|
|
117
|
-
function translateQuery(
|
|
118
|
-
return translateQueryAST(
|
|
116
|
+
function translateQuery(resolved) {
|
|
117
|
+
return translateQueryAST(resolved);
|
|
119
118
|
}
|
|
120
119
|
export {
|
|
121
120
|
translateQuery,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translator.js","sources":["../../../src/providers/eric/translator.ts"],"sourcesContent":["/**\n * ERIC query translator.\n * Converts
|
|
1
|
+
{"version":3,"file":"translator.js","sources":["../../../src/providers/eric/translator.ts"],"sourcesContent":["/**\n * ERIC query translator.\n * Converts ResolvedAST to ERIC-native query syntax.\n */\n\nimport type { QueryBlock, Filters, ResolvedAST } from '../../query/types';\nimport type { TranslatedQuery } from '../base/types';\nimport { collectUnsupportedVocabWarnings } from '../base/warnings';\n\n/**\n * Field prefix mappings for ERIC.\n * Maps DSL field types to ERIC query syntax prefixes.\n */\nconst FIELD_PREFIXES: Record<string, string> = {\n title: 'title:',\n abstract: 'description:', // ERIC uses 'description' field for abstracts\n author: 'author:',\n keyword: 'subject:', // ERIC uses subject for descriptors\n all: '', // No prefix for all-field search\n};\n\n/**\n * Check if a term needs quoting (contains spaces or special characters).\n */\nfunction needsQuotes(term: string): boolean {\n return /\\s|[()[\\]{}:\"]/.test(term);\n}\n\n/**\n * Format a term with optional field prefix.\n * Quotes multi-word phrases automatically.\n */\nfunction formatTerm(term: string, prefix: string): string {\n const quoted = needsQuotes(term) ? `\"${term}\"` : term;\n return `${prefix}${quoted}`;\n}\n\n/**\n * Translate a single term for title_abstract field.\n * Expands to (title:term OR description:term).\n * Note: ERIC uses 'description' field for abstracts.\n */\nfunction translateTitleAbstractTerm(term: string): string {\n const quoted = needsQuotes(term) ? `\"${term}\"` : term;\n return `(title:${quoted} OR description:${quoted})`;\n}\n\n/**\n * Translate exclude terms to NOT clause.\n */\nfunction translateExcludeTerms(\n exclude: string[],\n field: QueryBlock['field']\n): string | null {\n if (exclude.length === 0) {\n return null;\n }\n\n // Handle title_abstract expansion\n if (field === 'title_abstract') {\n const expandedTerms = exclude.map(translateTitleAbstractTerm);\n if (expandedTerms.length === 1) {\n return `NOT ${expandedTerms[0]}`;\n }\n return `NOT (${expandedTerms.join(' OR ')})`;\n }\n\n // Standard field translation\n const prefix = FIELD_PREFIXES[field] ?? '';\n const translatedTerms = exclude.map((term) => formatTerm(term, prefix));\n\n if (translatedTerms.length === 1) {\n return `NOT ${translatedTerms[0]}`;\n }\n return `NOT (${translatedTerms.join(' OR ')})`;\n}\n\n/**\n * Translate ERIC Descriptors to subject: field syntax.\n */\nfunction translateEricDescriptors(descriptors: string[]): string[] {\n return descriptors.map((term) => formatTerm(term, 'subject:'));\n}\n\n/**\n * Translate a single query block to ERIC syntax.\n * Returns an object with the main query part and optional NOT clause.\n */\nfunction translateBlock(block: QueryBlock): { query: string; notClause: string | null } {\n const { field, terms, operator } = block;\n const keywords = terms.keywords ?? [];\n const eric = terms.eric ?? [];\n\n const allTerms: string[] = [];\n\n // Translate keywords\n if (keywords.length > 0) {\n // Handle title_abstract special case\n if (field === 'title_abstract') {\n const expandedTerms = keywords.map(translateTitleAbstractTerm);\n allTerms.push(...expandedTerms);\n } else {\n // Standard field translation\n const prefix = FIELD_PREFIXES[field] ?? '';\n const translatedTerms = keywords.map((term) => formatTerm(term, prefix));\n allTerms.push(...translatedTerms);\n }\n }\n\n // Translate ERIC Descriptors (always use subject: field)\n if (eric.length > 0) {\n const ericTerms = translateEricDescriptors(eric);\n allTerms.push(...ericTerms);\n }\n\n // Combine all terms\n let query = '';\n if (allTerms.length === 1) {\n query = allTerms[0]!;\n } else if (allTerms.length > 1) {\n query = `(${allTerms.join(` ${operator} `)})`;\n }\n\n // Translate exclude terms\n const notClause = terms.exclude\n ? translateExcludeTerms(terms.exclude, field)\n : null;\n\n return { query, notClause };\n}\n\n/**\n * Translate date filters to ERIC syntax.\n * ERIC uses publicationdateyear:[YYYY TO YYYY] format.\n */\nfunction translateDateFilters(filters: Filters): string | null {\n const { yearFrom, yearTo } = filters;\n\n if (yearFrom === undefined && yearTo === undefined) {\n return null;\n }\n\n const from = yearFrom !== undefined ? yearFrom.toString() : '*';\n const to = yearTo !== undefined ? yearTo.toString() : '*';\n\n return `publicationdateyear:[${from} TO ${to}]`;\n}\n\n/**\n * Translate a ResolvedAST to ERIC-native query syntax.\n */\nexport function translateQueryAST(resolved: ResolvedAST): TranslatedQuery {\n const blockQueries: string[] = [];\n const notClauses: string[] = [];\n\n // Translate each block\n for (const block of resolved.blocks) {\n const { query, notClause } = translateBlock(block);\n if (query) {\n blockQueries.push(query);\n }\n if (notClause) {\n notClauses.push(notClause);\n }\n }\n\n // Combine blocks with AND\n let native = blockQueries.join(' AND ');\n\n // Append NOT clauses\n for (const notClause of notClauses) {\n if (native) {\n native = `${native} ${notClause}`;\n } else {\n native = notClause;\n }\n }\n\n // Apply date filters\n const dateFilter = translateDateFilters(resolved.filters);\n if (dateFilter) {\n if (native) {\n native = `${native} AND ${dateFilter}`;\n } else {\n native = dateFilter;\n }\n }\n\n // Collect warnings for unsupported controlled vocabulary\n // ERIC supports eric descriptors but not mesh or emtree\n const warnings = collectUnsupportedVocabWarnings(resolved.blocks, 'ERIC', new Set(['eric']));\n\n return {\n native,\n provider: 'eric',\n ...(warnings.length > 0 ? { warnings } : {}),\n };\n}\n\n/**\n * Translate a ResolvedAST to ERIC query.\n * This is the Provider interface method signature.\n */\nexport function translateQuery(resolved: ResolvedAST): TranslatedQuery {\n return translateQueryAST(resolved);\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseProvider } from '../base/provider.js';
|
|
2
|
-
import { Article,
|
|
2
|
+
import { Article, ResolvedAST, SearchOptions, SearchState, TranslatedQuery, SearchResumeResult, ConnectionTestResult } from '../base/types.js';
|
|
3
3
|
import { PubMedConfig } from './types.js';
|
|
4
4
|
/**
|
|
5
5
|
* PubMed provider for searching biomedical literature.
|
|
@@ -28,9 +28,9 @@ export declare class PubMedProvider extends BaseProvider {
|
|
|
28
28
|
*/
|
|
29
29
|
count(query: TranslatedQuery): Promise<number>;
|
|
30
30
|
/**
|
|
31
|
-
* Convert
|
|
31
|
+
* Convert ResolvedAST to PubMed native syntax.
|
|
32
32
|
*/
|
|
33
|
-
translateQuery(
|
|
33
|
+
translateQuery(resolved: ResolvedAST): TranslatedQuery;
|
|
34
34
|
/**
|
|
35
35
|
* Test connection to PubMed API.
|
|
36
36
|
*/
|
|
@@ -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,
|
|
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"}
|
|
@@ -114,10 +114,10 @@ class PubMedProvider extends BaseProvider {
|
|
|
114
114
|
return this.withRetry(() => this.client.searchCount(query.native));
|
|
115
115
|
}
|
|
116
116
|
/**
|
|
117
|
-
* Convert
|
|
117
|
+
* Convert ResolvedAST to PubMed native syntax.
|
|
118
118
|
*/
|
|
119
|
-
translateQuery(
|
|
120
|
-
return translateQuery(
|
|
119
|
+
translateQuery(resolved) {
|
|
120
|
+
return translateQuery(resolved);
|
|
121
121
|
}
|
|
122
122
|
/**
|
|
123
123
|
* Test connection to PubMed API.
|
|
@@ -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 QueryAST,\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 QueryAST to PubMed native syntax.\n */\n translateQuery(ast: QueryAST): TranslatedQuery {\n return translateQuery(ast);\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 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,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ResolvedAST } from '../../query/types';
|
|
2
2
|
import { TranslatedQuery } from '../base/types';
|
|
3
3
|
/**
|
|
4
|
-
* Translate a
|
|
4
|
+
* Translate a ResolvedAST to PubMed search syntax.
|
|
5
5
|
*/
|
|
6
|
-
export declare function translateQuery(
|
|
6
|
+
export declare function translateQuery(resolved: ResolvedAST): TranslatedQuery;
|
|
7
7
|
//# sourceMappingURL=translator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translator.d.ts","sourceRoot":"","sources":["../../../src/providers/pubmed/translator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"translator.d.ts","sourceRoot":"","sources":["../../../src/providers/pubmed/translator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAkC,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AA+KrD;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,WAAW,GAAG,eAAe,CAoErE"}
|
|
@@ -108,27 +108,9 @@ function translatePublicationTypeFilters(pubTypes) {
|
|
|
108
108
|
}
|
|
109
109
|
return filters;
|
|
110
110
|
}
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
return {
|
|
116
|
-
yearFrom: overrides.yearFrom ?? global.yearFrom,
|
|
117
|
-
yearTo: overrides.yearTo ?? global.yearTo,
|
|
118
|
-
languages: overrides.languages ?? global.languages,
|
|
119
|
-
publicationTypes: overrides.publicationTypes ? {
|
|
120
|
-
include: overrides.publicationTypes.include ?? global.publicationTypes?.include,
|
|
121
|
-
exclude: [
|
|
122
|
-
...global.publicationTypes?.exclude ?? [],
|
|
123
|
-
...overrides.publicationTypes.exclude ?? []
|
|
124
|
-
]
|
|
125
|
-
} : global.publicationTypes
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
function translateQuery(ast) {
|
|
129
|
-
const pubmedOverride = ast.overrides.pubmed;
|
|
130
|
-
const filters = mergeFilters(ast.filters, pubmedOverride?.filters);
|
|
131
|
-
const blockResults = ast.blocks.map((block) => translateBlock(block));
|
|
111
|
+
function translateQuery(resolved) {
|
|
112
|
+
const { filters } = resolved;
|
|
113
|
+
const blockResults = resolved.blocks.map((block) => translateBlock(block));
|
|
132
114
|
const blockStrings = blockResults.map((r) => r.query).filter((s) => s.length > 0);
|
|
133
115
|
const blockNotClauses = blockResults.map((r) => r.notClause).filter((s) => s !== null);
|
|
134
116
|
const parts = [];
|
|
@@ -160,10 +142,9 @@ function translateQuery(ast) {
|
|
|
160
142
|
} else {
|
|
161
143
|
native = andSection;
|
|
162
144
|
}
|
|
163
|
-
const warnings = collectUnsupportedVocabWarnings(
|
|
145
|
+
const warnings = collectUnsupportedVocabWarnings(resolved.blocks, "PubMed", /* @__PURE__ */ new Set(["mesh"]));
|
|
164
146
|
return {
|
|
165
147
|
native,
|
|
166
|
-
originalAst: ast,
|
|
167
148
|
provider: "pubmed",
|
|
168
149
|
...warnings.length > 0 ? { warnings } : {}
|
|
169
150
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translator.js","sources":["../../../src/providers/pubmed/translator.ts"],"sourcesContent":["/**\n * PubMed query translator.\n * Converts QueryAST to PubMed E-utilities search syntax.\n */\n\nimport type { QueryAST, FieldType, QueryBlock, Filters } from '../../query/types';\nimport type { TranslatedQuery } from '../base/types';\nimport { collectUnsupportedVocabWarnings } from '../base/warnings';\n\n/**\n * Field type to PubMed qualifier mapping.\n */\nconst FIELD_QUALIFIERS: Record<FieldType, string> = {\n title: 'ti',\n abstract: 'ab',\n title_abstract: 'tiab',\n author: 'au',\n keyword: 'mh',\n all: 'all',\n};\n\n/**\n * Language code to PubMed language name mapping.\n */\nconst LANGUAGE_NAMES: Record<string, string> = {\n en: 'english',\n ja: 'japanese',\n de: 'german',\n fr: 'french',\n es: 'spanish',\n it: 'italian',\n pt: 'portuguese',\n zh: 'chinese',\n ko: 'korean',\n ru: 'russian',\n};\n\n/**\n * Quote a term if it contains spaces and is not already quoted.\n */\nfunction quoteTerm(term: string): string {\n // Already quoted\n if (term.startsWith('\"') && term.endsWith('\"')) {\n return term;\n }\n // Contains spaces - needs quoting\n if (term.includes(' ')) {\n return `\"${term}\"`;\n }\n return term;\n}\n\n/**\n * Translate a single term with field qualifier.\n */\nfunction translateTerm(term: string, qualifier: string): string {\n const quoted = quoteTerm(term);\n return `${quoted}[${qualifier}]`;\n}\n\n/**\n * Translate exclude terms to NOT clause.\n */\nfunction translateExcludeTerms(exclude: string[], qualifier: string): string | null {\n if (exclude.length === 0) {\n return null;\n }\n\n const excludeTerms = exclude.map((term) => translateTerm(term, qualifier));\n\n if (excludeTerms.length === 1) {\n return `NOT ${excludeTerms[0]}`;\n }\n return `NOT (${excludeTerms.join(' OR ')})`;\n}\n\n/**\n * Translate a query block to PubMed syntax.\n * Returns an object with the main query part and optional NOT clause.\n */\nfunction translateBlock(block: QueryBlock): { query: string; notClause: string | null } {\n const qualifier = FIELD_QUALIFIERS[block.field];\n const terms: string[] = [];\n\n // Translate keywords\n for (const keyword of block.terms.keywords ?? []) {\n terms.push(translateTerm(keyword, qualifier));\n }\n\n // Translate MeSH terms (always use [mh] regardless of field)\n if (block.terms.mesh) {\n for (const meshTerm of block.terms.mesh) {\n terms.push(translateTerm(meshTerm, 'mh'));\n }\n }\n\n // Build query part\n let query = '';\n if (terms.length === 1) {\n query = `(${terms[0]})`;\n } else if (terms.length > 1) {\n query = `(${terms.join(` ${block.operator} `)})`;\n }\n\n // Translate exclude terms\n const notClause = block.terms.exclude\n ? translateExcludeTerms(block.terms.exclude, qualifier)\n : null;\n\n return { query, notClause };\n}\n\n/**\n * Translate date filters to PubMed syntax.\n */\nfunction translateDateFilters(filters: Filters): string | null {\n const yearFrom = filters.yearFrom ?? 1900;\n const yearTo = filters.yearTo ?? 3000;\n\n if (filters.yearFrom !== undefined || filters.yearTo !== undefined) {\n return `${yearFrom}:${yearTo}[dp]`;\n }\n return null;\n}\n\n/**\n * Translate language filters to PubMed syntax.\n */\nfunction translateLanguageFilters(languages: string[]): string | null {\n if (languages.length === 0) {\n return null;\n }\n\n const langTerms = languages.map((code) => {\n const langName = LANGUAGE_NAMES[code] ?? code;\n return `${langName}[la]`;\n });\n\n if (langTerms.length === 1) {\n return langTerms[0]!;\n }\n return `(${langTerms.join(' OR ')})`;\n}\n\n/**\n * Translate publication type filters to PubMed syntax.\n */\nfunction translatePublicationTypeFilters(\n pubTypes: Filters['publicationTypes']\n): string[] {\n const filters: string[] = [];\n\n if (!pubTypes) {\n return filters;\n }\n\n // Include filters\n if (pubTypes.include && pubTypes.include.length > 0) {\n const includeTerms = pubTypes.include.map(\n (pt) => `\"${pt.toLowerCase()}\"[pt]`\n );\n if (includeTerms.length === 1) {\n filters.push(includeTerms[0]!);\n } else {\n filters.push(`(${includeTerms.join(' OR ')})`);\n }\n }\n\n // Exclude filters - single grouped NOT clause\n if (pubTypes.exclude && pubTypes.exclude.length > 0) {\n const excludeTerms = pubTypes.exclude.map((pt) => `${pt.toLowerCase()}[pt]`);\n if (excludeTerms.length === 1) {\n filters.push(`NOT ${excludeTerms[0]}`);\n } else {\n filters.push(`NOT (${excludeTerms.join(\" OR \")})`);\n }\n }\n\n return filters;\n}\n\n/**\n * Merge global filters with provider-specific overrides.\n */\nfunction mergeFilters(global: Filters, overrides?: Filters): Filters {\n if (!overrides) {\n return global;\n }\n\n return {\n yearFrom: overrides.yearFrom ?? global.yearFrom,\n yearTo: overrides.yearTo ?? global.yearTo,\n languages: overrides.languages ?? global.languages,\n publicationTypes: overrides.publicationTypes\n ? {\n include:\n overrides.publicationTypes.include ??\n global.publicationTypes?.include,\n exclude: [\n ...(global.publicationTypes?.exclude ?? []),\n ...(overrides.publicationTypes.exclude ?? []),\n ],\n }\n : global.publicationTypes,\n };\n}\n\n/**\n * Translate a QueryAST to PubMed search syntax.\n */\nexport function translateQuery(ast: QueryAST): TranslatedQuery {\n // Merge filters with PubMed-specific overrides\n const pubmedOverride = ast.overrides.pubmed;\n const filters = mergeFilters(ast.filters, pubmedOverride?.filters);\n\n // Translate query blocks\n const blockResults = ast.blocks.map((block) => translateBlock(block));\n\n // Collect query parts and NOT clauses\n const blockStrings = blockResults\n .map((r) => r.query)\n .filter((s) => s.length > 0);\n const blockNotClauses = blockResults\n .map((r) => r.notClause)\n .filter((s): s is string => s !== null);\n\n // Build the main query\n const parts: string[] = [];\n\n // Add query blocks (AND'd together)\n if (blockStrings.length > 0) {\n parts.push(blockStrings.join(' AND '));\n }\n\n // Add date filter\n const dateFilter = translateDateFilters(filters);\n if (dateFilter) {\n parts.push(dateFilter);\n }\n\n // Add language filter\n if (filters.languages && filters.languages.length > 0) {\n const langFilter = translateLanguageFilters(filters.languages);\n if (langFilter) {\n parts.push(langFilter);\n }\n }\n\n // Add publication type filters\n const pubTypeFilters = translatePublicationTypeFilters(filters.publicationTypes);\n parts.push(...pubTypeFilters);\n\n // Add block-level NOT clauses (from exclude terms)\n parts.push(...blockNotClauses);\n\n // Separate NOT clauses from AND-joined parts\n // PubMed treats NOT as a standalone binary operator, not AND NOT\n const notParts = parts.filter((p) => p.startsWith('NOT '));\n const andParts = parts.filter((p) => !p.startsWith('NOT '));\n\n const andSection = andParts.join(' AND ');\n const notSection = notParts.join(' ');\n let native: string;\n if (andSection && notSection) {\n native = andSection + ' ' + notSection;\n } else if (notSection) {\n native = notSection;\n } else {\n native = andSection;\n }\n\n // Collect warnings for unsupported controlled vocabulary\n // PubMed supports mesh but not emtree or eric\n const warnings = collectUnsupportedVocabWarnings(ast.blocks, 'PubMed', new Set(['mesh']));\n\n return {\n native,\n originalAst: ast,\n provider: 'pubmed',\n ...(warnings.length > 0 ? { warnings } : {}),\n };\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"translator.js","sources":["../../../src/providers/pubmed/translator.ts"],"sourcesContent":["/**\n * PubMed query translator.\n * Converts ResolvedAST to PubMed E-utilities search syntax.\n */\n\nimport type { FieldType, QueryBlock, Filters, ResolvedAST } from '../../query/types';\nimport type { TranslatedQuery } from '../base/types';\nimport { collectUnsupportedVocabWarnings } from '../base/warnings';\n\n/**\n * Field type to PubMed qualifier mapping.\n */\nconst FIELD_QUALIFIERS: Record<FieldType, string> = {\n title: 'ti',\n abstract: 'ab',\n title_abstract: 'tiab',\n author: 'au',\n keyword: 'mh',\n all: 'all',\n};\n\n/**\n * Language code to PubMed language name mapping.\n */\nconst LANGUAGE_NAMES: Record<string, string> = {\n en: 'english',\n ja: 'japanese',\n de: 'german',\n fr: 'french',\n es: 'spanish',\n it: 'italian',\n pt: 'portuguese',\n zh: 'chinese',\n ko: 'korean',\n ru: 'russian',\n};\n\n/**\n * Quote a term if it contains spaces and is not already quoted.\n */\nfunction quoteTerm(term: string): string {\n // Already quoted\n if (term.startsWith('\"') && term.endsWith('\"')) {\n return term;\n }\n // Contains spaces - needs quoting\n if (term.includes(' ')) {\n return `\"${term}\"`;\n }\n return term;\n}\n\n/**\n * Translate a single term with field qualifier.\n */\nfunction translateTerm(term: string, qualifier: string): string {\n const quoted = quoteTerm(term);\n return `${quoted}[${qualifier}]`;\n}\n\n/**\n * Translate exclude terms to NOT clause.\n */\nfunction translateExcludeTerms(exclude: string[], qualifier: string): string | null {\n if (exclude.length === 0) {\n return null;\n }\n\n const excludeTerms = exclude.map((term) => translateTerm(term, qualifier));\n\n if (excludeTerms.length === 1) {\n return `NOT ${excludeTerms[0]}`;\n }\n return `NOT (${excludeTerms.join(' OR ')})`;\n}\n\n/**\n * Translate a query block to PubMed syntax.\n * Returns an object with the main query part and optional NOT clause.\n */\nfunction translateBlock(block: QueryBlock): { query: string; notClause: string | null } {\n const qualifier = FIELD_QUALIFIERS[block.field];\n const terms: string[] = [];\n\n // Translate keywords\n for (const keyword of block.terms.keywords ?? []) {\n terms.push(translateTerm(keyword, qualifier));\n }\n\n // Translate MeSH terms (always use [mh] regardless of field)\n if (block.terms.mesh) {\n for (const meshTerm of block.terms.mesh) {\n terms.push(translateTerm(meshTerm, 'mh'));\n }\n }\n\n // Build query part\n let query = '';\n if (terms.length === 1) {\n query = `(${terms[0]})`;\n } else if (terms.length > 1) {\n query = `(${terms.join(` ${block.operator} `)})`;\n }\n\n // Translate exclude terms\n const notClause = block.terms.exclude\n ? translateExcludeTerms(block.terms.exclude, qualifier)\n : null;\n\n return { query, notClause };\n}\n\n/**\n * Translate date filters to PubMed syntax.\n */\nfunction translateDateFilters(filters: Filters): string | null {\n const yearFrom = filters.yearFrom ?? 1900;\n const yearTo = filters.yearTo ?? 3000;\n\n if (filters.yearFrom !== undefined || filters.yearTo !== undefined) {\n return `${yearFrom}:${yearTo}[dp]`;\n }\n return null;\n}\n\n/**\n * Translate language filters to PubMed syntax.\n */\nfunction translateLanguageFilters(languages: string[]): string | null {\n if (languages.length === 0) {\n return null;\n }\n\n const langTerms = languages.map((code) => {\n const langName = LANGUAGE_NAMES[code] ?? code;\n return `${langName}[la]`;\n });\n\n if (langTerms.length === 1) {\n return langTerms[0]!;\n }\n return `(${langTerms.join(' OR ')})`;\n}\n\n/**\n * Translate publication type filters to PubMed syntax.\n */\nfunction translatePublicationTypeFilters(\n pubTypes: Filters['publicationTypes']\n): string[] {\n const filters: string[] = [];\n\n if (!pubTypes) {\n return filters;\n }\n\n // Include filters\n if (pubTypes.include && pubTypes.include.length > 0) {\n const includeTerms = pubTypes.include.map(\n (pt) => `\"${pt.toLowerCase()}\"[pt]`\n );\n if (includeTerms.length === 1) {\n filters.push(includeTerms[0]!);\n } else {\n filters.push(`(${includeTerms.join(' OR ')})`);\n }\n }\n\n // Exclude filters - single grouped NOT clause\n if (pubTypes.exclude && pubTypes.exclude.length > 0) {\n const excludeTerms = pubTypes.exclude.map((pt) => `${pt.toLowerCase()}[pt]`);\n if (excludeTerms.length === 1) {\n filters.push(`NOT ${excludeTerms[0]}`);\n } else {\n filters.push(`NOT (${excludeTerms.join(\" OR \")})`);\n }\n }\n\n return filters;\n}\n\n/**\n * Translate a ResolvedAST to PubMed search syntax.\n */\nexport function translateQuery(resolved: ResolvedAST): TranslatedQuery {\n const { filters } = resolved;\n\n // Translate query blocks\n const blockResults = resolved.blocks.map((block) => translateBlock(block));\n\n // Collect query parts and NOT clauses\n const blockStrings = blockResults\n .map((r) => r.query)\n .filter((s) => s.length > 0);\n const blockNotClauses = blockResults\n .map((r) => r.notClause)\n .filter((s): s is string => s !== null);\n\n // Build the main query\n const parts: string[] = [];\n\n // Add query blocks (AND'd together)\n if (blockStrings.length > 0) {\n parts.push(blockStrings.join(' AND '));\n }\n\n // Add date filter\n const dateFilter = translateDateFilters(filters);\n if (dateFilter) {\n parts.push(dateFilter);\n }\n\n // Add language filter\n if (filters.languages && filters.languages.length > 0) {\n const langFilter = translateLanguageFilters(filters.languages);\n if (langFilter) {\n parts.push(langFilter);\n }\n }\n\n // Add publication type filters\n const pubTypeFilters = translatePublicationTypeFilters(filters.publicationTypes);\n parts.push(...pubTypeFilters);\n\n // Add block-level NOT clauses (from exclude terms)\n parts.push(...blockNotClauses);\n\n // Separate NOT clauses from AND-joined parts\n // PubMed treats NOT as a standalone binary operator, not AND NOT\n const notParts = parts.filter((p) => p.startsWith('NOT '));\n const andParts = parts.filter((p) => !p.startsWith('NOT '));\n\n const andSection = andParts.join(' AND ');\n const notSection = notParts.join(' ');\n let native: string;\n if (andSection && notSection) {\n native = andSection + ' ' + notSection;\n } else if (notSection) {\n native = notSection;\n } else {\n native = andSection;\n }\n\n // Collect warnings for unsupported controlled vocabulary\n // PubMed supports mesh but not emtree or eric\n const warnings = collectUnsupportedVocabWarnings(resolved.blocks, 'PubMed', new Set(['mesh']));\n\n return {\n native,\n provider: 'pubmed',\n ...(warnings.length > 0 ? { warnings } : {}),\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseProvider } from '../base/provider';
|
|
2
|
-
import { Article, TranslatedQuery, SearchOptions,
|
|
2
|
+
import { Article, TranslatedQuery, SearchOptions, ResolvedAST, SearchState, SearchResumeResult, ConnectionTestResult } from '../base/types';
|
|
3
3
|
import { ScopusConfig } from './types';
|
|
4
4
|
/**
|
|
5
5
|
* Scopus database provider.
|
|
@@ -12,9 +12,9 @@ export declare class ScopusProvider extends BaseProvider {
|
|
|
12
12
|
private currentState;
|
|
13
13
|
constructor(config: ScopusConfig);
|
|
14
14
|
/**
|
|
15
|
-
* Translate
|
|
15
|
+
* Translate ResolvedAST to Scopus search syntax.
|
|
16
16
|
*/
|
|
17
|
-
translateQuery(
|
|
17
|
+
translateQuery(resolved: ResolvedAST): TranslatedQuery;
|
|
18
18
|
/**
|
|
19
19
|
* Get total hit count for a query without downloading results.
|
|
20
20
|
* Uses a minimal search with count=1 to get the total from response metadata.
|
|
@@ -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,
|
|
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"}
|
|
@@ -31,10 +31,10 @@ class ScopusProvider extends BaseProvider {
|
|
|
31
31
|
this.client = new ScopusClient(config);
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
* Translate
|
|
34
|
+
* Translate ResolvedAST to Scopus search syntax.
|
|
35
35
|
*/
|
|
36
|
-
translateQuery(
|
|
37
|
-
return translateQuery(
|
|
36
|
+
translateQuery(resolved) {
|
|
37
|
+
return translateQuery(resolved);
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
40
|
* Get total hit count for a query without downloading results.
|
|
@@ -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 QueryAST,\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 QueryAST to Scopus search syntax.\n */\n translateQuery(ast: QueryAST): TranslatedQuery {\n return translateQuery(ast);\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 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,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ResolvedAST } from '../../query/types';
|
|
2
2
|
import { TranslatedQuery } from '../base/types';
|
|
3
3
|
/**
|
|
4
|
-
* Translate a
|
|
4
|
+
* Translate a ResolvedAST to Scopus search syntax.
|
|
5
5
|
*/
|
|
6
|
-
export declare function translateQuery(
|
|
6
|
+
export declare function translateQuery(resolved: ResolvedAST): TranslatedQuery;
|
|
7
7
|
//# sourceMappingURL=translator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translator.d.ts","sourceRoot":"","sources":["../../../src/providers/scopus/translator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"translator.d.ts","sourceRoot":"","sources":["../../../src/providers/scopus/translator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAkC,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AA6IrD;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,WAAW,GAAG,eAAe,CA4BrE"}
|
|
@@ -69,7 +69,7 @@ function translateBlock(block) {
|
|
|
69
69
|
}
|
|
70
70
|
return { query, notClause };
|
|
71
71
|
}
|
|
72
|
-
function translateFilters(filters
|
|
72
|
+
function translateFilters(filters) {
|
|
73
73
|
const parts = [];
|
|
74
74
|
if (filters.yearFrom !== void 0) {
|
|
75
75
|
parts.push(`PUBYEAR > ${filters.yearFrom - 1}`);
|
|
@@ -81,24 +81,22 @@ function translateFilters(filters, scopusOverrides) {
|
|
|
81
81
|
const languages = filters.languages.map((code) => LANGUAGE_MAP[code] || code).join(" OR ");
|
|
82
82
|
parts.push(`LANGUAGE(${languages})`);
|
|
83
83
|
}
|
|
84
|
-
if (
|
|
85
|
-
const sourceTypes =
|
|
84
|
+
if (filters.sourceTypes && filters.sourceTypes.length > 0) {
|
|
85
|
+
const sourceTypes = filters.sourceTypes.map((type) => SOURCE_TYPE_MAP[type] || type).join(" OR ");
|
|
86
86
|
parts.push(`SRCTYPE(${sourceTypes})`);
|
|
87
87
|
}
|
|
88
88
|
return parts;
|
|
89
89
|
}
|
|
90
|
-
function translateQuery(
|
|
91
|
-
const blockResults =
|
|
90
|
+
function translateQuery(resolved) {
|
|
91
|
+
const blockResults = resolved.blocks.map(translateBlock);
|
|
92
92
|
const blockParts = blockResults.map((r) => r.query).filter((s) => s.length > 0);
|
|
93
93
|
const notClauses = blockResults.map((r) => r.notClause).filter((s) => s !== null);
|
|
94
|
-
const
|
|
95
|
-
const filterParts = translateFilters(ast.filters, scopusOverrides);
|
|
94
|
+
const filterParts = translateFilters(resolved.filters);
|
|
96
95
|
const allParts = [...blockParts, ...notClauses, ...filterParts];
|
|
97
96
|
const native = allParts.join(" AND ");
|
|
98
|
-
const warnings = collectUnsupportedVocabWarnings(
|
|
97
|
+
const warnings = collectUnsupportedVocabWarnings(resolved.blocks, "Scopus", /* @__PURE__ */ new Set(["emtree"]));
|
|
99
98
|
return {
|
|
100
99
|
native,
|
|
101
|
-
originalAst: ast,
|
|
102
100
|
provider: "scopus",
|
|
103
101
|
...warnings.length > 0 ? { warnings } : {}
|
|
104
102
|
};
|