@ncukondo/search-hub 0.12.2 → 0.13.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/dist/cli/commands/diff.js +2 -2
- package/dist/cli/commands/diff.js.map +1 -1
- package/dist/cli/commands/query/init.d.ts +5 -0
- package/dist/cli/commands/query/init.d.ts.map +1 -1
- package/dist/cli/commands/query/init.js +9 -1
- package/dist/cli/commands/query/init.js.map +1 -1
- package/dist/cli/commands/query/translate.d.ts.map +1 -1
- package/dist/cli/commands/query/translate.js +5 -0
- package/dist/cli/commands/query/translate.js.map +1 -1
- package/dist/cli/commands/query/validate.d.ts +22 -1
- package/dist/cli/commands/query/validate.d.ts.map +1 -1
- package/dist/cli/commands/query/validate.js +65 -22
- package/dist/cli/commands/query/validate.js.map +1 -1
- package/dist/cli/commands/review/extract.d.ts.map +1 -1
- package/dist/cli/commands/review/extract.js +1 -2
- package/dist/cli/commands/review/extract.js.map +1 -1
- package/dist/cli/commands/review/finalize.d.ts.map +1 -1
- package/dist/cli/commands/review/finalize.js +1 -2
- package/dist/cli/commands/review/finalize.js.map +1 -1
- package/dist/cli/commands/review/init.d.ts.map +1 -1
- package/dist/cli/commands/review/init.js +2 -5
- package/dist/cli/commands/review/init.js.map +1 -1
- package/dist/cli/commands/review/merge.d.ts.map +1 -1
- package/dist/cli/commands/review/merge.js +1 -2
- package/dist/cli/commands/review/merge.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +81 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/suggestions/index.d.ts.map +1 -1
- package/dist/cli/suggestions/index.js +10 -0
- package/dist/cli/suggestions/index.js.map +1 -1
- package/dist/cli/suggestions/rules.d.ts.map +1 -1
- package/dist/cli/suggestions/rules.js +21 -8
- package/dist/cli/suggestions/rules.js.map +1 -1
- package/dist/cli/suggestions/types.d.ts +11 -0
- package/dist/cli/suggestions/types.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/arxiv/translator.d.ts.map +1 -1
- package/dist/providers/arxiv/translator.js +5 -2
- package/dist/providers/arxiv/translator.js.map +1 -1
- package/dist/providers/base/types.d.ts +2 -0
- package/dist/providers/base/types.d.ts.map +1 -1
- package/dist/providers/base/types.js.map +1 -1
- package/dist/providers/base/warnings.d.ts +14 -0
- package/dist/providers/base/warnings.d.ts.map +1 -0
- package/dist/providers/base/warnings.js +33 -0
- package/dist/providers/base/warnings.js.map +1 -0
- package/dist/providers/eric/translator.d.ts.map +1 -1
- package/dist/providers/eric/translator.js +5 -2
- package/dist/providers/eric/translator.js.map +1 -1
- package/dist/providers/pubmed/translator.d.ts.map +1 -1
- package/dist/providers/pubmed/translator.js +5 -2
- package/dist/providers/pubmed/translator.js.map +1 -1
- package/dist/providers/scopus/translator.d.ts.map +1 -1
- package/dist/providers/scopus/translator.js +22 -5
- package/dist/providers/scopus/translator.js.map +1 -1
- package/dist/query/__test-helpers__/mock-mesh-client.d.ts +12 -0
- package/dist/query/__test-helpers__/mock-mesh-client.d.ts.map +1 -0
- package/dist/query/index.d.ts +4 -0
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/json-schema.d.ts +3 -0
- package/dist/query/json-schema.d.ts.map +1 -0
- package/dist/query/json-schema.js +48 -0
- package/dist/query/json-schema.js.map +1 -0
- package/dist/query/mesh-lookup.d.ts +47 -0
- package/dist/query/mesh-lookup.d.ts.map +1 -0
- package/dist/query/mesh-lookup.js +151 -0
- package/dist/query/mesh-lookup.js.map +1 -0
- package/dist/query/parser.js +1 -1
- package/dist/query/parser.js.map +1 -1
- package/dist/query/types.d.ts +2 -2
- package/dist/query/types.d.ts.map +1 -1
- package/dist/query/validator.d.ts +5 -5
- package/dist/query/validator.d.ts.map +1 -1
- package/dist/query/validator.js +5 -2
- package/dist/query/validator.js.map +1 -1
- package/dist/query/vocab-cache.d.ts +15 -0
- package/dist/query/vocab-cache.d.ts.map +1 -0
- package/dist/query/vocab-cache.js +44 -0
- package/dist/query/vocab-cache.js.map +1 -0
- package/dist/query/vocab-validator.d.ts +71 -0
- package/dist/query/vocab-validator.d.ts.map +1 -0
- package/dist/query/vocab-validator.js +153 -0
- package/dist/query/vocab-validator.js.map +1 -0
- package/dist/utils/levenshtein.d.ts +6 -0
- package/dist/utils/levenshtein.d.ts.map +1 -0
- package/dist/utils/levenshtein.js +21 -0
- package/dist/utils/levenshtein.js.map +1 -0
- package/package.json +1 -1
|
@@ -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';\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 return {\n native,\n originalAst: ast,\n provider: 'pubmed',\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 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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translator.d.ts","sourceRoot":"","sources":["../../../src/providers/scopus/translator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAiD,MAAM,mBAAmB,CAAC;AACjG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"translator.d.ts","sourceRoot":"","sources":["../../../src/providers/scopus/translator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAiD,MAAM,mBAAmB,CAAC;AACjG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AA4IrD;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,QAAQ,GAAG,eAAe,CA8B7D"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectUnsupportedVocabWarnings } from "../base/warnings.js";
|
|
1
2
|
const FIELD_MAP = {
|
|
2
3
|
title: "TITLE",
|
|
3
4
|
abstract: "ABS",
|
|
@@ -42,10 +43,24 @@ function quoteTerm(term) {
|
|
|
42
43
|
}
|
|
43
44
|
function translateBlock(block) {
|
|
44
45
|
const field = FIELD_MAP[block.field];
|
|
45
|
-
const terms = block.terms.keywords.map(quoteTerm);
|
|
46
46
|
const operator = block.operator;
|
|
47
|
-
const
|
|
48
|
-
const
|
|
47
|
+
const parts = [];
|
|
48
|
+
const keywords = (block.terms.keywords ?? []).map(quoteTerm);
|
|
49
|
+
if (keywords.length > 0) {
|
|
50
|
+
parts.push(`${field}(${keywords.join(` ${operator} `)})`);
|
|
51
|
+
}
|
|
52
|
+
const emtree = (block.terms.emtree ?? []).map(quoteTerm);
|
|
53
|
+
if (emtree.length > 0) {
|
|
54
|
+
parts.push(`INDEXTERMS(${emtree.join(` ${operator} `)})`);
|
|
55
|
+
}
|
|
56
|
+
let query;
|
|
57
|
+
if (parts.length === 0) {
|
|
58
|
+
query = "";
|
|
59
|
+
} else if (parts.length === 1) {
|
|
60
|
+
query = parts[0];
|
|
61
|
+
} else {
|
|
62
|
+
query = parts.join(` ${operator} `);
|
|
63
|
+
}
|
|
49
64
|
let notClause = null;
|
|
50
65
|
if (block.terms.exclude && block.terms.exclude.length > 0) {
|
|
51
66
|
const excludeTerms = block.terms.exclude.map(quoteTerm);
|
|
@@ -74,16 +89,18 @@ function translateFilters(filters, scopusOverrides) {
|
|
|
74
89
|
}
|
|
75
90
|
function translateQuery(ast) {
|
|
76
91
|
const blockResults = ast.blocks.map(translateBlock);
|
|
77
|
-
const blockParts = blockResults.map((r) => r.query);
|
|
92
|
+
const blockParts = blockResults.map((r) => r.query).filter((s) => s.length > 0);
|
|
78
93
|
const notClauses = blockResults.map((r) => r.notClause).filter((s) => s !== null);
|
|
79
94
|
const scopusOverrides = ast.overrides.scopus;
|
|
80
95
|
const filterParts = translateFilters(ast.filters, scopusOverrides);
|
|
81
96
|
const allParts = [...blockParts, ...notClauses, ...filterParts];
|
|
82
97
|
const native = allParts.join(" AND ");
|
|
98
|
+
const warnings = collectUnsupportedVocabWarnings(ast.blocks, "Scopus", /* @__PURE__ */ new Set(["emtree"]));
|
|
83
99
|
return {
|
|
84
100
|
native,
|
|
85
101
|
originalAst: ast,
|
|
86
|
-
provider: "scopus"
|
|
102
|
+
provider: "scopus",
|
|
103
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
87
104
|
};
|
|
88
105
|
}
|
|
89
106
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translator.js","sources":["../../../src/providers/scopus/translator.ts"],"sourcesContent":["/**\n * Scopus Query Translator\n *\n * Translates QueryAST to Scopus search syntax.\n */\n\nimport type { QueryAST, FieldType, QueryBlock, Filters, OverrideBlock } from '../../query/types';\nimport type { TranslatedQuery } from '../base/types';\n\n/**\n * Field function mappings for Scopus.\n */\nconst FIELD_MAP: Record<FieldType, string> = {\n title: 'TITLE',\n abstract: 'ABS',\n title_abstract: 'TITLE-ABS-KEY',\n author: 'AUTH',\n keyword: 'KEY',\n all: 'ALL',\n};\n\n/**\n * Source type code mappings for Scopus.\n */\nconst SOURCE_TYPE_MAP: Record<string, string> = {\n journal: 'j',\n conference: 'p',\n book: 'b',\n 'book series': 'k',\n 'trade journal': 'd',\n};\n\n/**\n * Language code mappings for Scopus.\n */\nconst LANGUAGE_MAP: Record<string, string> = {\n en: 'english',\n de: 'german',\n fr: 'french',\n es: 'spanish',\n it: 'italian',\n ja: 'japanese',\n zh: 'chinese',\n ko: 'korean',\n pt: 'portuguese',\n ru: 'russian',\n};\n\n/**\n * Check if a term needs to be quoted (contains spaces and isn't already quoted).\n */\nfunction needsQuotes(term: string): boolean {\n if (term.startsWith('\"') && term.endsWith('\"')) {\n return false;\n }\n if (term.startsWith('{') && term.endsWith('}')) {\n return false;\n }\n return term.includes(' ');\n}\n\n/**\n * Quote a term if it contains spaces.\n */\nfunction quoteTerm(term: string): string {\n if (needsQuotes(term)) {\n return `\"${term}\"`;\n }\n return term;\n}\n\n/**\n * Translate a single query block to Scopus 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 = FIELD_MAP[block.field];\n const
|
|
1
|
+
{"version":3,"file":"translator.js","sources":["../../../src/providers/scopus/translator.ts"],"sourcesContent":["/**\n * Scopus Query Translator\n *\n * Translates QueryAST to Scopus search syntax.\n */\n\nimport type { QueryAST, FieldType, QueryBlock, Filters, OverrideBlock } from '../../query/types';\nimport type { TranslatedQuery } from '../base/types';\nimport { collectUnsupportedVocabWarnings } from '../base/warnings';\n\n/**\n * Field function mappings for Scopus.\n */\nconst FIELD_MAP: Record<FieldType, string> = {\n title: 'TITLE',\n abstract: 'ABS',\n title_abstract: 'TITLE-ABS-KEY',\n author: 'AUTH',\n keyword: 'KEY',\n all: 'ALL',\n};\n\n/**\n * Source type code mappings for Scopus.\n */\nconst SOURCE_TYPE_MAP: Record<string, string> = {\n journal: 'j',\n conference: 'p',\n book: 'b',\n 'book series': 'k',\n 'trade journal': 'd',\n};\n\n/**\n * Language code mappings for Scopus.\n */\nconst LANGUAGE_MAP: Record<string, string> = {\n en: 'english',\n de: 'german',\n fr: 'french',\n es: 'spanish',\n it: 'italian',\n ja: 'japanese',\n zh: 'chinese',\n ko: 'korean',\n pt: 'portuguese',\n ru: 'russian',\n};\n\n/**\n * Check if a term needs to be quoted (contains spaces and isn't already quoted).\n */\nfunction needsQuotes(term: string): boolean {\n if (term.startsWith('\"') && term.endsWith('\"')) {\n return false;\n }\n if (term.startsWith('{') && term.endsWith('}')) {\n return false;\n }\n return term.includes(' ');\n}\n\n/**\n * Quote a term if it contains spaces.\n */\nfunction quoteTerm(term: string): string {\n if (needsQuotes(term)) {\n return `\"${term}\"`;\n }\n return term;\n}\n\n/**\n * Translate a single query block to Scopus 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 = FIELD_MAP[block.field];\n const operator = block.operator;\n const parts: string[] = [];\n\n // Translate keywords\n const keywords = (block.terms.keywords ?? []).map(quoteTerm);\n if (keywords.length > 0) {\n parts.push(`${field}(${keywords.join(` ${operator} `)})`);\n }\n\n // Translate Emtree terms (always use INDEXTERMS)\n const emtree = (block.terms.emtree ?? []).map(quoteTerm);\n if (emtree.length > 0) {\n parts.push(`INDEXTERMS(${emtree.join(` ${operator} `)})`);\n }\n\n // Combine parts (empty string when no supported terms)\n let query: string;\n if (parts.length === 0) {\n query = '';\n } else if (parts.length === 1) {\n query = parts[0]!;\n } else {\n query = parts.join(` ${operator} `);\n }\n\n // Translate exclude terms (without AND prefix - will be added during join)\n let notClause: string | null = null;\n if (block.terms.exclude && block.terms.exclude.length > 0) {\n const excludeTerms = block.terms.exclude.map(quoteTerm);\n const excludeStr = excludeTerms.join(' OR ');\n notClause = `NOT ${field}(${excludeStr})`;\n }\n\n return { query, notClause };\n}\n\n/**\n * Translate filters to Scopus syntax.\n */\nfunction translateFilters(filters: Filters, scopusOverrides?: OverrideBlock): string[] {\n const parts: string[] = [];\n\n // Year filters\n if (filters.yearFrom !== undefined) {\n parts.push(`PUBYEAR > ${filters.yearFrom - 1}`);\n }\n if (filters.yearTo !== undefined) {\n parts.push(`PUBYEAR < ${filters.yearTo + 1}`);\n }\n\n // Language filter\n if (filters.languages && filters.languages.length > 0) {\n const languages = filters.languages\n .map(code => LANGUAGE_MAP[code] || code)\n .join(' OR ');\n parts.push(`LANGUAGE(${languages})`);\n }\n\n // Source type filter from overrides\n if (scopusOverrides?.sourceTypes && scopusOverrides.sourceTypes.length > 0) {\n const sourceTypes = scopusOverrides.sourceTypes\n .map(type => SOURCE_TYPE_MAP[type] || type)\n .join(' OR ');\n parts.push(`SRCTYPE(${sourceTypes})`);\n }\n\n return parts;\n}\n\n/**\n * Translate a QueryAST to Scopus search syntax.\n */\nexport function translateQuery(ast: QueryAST): TranslatedQuery {\n // Translate query blocks\n const blockResults = ast.blocks.map(translateBlock);\n\n // Collect query parts (filter empty blocks) and NOT clauses\n const blockParts = blockResults\n .map((r) => r.query)\n .filter((s) => s.length > 0);\n const notClauses = blockResults\n .map((r) => r.notClause)\n .filter((s): s is string => s !== null);\n\n // Translate filters\n const scopusOverrides = ast.overrides.scopus;\n const filterParts = translateFilters(ast.filters, scopusOverrides);\n\n // Build native query: blocks AND NOT(excludes) AND filters\n const allParts: string[] = [...blockParts, ...notClauses, ...filterParts];\n const native = allParts.join(' AND ');\n\n // Collect warnings for unsupported controlled vocabulary\n // Scopus supports emtree but not mesh or eric\n const warnings = collectUnsupportedVocabWarnings(ast.blocks, 'Scopus', new Set(['emtree']));\n\n return {\n native,\n originalAst: ast,\n provider: 'scopus',\n ...(warnings.length > 0 ? { warnings } : {}),\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { MeSHLookupClient } from '../mesh-lookup.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create a mock MeSH lookup client for testing.
|
|
4
|
+
*
|
|
5
|
+
* @param results - Map of term to lookup result
|
|
6
|
+
* @returns A mock MeSHLookupClient
|
|
7
|
+
*/
|
|
8
|
+
export declare function createMockMeSHClient(results: Map<string, {
|
|
9
|
+
found: boolean;
|
|
10
|
+
suggestions?: string[];
|
|
11
|
+
}>): MeSHLookupClient;
|
|
12
|
+
//# sourceMappingURL=mock-mesh-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-mesh-client.d.ts","sourceRoot":"","sources":["../../../src/query/__test-helpers__/mock-mesh-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,GAC/D,gBAAgB,CAWlB"}
|
package/dist/query/index.d.ts
CHANGED
|
@@ -9,4 +9,8 @@
|
|
|
9
9
|
export type { FieldType, Operator, ProviderName, TermBlock, QueryBlock, Filters, PublicationTypeFilter, OverrideBlock, QueryAST, } from './types.js';
|
|
10
10
|
export { parseQueryFile, parseQueryString } from './parser.js';
|
|
11
11
|
export { validateQueryFile, formatValidationErrors, ValidationError, fieldTypeSchema, termBlockSchema, queryBlockSchema, filtersSchema, overrideBlockSchema, queryFileSchema, } from './validator.js';
|
|
12
|
+
export { MeSHLookupClient } from './mesh-lookup.js';
|
|
13
|
+
export type { MeSHLookupResult } from './mesh-lookup.js';
|
|
14
|
+
export { extractControlledVocabTerms, validateControlledVocab, } from './vocab-validator.js';
|
|
15
|
+
export type { VocabTerm, VocabTermError, VocabTermResult, VocabValidationResult, } from './vocab-validator.js';
|
|
12
16
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/query/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,YAAY,EACV,SAAS,EACT,QAAQ,EACR,YAAY,EACZ,SAAS,EACT,UAAU,EACV,OAAO,EACP,qBAAqB,EACrB,aAAa,EACb,QAAQ,GACT,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAG/D,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,eAAe,EAEf,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,mBAAmB,EACnB,eAAe,GAChB,MAAM,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/query/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,YAAY,EACV,SAAS,EACT,QAAQ,EACR,YAAY,EACZ,SAAS,EACT,UAAU,EACV,OAAO,EACP,qBAAqB,EACrB,aAAa,EACb,QAAQ,GACT,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAG/D,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,eAAe,EAEf,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,mBAAmB,EACnB,eAAe,GAChB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EACL,2BAA2B,EAC3B,uBAAuB,GACxB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,SAAS,EACT,cAAc,EACd,eAAe,EACf,qBAAqB,GACtB,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-schema.d.ts","sourceRoot":"","sources":["../../src/query/json-schema.ts"],"names":[],"mappings":"AAsEA,6DAA6D;AAC7D,wBAAgB,uBAAuB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIjE"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import { publicationTypeFilterSchema, operatorSchema, fieldTypeSchema } from "./validator.js";
|
|
3
|
+
const filtersInputSchema = z.object({
|
|
4
|
+
year_from: z.number().int().optional(),
|
|
5
|
+
year_to: z.number().int().optional(),
|
|
6
|
+
language: z.array(z.string()).optional(),
|
|
7
|
+
publication_types: publicationTypeFilterSchema.optional()
|
|
8
|
+
}).optional();
|
|
9
|
+
const overrideBlockInputSchema = z.object({
|
|
10
|
+
filters: filtersInputSchema,
|
|
11
|
+
categories: z.array(z.string()).optional(),
|
|
12
|
+
source_types: z.array(z.string()).optional()
|
|
13
|
+
}).optional();
|
|
14
|
+
const termBlockInputSchema = z.object({
|
|
15
|
+
keywords: z.array(z.string()).min(1).optional(),
|
|
16
|
+
mesh: z.array(z.string()).optional(),
|
|
17
|
+
emtree: z.array(z.string()).optional(),
|
|
18
|
+
eric: z.array(z.string()).optional(),
|
|
19
|
+
exclude: z.array(z.string()).optional()
|
|
20
|
+
});
|
|
21
|
+
const queryBlockInputSchema = z.object({
|
|
22
|
+
field: fieldTypeSchema,
|
|
23
|
+
terms: termBlockInputSchema,
|
|
24
|
+
operator: operatorSchema
|
|
25
|
+
});
|
|
26
|
+
const queryFileInputSchema = z.object({
|
|
27
|
+
name: z.string().min(1),
|
|
28
|
+
description: z.string().optional(),
|
|
29
|
+
query: z.array(queryBlockInputSchema).min(1),
|
|
30
|
+
filters: filtersInputSchema,
|
|
31
|
+
overrides: z.object({
|
|
32
|
+
pubmed: overrideBlockInputSchema,
|
|
33
|
+
scopus: overrideBlockInputSchema,
|
|
34
|
+
eric: overrideBlockInputSchema,
|
|
35
|
+
arxiv: overrideBlockInputSchema,
|
|
36
|
+
wos: overrideBlockInputSchema,
|
|
37
|
+
embase: overrideBlockInputSchema
|
|
38
|
+
}).optional()
|
|
39
|
+
});
|
|
40
|
+
function generateQueryJSONSchema() {
|
|
41
|
+
return z.toJSONSchema(queryFileInputSchema, {
|
|
42
|
+
target: "draft-2020-12"
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
generateQueryJSONSchema
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=json-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-schema.js","sources":["../../src/query/json-schema.ts"],"sourcesContent":["/**\n * JSON Schema generation for query YAML files.\n *\n * Defines an input-only schema (without transforms) that mirrors the structure\n * of queryFileSchema from validator.ts. This is necessary because Zod v4's\n * z.toJSONSchema() cannot handle .transform() calls.\n *\n * The generated JSON Schema enables editor autocompletion and validation\n * via the yaml-language-server $schema comment.\n */\nimport * as z from 'zod';\nimport {\n fieldTypeSchema,\n operatorSchema,\n publicationTypeFilterSchema,\n} from './validator.js';\n\n/** Filters input schema (without transform) */\nconst filtersInputSchema = z\n .object({\n year_from: z.number().int().optional(),\n year_to: z.number().int().optional(),\n language: z.array(z.string()).optional(),\n publication_types: publicationTypeFilterSchema.optional(),\n })\n .optional();\n\n/** Override block input schema (without transform) */\nconst overrideBlockInputSchema = z\n .object({\n filters: filtersInputSchema,\n categories: z.array(z.string()).optional(),\n source_types: z.array(z.string()).optional(),\n })\n .optional();\n\n/** Term block input schema (without refine) */\nconst termBlockInputSchema = z.object({\n keywords: z.array(z.string()).min(1).optional(),\n mesh: z.array(z.string()).optional(),\n emtree: z.array(z.string()).optional(),\n eric: z.array(z.string()).optional(),\n exclude: z.array(z.string()).optional(),\n});\n\n/** Query block input schema */\nconst queryBlockInputSchema = z.object({\n field: fieldTypeSchema,\n terms: termBlockInputSchema,\n operator: operatorSchema,\n});\n\n/** Query file input schema (without transform) - mirrors queryFileSchema input */\nconst queryFileInputSchema = z.object({\n name: z.string().min(1),\n description: z.string().optional(),\n query: z.array(queryBlockInputSchema).min(1),\n filters: filtersInputSchema,\n overrides: z\n .object({\n pubmed: overrideBlockInputSchema,\n scopus: overrideBlockInputSchema,\n eric: overrideBlockInputSchema,\n arxiv: overrideBlockInputSchema,\n wos: overrideBlockInputSchema,\n embase: overrideBlockInputSchema,\n })\n .optional(),\n});\n\n/** Generate a JSON Schema from the query file Zod schema. */\nexport function generateQueryJSONSchema(): Record<string, unknown> {\n return z.toJSONSchema(queryFileInputSchema, {\n target: 'draft-2020-12',\n });\n}\n"],"names":[],"mappings":";;AAkBA,MAAM,qBAAqB,EACxB,OAAO;AAAA,EACN,WAAW,EAAE,OAAA,EAAS,IAAA,EAAM,SAAA;AAAA,EAC5B,SAAS,EAAE,OAAA,EAAS,IAAA,EAAM,SAAA;AAAA,EAC1B,UAAU,EAAE,MAAM,EAAE,OAAA,CAAQ,EAAE,SAAA;AAAA,EAC9B,mBAAmB,4BAA4B,SAAA;AACjD,CAAC,EACA,SAAA;AAGH,MAAM,2BAA2B,EAC9B,OAAO;AAAA,EACN,SAAS;AAAA,EACT,YAAY,EAAE,MAAM,EAAE,OAAA,CAAQ,EAAE,SAAA;AAAA,EAChC,cAAc,EAAE,MAAM,EAAE,OAAA,CAAQ,EAAE,SAAA;AACpC,CAAC,EACA,SAAA;AAGH,MAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,UAAU,EAAE,MAAM,EAAE,OAAA,CAAQ,EAAE,IAAI,CAAC,EAAE,SAAA;AAAA,EACrC,MAAM,EAAE,MAAM,EAAE,OAAA,CAAQ,EAAE,SAAA;AAAA,EAC1B,QAAQ,EAAE,MAAM,EAAE,OAAA,CAAQ,EAAE,SAAA;AAAA,EAC5B,MAAM,EAAE,MAAM,EAAE,OAAA,CAAQ,EAAE,SAAA;AAAA,EAC1B,SAAS,EAAE,MAAM,EAAE,OAAA,CAAQ,EAAE,SAAA;AAC/B,CAAC;AAGD,MAAM,wBAAwB,EAAE,OAAO;AAAA,EACrC,OAAO;AAAA,EACP,OAAO;AAAA,EACP,UAAU;AACZ,CAAC;AAGD,MAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,MAAM,EAAE,SAAS,IAAI,CAAC;AAAA,EACtB,aAAa,EAAE,OAAA,EAAS,SAAA;AAAA,EACxB,OAAO,EAAE,MAAM,qBAAqB,EAAE,IAAI,CAAC;AAAA,EAC3C,SAAS;AAAA,EACT,WAAW,EACR,OAAO;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,IACP,KAAK;AAAA,IACL,QAAQ;AAAA,EAAA,CACT,EACA,SAAA;AACL,CAAC;AAGM,SAAS,0BAAmD;AACjE,SAAO,EAAE,aAAa,sBAAsB;AAAA,IAC1C,QAAQ;AAAA,EAAA,CACT;AACH;"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { RateLimiter } from '../providers/base/rate-limiter.js';
|
|
2
|
+
import { VocabCache } from './vocab-cache.js';
|
|
3
|
+
/**
|
|
4
|
+
* Result of a MeSH term lookup.
|
|
5
|
+
*/
|
|
6
|
+
export interface MeSHLookupResult {
|
|
7
|
+
/** The term that was looked up */
|
|
8
|
+
term: string;
|
|
9
|
+
/** Whether the term was found as a valid MeSH heading */
|
|
10
|
+
found: boolean;
|
|
11
|
+
/** Suggested terms if the lookup term was not found */
|
|
12
|
+
suggestions?: string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Client for the NLM MeSH Lookup API.
|
|
16
|
+
*/
|
|
17
|
+
export declare class MeSHLookupClient {
|
|
18
|
+
private readonly rateLimiter;
|
|
19
|
+
private readonly timeoutMs;
|
|
20
|
+
private readonly cache;
|
|
21
|
+
constructor(options?: {
|
|
22
|
+
rateLimiter?: RateLimiter;
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
cache?: VocabCache;
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* Look up a single MeSH term.
|
|
28
|
+
*
|
|
29
|
+
* Tries multiple match strategies in order:
|
|
30
|
+
* 1. exact — exact match
|
|
31
|
+
* 2. startsWith (full term) — prefix match
|
|
32
|
+
* 2b. startsWith (truncated) — suffix typo recovery (1-3 chars removed)
|
|
33
|
+
* 2c. startsWith (word1 + word2 prefix) — multi-word progressive prefix (max 3 calls)
|
|
34
|
+
* 3. contains (full term) — substring match
|
|
35
|
+
* 4. startsWith (first word, limit=25) — re-ranked by Levenshtein distance
|
|
36
|
+
*
|
|
37
|
+
* Returns on the first strategy that produces results.
|
|
38
|
+
* Results are cached when a VocabCache is provided.
|
|
39
|
+
*/
|
|
40
|
+
lookupTerm(term: string): Promise<MeSHLookupResult>;
|
|
41
|
+
/**
|
|
42
|
+
* Look up multiple MeSH terms.
|
|
43
|
+
*/
|
|
44
|
+
lookupTerms(terms: string[]): Promise<MeSHLookupResult[]>;
|
|
45
|
+
private fetchLookup;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=mesh-lookup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mesh-lookup.d.ts","sourceRoot":"","sources":["../../src/query/mesh-lookup.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AACrE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMnD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,KAAK,EAAE,OAAO,CAAC;IACf,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAOD;;GAEG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA0B;IACtD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyB;gBAEnC,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,WAAW,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,UAAU,CAAA;KAAE;IAM3F;;;;;;;;;;;;;OAaG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA4GzD;;OAEG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;YAQjD,WAAW;CAoC1B"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { levenshteinDistance } from "../utils/levenshtein.js";
|
|
2
|
+
const MESH_LOOKUP_BASE_URL = "https://id.nlm.nih.gov/mesh/lookup/term";
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
4
|
+
class MeSHLookupClient {
|
|
5
|
+
rateLimiter;
|
|
6
|
+
timeoutMs;
|
|
7
|
+
cache;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.rateLimiter = options?.rateLimiter;
|
|
10
|
+
this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
11
|
+
this.cache = options?.cache;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Look up a single MeSH term.
|
|
15
|
+
*
|
|
16
|
+
* Tries multiple match strategies in order:
|
|
17
|
+
* 1. exact — exact match
|
|
18
|
+
* 2. startsWith (full term) — prefix match
|
|
19
|
+
* 2b. startsWith (truncated) — suffix typo recovery (1-3 chars removed)
|
|
20
|
+
* 2c. startsWith (word1 + word2 prefix) — multi-word progressive prefix (max 3 calls)
|
|
21
|
+
* 3. contains (full term) — substring match
|
|
22
|
+
* 4. startsWith (first word, limit=25) — re-ranked by Levenshtein distance
|
|
23
|
+
*
|
|
24
|
+
* Returns on the first strategy that produces results.
|
|
25
|
+
* Results are cached when a VocabCache is provided.
|
|
26
|
+
*/
|
|
27
|
+
async lookupTerm(term) {
|
|
28
|
+
if (this.cache) {
|
|
29
|
+
const cached = this.cache.get("mesh", term);
|
|
30
|
+
if (cached) {
|
|
31
|
+
return cached;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const exactResults = await this.fetchLookup(term, "exact", 1);
|
|
35
|
+
if (exactResults.length > 0) {
|
|
36
|
+
const result2 = { term, found: true };
|
|
37
|
+
this.cache?.set("mesh", term, result2);
|
|
38
|
+
return result2;
|
|
39
|
+
}
|
|
40
|
+
const startsWithResults = await this.fetchLookup(term, "startswith", 5);
|
|
41
|
+
if (startsWithResults.length > 0) {
|
|
42
|
+
const result2 = {
|
|
43
|
+
term,
|
|
44
|
+
found: false,
|
|
45
|
+
suggestions: startsWithResults.map((s) => s.label)
|
|
46
|
+
};
|
|
47
|
+
this.cache?.set("mesh", term, result2);
|
|
48
|
+
return result2;
|
|
49
|
+
}
|
|
50
|
+
if (term.length > 3) {
|
|
51
|
+
for (let len = term.length - 1; len >= Math.max(term.length - 3, 3); len--) {
|
|
52
|
+
const truncated = term.slice(0, len);
|
|
53
|
+
const truncatedResults = await this.fetchLookup(truncated, "startswith", 5);
|
|
54
|
+
if (truncatedResults.length > 0) {
|
|
55
|
+
const result2 = {
|
|
56
|
+
term,
|
|
57
|
+
found: false,
|
|
58
|
+
suggestions: truncatedResults.map((s) => s.label)
|
|
59
|
+
};
|
|
60
|
+
this.cache?.set("mesh", term, result2);
|
|
61
|
+
return result2;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const words = term.split(/\s+/);
|
|
66
|
+
if (words.length >= 2 && words[1].length > 3) {
|
|
67
|
+
const startN = Math.min(words[1].length - 4, words[1].length - 1);
|
|
68
|
+
const endN = 3;
|
|
69
|
+
let iterations = 0;
|
|
70
|
+
for (let n = startN; n >= endN && iterations < 3; n--, iterations++) {
|
|
71
|
+
const prefix = words[0] + " " + words[1].slice(0, n);
|
|
72
|
+
const prefixResults = await this.fetchLookup(prefix, "startswith", 5);
|
|
73
|
+
if (prefixResults.length > 0) {
|
|
74
|
+
const result2 = {
|
|
75
|
+
term,
|
|
76
|
+
found: false,
|
|
77
|
+
suggestions: prefixResults.map((s) => s.label)
|
|
78
|
+
};
|
|
79
|
+
this.cache?.set("mesh", term, result2);
|
|
80
|
+
return result2;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const containsResults = await this.fetchLookup(term, "contains", 5);
|
|
85
|
+
if (containsResults.length > 0) {
|
|
86
|
+
const result2 = {
|
|
87
|
+
term,
|
|
88
|
+
found: false,
|
|
89
|
+
suggestions: containsResults.map((s) => s.label)
|
|
90
|
+
};
|
|
91
|
+
this.cache?.set("mesh", term, result2);
|
|
92
|
+
return result2;
|
|
93
|
+
}
|
|
94
|
+
if (words.length > 1) {
|
|
95
|
+
const firstWord = words[0];
|
|
96
|
+
const firstWordResults = await this.fetchLookup(firstWord, "startswith", 25);
|
|
97
|
+
if (firstWordResults.length > 0) {
|
|
98
|
+
const ranked = firstWordResults.map((s) => ({
|
|
99
|
+
label: s.label,
|
|
100
|
+
distance: levenshteinDistance(term.toLowerCase(), s.label.toLowerCase())
|
|
101
|
+
})).sort((a, b) => a.distance - b.distance).slice(0, 5).map((s) => s.label);
|
|
102
|
+
const result2 = { term, found: false, suggestions: ranked };
|
|
103
|
+
this.cache?.set("mesh", term, result2);
|
|
104
|
+
return result2;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const result = { term, found: false };
|
|
108
|
+
this.cache?.set("mesh", term, result);
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Look up multiple MeSH terms.
|
|
113
|
+
*/
|
|
114
|
+
async lookupTerms(terms) {
|
|
115
|
+
const results = [];
|
|
116
|
+
for (const term of terms) {
|
|
117
|
+
results.push(await this.lookupTerm(term));
|
|
118
|
+
}
|
|
119
|
+
return results;
|
|
120
|
+
}
|
|
121
|
+
async fetchLookup(label, match, limit) {
|
|
122
|
+
if (this.rateLimiter) {
|
|
123
|
+
await this.rateLimiter.acquire();
|
|
124
|
+
}
|
|
125
|
+
const params = new URLSearchParams({
|
|
126
|
+
label,
|
|
127
|
+
match,
|
|
128
|
+
limit: String(limit)
|
|
129
|
+
});
|
|
130
|
+
const url = `${MESH_LOOKUP_BASE_URL}?${params.toString()}`;
|
|
131
|
+
let response;
|
|
132
|
+
try {
|
|
133
|
+
response = await fetch(url, {
|
|
134
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
135
|
+
});
|
|
136
|
+
} catch (error) {
|
|
137
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
138
|
+
throw new Error(`MeSH lookup failed: ${message}`);
|
|
139
|
+
}
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`MeSH lookup failed: HTTP ${response.status} ${response.statusText}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return await response.json();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export {
|
|
149
|
+
MeSHLookupClient
|
|
150
|
+
};
|
|
151
|
+
//# sourceMappingURL=mesh-lookup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mesh-lookup.js","sources":["../../src/query/mesh-lookup.ts"],"sourcesContent":["/**\n * MeSH Lookup API client.\n *\n * Validates MeSH (Medical Subject Headings) terms against the NLM MeSH Lookup API.\n * No API key required.\n *\n * API docs: https://id.nlm.nih.gov/mesh/lookup/term\n */\n\nimport type { RateLimiter } from '../providers/base/rate-limiter.js';\nimport type { VocabCache } from './vocab-cache.js';\nimport { levenshteinDistance } from '../utils/levenshtein.js';\n\nconst MESH_LOOKUP_BASE_URL = 'https://id.nlm.nih.gov/mesh/lookup/term';\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\n/**\n * Result of a MeSH term lookup.\n */\nexport interface MeSHLookupResult {\n /** The term that was looked up */\n term: string;\n /** Whether the term was found as a valid MeSH heading */\n found: boolean;\n /** Suggested terms if the lookup term was not found */\n suggestions?: string[];\n}\n\ninterface MeSHApiEntry {\n resource: string;\n label: string;\n}\n\n/**\n * Client for the NLM MeSH Lookup API.\n */\nexport class MeSHLookupClient {\n private readonly rateLimiter: RateLimiter | undefined;\n private readonly timeoutMs: number;\n private readonly cache: VocabCache | undefined;\n\n constructor(options?: { rateLimiter?: RateLimiter; timeoutMs?: number; cache?: VocabCache }) {\n this.rateLimiter = options?.rateLimiter;\n this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.cache = options?.cache;\n }\n\n /**\n * Look up a single MeSH term.\n *\n * Tries multiple match strategies in order:\n * 1. exact — exact match\n * 2. startsWith (full term) — prefix match\n * 2b. startsWith (truncated) — suffix typo recovery (1-3 chars removed)\n * 2c. startsWith (word1 + word2 prefix) — multi-word progressive prefix (max 3 calls)\n * 3. contains (full term) — substring match\n * 4. startsWith (first word, limit=25) — re-ranked by Levenshtein distance\n *\n * Returns on the first strategy that produces results.\n * Results are cached when a VocabCache is provided.\n */\n async lookupTerm(term: string): Promise<MeSHLookupResult> {\n // Check cache first\n if (this.cache) {\n const cached = this.cache.get('mesh', term);\n if (cached) {\n return cached;\n }\n }\n\n // 1. Try exact match first\n const exactResults = await this.fetchLookup(term, 'exact', 1);\n\n if (exactResults.length > 0) {\n const result: MeSHLookupResult = { term, found: true };\n this.cache?.set('mesh', term, result);\n return result;\n }\n\n // 2. Try startsWith (full term) for suggestions\n const startsWithResults = await this.fetchLookup(term, 'startswith', 5);\n\n if (startsWithResults.length > 0) {\n const result: MeSHLookupResult = {\n term,\n found: false,\n suggestions: startsWithResults.map((s) => s.label),\n };\n this.cache?.set('mesh', term, result);\n return result;\n }\n\n // 2b. Try startsWith with progressively shorter input (handles suffix typos)\n if (term.length > 3) {\n for (let len = term.length - 1; len >= Math.max(term.length - 3, 3); len--) {\n const truncated = term.slice(0, len);\n const truncatedResults = await this.fetchLookup(truncated, 'startswith', 5);\n if (truncatedResults.length > 0) {\n const result: MeSHLookupResult = {\n term,\n found: false,\n suggestions: truncatedResults.map((s) => s.label),\n };\n this.cache?.set('mesh', term, result);\n return result;\n }\n }\n }\n\n // 2c. Multi-word progressive prefix: try word1 + word2.slice(0, N)\n const words = term.split(/\\s+/);\n if (words.length >= 2 && words[1]!.length > 3) {\n const startN = Math.min(words[1]!.length - 4, words[1]!.length - 1);\n const endN = 3;\n let iterations = 0;\n for (let n = startN; n >= endN && iterations < 3; n--, iterations++) {\n const prefix = words[0]! + ' ' + words[1]!.slice(0, n);\n const prefixResults = await this.fetchLookup(prefix, 'startswith', 5);\n if (prefixResults.length > 0) {\n const result: MeSHLookupResult = {\n term,\n found: false,\n suggestions: prefixResults.map((s) => s.label),\n };\n this.cache?.set('mesh', term, result);\n return result;\n }\n }\n }\n\n // 3. Try contains (full term) for typos and variant spellings\n const containsResults = await this.fetchLookup(term, 'contains', 5);\n\n if (containsResults.length > 0) {\n const result: MeSHLookupResult = {\n term,\n found: false,\n suggestions: containsResults.map((s) => s.label),\n };\n this.cache?.set('mesh', term, result);\n return result;\n }\n\n // 4. Try startsWith with first word only (for multi-word terms)\n // Fetch up to 25 results and re-rank by Levenshtein distance\n if (words.length > 1) {\n const firstWord = words[0]!;\n const firstWordResults = await this.fetchLookup(firstWord, 'startswith', 25);\n\n if (firstWordResults.length > 0) {\n const ranked = firstWordResults\n .map((s) => ({\n label: s.label,\n distance: levenshteinDistance(term.toLowerCase(), s.label.toLowerCase()),\n }))\n .sort((a, b) => a.distance - b.distance)\n .slice(0, 5)\n .map((s) => s.label);\n const result: MeSHLookupResult = { term, found: false, suggestions: ranked };\n this.cache?.set('mesh', term, result);\n return result;\n }\n }\n\n const result: MeSHLookupResult = { term, found: false };\n this.cache?.set('mesh', term, result);\n return result;\n }\n\n /**\n * Look up multiple MeSH terms.\n */\n async lookupTerms(terms: string[]): Promise<MeSHLookupResult[]> {\n const results: MeSHLookupResult[] = [];\n for (const term of terms) {\n results.push(await this.lookupTerm(term));\n }\n return results;\n }\n\n private async fetchLookup(\n label: string,\n match: 'exact' | 'startswith' | 'contains',\n limit: number\n ): Promise<MeSHApiEntry[]> {\n if (this.rateLimiter) {\n await this.rateLimiter.acquire();\n }\n\n const params = new URLSearchParams({\n label,\n match,\n limit: String(limit),\n });\n\n const url = `${MESH_LOOKUP_BASE_URL}?${params.toString()}`;\n\n let response: Response;\n try {\n response = await fetch(url, {\n signal: AbortSignal.timeout(this.timeoutMs),\n });\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Unknown error';\n throw new Error(`MeSH lookup failed: ${message}`);\n }\n\n if (!response.ok) {\n throw new Error(\n `MeSH lookup failed: HTTP ${response.status} ${response.statusText}`\n );\n }\n\n return (await response.json()) as MeSHApiEntry[];\n }\n}\n"],"names":["result"],"mappings":"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"}
|
package/dist/query/parser.js
CHANGED
|
@@ -12,7 +12,7 @@ async function parseQueryFile(filePath) {
|
|
|
12
12
|
function detectShortKeywords(ast, threshold = 3) {
|
|
13
13
|
const shortKeywords = /* @__PURE__ */ new Set();
|
|
14
14
|
for (const block of ast.blocks) {
|
|
15
|
-
for (const keyword of block.terms.keywords) {
|
|
15
|
+
for (const keyword of block.terms.keywords ?? []) {
|
|
16
16
|
if (keyword.length <= threshold) {
|
|
17
17
|
shortKeywords.add(keyword);
|
|
18
18
|
}
|
package/dist/query/parser.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parser.js","sources":["../../src/query/parser.ts"],"sourcesContent":["/**\n * Query YAML Parser\n *\n * Parses YAML query files into validated QueryAST.\n * See spec/models/query-dsl.md for the full specification.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport { parse } from 'yaml';\nimport type { QueryAST } from './types.js';\nimport { validateQueryFile } from './validator.js';\n\n/**\n * Parse a YAML string into a validated QueryAST.\n *\n * @param yaml - YAML string to parse\n * @returns Validated QueryAST\n * @throws Error if YAML is invalid or doesn't match schema\n */\nexport function parseQueryString(yaml: string): QueryAST {\n const data = parse(yaml);\n return validateQueryFile(data);\n}\n\n/**\n * Parse a YAML file into a validated QueryAST.\n *\n * @param filePath - Path to the YAML file\n * @returns Promise resolving to validated QueryAST\n * @throws Error if file doesn't exist, YAML is invalid, or doesn't match schema\n */\nexport async function parseQueryFile(filePath: string): Promise<QueryAST> {\n const content = await readFile(filePath, 'utf-8');\n return parseQueryString(content);\n}\n\n\n/**\n * Detect short keywords (potential acronyms) in a query.\n *\n * Short keywords (3 characters or fewer by default) may match unrelated\n * acronyms in different fields, producing noisy results.\n *\n * @param ast - Parsed QueryAST\n * @param threshold - Maximum length to consider \"short\" (default: 3)\n * @returns Array of unique short keywords found\n */\nexport function detectShortKeywords(ast: QueryAST, threshold = 3): string[] {\n const shortKeywords = new Set<string>();\n\n for (const block of ast.blocks) {\n for (const keyword of block.terms.keywords) {\n if (keyword.length <= threshold) {\n shortKeywords.add(keyword);\n }\n }\n }\n\n return Array.from(shortKeywords);\n}\n"],"names":[],"mappings":";;;AAmBO,SAAS,iBAAiB,MAAwB;AACvD,QAAM,OAAO,MAAM,IAAI;AACvB,SAAO,kBAAkB,IAAI;AAC/B;AASA,eAAsB,eAAe,UAAqC;AACxE,QAAM,UAAU,MAAM,SAAS,UAAU,OAAO;AAChD,SAAO,iBAAiB,OAAO;AACjC;AAaO,SAAS,oBAAoB,KAAe,YAAY,GAAa;AAC1E,QAAM,oCAAoB,IAAA;AAE1B,aAAW,SAAS,IAAI,QAAQ;AAC9B,eAAW,WAAW,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"parser.js","sources":["../../src/query/parser.ts"],"sourcesContent":["/**\n * Query YAML Parser\n *\n * Parses YAML query files into validated QueryAST.\n * See spec/models/query-dsl.md for the full specification.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport { parse } from 'yaml';\nimport type { QueryAST } from './types.js';\nimport { validateQueryFile } from './validator.js';\n\n/**\n * Parse a YAML string into a validated QueryAST.\n *\n * @param yaml - YAML string to parse\n * @returns Validated QueryAST\n * @throws Error if YAML is invalid or doesn't match schema\n */\nexport function parseQueryString(yaml: string): QueryAST {\n const data = parse(yaml);\n return validateQueryFile(data);\n}\n\n/**\n * Parse a YAML file into a validated QueryAST.\n *\n * @param filePath - Path to the YAML file\n * @returns Promise resolving to validated QueryAST\n * @throws Error if file doesn't exist, YAML is invalid, or doesn't match schema\n */\nexport async function parseQueryFile(filePath: string): Promise<QueryAST> {\n const content = await readFile(filePath, 'utf-8');\n return parseQueryString(content);\n}\n\n\n/**\n * Detect short keywords (potential acronyms) in a query.\n *\n * Short keywords (3 characters or fewer by default) may match unrelated\n * acronyms in different fields, producing noisy results.\n *\n * @param ast - Parsed QueryAST\n * @param threshold - Maximum length to consider \"short\" (default: 3)\n * @returns Array of unique short keywords found\n */\nexport function detectShortKeywords(ast: QueryAST, threshold = 3): string[] {\n const shortKeywords = new Set<string>();\n\n for (const block of ast.blocks) {\n for (const keyword of block.terms.keywords ?? []) {\n if (keyword.length <= threshold) {\n shortKeywords.add(keyword);\n }\n }\n }\n\n return Array.from(shortKeywords);\n}\n"],"names":[],"mappings":";;;AAmBO,SAAS,iBAAiB,MAAwB;AACvD,QAAM,OAAO,MAAM,IAAI;AACvB,SAAO,kBAAkB,IAAI;AAC/B;AASA,eAAsB,eAAe,UAAqC;AACxE,QAAM,UAAU,MAAM,SAAS,UAAU,OAAO;AAChD,SAAO,iBAAiB,OAAO;AACjC;AAaO,SAAS,oBAAoB,KAAe,YAAY,GAAa;AAC1E,QAAM,oCAAoB,IAAA;AAE1B,aAAW,SAAS,IAAI,QAAQ;AAC9B,eAAW,WAAW,MAAM,MAAM,YAAY,CAAA,GAAI;AAChD,UAAI,QAAQ,UAAU,WAAW;AAC/B,sBAAc,IAAI,OAAO;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,aAAa;AACjC;"}
|
package/dist/query/types.d.ts
CHANGED
|
@@ -15,10 +15,10 @@ export type Operator = 'AND' | 'OR';
|
|
|
15
15
|
*/
|
|
16
16
|
export interface TermBlock {
|
|
17
17
|
/** Free-text keywords (supported by all databases) */
|
|
18
|
-
keywords
|
|
18
|
+
keywords?: string[] | undefined;
|
|
19
19
|
/** MeSH terms (PubMed only) */
|
|
20
20
|
mesh?: string[] | undefined;
|
|
21
|
-
/** Emtree terms (Embase
|
|
21
|
+
/** Emtree terms (Embase/Scopus) */
|
|
22
22
|
emtree?: string[] | undefined;
|
|
23
23
|
/** ERIC Descriptors (ERIC only) */
|
|
24
24
|
eric?: string[] | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/query/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,YAAY,EAAE,YAAY,EAAE,CAAC;AAE7B;;;GAGG;AACH,MAAM,MAAM,SAAS,GACjB,OAAO,GACP,UAAU,GACV,gBAAgB,GAChB,QAAQ,GACR,SAAS,GACT,KAAK,CAAC;AAEV;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,IAAI,CAAC;AAEpC;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,sDAAsD;IACtD,QAAQ,EAAE,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/query/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,YAAY,EAAE,YAAY,EAAE,CAAC;AAE7B;;;GAGG;AACH,MAAM,MAAM,SAAS,GACjB,OAAO,GACP,UAAU,GACV,gBAAgB,GAChB,QAAQ,GACR,SAAS,GACT,KAAK,CAAC;AAEV;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,IAAI,CAAC;AAEpC;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAChC,+BAA+B;IAC/B,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC5B,mCAAmC;IACnC,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC9B,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC5B,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,kCAAkC;IAClC,KAAK,EAAE,SAAS,CAAC;IACjB,mBAAmB;IACnB,KAAK,EAAE,SAAS,CAAC;IACjB,6CAA6C;IAC7C,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,mCAAmC;IACnC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC/B,mCAAmC;IACnC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,OAAO;IACtB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IACjC,+BAA+B;IAC/B,gBAAgB,CAAC,EAAE,qBAAqB,GAAG,SAAS,CAAC;CACtD;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,gDAAgD;IAChD,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,oCAAoC;IACpC,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAClC,iCAAiC;IACjC,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;CACpC;AAED;;;GAGG;AACH,MAAM,WAAW,QAAQ;IACvB,uBAAuB;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,4CAA4C;IAC5C,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,qBAAqB;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,kCAAkC;IAClC,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa,GAAG,SAAS,CAAC,CAAC,CAAC;CACrE"}
|
|
@@ -15,7 +15,7 @@ export declare const fieldTypeSchema: z.ZodEnum<{
|
|
|
15
15
|
* Schema for term block containing search terms.
|
|
16
16
|
*/
|
|
17
17
|
export declare const termBlockSchema: z.ZodObject<{
|
|
18
|
-
keywords: z.ZodArray<z.ZodString
|
|
18
|
+
keywords: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
19
19
|
mesh: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
20
20
|
emtree: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
21
21
|
eric: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -41,7 +41,7 @@ export declare const queryBlockSchema: z.ZodObject<{
|
|
|
41
41
|
all: "all";
|
|
42
42
|
}>;
|
|
43
43
|
terms: z.ZodObject<{
|
|
44
|
-
keywords: z.ZodArray<z.ZodString
|
|
44
|
+
keywords: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
45
45
|
mesh: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
46
46
|
emtree: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
47
47
|
eric: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -156,7 +156,7 @@ export declare const queryFileSchema: z.ZodPipe<z.ZodObject<{
|
|
|
156
156
|
all: "all";
|
|
157
157
|
}>;
|
|
158
158
|
terms: z.ZodObject<{
|
|
159
|
-
keywords: z.ZodArray<z.ZodString
|
|
159
|
+
keywords: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
160
160
|
mesh: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
161
161
|
emtree: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
162
162
|
eric: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -422,7 +422,7 @@ export declare const queryFileSchema: z.ZodPipe<z.ZodObject<{
|
|
|
422
422
|
blocks: {
|
|
423
423
|
field: "title" | "abstract" | "title_abstract" | "author" | "keyword" | "all";
|
|
424
424
|
terms: {
|
|
425
|
-
keywords
|
|
425
|
+
keywords?: string[] | undefined;
|
|
426
426
|
mesh?: string[] | undefined;
|
|
427
427
|
emtree?: string[] | undefined;
|
|
428
428
|
eric?: string[] | undefined;
|
|
@@ -524,7 +524,7 @@ export declare const queryFileSchema: z.ZodPipe<z.ZodObject<{
|
|
|
524
524
|
query: {
|
|
525
525
|
field: "title" | "abstract" | "title_abstract" | "author" | "keyword" | "all";
|
|
526
526
|
terms: {
|
|
527
|
-
keywords
|
|
527
|
+
keywords?: string[] | undefined;
|
|
528
528
|
mesh?: string[] | undefined;
|
|
529
529
|
emtree?: string[] | undefined;
|
|
530
530
|
eric?: string[] | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/query/validator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;GAEG;AACH,eAAO,MAAM,eAAe;;;;;;;EAO1B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,eAAe;;;;;;
|
|
1
|
+
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/query/validator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;GAEG;AACH,eAAO,MAAM,eAAe;;;;;;;EAO1B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,eAAe;;;;;;iBAS3B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc;;;EAAwB,CAAC;AAEpD;;GAEG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;iBAI3B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,2BAA2B;;;iBAGtC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;GAcrB,CAAC;AAEN;;GAEG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwB3B,CAAC;AAEN;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;;;EAO7B,CAAC;AAiBH;;;GAGG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAcvB,CAAC;AAEN;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,CAEzD;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,KAAK;aAEtB,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM,EAC5B,OAAO,EAAE,MAAM;CAKlB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,OAAO,GAAG,eAAe,EAAE,CAWvE"}
|
package/dist/query/validator.js
CHANGED
|
@@ -8,12 +8,15 @@ const fieldTypeSchema = z.enum([
|
|
|
8
8
|
"all"
|
|
9
9
|
]);
|
|
10
10
|
const termBlockSchema = z.object({
|
|
11
|
-
keywords: z.array(z.string()).min(1),
|
|
11
|
+
keywords: z.array(z.string()).min(1).optional(),
|
|
12
12
|
mesh: z.array(z.string()).optional(),
|
|
13
13
|
emtree: z.array(z.string()).optional(),
|
|
14
14
|
eric: z.array(z.string()).optional(),
|
|
15
15
|
exclude: z.array(z.string()).optional()
|
|
16
|
-
})
|
|
16
|
+
}).refine(
|
|
17
|
+
(data) => data.keywords?.length || data.mesh?.length || data.emtree?.length || data.eric?.length,
|
|
18
|
+
{ message: "At least one of keywords, mesh, emtree, or eric is required" }
|
|
19
|
+
);
|
|
17
20
|
const operatorSchema = z.enum(["AND", "OR"]);
|
|
18
21
|
const queryBlockSchema = z.object({
|
|
19
22
|
field: fieldTypeSchema,
|