@cyanheads/openalex-mcp-server 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/AGENTS.md +25 -7
  2. package/CLAUDE.md +25 -7
  3. package/Dockerfile +17 -8
  4. package/README.md +28 -19
  5. package/changelog/0.10.x/0.10.0.md +30 -0
  6. package/changelog/0.9.x/0.9.0.md +48 -0
  7. package/dist/config/server-config.d.ts +2 -0
  8. package/dist/config/server-config.d.ts.map +1 -1
  9. package/dist/config/server-config.js +10 -0
  10. package/dist/config/server-config.js.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp-server/prompts/definitions/literature-review.prompt.js +1 -1
  14. package/dist/mcp-server/prompts/definitions/research-landscape.prompt.js +1 -1
  15. package/dist/mcp-server/tools/definitions/analyze-trends.tool.d.ts +35 -4
  16. package/dist/mcp-server/tools/definitions/analyze-trends.tool.d.ts.map +1 -1
  17. package/dist/mcp-server/tools/definitions/analyze-trends.tool.js +86 -41
  18. package/dist/mcp-server/tools/definitions/analyze-trends.tool.js.map +1 -1
  19. package/dist/mcp-server/tools/definitions/citation-graph.tool.d.ts +48 -5
  20. package/dist/mcp-server/tools/definitions/citation-graph.tool.d.ts.map +1 -1
  21. package/dist/mcp-server/tools/definitions/citation-graph.tool.js +77 -24
  22. package/dist/mcp-server/tools/definitions/citation-graph.tool.js.map +1 -1
  23. package/dist/mcp-server/tools/definitions/describe-fields.tool.d.ts.map +1 -1
  24. package/dist/mcp-server/tools/definitions/describe-fields.tool.js +34 -4
  25. package/dist/mcp-server/tools/definitions/describe-fields.tool.js.map +1 -1
  26. package/dist/mcp-server/tools/definitions/resolve-name.tool.d.ts +43 -1
  27. package/dist/mcp-server/tools/definitions/resolve-name.tool.d.ts.map +1 -1
  28. package/dist/mcp-server/tools/definitions/resolve-name.tool.js +182 -40
  29. package/dist/mcp-server/tools/definitions/resolve-name.tool.js.map +1 -1
  30. package/dist/mcp-server/tools/definitions/search-entities.tool.d.ts +64 -6
  31. package/dist/mcp-server/tools/definitions/search-entities.tool.d.ts.map +1 -1
  32. package/dist/mcp-server/tools/definitions/search-entities.tool.js +138 -35
  33. package/dist/mcp-server/tools/definitions/search-entities.tool.js.map +1 -1
  34. package/dist/mcp-server/tools/page-size.d.ts +18 -0
  35. package/dist/mcp-server/tools/page-size.d.ts.map +1 -0
  36. package/dist/mcp-server/tools/page-size.js +28 -0
  37. package/dist/mcp-server/tools/page-size.js.map +1 -0
  38. package/dist/mcp-server/tools/render-budget.d.ts +4 -2
  39. package/dist/mcp-server/tools/render-budget.d.ts.map +1 -1
  40. package/dist/mcp-server/tools/render-budget.js +4 -2
  41. package/dist/mcp-server/tools/render-budget.js.map +1 -1
  42. package/dist/mcp-server/tools/response-budget.d.ts +3 -3
  43. package/dist/mcp-server/tools/response-budget.d.ts.map +1 -1
  44. package/dist/mcp-server/tools/response-budget.js +64 -35
  45. package/dist/mcp-server/tools/response-budget.js.map +1 -1
  46. package/dist/services/openalex/budget.d.ts +3 -1
  47. package/dist/services/openalex/budget.d.ts.map +1 -1
  48. package/dist/services/openalex/budget.js +3 -1
  49. package/dist/services/openalex/budget.js.map +1 -1
  50. package/dist/services/openalex/field-catalog.json +50 -1
  51. package/dist/services/openalex/filter-values.d.ts +48 -0
  52. package/dist/services/openalex/filter-values.d.ts.map +1 -0
  53. package/dist/services/openalex/filter-values.js +350 -0
  54. package/dist/services/openalex/filter-values.js.map +1 -0
  55. package/dist/services/openalex/iso-codes.json +1565 -0
  56. package/dist/services/openalex/openalex-service.d.ts +176 -20
  57. package/dist/services/openalex/openalex-service.d.ts.map +1 -1
  58. package/dist/services/openalex/openalex-service.js +573 -127
  59. package/dist/services/openalex/openalex-service.js.map +1 -1
  60. package/dist/services/openalex/provider-text.d.ts +7 -6
  61. package/dist/services/openalex/provider-text.d.ts.map +1 -1
  62. package/dist/services/openalex/provider-text.js +485 -61
  63. package/dist/services/openalex/provider-text.js.map +1 -1
  64. package/dist/services/openalex/types.d.ts +5 -2
  65. package/dist/services/openalex/types.d.ts.map +1 -1
  66. package/package.json +14 -12
  67. package/server.json +35 -21
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter-values.d.ts","sourceRoot":"","sources":["../../../src/services/openalex/filter-values.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAuBH;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,qFAAqF;AACrF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAkDxE;AAoID;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAY/E;AA4ED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAuCtE"}
@@ -0,0 +1,350 @@
1
+ /**
2
+ * @fileoverview A filter value in the form OpenAlex reads: country and `language` values
3
+ * converted to the codes OpenAlex stores; a value with spaces or a comma on a key OpenAlex matches
4
+ * as whole text — a name, city, region, raw affiliation string, award number, or `biblio` field —
5
+ * double-quoted, so its spaces and commas stay part of it; and the AND operator — a `+`, or on a
6
+ * key where words mean AND, a space between them — sent as the AND it spells. Values that are
7
+ * text rather than filter syntax keep their `+` literal: free-text search phrases, DOIs, dates,
8
+ * URLs, exactly matched names, `biblio` fields, award numbers, and any value the caller wrapped
9
+ * in quotes. A value that puts an AND operator beside a `|` (OR) fails, since OpenAlex splits on
10
+ * `|` first and drops every alternative that ANDs more than one value. A value sent double-quoted
11
+ * holds no `"` of its own, which would end the quoted phrase early and let the text after it
12
+ * parse as further filter clauses.
13
+ * @module services/openalex/filter-values
14
+ */
15
+ import { validationError } from '@cyanheads/mcp-ts-core/errors';
16
+ import isoCodes from './iso-codes.json' with { type: 'json' };
17
+ /**
18
+ * Lookups from the bundled ISO 3166-1 / ISO 639-2 table (`scripts/generate-iso-codes.ts`).
19
+ * Maps rather than the JSON objects, so a segment such as `constructor` or `__proto__` reads as
20
+ * no entry instead of an inherited member.
21
+ */
22
+ const COUNTRY_BY_ALPHA3 = new Map(Object.entries(isoCodes.countries.alpha3));
23
+ const COUNTRY_BY_NUMERIC = new Map(Object.entries(isoCodes.countries.numeric));
24
+ const COUNTRY_BY_NAME = new Map(Object.entries(isoCodes.countries.names));
25
+ const LANGUAGE_BY_ALPHA3 = new Map(Object.entries(isoCodes.languages.alpha3));
26
+ const LANGUAGE_BY_NAME = new Map(Object.entries(isoCodes.languages.names));
27
+ const TWO_LETTERS = /^[A-Za-z]{2}$/;
28
+ const THREE_LETTERS = /^[A-Za-z]{3}$/;
29
+ /** The most of a caller's text an error message quotes; the error's `data` keeps it whole. */
30
+ const ECHO_MAX_CHARS = 200;
31
+ /**
32
+ * Caller text as an error message quotes it: JSON-encoded, so a line break stays escaped, and
33
+ * capped, so caller-sized text cannot bloat the message.
34
+ */
35
+ export function quoteCallerText(text) {
36
+ return JSON.stringify(text.length > ECHO_MAX_CHARS ? `${text.slice(0, ECHO_MAX_CHARS)}…` : text);
37
+ }
38
+ /** Parse native clauses without splitting quoted commas or colons inside a value. */
39
+ export function parseNativeFilters(input) {
40
+ const filters = Object.create(null);
41
+ let quoted = false;
42
+ let start = 0;
43
+ let colon = -1;
44
+ for (let index = 0; index <= input.length; index++) {
45
+ const char = input[index];
46
+ if (char === '"')
47
+ quoted = !quoted;
48
+ if (char === ':' && colon === -1 && !quoted)
49
+ colon = index;
50
+ if (index !== input.length && (char !== ',' || quoted))
51
+ continue;
52
+ if (quoted) {
53
+ throw validationError('Native filter string has unmatched double quotes. Pair the quotes, or pass filters as a record object with one key per field.', {
54
+ reason: 'invalid_filter_string',
55
+ recovery: {
56
+ hint: 'Pass filters as a record object with one key per field, and pair any double quotes in search phrases.',
57
+ },
58
+ });
59
+ }
60
+ const key = input.slice(start, colon === -1 ? start : colon).trim();
61
+ if (colon < start || key.length === 0) {
62
+ throw validationError('Each native filter clause needs a nonempty key followed by a colon. Pass filters as a record object with one key per field.', {
63
+ reason: 'invalid_filter_string',
64
+ recovery: {
65
+ hint: 'Pass filters as a record object with one key per field, and pair any double quotes in search phrases.',
66
+ },
67
+ });
68
+ }
69
+ if (Object.hasOwn(filters, key)) {
70
+ throw validationError('Native filter string repeats a key. A filters record object holds one key per field; combine alternatives with "|", or run separate calls.', {
71
+ reason: 'duplicate_filter_string_key',
72
+ filterKey: key,
73
+ recovery: {
74
+ hint: 'A filters record object holds one key per field; combine alternatives with "|", or run separate calls.',
75
+ },
76
+ });
77
+ }
78
+ filters[key] = input.slice(colon + 1, index);
79
+ start = index + 1;
80
+ colon = -1;
81
+ }
82
+ return filters;
83
+ }
84
+ /** Reject an unpaired quote only on values that remain unquoted on the upstream wire. */
85
+ function assertPairedQuotes(key, value) {
86
+ let quoted = false;
87
+ for (let index = value.indexOf('"'); index !== -1; index = value.indexOf('"', index + 1))
88
+ quoted = !quoted;
89
+ if (quoted) {
90
+ throw validationError(`Filter ${quoteCallerText(key)} value has an unmatched double quote. Pair double quotes used for a search phrase, or remove the stray quote.`, { reason: 'unmatched_quote_in_filter_value', filterKey: key });
91
+ }
92
+ }
93
+ /**
94
+ * Reject a double-quoted value, `sent` exactly as the clause sends it, when a `"` sits inside its
95
+ * outer pair (a leading `!` aside). OpenAlex ends a quoted phrase at the next `"` with no escape
96
+ * for one inside, so an interior quote closes the phrase early and the text after it reads as
97
+ * further filter clauses. Checking the value as sent, after any wrapping, is what catches a
98
+ * caller's quote pair the wrap encloses (`! "New York"` goes out as `"! "New York""`). Runs before
99
+ * any request.
100
+ */
101
+ function assertNoInteriorQuote(key, sent) {
102
+ const trimmed = sent.trim();
103
+ const body = trimmed.startsWith('!') ? trimmed.slice(1) : trimmed;
104
+ const inner = body.length >= 2 && body.startsWith('"') && body.endsWith('"') ? body.slice(1, -1) : body;
105
+ if (inner.includes('"')) {
106
+ throw validationError(`Filter ${quoteCallerText(key)} value is sent to OpenAlex as a quoted phrase and cannot contain a double quote of its own. Remove the double quote from the value.`, { reason: 'quote_in_filter_value', filterKey: key });
107
+ }
108
+ }
109
+ /**
110
+ * The key suffixes OpenAlex country-code fields end in, across every entity type — and nothing
111
+ * else: `countries_distinct_count` is a number.
112
+ */
113
+ const COUNTRY_KEY_SUFFIXES = ['country_code', 'country_codes', 'countries'];
114
+ /** A `.search` segment, at the end of the key or followed by a suffix such as `.exact`. */
115
+ const FREE_TEXT_SEARCH_KEY = /\.search(?:\.|$)/;
116
+ /**
117
+ * Whether `key` is a free-text search filter — `title.search`, `abstract.search.exact`,
118
+ * `title_abstract_keywords.search`, `semantic.search` — whose value is a phrase rather than
119
+ * filter syntax. `quotedCommaValue()` sends such a value holding a comma double-quoted, so the
120
+ * comma stays part of the phrase, and `upstreamFilterValue()` keeps its `+` literal.
121
+ */
122
+ function isFreeTextSearchKey(key) {
123
+ return FREE_TEXT_SEARCH_KEY.test(key);
124
+ }
125
+ /**
126
+ * Whether a `+` in this (hyphen-normalized) key's value is a literal character, because the value
127
+ * is text rather than filter syntax: a free-text search filter; a DOI filter (`doi`,
128
+ * `doi_starts_with`, `awards.doi`, `ids.doi`), whose values include SICI DOIs such as
129
+ * `10.1002/1097-0142(19940801)74:3+<1030::…>`; a date (every `_date` key), where the `+hh:mm` of
130
+ * a timestamp — which `from_created_date` and `to_updated_date` take, unlike `publication_date` —
131
+ * is its UTC offset; a URL
132
+ * (`primary_location.landing_page_url`), which can carry a `+` of its own — every `_url` key but a
133
+ * `has_` boolean such as `has_pdf_url`; a name matched exactly (`display_name`,
134
+ * `awards.funder_display_name`), such as the keyword `C++` or the funder `Erasmus+`; a `biblio`
135
+ * field, such as the combined issue `1+2`; and an award number as its funder deposited it
136
+ * (`awards.funder_award_id`).
137
+ */
138
+ function keepsPlusLiteral(field) {
139
+ if (isFreeTextSearchKey(field) || field.startsWith('biblio.'))
140
+ return true;
141
+ const leaf = field.slice(field.lastIndexOf('.') + 1);
142
+ return (leaf === 'doi' ||
143
+ leaf === 'doi_starts_with' ||
144
+ leaf === 'display_name' ||
145
+ leaf === 'funder_display_name' ||
146
+ leaf === 'funder_award_id' ||
147
+ leaf.endsWith('_date') ||
148
+ (leaf.endsWith('_url') && !leaf.startsWith('has_')));
149
+ }
150
+ /**
151
+ * The leaves of the (hyphen-normalized) keys whose values OpenAlex matches as whole text and
152
+ * stores with spaces: names (`display_name`, `awards.funder_display_name`), cities and regions
153
+ * (`geo.city`, `authorships.institutions.geo.region`), raw affiliation strings, award numbers as
154
+ * their funder deposited them (`DE AC02 05CH11231`), and an author's `block_key` (`j priem`).
155
+ * Every `biblio` field (issue `Supplement 1`) and `parsed_longest_name` part is such a key too.
156
+ */
157
+ const PHRASE_KEY_LEAVES = new Set([
158
+ 'display_name',
159
+ 'funder_display_name',
160
+ 'funder_award_id',
161
+ 'city',
162
+ 'region',
163
+ 'raw_affiliation_strings',
164
+ 'block_key',
165
+ ]);
166
+ function isPhraseKey(field) {
167
+ return (field.startsWith('biblio.') ||
168
+ field.startsWith('parsed_longest_name.') ||
169
+ PHRASE_KEY_LEAVES.has(field.slice(field.lastIndexOf('.') + 1)));
170
+ }
171
+ /** Whether `value` is already wrapped in double quotes, a leading `!` aside: `!"New York"`. */
172
+ function isQuoted(value) {
173
+ const body = value.startsWith('!') ? value.slice(1) : value;
174
+ return body.length >= 2 && body.startsWith('"') && body.endsWith('"');
175
+ }
176
+ /**
177
+ * The value as one double-quoted phrase, when a phrase key's value has a space between words and
178
+ * the caller has not quoted it; otherwise `undefined`. OpenAlex reads an unquoted space as AND,
179
+ * so `geo.city:New York` asks for a city named both `New` and `York` and matches nothing, while
180
+ * `geo.city:"New York"` matches the city. The quote goes around the whole value: OpenAlex strips
181
+ * it before reading `!` and `|`, so `"!New York"` excludes the city and `"New York|Boston"`
182
+ * matches either — and it reads a quote around a single `|` alternative as part of that
183
+ * alternative. It keeps edge whitespace inside the quotes (`" New York "` matches nothing), so the
184
+ * phrase is sent trimmed. A value with a `+` that means AND is left alone: no single clause ANDs a
185
+ * phrase with another value, quoted or not, so it goes out as it did before.
186
+ */
187
+ function quotedPhrase(field, value) {
188
+ if (!isPhraseKey(field))
189
+ return;
190
+ const phrase = value.trim();
191
+ if (!phrase.includes(' ') || isQuoted(phrase))
192
+ return;
193
+ if (phrase.includes('+') && !keepsPlusLiteral(field))
194
+ return;
195
+ return `"${phrase}"`;
196
+ }
197
+ /**
198
+ * A value holding a comma as its clause sends it, or `undefined` where the key takes no phrase
199
+ * and the comma could only read as OpenAlex's clause separator. A free-text search value or a
200
+ * phrase key's value goes out whole and double-quoted, as written — `|` still ORs inside the
201
+ * quotes, a leading `!` still negates the whole value, and a `+` is the literal character, which
202
+ * no clause could AND with a phrase anyway. A phrase key's value is trimmed first, as
203
+ * `quotedPhrase()` trims, and one the caller quoted (a leading `!` aside) is sent as written.
204
+ * Either way the value as sent cannot hold a `"` inside its outer pair, which would end the quoted
205
+ * phrase early (`quote_in_filter_value`).
206
+ */
207
+ export function quotedCommaValue(key, value) {
208
+ let sent;
209
+ if (isFreeTextSearchKey(key)) {
210
+ sent = value.startsWith('"') && value.endsWith('"') ? value : `"${value}"`;
211
+ }
212
+ else if (isPhraseKey(key.replaceAll('-', '_'))) {
213
+ const phrase = value.trim();
214
+ sent = isQuoted(phrase) ? phrase : `"${phrase}"`;
215
+ }
216
+ else {
217
+ return;
218
+ }
219
+ assertNoInteriorQuote(key, sent);
220
+ return sent;
221
+ }
222
+ /**
223
+ * One country segment as OpenAlex stores it: an alpha-3 code, numeric code, or English name —
224
+ * ISO's, or the one OpenAlex displays for the country (`Russia`, `Ivory Coast`) — becomes its
225
+ * alpha-2 code, and `UK` becomes `GB` (ISO 3166-1 reserves it for the United Kingdom). Two- and
226
+ * three-letter segments it does not map, `null`, empty segments, and the
227
+ * `countries/DE` path forms are sent as given — stored values include non-ISO two-letter codes
228
+ * (`XK`), MARC three-letter codes (`NYU`), and alpha-3 tags reachable only by path. Anything
229
+ * else cannot be a country code and matches nothing, so it fails here — a segment holding a space
230
+ * included, since a space is part of a name (`South Korea`) and never separates two codes
231
+ * (`DE US`), and the message says so.
232
+ */
233
+ function countrySegment(segment, key) {
234
+ if (segment === '' || segment === 'null' || segment.includes('/'))
235
+ return segment;
236
+ if (TWO_LETTERS.test(segment))
237
+ return segment.toUpperCase() === 'UK' ? 'GB' : segment;
238
+ if (THREE_LETTERS.test(segment))
239
+ return COUNTRY_BY_ALPHA3.get(segment.toUpperCase()) ?? segment;
240
+ const code = COUNTRY_BY_NUMERIC.get(segment) ?? COUNTRY_BY_NAME.get(segment.toLowerCase());
241
+ if (code !== undefined)
242
+ return code;
243
+ const spaced = segment.includes(' ')
244
+ ? ' A space is part of a country name ("South Korea"), never a separator: join codes with "|" for OR or "+" for AND.'
245
+ : '';
246
+ throw validationError(`Filter ${quoteCallerText(key)} value ${quoteCallerText(segment)} is not a country code. Country filters take ISO 3166-1 alpha-2 codes such as "DE"; an alpha-3 code ("DEU"), a numeric code ("276"), or an English country name ("Germany") is converted to one, and "null" matches records with no country.${spaced}`, { reason: 'invalid_country_code', filterKey: key, segment });
247
+ }
248
+ /**
249
+ * One `language` segment as OpenAlex stores it: an ISO 639-2 code with an ISO 639-1 equivalent
250
+ * becomes that code (`eng` → `en`), and an English language name becomes its code (`English` →
251
+ * `en`). Everything else is sent as given — stored values include three-letter codes with no
252
+ * ISO 639-1 equivalent (`ceb`), regional tags (`zh-cn`), and native names (`中文`) — so no
253
+ * language value is rejected.
254
+ */
255
+ function languageSegment(segment) {
256
+ if (TWO_LETTERS.test(segment))
257
+ return segment;
258
+ if (THREE_LETTERS.test(segment))
259
+ return LANGUAGE_BY_ALPHA3.get(segment.toLowerCase()) ?? segment;
260
+ return LANGUAGE_BY_NAME.get(segment.toLowerCase()) ?? segment;
261
+ }
262
+ /**
263
+ * Whether `value` puts a `|` beside a `+` that means AND, which no filter value can carry:
264
+ * OpenAlex splits a value on `|` first, and an alternative that ANDs more than one value matches
265
+ * nothing, so `DE|FR+US` answers with the German works alone. A `+` is no AND where
266
+ * `keepsPlusLiteral()`, nor inside a phrase key's value the caller wrapped in quotes, which
267
+ * OpenAlex reads as text split on `|`. Read from the caller's value before any conversion, so
268
+ * `Narnia|FR+US` fails for its operators, not its unknown country.
269
+ */
270
+ function mixesOrWithPlus(field, value) {
271
+ if (!value.includes('|') || !value.includes('+') || keepsPlusLiteral(field))
272
+ return false;
273
+ return !(isPhraseKey(field) && isQuoted(value.trim()));
274
+ }
275
+ /**
276
+ * Whether a `|` alternative of `value` holds a space where a space between words is the AND
277
+ * operator: on an ID or code key, `A1 A2|A3` drops to `A1|A3` (or matches nothing for a code)
278
+ * exactly as `A1+A2|A3` would. Read from the value as converted, where a country or `language`
279
+ * name is already its code: `South Korea|Japan` goes out as `KR|JP`, a multi-word country segment
280
+ * that is no name has failed as `invalid_country_code` (`DE US|FR`), and only a segment sent as
281
+ * given keeps its space — a `language` segment that names no language (`en fr|de`) or a country
282
+ * path form. Never on a phrase key, whose multi-word value is sent wholly double-quoted (its spaces
283
+ * literal), nor where `keepsPlusLiteral()`.
284
+ */
285
+ function mixesOrWithSpace(field, value) {
286
+ if (!value.includes('|') || keepsPlusLiteral(field) || isPhraseKey(field))
287
+ return false;
288
+ return value.split('|').some((alternative) => alternative.trim().includes(' '));
289
+ }
290
+ function mixedAndOrError(key) {
291
+ return validationError(`Filter ${quoteCallerText(key)} value mixes "|" (OR) with an AND operator — a "+" or a space — which one OpenAlex filter value cannot combine: OpenAlex splits the value on "|" first, and an alternative that ANDs more than one value matches nothing, so that alternative would be dropped without notice.`, { reason: 'mixed_and_or_in_filter_value', filterKey: key });
292
+ }
293
+ /**
294
+ * A filter value as its clause sends it. A value putting `|` beside an AND operator fails as
295
+ * `mixed_and_or_in_filter_value`: a `+` read from the caller's value (`mixesOrWithPlus()`), or a
296
+ * space on a key where words mean AND, read from the value as converted (`mixesOrWithSpace()`).
297
+ * On a country key or `language`, each `|`/`+` segment is trimmed and converted on its own, a
298
+ * leading `!` and the operators kept, so a multi-word name is one value (`South Korea|Japan` →
299
+ * `KR|JP`); upstream accepts a hyphenated key spelling, so `country-code` reads as
300
+ * `country_code`. A phrase key's multi-word value goes out as `quotedPhrase()` writes it. A value
301
+ * the caller wrapped in quotes goes out as written, its `+` a literal character. Otherwise, unless
302
+ * `keepsPlusLiteral()`, each `+` becomes a space: OpenAlex reads AND only from a decoded space — a
303
+ * raw `+` in the query string, which is what `URLSearchParams` sends for a space — and reads a
304
+ * `%2B` as a literal plus. Any value sent double-quoted is checked for a `"` of its own
305
+ * (`assertNoInteriorQuote()`).
306
+ */
307
+ export function upstreamFilterValue(key, value) {
308
+ const field = key.replaceAll('-', '_');
309
+ if (mixesOrWithPlus(field, value))
310
+ throw mixedAndOrError(key);
311
+ const convert = COUNTRY_KEY_SUFFIXES.some((suffix) => field.endsWith(suffix))
312
+ ? (segment) => countrySegment(segment, key)
313
+ : field === 'language'
314
+ ? languageSegment
315
+ : undefined;
316
+ const converted = convert === undefined
317
+ ? value
318
+ : value
319
+ .split(/([|+])/)
320
+ .map((part, index) => {
321
+ if (index % 2 === 1)
322
+ return part;
323
+ const trimmed = part.trim();
324
+ return trimmed.startsWith('!')
325
+ ? `!${convert(trimmed.slice(1).trim())}`
326
+ : convert(trimmed);
327
+ })
328
+ .join('');
329
+ if (mixesOrWithSpace(field, converted))
330
+ throw mixedAndOrError(key);
331
+ const phrase = quotedPhrase(field, converted);
332
+ if (phrase !== undefined) {
333
+ assertNoInteriorQuote(key, phrase);
334
+ return phrase;
335
+ }
336
+ /**
337
+ * A value the caller wrapped in quotes goes out as written, its `+` a literal character, and
338
+ * carries no `"` beyond that pair. Free-text search values are the exception: OpenAlex reads a
339
+ * `"` there as its own exact-phrase syntax, so a search value keeps its quotes and is left as-is.
340
+ */
341
+ if (!isFreeTextSearchKey(field) && isQuoted(converted.trim())) {
342
+ assertNoInteriorQuote(key, converted);
343
+ return converted;
344
+ }
345
+ assertPairedQuotes(key, converted);
346
+ if (keepsPlusLiteral(field))
347
+ return converted;
348
+ return converted.replaceAll('+', ' ');
349
+ }
350
+ //# sourceMappingURL=filter-values.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter-values.js","sourceRoot":"","sources":["../../../src/services/openalex/filter-values.ts"],"names":[],"mappings":"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"}