@digital-science-dsl/dimensions-analytics-mcp 0.5.3

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 (722) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +152 -0
  3. package/bin/dimensions-analytics-mcp-http.js +2 -0
  4. package/bin/dimensions-analytics-mcp.js +2 -0
  5. package/dist/client/auth/config-loaders.d.ts +22 -0
  6. package/dist/client/auth/config-loaders.d.ts.map +1 -0
  7. package/dist/client/auth/config-loaders.js +68 -0
  8. package/dist/client/auth/config-loaders.js.map +1 -0
  9. package/dist/client/auth/decorators/index.d.ts +7 -0
  10. package/dist/client/auth/decorators/index.d.ts.map +1 -0
  11. package/dist/client/auth/decorators/index.js +6 -0
  12. package/dist/client/auth/decorators/index.js.map +1 -0
  13. package/dist/client/auth/decorators/with-caching.d.ts +81 -0
  14. package/dist/client/auth/decorators/with-caching.d.ts.map +1 -0
  15. package/dist/client/auth/decorators/with-caching.js +168 -0
  16. package/dist/client/auth/decorators/with-caching.js.map +1 -0
  17. package/dist/client/auth/factory.d.ts +13 -0
  18. package/dist/client/auth/factory.d.ts.map +1 -0
  19. package/dist/client/auth/factory.js +29 -0
  20. package/dist/client/auth/factory.js.map +1 -0
  21. package/dist/client/auth/index.d.ts +7 -0
  22. package/dist/client/auth/index.d.ts.map +1 -0
  23. package/dist/client/auth/index.js +6 -0
  24. package/dist/client/auth/index.js.map +1 -0
  25. package/dist/client/auth/jwt-utils.d.ts +17 -0
  26. package/dist/client/auth/jwt-utils.d.ts.map +1 -0
  27. package/dist/client/auth/jwt-utils.js +33 -0
  28. package/dist/client/auth/jwt-utils.js.map +1 -0
  29. package/dist/client/auth/node.d.ts +16 -0
  30. package/dist/client/auth/node.d.ts.map +1 -0
  31. package/dist/client/auth/node.js +15 -0
  32. package/dist/client/auth/node.js.map +1 -0
  33. package/dist/client/auth/providers/index.d.ts +2 -0
  34. package/dist/client/auth/providers/index.d.ts.map +1 -0
  35. package/dist/client/auth/providers/index.js +2 -0
  36. package/dist/client/auth/providers/index.js.map +1 -0
  37. package/dist/client/auth/providers/jwt-auth-provider.d.ts +70 -0
  38. package/dist/client/auth/providers/jwt-auth-provider.d.ts.map +1 -0
  39. package/dist/client/auth/providers/jwt-auth-provider.js +134 -0
  40. package/dist/client/auth/providers/jwt-auth-provider.js.map +1 -0
  41. package/dist/client/auth/schemas.d.ts +19 -0
  42. package/dist/client/auth/schemas.d.ts.map +1 -0
  43. package/dist/client/auth/schemas.js +23 -0
  44. package/dist/client/auth/schemas.js.map +1 -0
  45. package/dist/client/auth/types.d.ts +25 -0
  46. package/dist/client/auth/types.d.ts.map +1 -0
  47. package/dist/client/auth/types.js +9 -0
  48. package/dist/client/auth/types.js.map +1 -0
  49. package/dist/client/command.d.ts +44 -0
  50. package/dist/client/command.d.ts.map +1 -0
  51. package/dist/client/command.js +23 -0
  52. package/dist/client/command.js.map +1 -0
  53. package/dist/client/config/index.d.ts +11 -0
  54. package/dist/client/config/index.d.ts.map +1 -0
  55. package/dist/client/config/index.js +8 -0
  56. package/dist/client/config/index.js.map +1 -0
  57. package/dist/client/config/loader.d.ts +27 -0
  58. package/dist/client/config/loader.d.ts.map +1 -0
  59. package/dist/client/config/loader.js +128 -0
  60. package/dist/client/config/loader.js.map +1 -0
  61. package/dist/client/config/node.d.ts +18 -0
  62. package/dist/client/config/node.d.ts.map +1 -0
  63. package/dist/client/config/node.js +17 -0
  64. package/dist/client/config/node.js.map +1 -0
  65. package/dist/client/config/schemas.d.ts +23 -0
  66. package/dist/client/config/schemas.d.ts.map +1 -0
  67. package/dist/client/config/schemas.js +24 -0
  68. package/dist/client/config/schemas.js.map +1 -0
  69. package/dist/client/config/types.d.ts +37 -0
  70. package/dist/client/config/types.d.ts.map +1 -0
  71. package/dist/client/config/types.js +6 -0
  72. package/dist/client/config/types.js.map +1 -0
  73. package/dist/client/config/unified-loader.d.ts +41 -0
  74. package/dist/client/config/unified-loader.d.ts.map +1 -0
  75. package/dist/client/config/unified-loader.js +86 -0
  76. package/dist/client/config/unified-loader.js.map +1 -0
  77. package/dist/client/config.d.ts +43 -0
  78. package/dist/client/config.d.ts.map +1 -0
  79. package/dist/client/config.js +56 -0
  80. package/dist/client/config.js.map +1 -0
  81. package/dist/client/deployment-config.d.ts +36 -0
  82. package/dist/client/deployment-config.d.ts.map +1 -0
  83. package/dist/client/deployment-config.js +79 -0
  84. package/dist/client/deployment-config.js.map +1 -0
  85. package/dist/client/errors.d.ts +180 -0
  86. package/dist/client/errors.d.ts.map +1 -0
  87. package/dist/client/errors.js +253 -0
  88. package/dist/client/errors.js.map +1 -0
  89. package/dist/client/http-client.d.ts +215 -0
  90. package/dist/client/http-client.d.ts.map +1 -0
  91. package/dist/client/http-client.js +419 -0
  92. package/dist/client/http-client.js.map +1 -0
  93. package/dist/client/index.d.ts +28 -0
  94. package/dist/client/index.d.ts.map +1 -0
  95. package/dist/client/index.js +18 -0
  96. package/dist/client/index.js.map +1 -0
  97. package/dist/client/internal-dsl-client.d.ts +30 -0
  98. package/dist/client/internal-dsl-client.d.ts.map +1 -0
  99. package/dist/client/internal-dsl-client.js +101 -0
  100. package/dist/client/internal-dsl-client.js.map +1 -0
  101. package/dist/client/rate-limiter.d.ts +79 -0
  102. package/dist/client/rate-limiter.d.ts.map +1 -0
  103. package/dist/client/rate-limiter.js +118 -0
  104. package/dist/client/rate-limiter.js.map +1 -0
  105. package/dist/client/resolve-api-key-user.d.ts +24 -0
  106. package/dist/client/resolve-api-key-user.d.ts.map +1 -0
  107. package/dist/client/resolve-api-key-user.js +80 -0
  108. package/dist/client/resolve-api-key-user.js.map +1 -0
  109. package/dist/client/types.d.ts +44 -0
  110. package/dist/client/types.d.ts.map +1 -0
  111. package/dist/client/types.js +10 -0
  112. package/dist/client/types.js.map +1 -0
  113. package/dist/dsl/client.d.ts +330 -0
  114. package/dist/dsl/client.d.ts.map +1 -0
  115. package/dist/dsl/client.js +433 -0
  116. package/dist/dsl/client.js.map +1 -0
  117. package/dist/dsl/commands/apply-filters.d.ts +15 -0
  118. package/dist/dsl/commands/apply-filters.d.ts.map +1 -0
  119. package/dist/dsl/commands/apply-filters.js +29 -0
  120. package/dist/dsl/commands/apply-filters.js.map +1 -0
  121. package/dist/dsl/commands/facet/index.d.ts +6 -0
  122. package/dist/dsl/commands/facet/index.d.ts.map +1 -0
  123. package/dist/dsl/commands/facet/index.js +6 -0
  124. package/dist/dsl/commands/facet/index.js.map +1 -0
  125. package/dist/dsl/commands/facet/schemas.d.ts +71 -0
  126. package/dist/dsl/commands/facet/schemas.d.ts.map +1 -0
  127. package/dist/dsl/commands/facet/schemas.js +84 -0
  128. package/dist/dsl/commands/facet/schemas.js.map +1 -0
  129. package/dist/dsl/commands/functions/ClassifyCommand.d.ts +96 -0
  130. package/dist/dsl/commands/functions/ClassifyCommand.d.ts.map +1 -0
  131. package/dist/dsl/commands/functions/ClassifyCommand.js +95 -0
  132. package/dist/dsl/commands/functions/ClassifyCommand.js.map +1 -0
  133. package/dist/dsl/commands/functions/ExtractAffiliationsCommand.d.ts +86 -0
  134. package/dist/dsl/commands/functions/ExtractAffiliationsCommand.d.ts.map +1 -0
  135. package/dist/dsl/commands/functions/ExtractAffiliationsCommand.js +131 -0
  136. package/dist/dsl/commands/functions/ExtractAffiliationsCommand.js.map +1 -0
  137. package/dist/dsl/commands/functions/ExtractConceptsCommand.d.ts +73 -0
  138. package/dist/dsl/commands/functions/ExtractConceptsCommand.d.ts.map +1 -0
  139. package/dist/dsl/commands/functions/ExtractConceptsCommand.js +110 -0
  140. package/dist/dsl/commands/functions/ExtractConceptsCommand.js.map +1 -0
  141. package/dist/dsl/commands/functions/ExtractGrantsCommand.d.ts +66 -0
  142. package/dist/dsl/commands/functions/ExtractGrantsCommand.d.ts.map +1 -0
  143. package/dist/dsl/commands/functions/ExtractGrantsCommand.js +87 -0
  144. package/dist/dsl/commands/functions/ExtractGrantsCommand.js.map +1 -0
  145. package/dist/dsl/commands/functions/index.d.ts +10 -0
  146. package/dist/dsl/commands/functions/index.d.ts.map +1 -0
  147. package/dist/dsl/commands/functions/index.js +10 -0
  148. package/dist/dsl/commands/functions/index.js.map +1 -0
  149. package/dist/dsl/commands/index.d.ts +8 -0
  150. package/dist/dsl/commands/index.d.ts.map +1 -0
  151. package/dist/dsl/commands/index.js +7 -0
  152. package/dist/dsl/commands/index.js.map +1 -0
  153. package/dist/dsl/commands/validate-input.d.ts +18 -0
  154. package/dist/dsl/commands/validate-input.d.ts.map +1 -0
  155. package/dist/dsl/commands/validate-input.js +27 -0
  156. package/dist/dsl/commands/validate-input.js.map +1 -0
  157. package/dist/dsl/create-client.d.ts +32 -0
  158. package/dist/dsl/create-client.d.ts.map +1 -0
  159. package/dist/dsl/create-client.js +58 -0
  160. package/dist/dsl/create-client.js.map +1 -0
  161. package/dist/dsl/fluent-query-builder.d.ts +439 -0
  162. package/dist/dsl/fluent-query-builder.d.ts.map +1 -0
  163. package/dist/dsl/fluent-query-builder.js +534 -0
  164. package/dist/dsl/fluent-query-builder.js.map +1 -0
  165. package/dist/dsl/index.d.ts +32 -0
  166. package/dist/dsl/index.d.ts.map +1 -0
  167. package/dist/dsl/index.js +31 -0
  168. package/dist/dsl/index.js.map +1 -0
  169. package/dist/dsl/pagination.d.ts +82 -0
  170. package/dist/dsl/pagination.d.ts.map +1 -0
  171. package/dist/dsl/pagination.js +135 -0
  172. package/dist/dsl/pagination.js.map +1 -0
  173. package/dist/dsl/query-builder.d.ts +456 -0
  174. package/dist/dsl/query-builder.d.ts.map +1 -0
  175. package/dist/dsl/query-builder.js +994 -0
  176. package/dist/dsl/query-builder.js.map +1 -0
  177. package/dist/dsl/response-parser.d.ts +111 -0
  178. package/dist/dsl/response-parser.d.ts.map +1 -0
  179. package/dist/dsl/response-parser.js +210 -0
  180. package/dist/dsl/response-parser.js.map +1 -0
  181. package/dist/dsl/schema/cache.d.ts +52 -0
  182. package/dist/dsl/schema/cache.d.ts.map +1 -0
  183. package/dist/dsl/schema/cache.js +97 -0
  184. package/dist/dsl/schema/cache.js.map +1 -0
  185. package/dist/dsl/schema/extract.d.ts +18 -0
  186. package/dist/dsl/schema/extract.d.ts.map +1 -0
  187. package/dist/dsl/schema/extract.js +41 -0
  188. package/dist/dsl/schema/extract.js.map +1 -0
  189. package/dist/dsl/schema/index.d.ts +13 -0
  190. package/dist/dsl/schema/index.d.ts.map +1 -0
  191. package/dist/dsl/schema/index.js +12 -0
  192. package/dist/dsl/schema/index.js.map +1 -0
  193. package/dist/dsl/schema/load.d.ts +41 -0
  194. package/dist/dsl/schema/load.d.ts.map +1 -0
  195. package/dist/dsl/schema/load.js +119 -0
  196. package/dist/dsl/schema/load.js.map +1 -0
  197. package/dist/dsl/schema/store.d.ts +137 -0
  198. package/dist/dsl/schema/store.d.ts.map +1 -0
  199. package/dist/dsl/schema/store.js +204 -0
  200. package/dist/dsl/schema/store.js.map +1 -0
  201. package/dist/dsl/schema/structured-entities.d.ts +21 -0
  202. package/dist/dsl/schema/structured-entities.d.ts.map +1 -0
  203. package/dist/dsl/schema/structured-entities.js +39 -0
  204. package/dist/dsl/schema/structured-entities.js.map +1 -0
  205. package/dist/dsl/schema/summary.d.ts +51 -0
  206. package/dist/dsl/schema/summary.d.ts.map +1 -0
  207. package/dist/dsl/schema/summary.js +61 -0
  208. package/dist/dsl/schema/summary.js.map +1 -0
  209. package/dist/dsl/schema/types.d.ts +36 -0
  210. package/dist/dsl/schema/types.d.ts.map +1 -0
  211. package/dist/dsl/schema/types.js +6 -0
  212. package/dist/dsl/schema/types.js.map +1 -0
  213. package/dist/dsl/schema/validation.d.ts +21 -0
  214. package/dist/dsl/schema/validation.d.ts.map +1 -0
  215. package/dist/dsl/schema/validation.js +44 -0
  216. package/dist/dsl/schema/validation.js.map +1 -0
  217. package/dist/dsl/types/buckets.d.ts +18 -0
  218. package/dist/dsl/types/buckets.d.ts.map +1 -0
  219. package/dist/dsl/types/buckets.js +6 -0
  220. package/dist/dsl/types/buckets.js.map +1 -0
  221. package/dist/dsl/types/common.d.ts +91 -0
  222. package/dist/dsl/types/common.d.ts.map +1 -0
  223. package/dist/dsl/types/common.js +6 -0
  224. package/dist/dsl/types/common.js.map +1 -0
  225. package/dist/dsl/types/entities.d.ts +35 -0
  226. package/dist/dsl/types/entities.d.ts.map +1 -0
  227. package/dist/dsl/types/entities.js +11 -0
  228. package/dist/dsl/types/entities.js.map +1 -0
  229. package/dist/dsl/types/index.d.ts +12 -0
  230. package/dist/dsl/types/index.d.ts.map +1 -0
  231. package/dist/dsl/types/index.js +7 -0
  232. package/dist/dsl/types/index.js.map +1 -0
  233. package/dist/dsl/types/return-clauses.d.ts +107 -0
  234. package/dist/dsl/types/return-clauses.d.ts.map +1 -0
  235. package/dist/dsl/types/return-clauses.js +22 -0
  236. package/dist/dsl/types/return-clauses.js.map +1 -0
  237. package/dist/dsl/types/special-functions.d.ts +175 -0
  238. package/dist/dsl/types/special-functions.d.ts.map +1 -0
  239. package/dist/dsl/types/special-functions.js +25 -0
  240. package/dist/dsl/types/special-functions.js.map +1 -0
  241. package/dist/dsl/types/vocabulary.d.ts +21 -0
  242. package/dist/dsl/types/vocabulary.d.ts.map +1 -0
  243. package/dist/dsl/types/vocabulary.js +44 -0
  244. package/dist/dsl/types/vocabulary.js.map +1 -0
  245. package/dist/dsl/usage-policy.d.ts +87 -0
  246. package/dist/dsl/usage-policy.d.ts.map +1 -0
  247. package/dist/dsl/usage-policy.js +133 -0
  248. package/dist/dsl/usage-policy.js.map +1 -0
  249. package/dist/dsl/utils/escape.d.ts +24 -0
  250. package/dist/dsl/utils/escape.d.ts.map +1 -0
  251. package/dist/dsl/utils/escape.js +124 -0
  252. package/dist/dsl/utils/escape.js.map +1 -0
  253. package/dist/dsl/utils/sanitize.d.ts +13 -0
  254. package/dist/dsl/utils/sanitize.d.ts.map +1 -0
  255. package/dist/dsl/utils/sanitize.js +19 -0
  256. package/dist/dsl/utils/sanitize.js.map +1 -0
  257. package/dist/dsl/utils/string-utils.d.ts +35 -0
  258. package/dist/dsl/utils/string-utils.d.ts.map +1 -0
  259. package/dist/dsl/utils/string-utils.js +145 -0
  260. package/dist/dsl/utils/string-utils.js.map +1 -0
  261. package/dist/dsl/utils/validate-field-name.d.ts +11 -0
  262. package/dist/dsl/utils/validate-field-name.d.ts.map +1 -0
  263. package/dist/dsl/utils/validate-field-name.js +18 -0
  264. package/dist/dsl/utils/validate-field-name.js.map +1 -0
  265. package/dist/examples/dsl-examples.d.ts +11 -0
  266. package/dist/examples/dsl-examples.d.ts.map +1 -0
  267. package/dist/examples/dsl-examples.js +49 -0
  268. package/dist/examples/dsl-examples.js.map +1 -0
  269. package/dist/examples/usage-scenarios.d.ts +22 -0
  270. package/dist/examples/usage-scenarios.d.ts.map +1 -0
  271. package/dist/examples/usage-scenarios.js +173 -0
  272. package/dist/examples/usage-scenarios.js.map +1 -0
  273. package/dist/funder-org-names.d.ts +12 -0
  274. package/dist/funder-org-names.d.ts.map +1 -0
  275. package/dist/funder-org-names.js +30 -0
  276. package/dist/funder-org-names.js.map +1 -0
  277. package/dist/http-main.d.ts +7 -0
  278. package/dist/http-main.d.ts.map +1 -0
  279. package/dist/http-main.js +36 -0
  280. package/dist/http-main.js.map +1 -0
  281. package/dist/http.d.ts +29 -0
  282. package/dist/http.d.ts.map +1 -0
  283. package/dist/http.js +173 -0
  284. package/dist/http.js.map +1 -0
  285. package/dist/index.d.ts +9 -0
  286. package/dist/index.d.ts.map +1 -0
  287. package/dist/index.js +7 -0
  288. package/dist/index.js.map +1 -0
  289. package/dist/kwq/build-query.d.ts +26 -0
  290. package/dist/kwq/build-query.d.ts.map +1 -0
  291. package/dist/kwq/build-query.js +37 -0
  292. package/dist/kwq/build-query.js.map +1 -0
  293. package/dist/kwq/concept-groups.d.ts +49 -0
  294. package/dist/kwq/concept-groups.d.ts.map +1 -0
  295. package/dist/kwq/concept-groups.js +55 -0
  296. package/dist/kwq/concept-groups.js.map +1 -0
  297. package/dist/kwq/dsl-assembler.d.ts +28 -0
  298. package/dist/kwq/dsl-assembler.d.ts.map +1 -0
  299. package/dist/kwq/dsl-assembler.js +124 -0
  300. package/dist/kwq/dsl-assembler.js.map +1 -0
  301. package/dist/kwq/ensemble.d.ts +38 -0
  302. package/dist/kwq/ensemble.d.ts.map +1 -0
  303. package/dist/kwq/ensemble.js +107 -0
  304. package/dist/kwq/ensemble.js.map +1 -0
  305. package/dist/kwq/index.d.ts +16 -0
  306. package/dist/kwq/index.d.ts.map +1 -0
  307. package/dist/kwq/index.js +14 -0
  308. package/dist/kwq/index.js.map +1 -0
  309. package/dist/kwq/types.d.ts +120 -0
  310. package/dist/kwq/types.d.ts.map +1 -0
  311. package/dist/kwq/types.js +11 -0
  312. package/dist/kwq/types.js.map +1 -0
  313. package/dist/lambda.d.ts +10 -0
  314. package/dist/lambda.d.ts.map +1 -0
  315. package/dist/lambda.js +4 -0
  316. package/dist/lambda.js.map +1 -0
  317. package/dist/main.d.ts +8 -0
  318. package/dist/main.d.ts.map +1 -0
  319. package/dist/main.js +33 -0
  320. package/dist/main.js.map +1 -0
  321. package/dist/mcp/batch-fetch.d.ts +89 -0
  322. package/dist/mcp/batch-fetch.d.ts.map +1 -0
  323. package/dist/mcp/batch-fetch.js +178 -0
  324. package/dist/mcp/batch-fetch.js.map +1 -0
  325. package/dist/mcp/examples/dsl-examples.d.ts +21 -0
  326. package/dist/mcp/examples/dsl-examples.d.ts.map +1 -0
  327. package/dist/mcp/examples/dsl-examples.js +61 -0
  328. package/dist/mcp/examples/dsl-examples.js.map +1 -0
  329. package/dist/mcp/examples/usage-scenarios.d.ts +20 -0
  330. package/dist/mcp/examples/usage-scenarios.d.ts.map +1 -0
  331. package/dist/mcp/examples/usage-scenarios.js +169 -0
  332. package/dist/mcp/examples/usage-scenarios.js.map +1 -0
  333. package/dist/mcp/export-format.d.ts +43 -0
  334. package/dist/mcp/export-format.d.ts.map +1 -0
  335. package/dist/mcp/export-format.js +92 -0
  336. package/dist/mcp/export-format.js.map +1 -0
  337. package/dist/mcp/funder-org-names.d.ts +12 -0
  338. package/dist/mcp/funder-org-names.d.ts.map +1 -0
  339. package/dist/mcp/funder-org-names.js +30 -0
  340. package/dist/mcp/funder-org-names.js.map +1 -0
  341. package/dist/mcp/http-server.d.ts +20 -0
  342. package/dist/mcp/http-server.d.ts.map +1 -0
  343. package/dist/mcp/http-server.js +118 -0
  344. package/dist/mcp/http-server.js.map +1 -0
  345. package/dist/mcp/middleware/field-aliases.d.ts +97 -0
  346. package/dist/mcp/middleware/field-aliases.d.ts.map +1 -0
  347. package/dist/mcp/middleware/field-aliases.js +208 -0
  348. package/dist/mcp/middleware/field-aliases.js.map +1 -0
  349. package/dist/mcp/resources/schema.d.ts +13 -0
  350. package/dist/mcp/resources/schema.d.ts.map +1 -0
  351. package/dist/mcp/resources/schema.js +262 -0
  352. package/dist/mcp/resources/schema.js.map +1 -0
  353. package/dist/mcp/schema/context.d.ts +10 -0
  354. package/dist/mcp/schema/context.d.ts.map +1 -0
  355. package/dist/mcp/schema/context.js +6 -0
  356. package/dist/mcp/schema/context.js.map +1 -0
  357. package/dist/mcp/schema/index.d.ts +8 -0
  358. package/dist/mcp/schema/index.d.ts.map +1 -0
  359. package/dist/mcp/schema/index.js +6 -0
  360. package/dist/mcp/schema/index.js.map +1 -0
  361. package/dist/mcp/server.d.ts +56 -0
  362. package/dist/mcp/server.d.ts.map +1 -0
  363. package/dist/mcp/server.js +168 -0
  364. package/dist/mcp/server.js.map +1 -0
  365. package/dist/mcp/shared-schema.d.ts +13 -0
  366. package/dist/mcp/shared-schema.d.ts.map +1 -0
  367. package/dist/mcp/shared-schema.js +28 -0
  368. package/dist/mcp/shared-schema.js.map +1 -0
  369. package/dist/mcp/tools/analytics-filters.d.ts +18 -0
  370. package/dist/mcp/tools/analytics-filters.d.ts.map +1 -0
  371. package/dist/mcp/tools/analytics-filters.js +35 -0
  372. package/dist/mcp/tools/analytics-filters.js.map +1 -0
  373. package/dist/mcp/tools/analytics.d.ts +16 -0
  374. package/dist/mcp/tools/analytics.d.ts.map +1 -0
  375. package/dist/mcp/tools/analytics.js +381 -0
  376. package/dist/mcp/tools/analytics.js.map +1 -0
  377. package/dist/mcp/tools/concept-profile.d.ts +14 -0
  378. package/dist/mcp/tools/concept-profile.d.ts.map +1 -0
  379. package/dist/mcp/tools/concept-profile.js +130 -0
  380. package/dist/mcp/tools/concept-profile.js.map +1 -0
  381. package/dist/mcp/tools/dataset-profile.d.ts +15 -0
  382. package/dist/mcp/tools/dataset-profile.d.ts.map +1 -0
  383. package/dist/mcp/tools/dataset-profile.js +194 -0
  384. package/dist/mcp/tools/dataset-profile.js.map +1 -0
  385. package/dist/mcp/tools/fetch-search-pages.d.ts +15 -0
  386. package/dist/mcp/tools/fetch-search-pages.d.ts.map +1 -0
  387. package/dist/mcp/tools/fetch-search-pages.js +237 -0
  388. package/dist/mcp/tools/fetch-search-pages.js.map +1 -0
  389. package/dist/mcp/tools/functions.d.ts +14 -0
  390. package/dist/mcp/tools/functions.d.ts.map +1 -0
  391. package/dist/mcp/tools/functions.js +87 -0
  392. package/dist/mcp/tools/functions.js.map +1 -0
  393. package/dist/mcp/tools/impact-chain.d.ts +15 -0
  394. package/dist/mcp/tools/impact-chain.d.ts.map +1 -0
  395. package/dist/mcp/tools/impact-chain.js +96 -0
  396. package/dist/mcp/tools/impact-chain.js.map +1 -0
  397. package/dist/mcp/tools/kwq.d.ts +19 -0
  398. package/dist/mcp/tools/kwq.d.ts.map +1 -0
  399. package/dist/mcp/tools/kwq.js +156 -0
  400. package/dist/mcp/tools/kwq.js.map +1 -0
  401. package/dist/mcp/tools/lookup.d.ts +14 -0
  402. package/dist/mcp/tools/lookup.d.ts.map +1 -0
  403. package/dist/mcp/tools/lookup.js +146 -0
  404. package/dist/mcp/tools/lookup.js.map +1 -0
  405. package/dist/mcp/tools/policy-profile.d.ts +15 -0
  406. package/dist/mcp/tools/policy-profile.d.ts.map +1 -0
  407. package/dist/mcp/tools/policy-profile.js +174 -0
  408. package/dist/mcp/tools/policy-profile.js.map +1 -0
  409. package/dist/mcp/tools/profile.d.ts +14 -0
  410. package/dist/mcp/tools/profile.d.ts.map +1 -0
  411. package/dist/mcp/tools/profile.js +349 -0
  412. package/dist/mcp/tools/profile.js.map +1 -0
  413. package/dist/mcp/tools/query.d.ts +16 -0
  414. package/dist/mcp/tools/query.d.ts.map +1 -0
  415. package/dist/mcp/tools/query.js +75 -0
  416. package/dist/mcp/tools/query.js.map +1 -0
  417. package/dist/mcp/tools/schema.d.ts +23 -0
  418. package/dist/mcp/tools/schema.d.ts.map +1 -0
  419. package/dist/mcp/tools/schema.js +124 -0
  420. package/dist/mcp/tools/schema.js.map +1 -0
  421. package/dist/mcp/tools/search-entity-metadata.d.ts +18 -0
  422. package/dist/mcp/tools/search-entity-metadata.d.ts.map +1 -0
  423. package/dist/mcp/tools/search-entity-metadata.js +221 -0
  424. package/dist/mcp/tools/search-entity-metadata.js.map +1 -0
  425. package/dist/mcp/tools/search-input.d.ts +58 -0
  426. package/dist/mcp/tools/search-input.d.ts.map +1 -0
  427. package/dist/mcp/tools/search-input.js +76 -0
  428. package/dist/mcp/tools/search-input.js.map +1 -0
  429. package/dist/mcp/tools/search.d.ts +24 -0
  430. package/dist/mcp/tools/search.d.ts.map +1 -0
  431. package/dist/mcp/tools/search.js +115 -0
  432. package/dist/mcp/tools/search.js.map +1 -0
  433. package/dist/mcp/utils.d.ts +82 -0
  434. package/dist/mcp/utils.d.ts.map +1 -0
  435. package/dist/mcp/utils.js +126 -0
  436. package/dist/mcp/utils.js.map +1 -0
  437. package/dist/middleware/field-aliases.d.ts +97 -0
  438. package/dist/middleware/field-aliases.d.ts.map +1 -0
  439. package/dist/middleware/field-aliases.js +208 -0
  440. package/dist/middleware/field-aliases.js.map +1 -0
  441. package/dist/resources/schema.d.ts +13 -0
  442. package/dist/resources/schema.d.ts.map +1 -0
  443. package/dist/resources/schema.js +204 -0
  444. package/dist/resources/schema.js.map +1 -0
  445. package/dist/schema/context.d.ts +10 -0
  446. package/dist/schema/context.d.ts.map +1 -0
  447. package/dist/schema/context.js +6 -0
  448. package/dist/schema/context.js.map +1 -0
  449. package/dist/schema/index.d.ts +9 -0
  450. package/dist/schema/index.d.ts.map +1 -0
  451. package/dist/schema/index.js +6 -0
  452. package/dist/schema/index.js.map +1 -0
  453. package/dist/schema/load.d.ts +43 -0
  454. package/dist/schema/load.d.ts.map +1 -0
  455. package/dist/schema/load.js +139 -0
  456. package/dist/schema/load.js.map +1 -0
  457. package/dist/schema/store.d.ts +112 -0
  458. package/dist/schema/store.d.ts.map +1 -0
  459. package/dist/schema/store.js +173 -0
  460. package/dist/schema/store.js.map +1 -0
  461. package/dist/schema/structured-entities.d.ts +21 -0
  462. package/dist/schema/structured-entities.d.ts.map +1 -0
  463. package/dist/schema/structured-entities.js +39 -0
  464. package/dist/schema/structured-entities.js.map +1 -0
  465. package/dist/schema/types.d.ts +36 -0
  466. package/dist/schema/types.d.ts.map +1 -0
  467. package/dist/schema/types.js +6 -0
  468. package/dist/schema/types.js.map +1 -0
  469. package/dist/server.d.ts +57 -0
  470. package/dist/server.d.ts.map +1 -0
  471. package/dist/server.js +144 -0
  472. package/dist/server.js.map +1 -0
  473. package/dist/telemetry.d.ts +172 -0
  474. package/dist/telemetry.d.ts.map +1 -0
  475. package/dist/telemetry.js +233 -0
  476. package/dist/telemetry.js.map +1 -0
  477. package/dist/tools/analytics-filters.d.ts +18 -0
  478. package/dist/tools/analytics-filters.d.ts.map +1 -0
  479. package/dist/tools/analytics-filters.js +35 -0
  480. package/dist/tools/analytics-filters.js.map +1 -0
  481. package/dist/tools/analytics.d.ts +16 -0
  482. package/dist/tools/analytics.d.ts.map +1 -0
  483. package/dist/tools/analytics.js +381 -0
  484. package/dist/tools/analytics.js.map +1 -0
  485. package/dist/tools/concept-profile.d.ts +14 -0
  486. package/dist/tools/concept-profile.d.ts.map +1 -0
  487. package/dist/tools/concept-profile.js +130 -0
  488. package/dist/tools/concept-profile.js.map +1 -0
  489. package/dist/tools/dataset-profile.d.ts +15 -0
  490. package/dist/tools/dataset-profile.d.ts.map +1 -0
  491. package/dist/tools/dataset-profile.js +194 -0
  492. package/dist/tools/dataset-profile.js.map +1 -0
  493. package/dist/tools/functions.d.ts +14 -0
  494. package/dist/tools/functions.d.ts.map +1 -0
  495. package/dist/tools/functions.js +294 -0
  496. package/dist/tools/functions.js.map +1 -0
  497. package/dist/tools/impact-chain.d.ts +15 -0
  498. package/dist/tools/impact-chain.d.ts.map +1 -0
  499. package/dist/tools/impact-chain.js +96 -0
  500. package/dist/tools/impact-chain.js.map +1 -0
  501. package/dist/tools/kwq.d.ts +19 -0
  502. package/dist/tools/kwq.d.ts.map +1 -0
  503. package/dist/tools/kwq.js +156 -0
  504. package/dist/tools/kwq.js.map +1 -0
  505. package/dist/tools/lookup.d.ts +14 -0
  506. package/dist/tools/lookup.d.ts.map +1 -0
  507. package/dist/tools/lookup.js +213 -0
  508. package/dist/tools/lookup.js.map +1 -0
  509. package/dist/tools/policy-profile.d.ts +15 -0
  510. package/dist/tools/policy-profile.d.ts.map +1 -0
  511. package/dist/tools/policy-profile.js +174 -0
  512. package/dist/tools/policy-profile.js.map +1 -0
  513. package/dist/tools/profile.d.ts +14 -0
  514. package/dist/tools/profile.d.ts.map +1 -0
  515. package/dist/tools/profile.js +349 -0
  516. package/dist/tools/profile.js.map +1 -0
  517. package/dist/tools/query.d.ts +16 -0
  518. package/dist/tools/query.d.ts.map +1 -0
  519. package/dist/tools/query.js +53 -0
  520. package/dist/tools/query.js.map +1 -0
  521. package/dist/tools/schema.d.ts +23 -0
  522. package/dist/tools/schema.d.ts.map +1 -0
  523. package/dist/tools/schema.js +124 -0
  524. package/dist/tools/schema.js.map +1 -0
  525. package/dist/tools/search-entity-metadata.d.ts +18 -0
  526. package/dist/tools/search-entity-metadata.d.ts.map +1 -0
  527. package/dist/tools/search-entity-metadata.js +221 -0
  528. package/dist/tools/search-entity-metadata.js.map +1 -0
  529. package/dist/tools/search.d.ts +23 -0
  530. package/dist/tools/search.d.ts.map +1 -0
  531. package/dist/tools/search.js +110 -0
  532. package/dist/tools/search.js.map +1 -0
  533. package/dist/tools/solr-fields.d.ts +39 -0
  534. package/dist/tools/solr-fields.d.ts.map +1 -0
  535. package/dist/tools/solr-fields.js +40 -0
  536. package/dist/tools/solr-fields.js.map +1 -0
  537. package/dist/utils.d.ts +70 -0
  538. package/dist/utils.d.ts.map +1 -0
  539. package/dist/utils.js +104 -0
  540. package/dist/utils.js.map +1 -0
  541. package/package.json +41 -0
  542. package/src/client/auth/config-loaders.ts +81 -0
  543. package/src/client/auth/decorators/index.ts +7 -0
  544. package/src/client/auth/decorators/with-caching.ts +237 -0
  545. package/src/client/auth/factory.ts +34 -0
  546. package/src/client/auth/index.ts +6 -0
  547. package/src/client/auth/jwt-utils.ts +36 -0
  548. package/src/client/auth/node.ts +16 -0
  549. package/src/client/auth/providers/index.ts +1 -0
  550. package/src/client/auth/providers/jwt-auth-provider.ts +153 -0
  551. package/src/client/auth/schemas.ts +25 -0
  552. package/src/client/auth/types.ts +30 -0
  553. package/src/client/command.ts +54 -0
  554. package/src/client/config/index.ts +15 -0
  555. package/src/client/config/loader.ts +155 -0
  556. package/src/client/config/node.ts +22 -0
  557. package/src/client/config/schemas.ts +28 -0
  558. package/src/client/config/types.ts +39 -0
  559. package/src/client/config/unified-loader.ts +118 -0
  560. package/src/client/config.ts +69 -0
  561. package/src/client/deployment-config.ts +98 -0
  562. package/src/client/errors.ts +295 -0
  563. package/src/client/http-client.ts +577 -0
  564. package/src/client/index.ts +68 -0
  565. package/src/client/internal-dsl-client.ts +128 -0
  566. package/src/client/rate-limiter.ts +139 -0
  567. package/src/client/resolve-api-key-user.ts +99 -0
  568. package/src/client/types.ts +44 -0
  569. package/src/dsl/client.ts +538 -0
  570. package/src/dsl/commands/apply-filters.ts +35 -0
  571. package/src/dsl/commands/facet/index.ts +12 -0
  572. package/src/dsl/commands/facet/schemas.ts +101 -0
  573. package/src/dsl/commands/functions/ClassifyCommand.ts +129 -0
  574. package/src/dsl/commands/functions/ExtractAffiliationsCommand.ts +163 -0
  575. package/src/dsl/commands/functions/ExtractConceptsCommand.ts +141 -0
  576. package/src/dsl/commands/functions/ExtractGrantsCommand.ts +112 -0
  577. package/src/dsl/commands/functions/index.ts +31 -0
  578. package/src/dsl/commands/index.ts +29 -0
  579. package/src/dsl/commands/validate-input.ts +29 -0
  580. package/src/dsl/create-client.ts +80 -0
  581. package/src/dsl/fluent-query-builder.ts +684 -0
  582. package/src/dsl/index.ts +206 -0
  583. package/src/dsl/pagination.ts +202 -0
  584. package/src/dsl/query-builder.ts +1120 -0
  585. package/src/dsl/response-parser.ts +299 -0
  586. package/src/dsl/schema/cache.ts +124 -0
  587. package/src/dsl/schema/extract.ts +44 -0
  588. package/src/dsl/schema/index.ts +50 -0
  589. package/src/dsl/schema/load.ts +160 -0
  590. package/src/dsl/schema/store.ts +253 -0
  591. package/src/dsl/schema/structured-entities.ts +44 -0
  592. package/src/dsl/schema/summary.ts +94 -0
  593. package/src/dsl/schema/types.ts +40 -0
  594. package/src/dsl/schema/validation.ts +56 -0
  595. package/src/dsl/types/buckets.ts +22 -0
  596. package/src/dsl/types/common.ts +99 -0
  597. package/src/dsl/types/entities.ts +43 -0
  598. package/src/dsl/types/index.ts +63 -0
  599. package/src/dsl/types/return-clauses.ts +147 -0
  600. package/src/dsl/types/special-functions.ts +220 -0
  601. package/src/dsl/types/vocabulary.ts +82 -0
  602. package/src/dsl/usage-policy.ts +219 -0
  603. package/src/dsl/utils/escape.ts +123 -0
  604. package/src/dsl/utils/sanitize.ts +21 -0
  605. package/src/dsl/utils/string-utils.ts +147 -0
  606. package/src/dsl/utils/validate-field-name.ts +19 -0
  607. package/src/http-main.ts +41 -0
  608. package/src/index.ts +14 -0
  609. package/src/main.ts +40 -0
  610. package/src/mcp/batch-fetch.ts +255 -0
  611. package/src/mcp/examples/dsl-examples.ts +66 -0
  612. package/src/mcp/examples/usage-scenarios.ts +185 -0
  613. package/src/mcp/export-format.ts +113 -0
  614. package/src/mcp/funder-org-names.ts +30 -0
  615. package/src/mcp/http-server.ts +148 -0
  616. package/src/mcp/middleware/field-aliases.ts +274 -0
  617. package/src/mcp/resources/schema.ts +364 -0
  618. package/src/mcp/schema/context.ts +11 -0
  619. package/src/mcp/schema/index.ts +38 -0
  620. package/src/mcp/server.ts +247 -0
  621. package/src/mcp/shared-schema.ts +34 -0
  622. package/src/mcp/tools/analytics-filters.ts +46 -0
  623. package/src/mcp/tools/analytics.ts +453 -0
  624. package/src/mcp/tools/fetch-search-pages.ts +329 -0
  625. package/src/mcp/tools/functions.ts +105 -0
  626. package/src/mcp/tools/lookup.ts +182 -0
  627. package/src/mcp/tools/query.ts +91 -0
  628. package/src/mcp/tools/schema.ts +147 -0
  629. package/src/mcp/tools/search-entity-metadata.ts +253 -0
  630. package/src/mcp/tools/search-input.ts +87 -0
  631. package/src/mcp/tools/search.ts +168 -0
  632. package/src/mcp/utils.ts +199 -0
  633. package/test/client/auth/config-loaders.test.ts +92 -0
  634. package/test/client/auth/decorators/with-caching.test.ts +482 -0
  635. package/test/client/auth/factory.test.ts +68 -0
  636. package/test/client/auth/providers/jwt-auth-provider.test.ts +398 -0
  637. package/test/client/auth/types.test.ts +18 -0
  638. package/test/client/command.test.ts +163 -0
  639. package/test/client/config/loader.test.ts +377 -0
  640. package/test/client/config/unified-loader.test.ts +136 -0
  641. package/test/client/deployment-config.test.ts +69 -0
  642. package/test/client/errors/index.test.ts +408 -0
  643. package/test/client/helpers/mock-fetch.ts +42 -0
  644. package/test/client/http-client.test.ts +1148 -0
  645. package/test/client/integration/auth-providers.integration.test.ts +186 -0
  646. package/test/client/internal-dsl-client.test.ts +76 -0
  647. package/test/client/rate-limiter.test.ts +204 -0
  648. package/test/client/resolve-api-key-user.test.ts +87 -0
  649. package/test/dsl/client.special-functions.test.ts +387 -0
  650. package/test/dsl/client.test.ts +457 -0
  651. package/test/dsl/commands/apply-filters.test.ts +147 -0
  652. package/test/dsl/commands/functions/ClassifyCommand.test.ts +170 -0
  653. package/test/dsl/commands/functions/ExtractAffiliationsCommand.test.ts +306 -0
  654. package/test/dsl/commands/functions/ExtractConceptsCommand.test.ts +136 -0
  655. package/test/dsl/commands/functions/ExtractGrantsCommand.test.ts +202 -0
  656. package/test/dsl/commands/search/filters.test.ts +42 -0
  657. package/test/dsl/fluent-query-builder.test.ts +865 -0
  658. package/test/dsl/integration/client.integration.test.ts +294 -0
  659. package/test/dsl/integration/facet-api.integration.test.ts +276 -0
  660. package/test/dsl/integration/field-existence.integration.test.ts +148 -0
  661. package/test/dsl/integration/fluent-api-docs-examples.integration.test.ts +988 -0
  662. package/test/dsl/integration/fluent-api.integration.test.ts +615 -0
  663. package/test/dsl/integration/search-helpers.ts +27 -0
  664. package/test/dsl/integration/special-functions.integration.test.ts +224 -0
  665. package/test/dsl/integration/test-config.ts +52 -0
  666. package/test/dsl/pagination.test.ts +86 -0
  667. package/test/dsl/query-builder.test.ts +1775 -0
  668. package/test/dsl/response-parser.test.ts +520 -0
  669. package/test/dsl/schema/cache.test.ts +53 -0
  670. package/test/dsl/schema/load.test.ts +165 -0
  671. package/test/dsl/schema/summary.test.ts +61 -0
  672. package/test/dsl/schema/validation.test.ts +46 -0
  673. package/test/dsl/time-series.test.ts +135 -0
  674. package/test/dsl/unnest.test.ts +151 -0
  675. package/test/dsl/usage-policy.test.ts +100 -0
  676. package/test/dsl/utils/escape.property.test.ts +109 -0
  677. package/test/dsl/utils/escape.test.ts +140 -0
  678. package/test/dsl/utils/validate-field-name.property.test.ts +99 -0
  679. package/test/dsl/utils/validate-field-name.test.ts +55 -0
  680. package/test/examples/dsl-examples.test.ts +29 -0
  681. package/test/examples/usage-scenarios.test.ts +90 -0
  682. package/test/fixtures/describe-schema.json +4000 -0
  683. package/test/funder-org-names.test.ts +20 -0
  684. package/test/helpers/schema-fixture.ts +18 -0
  685. package/test/helpers/tool-test-harness.ts +114 -0
  686. package/test/integration/assertions.ts +263 -0
  687. package/test/integration/env.ts +108 -0
  688. package/test/integration/harness.ts +307 -0
  689. package/test/integration/hosted-client.ts +56 -0
  690. package/test/integration/hosted-mocks.ts +81 -0
  691. package/test/integration/hosted-run.ts +86 -0
  692. package/test/integration/hosted.e2e.test.ts +101 -0
  693. package/test/integration/reporter.ts +100 -0
  694. package/test/integration/run.ts +74 -0
  695. package/test/integration/suites/analytics.integration.ts +72 -0
  696. package/test/integration/suites/functions.integration.ts +37 -0
  697. package/test/integration/suites/hosted-smoke.integration.ts +38 -0
  698. package/test/integration/suites/index.ts +22 -0
  699. package/test/integration/suites/lookup.integration.ts +57 -0
  700. package/test/integration/suites/query.integration.ts +49 -0
  701. package/test/integration/suites/resources.integration.ts +68 -0
  702. package/test/integration/suites/search.integration.ts +80 -0
  703. package/test/integration/types.ts +92 -0
  704. package/test/integration/verify-usage-scenarios.ts +162 -0
  705. package/test/mcp/export-format.test.ts +67 -0
  706. package/test/mcp/http-server.test.ts +80 -0
  707. package/test/middleware/field-aliases.test.ts +287 -0
  708. package/test/resources/schema.test.ts +28 -0
  709. package/test/schema/describe-contract.test.ts +24 -0
  710. package/test/schema/store.test.ts +45 -0
  711. package/test/server.test.ts +170 -0
  712. package/test/tools/analytics-filters.test.ts +35 -0
  713. package/test/tools/analytics.test.ts +419 -0
  714. package/test/tools/fetch-search-pages.test.ts +200 -0
  715. package/test/tools/functions.test.ts +153 -0
  716. package/test/tools/lookup.test.ts +296 -0
  717. package/test/tools/query.test.ts +100 -0
  718. package/test/tools/schema.test.ts +37 -0
  719. package/test/tools/search.test.ts +415 -0
  720. package/test/utils.test.ts +45 -0
  721. package/tsconfig.json +9 -0
  722. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,1775 @@
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
+ import { ValidationError } from "../../src/client/index.js";
3
+ import { QueryBuilder } from "../../src/dsl/query-builder.js";
4
+
5
+ describe("QueryBuilder", () => {
6
+ let builder: QueryBuilder;
7
+
8
+ beforeEach(() => {
9
+ builder = new QueryBuilder();
10
+ });
11
+
12
+ describe("simple search queries", () => {
13
+ it("builds a simple search query for publications", () => {
14
+ const query = builder.search("publications").for("machine learning").build();
15
+
16
+ expect(query).toBe('search publications for "machine learning"');
17
+ });
18
+
19
+ it("builds a search query for grants", () => {
20
+ const query = builder.search("grants").for("cancer research").build();
21
+
22
+ expect(query).toBe('search grants for "cancer research"');
23
+ });
24
+
25
+ it("builds a search query for researchers", () => {
26
+ const query = builder.search("researchers").for("smith").build();
27
+
28
+ expect(query).toBe('search researchers for "smith"');
29
+ });
30
+ });
31
+
32
+ describe("queries with specific fields", () => {
33
+ it("returns specified fields", () => {
34
+ const query = builder
35
+ .search("publications")
36
+ .for("covid")
37
+ .fields(["id", "title", "doi", "year"])
38
+ .build();
39
+
40
+ // API requires fields separated by '+' not ','
41
+ expect(query).toBe('search publications for "covid" return publications[id+title+doi+year]');
42
+ });
43
+
44
+ it("returns all fields with empty array", () => {
45
+ const query = builder.search("publications").for("quantum").fields([]).build();
46
+
47
+ expect(query).toBe('search publications for "quantum"');
48
+ });
49
+ });
50
+
51
+ describe("queries with where clauses", () => {
52
+ it("adds single where clause with equals", () => {
53
+ const query = builder.search("publications").for("climate").where("year", "=", 2023).build();
54
+
55
+ expect(query).toBe('search publications for "climate" where year = 2023');
56
+ });
57
+
58
+ it("adds single where clause with string value", () => {
59
+ const query = builder.search("publications").for("AI").where("type", "=", "article").build();
60
+
61
+ expect(query).toBe('search publications for "AI" where type = "article"');
62
+ });
63
+
64
+ it("adds multiple where clauses", () => {
65
+ const query = builder
66
+ .search("publications")
67
+ .for("genetics")
68
+ .where("year", ">=", 2020)
69
+ .where("year", "<=", 2024)
70
+ .build();
71
+
72
+ expect(query).toBe('search publications for "genetics" where year >= 2020 and year <= 2024');
73
+ });
74
+
75
+ it("supports not equals operator", () => {
76
+ const query = builder
77
+ .search("publications")
78
+ .for("physics")
79
+ .where("type", "!=", "preprint")
80
+ .build();
81
+
82
+ expect(query).toBe('search publications for "physics" where type != "preprint"');
83
+ });
84
+
85
+ it("supports greater than operator", () => {
86
+ const query = builder
87
+ .search("publications")
88
+ .for("biology")
89
+ .where("times_cited", ">", 100)
90
+ .build();
91
+
92
+ expect(query).toBe('search publications for "biology" where times_cited > 100');
93
+ });
94
+
95
+ it("supports less than operator", () => {
96
+ const query = builder
97
+ .search("grants")
98
+ .for("energy")
99
+ .where("funding_usd", "<", 1000000)
100
+ .build();
101
+
102
+ expect(query).toBe('search grants for "energy" where funding_usd < 1000000');
103
+ });
104
+ });
105
+
106
+ describe("queries with sort", () => {
107
+ it("sorts ascending by default", () => {
108
+ const query = builder.search("publications").for("neuroscience").sort("year").build();
109
+
110
+ // API requires return clause before sort
111
+ expect(query).toBe(
112
+ 'search publications for "neuroscience" return publications sort by year asc',
113
+ );
114
+ });
115
+
116
+ it("sorts descending when specified", () => {
117
+ const query = builder
118
+ .search("publications")
119
+ .for("chemistry")
120
+ .sort("times_cited", "desc")
121
+ .build();
122
+
123
+ expect(query).toBe(
124
+ 'search publications for "chemistry" return publications sort by times_cited desc',
125
+ );
126
+ });
127
+ });
128
+
129
+ describe("queries with limit", () => {
130
+ it("adds limit clause", () => {
131
+ const query = builder.search("publications").for("math").limit(50).build();
132
+
133
+ // API requires return clause before limit
134
+ expect(query).toBe('search publications for "math" return publications limit 50');
135
+ });
136
+ });
137
+
138
+ describe("queries with skip for pagination", () => {
139
+ it("adds skip clause", () => {
140
+ const query = builder.search("publications").for("astronomy").skip(100).build();
141
+
142
+ // API requires return clause before skip
143
+ expect(query).toBe('search publications for "astronomy" return publications skip 100');
144
+ });
145
+
146
+ it("combines skip and limit for pagination", () => {
147
+ const query = builder.search("publications").for("ecology").skip(50).limit(25).build();
148
+
149
+ // API requires limit before skip
150
+ expect(query).toBe('search publications for "ecology" return publications limit 25 skip 50');
151
+ });
152
+ });
153
+
154
+ describe("complex queries", () => {
155
+ it("combines all clauses in correct order", () => {
156
+ const query = builder
157
+ .search("publications")
158
+ .for("deep learning")
159
+ .where("year", ">=", 2020)
160
+ .where("type", "=", "article")
161
+ .fields(["id", "title", "authors"])
162
+ .sort("times_cited", "desc")
163
+ .skip(0)
164
+ .limit(100)
165
+ .build();
166
+
167
+ // API requires: fields with '+', limit before skip
168
+ expect(query).toBe(
169
+ 'search publications for "deep learning" where year >= 2020 and type = "article" return publications[id+title+authors] sort by times_cited desc limit 100 skip 0',
170
+ );
171
+ });
172
+ });
173
+
174
+ describe("error cases", () => {
175
+ it("throws when building without entity", () => {
176
+ expect(() => builder.build()).toThrow("Entity type must be specified");
177
+ });
178
+
179
+ it.each([
180
+ "publications",
181
+ "grants",
182
+ "researchers",
183
+ "patents",
184
+ "clinical_trials",
185
+ "datasets",
186
+ "policy_documents",
187
+ "organizations",
188
+ ] as const)("builds valid query without for() for %s", (entity) => {
189
+ expect(builder.search(entity).build()).toBe(`search ${entity}`);
190
+ });
191
+
192
+ it.each([
193
+ "publications",
194
+ "grants",
195
+ "researchers",
196
+ "patents",
197
+ "clinical_trials",
198
+ "datasets",
199
+ "policy_documents",
200
+ "organizations",
201
+ ] as const)("builds query without for() but with where clause for %s", (entity) => {
202
+ expect(builder.search(entity).where("year", "=", 2024).build()).toBe(
203
+ `search ${entity} where year = 2024`,
204
+ );
205
+ });
206
+
207
+ // Note: "*" is NOT a wildcard in the Dimensions DSL — it is sent as a literal string.
208
+ // To search all records, omit the for() call and use where() conditions instead.
209
+ it("builds query with for('*') as literal", () => {
210
+ expect(builder.search("publications").for("*").build()).toBe('search publications for "*"');
211
+ });
212
+
213
+ it("throws for invalid entity type", () => {
214
+ expect(() => builder.search("invalid_entity" as never)).toThrow("Invalid entity type");
215
+ });
216
+
217
+ it("throws for invalid operator", () => {
218
+ expect(() =>
219
+ builder
220
+ .search("publications")
221
+ .for("test")
222
+ .where("year", "like" as never, 2020),
223
+ ).toThrow("Invalid operator");
224
+ });
225
+
226
+ it("throws for negative limit", () => {
227
+ expect(() => builder.search("publications").for("test").limit(-1)).toThrow(
228
+ "Limit must be non-negative",
229
+ );
230
+ });
231
+
232
+ it("throws for negative skip", () => {
233
+ expect(() => builder.search("publications").for("test").skip(-1)).toThrow(
234
+ "Skip must be non-negative",
235
+ );
236
+ });
237
+
238
+ it("throws when search index is set without search terms", () => {
239
+ expect(() => builder.search("publications").in("title_abstract_only").build()).toThrow(
240
+ "Search index requires search terms: call for() before in()",
241
+ );
242
+ });
243
+
244
+ it("allows in() with for()", () => {
245
+ expect(builder.search("publications").for("test").in("title_abstract_only").build()).toBe(
246
+ 'search publications in title_abstract_only for "test"',
247
+ );
248
+ });
249
+
250
+ it("allows in() set to full_data without for()", () => {
251
+ expect(builder.search("publications").in("full_data").build()).toBe("search publications");
252
+ });
253
+ });
254
+
255
+ describe("reset functionality", () => {
256
+ it("resets builder state", () => {
257
+ builder.search("publications").for("initial query");
258
+
259
+ builder.reset();
260
+
261
+ expect(() => builder.build()).toThrow("Entity type must be specified");
262
+ });
263
+
264
+ it("allows building new query after reset", () => {
265
+ builder.search("publications").for("first query");
266
+
267
+ builder.reset();
268
+
269
+ const query = builder.search("grants").for("new query").build();
270
+
271
+ expect(query).toBe('search grants for "new query"');
272
+ });
273
+ });
274
+
275
+ describe("entity types", () => {
276
+ it.each([
277
+ "publications",
278
+ "grants",
279
+ "researchers",
280
+ "patents",
281
+ "clinical_trials",
282
+ "datasets",
283
+ "policy_documents",
284
+ "organizations",
285
+ ] as const)("supports %s entity type", (entity) => {
286
+ const query = builder.search(entity).for("test").build();
287
+ expect(query).toBe(`search ${entity} for "test"`);
288
+ });
289
+ });
290
+
291
+ describe("queries with extended operators", () => {
292
+ it("supports partial match operator (~)", () => {
293
+ const query = builder
294
+ .search("grants")
295
+ .for("cancer")
296
+ .where("research_orgs.name", "~", "National Blood")
297
+ .build();
298
+
299
+ expect(query).toBe('search grants for "cancer" where research_orgs.name ~ "National Blood"');
300
+ });
301
+
302
+ it("supports lucene field search operator (@)", () => {
303
+ const query = builder
304
+ .search("publications")
305
+ .for("medicine")
306
+ .where("mesh_terms", "@", "(human OR animal)")
307
+ .build();
308
+
309
+ expect(query).toBe(
310
+ 'search publications for "medicine" where mesh_terms @ "(human OR animal)"',
311
+ );
312
+ });
313
+
314
+ it("supports is empty operator", () => {
315
+ const query = builder.search("researchers").for("smith").whereEmpty("orcid_id").build();
316
+
317
+ expect(query).toBe('search researchers for "smith" where orcid_id is empty');
318
+ });
319
+
320
+ it("supports is not empty operator", () => {
321
+ const query = builder.search("publications").for("covid").whereNotEmpty("doi").build();
322
+
323
+ expect(query).toBe('search publications for "covid" where doi is not empty');
324
+ });
325
+
326
+ it("combines emptiness checks with other conditions", () => {
327
+ const query = builder
328
+ .search("publications")
329
+ .for("CRISPR")
330
+ .where("year", ">=", 2020)
331
+ .whereNotEmpty("doi")
332
+ .build();
333
+
334
+ expect(query).toBe(
335
+ 'search publications for "CRISPR" where year >= 2020 and doi is not empty',
336
+ );
337
+ });
338
+ });
339
+
340
+ describe("queries with list and range filters", () => {
341
+ it("supports list filter with strings", () => {
342
+ const query = builder
343
+ .search("publications")
344
+ .for("research")
345
+ .whereIn("type", ["article", "review", "preprint"])
346
+ .build();
347
+
348
+ expect(query).toBe(
349
+ 'search publications for "research" where type in ["article", "review", "preprint"]',
350
+ );
351
+ });
352
+
353
+ it("supports list filter with numbers", () => {
354
+ const query = builder
355
+ .search("publications")
356
+ .for("data")
357
+ .whereIn("year", [2020, 2021, 2022])
358
+ .build();
359
+
360
+ expect(query).toBe('search publications for "data" where year in [2020, 2021, 2022]');
361
+ });
362
+
363
+ it("supports range filter with numbers", () => {
364
+ const query = builder
365
+ .search("publications")
366
+ .for("climate")
367
+ .whereRange("year", 2018, 2023)
368
+ .build();
369
+
370
+ expect(query).toBe('search publications for "climate" where year in [2018:2023]');
371
+ });
372
+
373
+ it("supports range filter with dates", () => {
374
+ const query = builder
375
+ .search("publications")
376
+ .for("covid")
377
+ .whereRange("date", "2020-01-01", "2020-12-31")
378
+ .build();
379
+
380
+ expect(query).toBe(
381
+ 'search publications for "covid" where date in ["2020-01-01":"2020-12-31"]',
382
+ );
383
+ });
384
+
385
+ it("validates list filter max items", () => {
386
+ const items = Array.from({ length: 401 }, (_, i) => `item${i}`);
387
+ expect(() => builder.search("publications").for("test").whereIn("id", items)).toThrow(
388
+ "List filter cannot exceed 400 items",
389
+ );
390
+ });
391
+
392
+ it("validates list filter has at least one value", () => {
393
+ expect(() => builder.search("publications").for("test").whereIn("type", [])).toThrow(
394
+ "List filter must have at least one value",
395
+ );
396
+ });
397
+
398
+ it("supports mixed string and number values in list filter", () => {
399
+ const query = builder
400
+ .search("publications")
401
+ .for("test")
402
+ .whereIn("field", ["string", 123, "another"])
403
+ .build();
404
+
405
+ expect(query).toBe(
406
+ 'search publications for "test" where field in ["string", 123, "another"]',
407
+ );
408
+ });
409
+
410
+ it("validates range start is not greater than end for numbers", () => {
411
+ expect(() =>
412
+ builder.search("publications").for("test").whereRange("year", 2023, 2020),
413
+ ).toThrow("Range start must not be greater than end");
414
+ });
415
+
416
+ it("allows string ranges without validation", () => {
417
+ const query = builder
418
+ .search("publications")
419
+ .for("test")
420
+ .whereRange("date", "2023-01-01", "2020-01-01")
421
+ .build();
422
+
423
+ // String ranges are not validated (dates may have different formats)
424
+ expect(query).toBe(
425
+ 'search publications for "test" where date in ["2023-01-01":"2020-01-01"]',
426
+ );
427
+ });
428
+ });
429
+
430
+ describe("combined filter types", () => {
431
+ it("combines all filter types in correct order", () => {
432
+ const query = builder
433
+ .search("publications")
434
+ .for("AI research")
435
+ .where("year", ">=", 2020)
436
+ .where("title", "~", "neural")
437
+ .where("mesh_terms", "@", "(human OR mouse)")
438
+ .whereNotEmpty("doi")
439
+ .whereIn("type", ["article", "review"])
440
+ .whereRange("times_cited", 10, 100)
441
+ .fields(["id", "title"])
442
+ .build();
443
+
444
+ expect(query).toBe(
445
+ 'search publications for "AI research" ' +
446
+ 'where year >= 2020 and title ~ "neural" and mesh_terms @ "(human OR mouse)" ' +
447
+ 'and doi is not empty and type in ["article", "review"] and times_cited in [10:100] ' +
448
+ "return publications[id+title]",
449
+ );
450
+ });
451
+ });
452
+
453
+ describe("queries with search indexes", () => {
454
+ it("searches in title_abstract_only index", () => {
455
+ const query = builder
456
+ .search("publications")
457
+ .in("title_abstract_only")
458
+ .for("CRISPR gene editing")
459
+ .build();
460
+
461
+ expect(query).toBe('search publications in title_abstract_only for "CRISPR gene editing"');
462
+ });
463
+
464
+ it("searches in authors index", () => {
465
+ const query = builder.search("publications").in("authors").for("Jennifer A Doudna").build();
466
+
467
+ expect(query).toBe('search publications in authors for "Jennifer A Doudna"');
468
+ });
469
+
470
+ it("searches in concepts index", () => {
471
+ const query = builder.search("publications").in("concepts").for("machine learning").build();
472
+
473
+ expect(query).toBe('search publications in concepts for "machine learning"');
474
+ });
475
+
476
+ it("combines index with where clauses", () => {
477
+ const query = builder
478
+ .search("publications")
479
+ .in("title_only")
480
+ .for("graphene")
481
+ .where("year", ">=", 2020)
482
+ .build();
483
+
484
+ expect(query).toBe('search publications in title_only for "graphene" where year >= 2020');
485
+ });
486
+
487
+ it("validates search index", () => {
488
+ expect(() =>
489
+ builder
490
+ .search("publications")
491
+ .in("invalid_index" as never)
492
+ .for("test"),
493
+ ).toThrow("Invalid search index");
494
+ });
495
+ });
496
+
497
+ describe("queries with boolean logic", () => {
498
+ it("supports explicit AND connector", () => {
499
+ const query = builder
500
+ .search("publications")
501
+ .for("research")
502
+ .where("year", ">=", 2020)
503
+ .and()
504
+ .where("type", "=", "article")
505
+ .build();
506
+
507
+ expect(query).toBe(
508
+ 'search publications for "research" where year >= 2020 and type = "article"',
509
+ );
510
+ });
511
+
512
+ it("explicit AND produces same result as implicit AND", () => {
513
+ const explicitAnd = new QueryBuilder()
514
+ .search("publications")
515
+ .for("test")
516
+ .where("year", "=", 2020)
517
+ .and()
518
+ .where("type", "=", "article")
519
+ .build();
520
+
521
+ const implicitAnd = new QueryBuilder()
522
+ .search("publications")
523
+ .for("test")
524
+ .where("year", "=", 2020)
525
+ .where("type", "=", "article")
526
+ .build();
527
+
528
+ expect(explicitAnd).toBe(implicitAnd);
529
+ });
530
+
531
+ it("supports OR conditions", () => {
532
+ const query = builder
533
+ .search("publications")
534
+ .for("research")
535
+ .where("type", "=", "article")
536
+ .or()
537
+ .where("type", "=", "review")
538
+ .build();
539
+
540
+ expect(query).toBe(
541
+ 'search publications for "research" where type = "article" or type = "review"',
542
+ );
543
+ });
544
+
545
+ it("supports NOT conditions", () => {
546
+ const query = builder
547
+ .search("publications")
548
+ .for("cancer")
549
+ .where("year", ">=", 2020)
550
+ .not()
551
+ .where("open_access", "=", "closed")
552
+ .build();
553
+
554
+ expect(query).toBe(
555
+ 'search publications for "cancer" where year >= 2020 not open_access = "closed"',
556
+ );
557
+ });
558
+
559
+ it("supports parenthesized expressions", () => {
560
+ const query = builder
561
+ .search("publications")
562
+ .for("AI")
563
+ .openGroup()
564
+ .where("type", "=", "article")
565
+ .or()
566
+ .where("type", "=", "review")
567
+ .closeGroup()
568
+ .where("year", ">=", 2020)
569
+ .build();
570
+
571
+ expect(query).toBe(
572
+ 'search publications for "AI" where (type = "article" or type = "review") and year >= 2020',
573
+ );
574
+ });
575
+
576
+ it("supports nested parentheses", () => {
577
+ const query = builder
578
+ .search("grants")
579
+ .for("energy")
580
+ .openGroup()
581
+ .openGroup()
582
+ .where("start_year", ">", 2020)
583
+ .where("funding_usd", ">", 1000000)
584
+ .closeGroup()
585
+ .or()
586
+ .where("funder_org_acronym", "=", "NIH")
587
+ .closeGroup()
588
+ .where("active_status", "=", "active")
589
+ .build();
590
+
591
+ expect(query).toBe(
592
+ 'search grants for "energy" where ((start_year > 2020 and funding_usd > 1000000) or funder_org_acronym = "NIH") and active_status = "active"',
593
+ );
594
+ });
595
+
596
+ it("throws on unbalanced parentheses (unclosed)", () => {
597
+ expect(() =>
598
+ builder.search("publications").for("test").openGroup().where("year", "=", 2020).build(),
599
+ ).toThrow("Unbalanced parentheses");
600
+ });
601
+
602
+ it("throws on unbalanced parentheses (extra close)", () => {
603
+ expect(() =>
604
+ builder.search("publications").for("test").where("year", "=", 2020).closeGroup(),
605
+ ).toThrow("No group to close");
606
+ });
607
+
608
+ it("combines OR with whereIn", () => {
609
+ const query = builder
610
+ .search("publications")
611
+ .for("data")
612
+ .whereIn("type", ["article", "review"])
613
+ .or()
614
+ .where("year", ">", 2022)
615
+ .build();
616
+
617
+ expect(query).toBe(
618
+ 'search publications for "data" where type in ["article", "review"] or year > 2022',
619
+ );
620
+ });
621
+
622
+ it("combines OR with whereNotEmpty", () => {
623
+ const query = builder
624
+ .search("publications")
625
+ .for("research")
626
+ .whereNotEmpty("doi")
627
+ .or()
628
+ .whereNotEmpty("pmid")
629
+ .build();
630
+
631
+ expect(query).toBe(
632
+ 'search publications for "research" where doi is not empty or pmid is not empty',
633
+ );
634
+ });
635
+
636
+ it("supports deeply nested groups (3+ levels)", () => {
637
+ const query = builder
638
+ .search("publications")
639
+ .for("complex query")
640
+ .openGroup()
641
+ .openGroup()
642
+ .openGroup()
643
+ .where("year", ">=", 2020)
644
+ .or()
645
+ .where("year", "<=", 2010)
646
+ .closeGroup()
647
+ .where("type", "=", "article")
648
+ .closeGroup()
649
+ .or()
650
+ .where("times_cited", ">", 100)
651
+ .closeGroup()
652
+ .where("open_access", "=", "all")
653
+ .build();
654
+
655
+ expect(query).toBe(
656
+ 'search publications for "complex query" where (((year >= 2020 or year <= 2010) and type = "article") or times_cited > 100) and open_access = "all"',
657
+ );
658
+ });
659
+
660
+ it("supports 4 levels of nesting with mixed operators", () => {
661
+ const query = builder
662
+ .search("grants")
663
+ .for("research funding")
664
+ .openGroup()
665
+ .openGroup()
666
+ .openGroup()
667
+ .openGroup()
668
+ .where("funding_usd", ">", 1000000)
669
+ .closeGroup()
670
+ .or()
671
+ .where("funder_org_acronym", "=", "NIH")
672
+ .closeGroup()
673
+ .where("start_year", ">=", 2020)
674
+ .closeGroup()
675
+ .not()
676
+ .where("active_status", "=", "closed")
677
+ .closeGroup()
678
+ .build();
679
+
680
+ expect(query).toBe(
681
+ 'search grants for "research funding" where ((((funding_usd > 1000000) or funder_org_acronym = "NIH") and start_year >= 2020) not active_status = "closed")',
682
+ );
683
+ });
684
+ });
685
+
686
+ describe("queries with count function", () => {
687
+ it("filters by count equals", () => {
688
+ const query = builder
689
+ .search("publications")
690
+ .for("research")
691
+ .whereCount("researchers", "=", 1)
692
+ .build();
693
+
694
+ expect(query).toBe('search publications for "research" where count(researchers) = 1');
695
+ });
696
+
697
+ it("filters by count greater than", () => {
698
+ const query = builder
699
+ .search("publications")
700
+ .for("collaboration")
701
+ .whereCount("research_org_countries", ">", 1)
702
+ .build();
703
+
704
+ expect(query).toBe(
705
+ 'search publications for "collaboration" where count(research_org_countries) > 1',
706
+ );
707
+ });
708
+
709
+ it("filters for hyper-authorship", () => {
710
+ const query = builder
711
+ .search("publications")
712
+ .for("physics")
713
+ .whereCount("researchers", ">=", 25)
714
+ .build();
715
+
716
+ expect(query).toBe('search publications for "physics" where count(researchers) >= 25');
717
+ });
718
+
719
+ it("combines count with other conditions", () => {
720
+ const query = builder
721
+ .search("publications")
722
+ .for("science")
723
+ .where("year", ">=", 2020)
724
+ .whereCount("researchers", ">", 5)
725
+ .build();
726
+
727
+ expect(query).toBe(
728
+ 'search publications for "science" where year >= 2020 and count(researchers) > 5',
729
+ );
730
+ });
731
+
732
+ it("combines count with OR operator", () => {
733
+ const query = builder
734
+ .search("publications")
735
+ .for("research")
736
+ .whereCount("researchers", ">", 5)
737
+ .or()
738
+ .whereCount("research_org_countries", ">", 2)
739
+ .build();
740
+
741
+ expect(query).toBe(
742
+ 'search publications for "research" where count(researchers) > 5 or count(research_org_countries) > 2',
743
+ );
744
+ });
745
+
746
+ it("combines count with NOT operator", () => {
747
+ const query = builder
748
+ .search("publications")
749
+ .for("research")
750
+ .whereCount("researchers", ">=", 1)
751
+ .not()
752
+ .whereCount("research_org_countries", "=", 1)
753
+ .build();
754
+
755
+ expect(query).toBe(
756
+ 'search publications for "research" where count(researchers) >= 1 not count(research_org_countries) = 1',
757
+ );
758
+ });
759
+
760
+ it("combines count with parenthesized boolean expressions", () => {
761
+ const query = builder
762
+ .search("publications")
763
+ .for("collaboration")
764
+ .where("year", ">=", 2020)
765
+ .openGroup()
766
+ .whereCount("researchers", ">", 10)
767
+ .or()
768
+ .whereCount("research_org_countries", ">", 3)
769
+ .closeGroup()
770
+ .build();
771
+
772
+ expect(query).toBe(
773
+ 'search publications for "collaboration" where year >= 2020 and (count(researchers) > 10 or count(research_org_countries) > 3)',
774
+ );
775
+ });
776
+ });
777
+
778
+ describe("facet return clauses", () => {
779
+ it("generates basic facet return clause", () => {
780
+ const query = builder.search("publications").for("CRISPR").returnFacet("year").build();
781
+
782
+ expect(query).toBe('search publications for "CRISPR" return year');
783
+ });
784
+
785
+ it("generates facet return clause with limit", () => {
786
+ const query = builder
787
+ .search("publications")
788
+ .for("CRISPR")
789
+ .returnFacet("funders", { limit: 10 })
790
+ .build();
791
+
792
+ expect(query).toBe('search publications for "CRISPR" return funders limit 10');
793
+ });
794
+
795
+ it("generates multiple facet return clauses", () => {
796
+ const query = builder
797
+ .search("publications")
798
+ .for("CRISPR")
799
+ .returnFacet("year")
800
+ .returnFacet("funders", { limit: 10 })
801
+ .build();
802
+
803
+ expect(query).toBe('search publications for "CRISPR" return year return funders limit 10');
804
+ });
805
+
806
+ it("generates aggregated facet return clause", () => {
807
+ const query = builder
808
+ .search("publications")
809
+ .for("CRISPR")
810
+ .returnAggregate("funders", ["rcr_avg"])
811
+ .build();
812
+
813
+ expect(query).toBe('search publications for "CRISPR" return funders aggregate rcr_avg');
814
+ });
815
+
816
+ it("generates aggregated facet with multiple indicators", () => {
817
+ const query = builder
818
+ .search("publications")
819
+ .for("CRISPR")
820
+ .returnAggregate("funders", ["rcr_avg", "altmetric_median"])
821
+ .build();
822
+
823
+ expect(query).toBe(
824
+ 'search publications for "CRISPR" return funders aggregate rcr_avg, altmetric_median',
825
+ );
826
+ });
827
+
828
+ it("generates aggregated facet with sort", () => {
829
+ const query = builder
830
+ .search("publications")
831
+ .for("CRISPR")
832
+ .returnAggregate("funders", ["rcr_avg"], {
833
+ sortBy: "rcr_avg",
834
+ sortOrder: "desc",
835
+ })
836
+ .build();
837
+
838
+ expect(query).toBe(
839
+ 'search publications for "CRISPR" return funders aggregate rcr_avg sort by rcr_avg desc',
840
+ );
841
+ });
842
+
843
+ it("generates aggregated facet with sort by count", () => {
844
+ const query = builder
845
+ .search("publications")
846
+ .for("CRISPR")
847
+ .returnAggregate("funders", ["rcr_avg"], {
848
+ sortBy: "count",
849
+ sortOrder: "asc",
850
+ })
851
+ .build();
852
+
853
+ expect(query).toBe(
854
+ 'search publications for "CRISPR" return funders aggregate rcr_avg sort by count asc',
855
+ );
856
+ });
857
+
858
+ it("generates aggregated facet with limit", () => {
859
+ const query = builder
860
+ .search("publications")
861
+ .for("CRISPR")
862
+ .returnAggregate("funders", ["rcr_avg"], { limit: 10 })
863
+ .build();
864
+
865
+ expect(query).toBe(
866
+ 'search publications for "CRISPR" return funders aggregate rcr_avg limit 10',
867
+ );
868
+ });
869
+
870
+ it("generates aggregated facet with all options", () => {
871
+ const query = builder
872
+ .search("publications")
873
+ .for("CRISPR")
874
+ .returnAggregate("funders", ["rcr_avg", "altmetric_median"], {
875
+ sortBy: "rcr_avg",
876
+ sortOrder: "desc",
877
+ limit: 10,
878
+ })
879
+ .build();
880
+
881
+ expect(query).toBe(
882
+ 'search publications for "CRISPR" return funders aggregate rcr_avg, altmetric_median sort by rcr_avg desc limit 10',
883
+ );
884
+ });
885
+
886
+ it("combines entity return with facet return", () => {
887
+ const query = builder
888
+ .search("publications")
889
+ .for("CRISPR")
890
+ .fields(["id", "title"])
891
+ .limit(100)
892
+ .returnFacet("year")
893
+ .build();
894
+
895
+ expect(query).toBe(
896
+ 'search publications for "CRISPR" return publications[id+title] limit 100 return year',
897
+ );
898
+ });
899
+
900
+ it("combines entity return with aggregated facet", () => {
901
+ const query = builder
902
+ .search("publications")
903
+ .for("CRISPR")
904
+ .where("year", ">=", 2020)
905
+ .fields(["id", "title"])
906
+ .returnAggregate("funders", ["rcr_avg"], {
907
+ sortBy: "rcr_avg",
908
+ limit: 10,
909
+ })
910
+ .build();
911
+
912
+ expect(query).toBe(
913
+ 'search publications for "CRISPR" where year >= 2020 return publications[id+title] return funders aggregate rcr_avg sort by rcr_avg desc limit 10',
914
+ );
915
+ });
916
+
917
+ it("generates grant facet with funding aggregation", () => {
918
+ const query = builder
919
+ .search("grants")
920
+ .for("cancer")
921
+ .returnAggregate("funders", ["funding"], {
922
+ sortBy: "funding",
923
+ sortOrder: "desc",
924
+ limit: 10,
925
+ })
926
+ .build();
927
+
928
+ expect(query).toBe(
929
+ 'search grants for "cancer" return funders aggregate funding sort by funding desc limit 10',
930
+ );
931
+ });
932
+
933
+ it("resets facet clauses on reset", () => {
934
+ builder.search("publications").for("test").returnFacet("year");
935
+
936
+ builder.reset();
937
+
938
+ expect(() => builder.build()).toThrow("Entity type must be specified");
939
+ });
940
+ });
941
+
942
+ describe("queries with similar_documents()", () => {
943
+ it("builds query with similar_documents function", () => {
944
+ const query = builder
945
+ .search("publications")
946
+ .forSimilar("Machine learning for drug discovery")
947
+ .build();
948
+
949
+ expect(query).toBe(
950
+ 'search publications for similar_documents("Machine learning for drug discovery")',
951
+ );
952
+ });
953
+
954
+ it("escapes quotes in similar text", () => {
955
+ const query = builder.search("publications").forSimilar('Text with "quotes" inside').build();
956
+
957
+ expect(query).toBe(
958
+ 'search publications for similar_documents("Text with \\"quotes\\" inside")',
959
+ );
960
+ });
961
+
962
+ it("normalizes newlines in similar text", () => {
963
+ const query = builder
964
+ .search("publications")
965
+ .forSimilar("Line one\nLine two\nLine three")
966
+ .build();
967
+
968
+ expect(query).toBe(
969
+ 'search publications for similar_documents("Line one\\nLine two\\nLine three")',
970
+ );
971
+ });
972
+
973
+ it("combines similar_documents with where clause", () => {
974
+ const query = builder
975
+ .search("publications")
976
+ .forSimilar("Spinal cord injury macrophage polarization")
977
+ .where("year", ">", 2015)
978
+ .build();
979
+
980
+ expect(query).toBe(
981
+ 'search publications for similar_documents("Spinal cord injury macrophage polarization") where year > 2015',
982
+ );
983
+ });
984
+
985
+ it("combines similar_documents with limit", () => {
986
+ const query = builder
987
+ .search("publications")
988
+ .forSimilar("Cancer immunotherapy")
989
+ .limit(10)
990
+ .build();
991
+
992
+ expect(query).toBe(
993
+ 'search publications for similar_documents("Cancer immunotherapy") return publications limit 10',
994
+ );
995
+ });
996
+
997
+ it("combines similar_documents with fields", () => {
998
+ const query = builder
999
+ .search("publications")
1000
+ .forSimilar("Neural networks deep learning")
1001
+ .fields(["id", "title", "year"])
1002
+ .build();
1003
+
1004
+ expect(query).toBe(
1005
+ 'search publications for similar_documents("Neural networks deep learning") return publications[id+title+year]',
1006
+ );
1007
+ });
1008
+
1009
+ it("works with grants entity", () => {
1010
+ const query = builder
1011
+ .search("grants")
1012
+ .forSimilar("Cancer immunotherapy research")
1013
+ .where("start_year", ">=", 2020)
1014
+ .limit(5)
1015
+ .build();
1016
+
1017
+ expect(query).toBe(
1018
+ 'search grants for similar_documents("Cancer immunotherapy research") where start_year >= 2020 return grants limit 5',
1019
+ );
1020
+ });
1021
+
1022
+ it("resets similar text on reset", () => {
1023
+ builder.search("publications").forSimilar("test text");
1024
+ builder.reset();
1025
+
1026
+ expect(() => builder.build()).toThrow("Entity type must be specified");
1027
+ });
1028
+ });
1029
+
1030
+ describe("facet return validation", () => {
1031
+ it("throws ValidationError on empty field name", () => {
1032
+ expect(() => builder.search("publications").for("test").returnFacet("")).toThrow(
1033
+ ValidationError,
1034
+ );
1035
+
1036
+ try {
1037
+ builder.search("publications").for("test").returnFacet("");
1038
+ } catch (e) {
1039
+ expect((e as ValidationError).message).toBe("Invalid field name: ");
1040
+ }
1041
+ });
1042
+
1043
+ it("throws ValidationError on whitespace-only field name", () => {
1044
+ expect(() => builder.search("publications").for("test").returnFacet(" ")).toThrow(
1045
+ ValidationError,
1046
+ );
1047
+ });
1048
+
1049
+ it("throws ValidationError on negative facet limit", () => {
1050
+ expect(() =>
1051
+ builder.search("publications").for("test").returnFacet("year", { limit: -1 }),
1052
+ ).toThrow(ValidationError);
1053
+
1054
+ try {
1055
+ builder.search("publications").for("test").returnFacet("year", { limit: -1 });
1056
+ } catch (e) {
1057
+ expect((e as ValidationError).message).toBe("Facet limit must be non-negative");
1058
+ expect((e as ValidationError).details?.limit).toBe(-1);
1059
+ }
1060
+ });
1061
+
1062
+ it("allows zero facet limit", () => {
1063
+ const query = builder
1064
+ .search("publications")
1065
+ .for("test")
1066
+ .returnFacet("year", { limit: 0 })
1067
+ .build();
1068
+
1069
+ expect(query).toContain("return year limit 0");
1070
+ });
1071
+
1072
+ it("throws ValidationError on empty field name for aggregate", () => {
1073
+ expect(() =>
1074
+ builder.search("publications").for("test").returnAggregate("", ["rcr_avg"]),
1075
+ ).toThrow(ValidationError);
1076
+ });
1077
+
1078
+ it("throws ValidationError on whitespace-only field name for aggregate", () => {
1079
+ expect(() =>
1080
+ builder.search("publications").for("test").returnAggregate(" ", ["rcr_avg"]),
1081
+ ).toThrow(ValidationError);
1082
+ });
1083
+
1084
+ it("throws ValidationError on empty indicators array", () => {
1085
+ expect(() =>
1086
+ builder.search("publications").for("test").returnAggregate("funders", []),
1087
+ ).toThrow(ValidationError);
1088
+
1089
+ try {
1090
+ builder.search("publications").for("test").returnAggregate("funders", []);
1091
+ } catch (e) {
1092
+ expect((e as ValidationError).message).toBe("Aggregate must have at least one indicator");
1093
+ expect((e as ValidationError).details?.field).toBe("funders");
1094
+ }
1095
+ });
1096
+
1097
+ it("throws ValidationError on negative aggregate limit", () => {
1098
+ expect(() =>
1099
+ builder
1100
+ .search("publications")
1101
+ .for("test")
1102
+ .returnAggregate("funders", ["rcr_avg"], { limit: -1 }),
1103
+ ).toThrow(ValidationError);
1104
+ });
1105
+
1106
+ it("throws ValidationError on invalid sortBy value", () => {
1107
+ try {
1108
+ builder
1109
+ .search("publications")
1110
+ .for("test")
1111
+ .returnAggregate("funders", ["rcr_avg"], { sortBy: "invalid" });
1112
+ } catch (e) {
1113
+ expect(e).toBeInstanceOf(ValidationError);
1114
+ expect((e as ValidationError).message).toContain("sortBy must be one of");
1115
+ expect((e as ValidationError).details?.sortBy).toBe("invalid");
1116
+ expect((e as ValidationError).details?.validSortTargets).toEqual(["rcr_avg", "count"]);
1117
+ }
1118
+ });
1119
+
1120
+ it("throws ValidationError on empty indicator string", () => {
1121
+ expect(() =>
1122
+ builder.search("publications").for("test").returnAggregate("funders", [""]),
1123
+ ).toThrow(ValidationError);
1124
+ });
1125
+
1126
+ it("throws ValidationError on whitespace-only indicator", () => {
1127
+ expect(() =>
1128
+ builder.search("publications").for("test").returnAggregate("funders", ["rcr_avg", " "]),
1129
+ ).toThrow(ValidationError);
1130
+ });
1131
+
1132
+ it("throws ValidationError on sortBy not matching any indicator", () => {
1133
+ expect(() =>
1134
+ builder
1135
+ .search("publications")
1136
+ .for("test")
1137
+ .returnAggregate("funders", ["rcr_avg", "altmetric_median"], {
1138
+ sortBy: "times_cited",
1139
+ }),
1140
+ ).toThrow(ValidationError);
1141
+ });
1142
+
1143
+ it("allows sortBy count", () => {
1144
+ const query = builder
1145
+ .search("publications")
1146
+ .for("test")
1147
+ .returnAggregate("funders", ["rcr_avg"], { sortBy: "count" })
1148
+ .build();
1149
+
1150
+ expect(query).toContain("sort by count");
1151
+ });
1152
+
1153
+ it("allows sortBy matching an indicator", () => {
1154
+ const query = builder
1155
+ .search("publications")
1156
+ .for("test")
1157
+ .returnAggregate("funders", ["rcr_avg", "altmetric_median"], {
1158
+ sortBy: "altmetric_median",
1159
+ })
1160
+ .build();
1161
+
1162
+ expect(query).toContain("sort by altmetric_median");
1163
+ });
1164
+ });
1165
+
1166
+ describe("grouped returns", () => {
1167
+ it("generates correct DSL for grouped entity return", () => {
1168
+ const query = builder
1169
+ .search("publications")
1170
+ .for("test")
1171
+ .returnGrouped("docs", { fields: ["id", "title"] })
1172
+ .build();
1173
+
1174
+ expect(query).toContain('return in "docs" publications[id+title]');
1175
+ });
1176
+
1177
+ it("generates correct DSL for grouped facet return", () => {
1178
+ const query = builder
1179
+ .search("publications")
1180
+ .for("test")
1181
+ .fields(["id", "title"])
1182
+ .returnGroupedFacet("facets", "funders")
1183
+ .build();
1184
+
1185
+ expect(query).toContain("return publications[id+title]");
1186
+ expect(query).toContain('return in "facets" funders');
1187
+ });
1188
+
1189
+ it("generates correct DSL for grouped aggregated facet", () => {
1190
+ const query = builder
1191
+ .search("publications")
1192
+ .for("test")
1193
+ .returnGroupedAggregate("metrics", "funders", ["rcr_avg", "funding_usd"], {
1194
+ sortBy: "rcr_avg",
1195
+ sortOrder: "desc",
1196
+ })
1197
+ .build();
1198
+
1199
+ expect(query).toContain(
1200
+ 'return in "metrics" funders aggregate rcr_avg, funding_usd sort by rcr_avg desc',
1201
+ );
1202
+ });
1203
+
1204
+ it("supports multiple grouped returns", () => {
1205
+ const query = builder
1206
+ .search("publications")
1207
+ .for("test")
1208
+ .returnGrouped("docs", { fields: ["id", "title"] })
1209
+ .returnGroupedFacet("years", "year", { limit: 10 })
1210
+ .returnGroupedAggregate("orgs", "research_orgs", ["rcr_avg"])
1211
+ .build();
1212
+
1213
+ expect(query).toContain('return in "docs" publications[id+title]');
1214
+ expect(query).toContain('return in "years" year limit 10');
1215
+ expect(query).toContain('return in "orgs" research_orgs aggregate rcr_avg');
1216
+ });
1217
+
1218
+ it("rejects empty group name", () => {
1219
+ expect(() => {
1220
+ builder
1221
+ .search("publications")
1222
+ .for("test")
1223
+ .returnGrouped("", { fields: ["id"] })
1224
+ .build();
1225
+ }).toThrow(ValidationError);
1226
+ });
1227
+
1228
+ it("rejects whitespace-only group name", () => {
1229
+ expect(() => {
1230
+ builder
1231
+ .search("publications")
1232
+ .for("test")
1233
+ .returnGrouped(" ", { fields: ["id"] })
1234
+ .build();
1235
+ }).toThrow(ValidationError);
1236
+ });
1237
+ });
1238
+
1239
+ describe("limit(0) with facets (facet-only queries)", () => {
1240
+ it("skips entity return when limit(0) with facets and no fields", () => {
1241
+ const query = builder
1242
+ .search("publications")
1243
+ .for("CRISPR")
1244
+ .limit(0)
1245
+ .returnFacet("year")
1246
+ .build();
1247
+ expect(query).toBe('search publications for "CRISPR" return year');
1248
+ });
1249
+
1250
+ it("skips entity return when limit(0) with time-series", () => {
1251
+ const query = builder
1252
+ .search("publications")
1253
+ .for("CRISPR")
1254
+ .limit(0)
1255
+ .returnCitationsPerYear(2010, 2023)
1256
+ .build();
1257
+ expect(query).toBe('search publications for "CRISPR" return citations_per_year(2010, 2023)');
1258
+ });
1259
+
1260
+ it("throws when limit(0) with fields", () => {
1261
+ expect(() =>
1262
+ builder
1263
+ .search("publications")
1264
+ .for("CRISPR")
1265
+ .fields(["id"])
1266
+ .limit(0)
1267
+ .returnFacet("year")
1268
+ .build(),
1269
+ ).toThrow(ValidationError);
1270
+
1271
+ try {
1272
+ builder
1273
+ .search("publications")
1274
+ .for("CRISPR")
1275
+ .fields(["id"])
1276
+ .limit(0)
1277
+ .returnFacet("year")
1278
+ .build();
1279
+ } catch (e) {
1280
+ expect((e as ValidationError).message).toContain("limit(0) cannot be used with fields()");
1281
+ }
1282
+ });
1283
+
1284
+ it("keeps entity return when limit > 0 with facets", () => {
1285
+ const query = builder
1286
+ .search("publications")
1287
+ .for("CRISPR")
1288
+ .limit(10)
1289
+ .returnFacet("year")
1290
+ .build();
1291
+ expect(query).toBe(
1292
+ 'search publications for "CRISPR" return publications limit 10 return year',
1293
+ );
1294
+ });
1295
+
1296
+ it("throws when limit(0) without facets", () => {
1297
+ expect(() => builder.search("publications").for("CRISPR").limit(0).build()).toThrow(
1298
+ ValidationError,
1299
+ );
1300
+
1301
+ try {
1302
+ builder.search("publications").for("CRISPR").limit(0).build();
1303
+ } catch (e) {
1304
+ expect((e as ValidationError).message).toContain("limit(0) requires at least one facet");
1305
+ }
1306
+ });
1307
+
1308
+ it("skips entity return when limit(0) with grouped returns", () => {
1309
+ const query = builder
1310
+ .search("publications")
1311
+ .for("CRISPR")
1312
+ .limit(0)
1313
+ .returnGroupedFacet("facets", "year")
1314
+ .build();
1315
+ expect(query).toBe('search publications for "CRISPR" return in "facets" year');
1316
+ });
1317
+
1318
+ it("skips entity return when limit(0) with multiple facets", () => {
1319
+ const query = builder
1320
+ .search("publications")
1321
+ .for("CRISPR")
1322
+ .limit(0)
1323
+ .returnFacet("year")
1324
+ .returnFacet("funders", { limit: 10 })
1325
+ .build();
1326
+ expect(query).toBe('search publications for "CRISPR" return year return funders limit 10');
1327
+ });
1328
+
1329
+ it("skips entity return AND skip when limit(0) with skip and facets", () => {
1330
+ const query = builder
1331
+ .search("publications")
1332
+ .for("CRISPR")
1333
+ .limit(0)
1334
+ .skip(10)
1335
+ .returnFacet("year")
1336
+ .build();
1337
+ // skip should also be suppressed since there's no entity return
1338
+ expect(query).toBe('search publications for "CRISPR" return year');
1339
+ });
1340
+
1341
+ it("skips entity return AND sort when limit(0) with sort and facets", () => {
1342
+ const query = builder
1343
+ .search("publications")
1344
+ .for("CRISPR")
1345
+ .limit(0)
1346
+ .sort("year", "desc")
1347
+ .returnFacet("funders")
1348
+ .build();
1349
+ // sort should also be suppressed since there's no entity return
1350
+ expect(query).toBe('search publications for "CRISPR" return funders');
1351
+ });
1352
+
1353
+ it("skips entity return AND all modifiers when limit(0) with sort+skip and facets", () => {
1354
+ const query = builder
1355
+ .search("publications")
1356
+ .for("CRISPR")
1357
+ .limit(0)
1358
+ .sort("times_cited", "desc")
1359
+ .skip(50)
1360
+ .returnFacet("year")
1361
+ .returnFacet("funders", { limit: 5 })
1362
+ .build();
1363
+ // All modifiers should be suppressed since there's no entity return
1364
+ expect(query).toBe('search publications for "CRISPR" return year return funders limit 5');
1365
+ });
1366
+
1367
+ it("keeps entity return and skip when skip(0) without limit(0)", () => {
1368
+ const query = builder
1369
+ .search("publications")
1370
+ .for("CRISPR")
1371
+ .skip(0)
1372
+ .returnFacet("year")
1373
+ .build();
1374
+ // skip(0) alone should still trigger entity return (skip=0 is redundant but valid)
1375
+ expect(query).toBe('search publications for "CRISPR" return publications skip 0 return year');
1376
+ });
1377
+
1378
+ it("keeps all modifiers when limit > 0 with facets", () => {
1379
+ const query = builder
1380
+ .search("publications")
1381
+ .for("CRISPR")
1382
+ .limit(10)
1383
+ .skip(5)
1384
+ .sort("year", "desc")
1385
+ .returnFacet("funders")
1386
+ .build();
1387
+ expect(query).toBe(
1388
+ 'search publications for "CRISPR" return publications sort by year desc limit 10 skip 5 return funders',
1389
+ );
1390
+ });
1391
+ });
1392
+
1393
+ describe("DSL injection prevention", () => {
1394
+ it("escapes injection payload in where() string value", () => {
1395
+ const query = builder
1396
+ .search("publications")
1397
+ .for("test")
1398
+ .where("doi", "=", 'test" return publications limit 9999 //')
1399
+ .build();
1400
+
1401
+ expect(query).toBe(
1402
+ 'search publications for "test" where doi = "test\\" return publications limit 9999 //"',
1403
+ );
1404
+ });
1405
+
1406
+ it("escapes injection payload in for() search terms", () => {
1407
+ const query = builder
1408
+ .search("publications")
1409
+ .for('test" return publications limit 9999 //')
1410
+ .build();
1411
+
1412
+ expect(query).toBe('search publications for "test\\" return publications limit 9999 //"');
1413
+ });
1414
+
1415
+ it("escapes injection payload in whereIn() string values", () => {
1416
+ const query = builder
1417
+ .search("publications")
1418
+ .for("test")
1419
+ .whereIn("type", ['article" or 1=1 //', "review"])
1420
+ .build();
1421
+
1422
+ expect(query).toBe(
1423
+ 'search publications for "test" where type in ["article\\" or 1=1 //", "review"]',
1424
+ );
1425
+ });
1426
+
1427
+ it("escapes injection payload in whereRange() string values", () => {
1428
+ const query = builder
1429
+ .search("publications")
1430
+ .for("test")
1431
+ .whereRange("date", '2020-01-01" or 1=1 //', "2020-12-31")
1432
+ .build();
1433
+
1434
+ expect(query).toBe(
1435
+ 'search publications for "test" where date in ["2020-01-01\\" or 1=1 //":"2020-12-31"]',
1436
+ );
1437
+ });
1438
+
1439
+ it("escapes backslashes in where() to prevent escape bypass", () => {
1440
+ const query = builder
1441
+ .search("publications")
1442
+ .for("test")
1443
+ .where("doi", "=", 'test\\" return publications limit 9999 //')
1444
+ .build();
1445
+
1446
+ expect(query).toContain('test\\\\\\"');
1447
+ });
1448
+
1449
+ it("escapes newlines in for() search terms", () => {
1450
+ const query = builder.search("publications").for("line1\nline2").build();
1451
+
1452
+ expect(query).toBe('search publications for "line1\\nline2"');
1453
+ });
1454
+ });
1455
+
1456
+ describe("field name validation", () => {
1457
+ const injectionPayloads = [
1458
+ "year >= 2020 return publications limit 1 //",
1459
+ 'year" return publications //',
1460
+ "field\nreturn publications",
1461
+ "field; DROP TABLE",
1462
+ "field name",
1463
+ "",
1464
+ "123field",
1465
+ "field--comment",
1466
+ ];
1467
+
1468
+ for (const payload of injectionPayloads) {
1469
+ it(`rejects field name: ${JSON.stringify(payload)}`, () => {
1470
+ expect(() => builder.search("publications").where(payload, "=", "value")).toThrow(
1471
+ ValidationError,
1472
+ );
1473
+ });
1474
+ }
1475
+
1476
+ const validFields = ["year", "times_cited", "authors.first_name", "FOR_first", "mesh_terms"];
1477
+
1478
+ for (const field of validFields) {
1479
+ it(`accepts valid field name: ${field}`, () => {
1480
+ expect(() => builder.search("publications").where(field, "=", "test")).not.toThrow();
1481
+ });
1482
+ }
1483
+
1484
+ it("validates field in whereEmpty()", () => {
1485
+ expect(() => builder.search("publications").whereEmpty("bad field")).toThrow(ValidationError);
1486
+ });
1487
+
1488
+ it("validates field in whereNotEmpty()", () => {
1489
+ expect(() => builder.search("publications").whereNotEmpty("bad\nfield")).toThrow(
1490
+ ValidationError,
1491
+ );
1492
+ });
1493
+
1494
+ it("validates field in whereIn()", () => {
1495
+ expect(() => builder.search("publications").whereIn("bad field", ["a"])).toThrow(
1496
+ ValidationError,
1497
+ );
1498
+ });
1499
+
1500
+ it("validates field in whereRange()", () => {
1501
+ expect(() => builder.search("publications").whereRange("bad field", "1", "2")).toThrow(
1502
+ ValidationError,
1503
+ );
1504
+ });
1505
+
1506
+ it("validates field in whereCount()", () => {
1507
+ expect(() => builder.search("publications").whereCount("bad field", ">", 5)).toThrow(
1508
+ ValidationError,
1509
+ );
1510
+ });
1511
+
1512
+ it("validates field in sort()", () => {
1513
+ expect(() => builder.search("publications").sort("bad field")).toThrow(ValidationError);
1514
+ });
1515
+
1516
+ it("validates field in fields()", () => {
1517
+ expect(() => builder.search("publications").fields(["id", "bad field"])).toThrow(
1518
+ ValidationError,
1519
+ );
1520
+ });
1521
+
1522
+ it("validates field in fieldsWithUnnest() regular fields", () => {
1523
+ expect(() =>
1524
+ builder.search("publications").fieldsWithUnnest(["bad field"], ["researchers"]),
1525
+ ).toThrow(ValidationError);
1526
+ });
1527
+
1528
+ it("validates field in fieldsWithUnnest() unnest fields", () => {
1529
+ expect(() => builder.search("publications").fieldsWithUnnest(["id"], ["bad field"])).toThrow(
1530
+ ValidationError,
1531
+ );
1532
+ });
1533
+
1534
+ it("validates field in addUnnest()", () => {
1535
+ expect(() => builder.search("publications").addUnnest("bad field")).toThrow(ValidationError);
1536
+ });
1537
+
1538
+ it("validates field in returnFacet()", () => {
1539
+ expect(() => builder.search("publications").returnFacet("bad field")).toThrow(
1540
+ ValidationError,
1541
+ );
1542
+ });
1543
+
1544
+ it("validates field in returnAggregate()", () => {
1545
+ expect(() => builder.search("publications").returnAggregate("bad field", ["count"])).toThrow(
1546
+ ValidationError,
1547
+ );
1548
+ });
1549
+
1550
+ it("validates fields in returnGrouped()", () => {
1551
+ expect(() =>
1552
+ builder.search("publications").returnGrouped("g", { fields: ["bad field"] }),
1553
+ ).toThrow(ValidationError);
1554
+ });
1555
+
1556
+ it("validates sortField in returnGrouped()", () => {
1557
+ expect(() =>
1558
+ builder.search("publications").returnGrouped("g", { sortField: "bad field" }),
1559
+ ).toThrow(ValidationError);
1560
+ });
1561
+
1562
+ it("validates field in returnGroupedFacet()", () => {
1563
+ expect(() => builder.search("publications").returnGroupedFacet("g", "bad field")).toThrow(
1564
+ ValidationError,
1565
+ );
1566
+ });
1567
+
1568
+ it("validates field in returnGroupedAggregate()", () => {
1569
+ expect(() =>
1570
+ builder.search("publications").returnGroupedAggregate("g", "bad field", ["count"]),
1571
+ ).toThrow(ValidationError);
1572
+ });
1573
+ });
1574
+
1575
+ describe("search text length with sanitization", () => {
1576
+ it("accepts text at limit after zero-width char stripping", () => {
1577
+ // 10000 real chars + 100 zero-width chars = 10100 raw length
1578
+ // After sanitization: 10000 chars — should pass
1579
+ const text = "a".repeat(10_000) + "\u200B".repeat(100);
1580
+ expect(() => new QueryBuilder().search("publications").for(text)).not.toThrow();
1581
+ });
1582
+ });
1583
+
1584
+ describe("clone", () => {
1585
+ it("creates an independent copy with the same state", () => {
1586
+ builder.search("publications").for("test").where("year", ">=", 2020);
1587
+ const cloned = builder.clone();
1588
+
1589
+ expect(cloned.build()).toBe(builder.build());
1590
+ });
1591
+
1592
+ it("does not affect the original when the clone is mutated", () => {
1593
+ builder.search("publications").for("test");
1594
+ const cloned = builder.clone();
1595
+ cloned.where("year", ">=", 2020);
1596
+
1597
+ expect(builder.build()).toBe('search publications for "test"');
1598
+ expect(cloned.build()).toBe('search publications for "test" where year >= 2020');
1599
+ });
1600
+
1601
+ it("does not affect the clone when the original is mutated", () => {
1602
+ builder.search("publications").for("test");
1603
+ const cloned = builder.clone();
1604
+ builder.where("type", "=", "article");
1605
+
1606
+ expect(cloned.build()).toBe('search publications for "test"');
1607
+ expect(builder.build()).toBe('search publications for "test" where type = "article"');
1608
+ });
1609
+
1610
+ it("clones facet clauses independently", () => {
1611
+ builder.search("publications").for("test").returnFacet("year");
1612
+ const cloned = builder.clone();
1613
+ cloned.returnFacet("funders");
1614
+
1615
+ expect(builder.build()).toBe('search publications for "test" return year');
1616
+ expect(cloned.build()).toBe('search publications for "test" return year return funders');
1617
+ });
1618
+
1619
+ it("clones fields independently", () => {
1620
+ builder.search("publications").for("test").fields(["id", "title"]);
1621
+ const cloned = builder.clone();
1622
+ cloned.fields(["id", "title", "doi"]);
1623
+
1624
+ expect(builder.build()).toBe('search publications for "test" return publications[id+title]');
1625
+ expect(cloned.build()).toBe(
1626
+ 'search publications for "test" return publications[id+title+doi]',
1627
+ );
1628
+ });
1629
+ });
1630
+
1631
+ describe("queries with complex()", () => {
1632
+ it("builds query with complex function", () => {
1633
+ const query = builder.search("publications").forComplex("quantum networking", 3).build();
1634
+
1635
+ expect(query).toBe('search publications for complex("quantum networking", 3)');
1636
+ });
1637
+
1638
+ it("escapes quotes in complex phrase", () => {
1639
+ const query = builder.search("publications").forComplex('phrase with "quotes"', 5).build();
1640
+
1641
+ expect(query).toBe('search publications for complex("phrase with \\"quotes\\"", 5)');
1642
+ });
1643
+
1644
+ it("combines complex with where clause", () => {
1645
+ const query = builder
1646
+ .search("publications")
1647
+ .forComplex("CRISPR gene editing", 5)
1648
+ .where("year", ">", 2020)
1649
+ .build();
1650
+
1651
+ expect(query).toBe(
1652
+ 'search publications for complex("CRISPR gene editing", 5) where year > 2020',
1653
+ );
1654
+ });
1655
+
1656
+ it("combines complex with in index", () => {
1657
+ const query = builder
1658
+ .search("publications")
1659
+ .in("title_abstract_only")
1660
+ .forComplex("quantum networking", 3)
1661
+ .build();
1662
+
1663
+ expect(query).toBe(
1664
+ 'search publications in title_abstract_only for complex("quantum networking", 3)',
1665
+ );
1666
+ });
1667
+
1668
+ it("combines complex with limit and fields", () => {
1669
+ const query = builder
1670
+ .search("publications")
1671
+ .forComplex("quantum networking", 3)
1672
+ .fields(["id", "title"])
1673
+ .limit(10)
1674
+ .build();
1675
+
1676
+ expect(query).toBe(
1677
+ 'search publications for complex("quantum networking", 3) return publications[id+title] limit 10',
1678
+ );
1679
+ });
1680
+
1681
+ it("throws on maxDist < 1", () => {
1682
+ expect(() => builder.search("publications").forComplex("test", 0)).toThrow(ValidationError);
1683
+ });
1684
+
1685
+ it("resets complex fields on reset", () => {
1686
+ builder.search("publications").forComplex("test", 3);
1687
+ builder.reset();
1688
+
1689
+ expect(() => builder.build()).toThrow("Entity type must be specified");
1690
+ });
1691
+ });
1692
+
1693
+ describe("queries with min_should_match()", () => {
1694
+ it("builds query with min_should_match function", () => {
1695
+ const query = builder
1696
+ .search("publications")
1697
+ .forMinShouldMatch("quantum OR optical networking", 2)
1698
+ .build();
1699
+
1700
+ expect(query).toBe(
1701
+ 'search publications for min_should_match("quantum OR optical networking", 2)',
1702
+ );
1703
+ });
1704
+
1705
+ it("escapes quotes in min_should_match phrase", () => {
1706
+ const query = builder
1707
+ .search("publications")
1708
+ .forMinShouldMatch('phrase with "quotes"', 2)
1709
+ .build();
1710
+
1711
+ expect(query).toBe('search publications for min_should_match("phrase with \\"quotes\\"", 2)');
1712
+ });
1713
+
1714
+ it("combines min_should_match with where clause", () => {
1715
+ const query = builder
1716
+ .search("publications")
1717
+ .forMinShouldMatch("machine learning deep neural", 2)
1718
+ .where("year", ">=", 2020)
1719
+ .build();
1720
+
1721
+ expect(query).toBe(
1722
+ 'search publications for min_should_match("machine learning deep neural", 2) where year >= 2020',
1723
+ );
1724
+ });
1725
+
1726
+ it("combines min_should_match with in index", () => {
1727
+ const query = builder
1728
+ .search("publications")
1729
+ .in("title_only")
1730
+ .forMinShouldMatch("quantum optical", 1)
1731
+ .build();
1732
+
1733
+ expect(query).toBe(
1734
+ 'search publications in title_only for min_should_match("quantum optical", 1)',
1735
+ );
1736
+ });
1737
+
1738
+ it("combines min_should_match with limit", () => {
1739
+ const query = builder
1740
+ .search("publications")
1741
+ .forMinShouldMatch("quantum OR optical networking", 2)
1742
+ .limit(5)
1743
+ .build();
1744
+
1745
+ expect(query).toBe(
1746
+ 'search publications for min_should_match("quantum OR optical networking", 2) return publications limit 5',
1747
+ );
1748
+ });
1749
+
1750
+ it("throws on min < 1", () => {
1751
+ expect(() => builder.search("publications").forMinShouldMatch("test", 0)).toThrow(
1752
+ ValidationError,
1753
+ );
1754
+ });
1755
+
1756
+ it("resets min_should_match fields on reset", () => {
1757
+ builder.search("publications").forMinShouldMatch("test", 2);
1758
+ builder.reset();
1759
+
1760
+ expect(() => builder.build()).toThrow("Entity type must be specified");
1761
+ });
1762
+ });
1763
+
1764
+ describe("queries with full_data_exact index", () => {
1765
+ it("builds query with full_data_exact index", () => {
1766
+ const query = builder.search("publications").in("full_data_exact").for("BRCA1").build();
1767
+
1768
+ expect(query).toBe('search publications in full_data_exact for "BRCA1"');
1769
+ });
1770
+
1771
+ it("validates full_data_exact as a valid index", () => {
1772
+ expect(() => builder.search("publications").in("full_data_exact")).not.toThrow();
1773
+ });
1774
+ });
1775
+ });