UNPKG

28.5 kBPlain TextView Raw
1import * as Immutable from 'seamless-immutable';
2
3import {
4 SolrResponse
5} from './Data';
6
7import * as fetchJsonp from 'fetch-jsonp';
8import * as _ from 'lodash';
9
10import { FacetValue } from '../component/facet/FacetTypes';
11
12function escape(value: QueryParam): string {
13 if (value === '*') {
14 return value;
15 }
16
17 return (
18 (value.toString().indexOf(' ') > 0) ? (
19 '"' + value.toString().replace(/ /g, '%20') + '"'
20 ) : (
21 value.toString()
22 )
23 );
24}
25
26// Note that the stored versions of these end up namespaced and/or aliased
27enum UrlParams {
28 ID = 'id',
29 QUERY = 'query',
30 FQ = 'fq',
31 START = 'start',
32 TYPE = 'type',
33 HL = 'highlight'
34}
35
36interface SavedSearch {
37 type?: 'QUERY' | 'MLT' | 'DETAILS';
38 query?: string;
39 boost?: string;
40 facets?: { [ key: string ]: string[] };
41}
42
43interface SearchParams extends SavedSearch {
44 start?: number;
45}
46
47type QueryParam = string | number;
48type NamespacedUrlParam = [UrlParams, QueryParam];
49type UrlFragment = [UrlParams | NamespacedUrlParam, QueryParam | [string, QueryParam[]]] | null; // k, v
50
51class QueryBeingBuilt {
52 solrUrlFragment: string;
53 appUrlFragment: UrlFragment;
54
55 constructor(solrUrlFragment: string, appUrlFragment: UrlFragment) {
56 this.solrUrlFragment = solrUrlFragment;
57
58 // TODO, these need to support the following:
59 // aliasing multiple parameters (i.e. any of the following values could be a 'q')
60 // this supports renames over time
61 // urls in the path or the query
62 // this is for seo
63 // aliasing groups of parameters (this is like the named search example)
64 // numeric indexes - e.g. fq
65 // facets should have some special handling;
66 // facets can be arrays
67 // facets can be hierarchies (probably there are different implementations of this)
68 // "named" values that wrap sets of parameters - e.g named sort fields or enum queries
69 // need to be able to namespace the output of this to be unique to support multiple searches on a site
70 // this needs to work 100% bidirectional (i.e. url -> objects, objects -> url)
71 this.appUrlFragment = appUrlFragment;
72 }
73}
74
75class SolrQueryBuilder<T> {
76 searchResponse?: SolrResponse<T>;
77
78 previous?: SolrQueryBuilder<T>;
79 op: () => QueryBeingBuilt;
80
81 constructor(op: () => QueryBeingBuilt, previous?: SolrQueryBuilder<T>) {
82 this.op = op;
83 this.previous = previous;
84 }
85
86 get(id: QueryParam) {
87 return new SolrQueryBuilder<T>(
88 () => new QueryBeingBuilt('get?id=' + id, [UrlParams.ID, id]),
89 this
90 );
91 }
92
93 select() {
94 return new SolrQueryBuilder<T>(
95 () => new QueryBeingBuilt('select?facet=true', [UrlParams.TYPE, 'QUERY']),
96 this
97 );
98 }
99
100 moreLikeThis(handler: string, col: string, id: QueryParam) {
101 return new SolrQueryBuilder<T>(
102 () => new QueryBeingBuilt(handler + '?q=' + col + ':' + id, [UrlParams.ID, id]),
103 this
104 );
105 }
106
107 start(start: number) {
108 return new SolrQueryBuilder<T>(
109 () => new QueryBeingBuilt('start=' + start, [UrlParams.START, start]),
110 this
111 );
112 }
113
114 jsonp(callback: string) {
115 return new SolrQueryBuilder<T>(
116 () => new QueryBeingBuilt('wt=json&json.wrf=' + callback, null),
117 this
118 );
119 }
120
121 export() {
122 return new SolrQueryBuilder<T>(
123 () => new QueryBeingBuilt('wt=csv', null),
124 this
125 );
126 }
127
128 qt(qt: string) {
129 return new SolrQueryBuilder<T>(
130 // TODO: tv.all here is probably wrong
131 () => new QueryBeingBuilt('tv.all=true&qt=' + qt, null),
132 this
133 );
134 }
135
136 q(searchFields: string[], value: QueryParam) {
137 return new SolrQueryBuilder<T>(
138 () =>
139 new QueryBeingBuilt(
140 'defType=edismax&q=' +
141 searchFields.map(
142 (field) => field + ':' + escape(value)
143 ).join('%20OR%20'),
144 [UrlParams.QUERY, value]
145 )
146 ,
147 this
148 );
149 }
150
151 bq(query: string) {
152 return new SolrQueryBuilder<T>(
153 () => {
154 return new QueryBeingBuilt(
155 'bq=' + query,
156 null
157 );
158 },
159 this
160 );
161 }
162
163 fq(field: string, values: QueryParam[]) {
164 return new SolrQueryBuilder<T>(
165 () => {
166 return new QueryBeingBuilt(
167 'fq=' +
168 '{!tag=' + field + '_q}' +
169 values.map(escape).map((v) => field + ':' + v).join('%20OR%20'),
170 [UrlParams.FQ, [field, values]]
171 );
172 },
173 this
174 );
175 }
176
177 highlight(query: HighlightQuery) {
178 if (query.fields.length > 0) {
179 return new SolrQueryBuilder<T>(
180 () => {
181 let params: string[] = [
182 'method',
183 'fields',
184 'query',
185 'qparser',
186 'requireFieldMatch',
187 'usePhraseHighlighter',
188 'highlightMultiTerm',
189 'snippets',
190 'fragsize',
191 'encoder',
192 'maxAnalyzedChars'].map(
193 (key: string) => query[key] ? (
194 'hl.' + key + '=' + query[key]
195 ) : ''
196 ).filter(
197 (key) => key !== ''
198 );
199
200 if (query.pre) {
201 params.push('hl.tag.pre=' + query.pre);
202 }
203
204 if (query.post) {
205 params.push('hl.tag.post=' + query.pre);
206 }
207
208 return new QueryBeingBuilt(
209 'hl=true&' +
210 'hl.fl=' + query.fields.join(',') + (
211 params.length > 0 ? ( '&' + params.join('&') ) : ''
212 ),
213 // Not saving these, because I'm assuming these will be
214 // configured as part of the search engine, rather than
215 // changed by user behavior
216 null
217 );
218 }
219 );
220 } else {
221 return null;
222 }
223 }
224
225 fl(fields: QueryParam[]) {
226 return new SolrQueryBuilder<T>(
227 () =>
228 new QueryBeingBuilt(
229 'fl=' + fields.join(','),
230 null
231 ),
232 this
233 );
234 }
235
236 requestFacet(field: string) {
237 return new SolrQueryBuilder<T>(
238 () =>
239 new QueryBeingBuilt(
240 `facet.field={!ex=${field}_q}${field}&` +
241 `facet.mincount=1&` +
242 `facet.${field}.limit=50&` +
243 `facet.${field}.sort=count`,
244 null
245 ),
246 this
247 );
248 }
249
250 rows(rows: number) {
251 return new SolrQueryBuilder<T>(
252 () =>
253 new QueryBeingBuilt(
254 'rows=' + rows,
255 null
256 ),
257 this
258 );
259 }
260
261 sort(fields: string[]) {
262 return new SolrQueryBuilder<T>(
263 () =>
264 new QueryBeingBuilt(
265 'sort=' + fields.map(escape).join(','),
266 // TODO: I think this should add a 'named sort' to the URL, because
267 // this could have a large amount of Solr specific stuff in it - i.e.
268 // add(field1, mul(field2, field3)) and you don't want to expose the
269 // internals to external search engines, or they'll index the site in
270 // a way that would make this hard to change
271 null
272 ),
273 this
274 );
275 }
276
277 schema() {
278 return new SolrQueryBuilder<T>(
279 () =>
280 new QueryBeingBuilt(
281 'admin/luke?',
282 null
283 )
284 );
285 }
286
287 construct(): [string, Array<UrlFragment>] {
288 let start: [string, Array<UrlFragment>] = ['', []];
289 if (this.previous) {
290 start = this.previous.construct();
291 }
292
293 if (start[0] !== '') {
294 start[0] = start[0] + '&';
295 }
296
297 const output = this.op();
298
299 return [
300 start[0] + output.solrUrlFragment,
301 start[1].concat([output.appUrlFragment])
302 ];
303 }
304
305 buildCurrentParameters(): SearchParams {
306 const initialParams = this.construct()[1];
307 const result: Array<UrlFragment> = initialParams;
308
309 const searchParams: SearchParams = {
310 };
311
312 result.map(
313 (p: UrlFragment) => {
314 if (p !== null) {
315 if (p[0] === UrlParams.FQ) {
316 const facet = p[1][0];
317 const values = p[1][1];
318
319 if (!searchParams.facets) {
320 searchParams.facets = {};
321 }
322
323 searchParams.facets[facet] = values;
324 } else {
325 searchParams[p[0] as string] = p[1];
326 }
327 }
328 }
329 );
330
331 return searchParams;
332 }
333
334 buildSolrUrl() {
335 return this.construct()[0];
336 }
337}
338
339interface PaginationData {
340 numFound: number;
341 start: number;
342 pageSize: number;
343}
344
345type QueryEvent<T> = (object: T[], paging: PaginationData) => void;
346type FacetEvent = (object: FacetValue[]) => void;
347type ErrorEvent = (error: object) => void;
348type GetEvent<T> = (object: T) => void;
349type MoreLikeThisEvent<T> = (object: T[]) => void;
350
351interface SolrGet<T> {
352 doGet: (id: string | number) => void;
353 onGet: (cb: GetEvent<T>) => void;
354}
355
356interface SolrUpdate {
357 doUpdate: (id: string | number, attr: string, value: string) => void;
358}
359
360interface SolrQuery<T> {
361 doQuery: (q: GenericSolrQuery) => void;
362 doExport: (q: GenericSolrQuery) => void;
363 onQuery: (cb: QueryEvent<T>) => void;
364 registerFacet: (facet: string[]) => (cb: FacetEvent) => void;
365 refine: (q: GenericSolrQuery) => SolrQuery<T>;
366}
367
368interface SolrHighlight<T> {
369 onHighlight: (cb: QueryEvent<T>) => void;
370}
371
372interface SolrMoreLikeThis<T> {
373 doMoreLikeThis: (id: string | number) => void;
374 onMoreLikeThis: (cb: MoreLikeThisEvent<T>) => void;
375}
376
377interface SolrTransitions {
378 clearEvents: () => void;
379 getCoreConfig: () => SolrConfig;
380 getNamespace: () => string;
381 getCurrentParameters: () => SearchParams;
382 stateTransition: (v: SearchParams) => void;
383}
384
385type SolrSchemaFieldDefinition = {
386 type: string;
387 schema: string;
388 dynamicBase: string;
389 docs: number;
390};
391
392function mergeQuery(
393 q1?: GenericSolrQuery,
394 q2?: GenericSolrQuery
395): GenericSolrQuery {
396 if (!q1) {
397 return q2 || { query: '*:* /* q2 */'};
398 }
399
400 if (!q2) {
401 return q1 || { query: '*:* /* q1 */'};
402 }
403
404 return {
405 query: '(' + q1.query + ') AND (' + q2.query + ')',
406 rows: q1.rows || q2.rows,
407 boost: q1.boost || q2.boost
408 };
409}
410
411type SolrSchemaDefinition = {
412 responseHeader: { status: number; QTime: number };
413 index: {
414 numDocs: number;
415 maxDoc: number,
416 deletedDocs: number,
417 indexHeapUsageBytes: number;
418 version: number;
419 segmentCount: number;
420 current: boolean;
421 hasDeletions: boolean;
422 directory: string;
423 segmentsFile: string;
424 segmentsFileSizeInBytes: number;
425 userData: {
426 commitTimeMSec: string;
427 commitCommandVer: string;
428 };
429
430 lastModified: string;
431 },
432 fields: { [key: string]: SolrSchemaFieldDefinition};
433 info: object;
434};
435
436interface SolrSchema {
437 getSchema: () => SolrSchemaDefinition;
438}
439
440// TODO - this needs a lot more definition to be useful
441interface GenericSolrQuery {
442 query: string;
443 boost?: string;
444 rows?: number;
445}
446
447/**
448 * See: https://lucene.apache.org/solr/guide/6_6/highlighting.html
449 */
450interface HighlightQuery {
451 method?: 'unified' | 'original' | 'fastVector' | 'postings';
452 fields: string[];
453 query?: string;
454 qparser?: string;
455 requireFieldMatch?: boolean;
456 usePhraseHighlighter?: boolean;
457 highlightMultiTerm?: boolean;
458 snippets?: number;
459 fragsize?: number;
460 pre?: string;
461 post?: string;
462 encoder?: string;
463 maxAnalyzedChars?: number;
464}
465
466interface SolrConfig {
467 url: string;
468 core: string;
469 primaryKey: string;
470 defaultSearchFields: string[];
471 fields: string[];
472 pageSize: number;
473 prefix: string;
474 fq?: [string, string];
475 qt?: string;
476}
477
478// This needs to be a global in case you trigger
479// a bunch of JSONP requests all at once
480let requestId: number = 0;
481
482class SolrCore<T> implements SolrTransitions {
483 solrConfig: SolrConfig;
484 private events: {
485 query: QueryEvent<T>[],
486 error: ErrorEvent[],
487 get: GetEvent<T>[],
488 mlt: MoreLikeThisEvent<T>[],
489 facet: { [key: string]: FacetEvent[] };
490 };
491
492 private currentParameters: SearchParams = {};
493
494 private query?: GenericSolrQuery;
495 private getCache = {};
496 private mltCache = {};
497 private refinements: SolrCore<T>[] = [];
498
499 constructor(solrConfig: SolrConfig) {
500 this.solrConfig = solrConfig;
501 this.clearEvents();
502
503 // this.onGet = this.memoize(this.onGet);
504 }
505
506 clearEvents() {
507 this.events = {
508 query: [],
509 error: [],
510 get: [],
511 mlt: [],
512 facet: {}
513 };
514
515 if (_.keys(this.getCache).length > 100) {
516 this.getCache = {};
517 }
518
519 if (_.keys(this.mltCache).length > 100) {
520 this.mltCache = {};
521 }
522 }
523
524 getCoreConfig() {
525 return this.solrConfig;
526 }
527
528 onQuery(op: QueryEvent<T>) {
529 this.events.query.push(op);
530 }
531
532 refine(query: GenericSolrQuery) {
533 // TODO - decide if the events should proxy
534
535 // https://stackoverflow.com/questions/728360/how-do-i-correctly-clone-a-javascript-object#728694
536 const obj: SolrCore<T> = this;
537 if (null == obj || 'object' !== typeof obj) {
538 return obj;
539 }
540
541 const copy: SolrCore<T> = new SolrCore<T>(this.solrConfig);
542 for (let attr in obj) {
543 if (
544 obj.hasOwnProperty(attr) &&
545 attr !== 'events' &&
546 attr !== 'refinements'
547 ) {
548 copy[attr] = obj[attr];
549 }
550 }
551
552 copy.clearEvents();
553
554 copy.query = mergeQuery(query, this.query);
555 this.refinements.push(copy);
556
557 return copy;
558 }
559
560 registerFacet(facetNames: string[]) {
561 const events = this.events.facet;
562
563 return function facetBind(cb: FacetEvent) {
564 // this works differently than the other event types because
565 // you may not know in advance what all the facets should be
566 facetNames.map(
567 (facetName) => {
568 events[facetName] =
569 (events[facetName] || []);
570
571 events[facetName].push(cb);
572 }
573 );
574
575 };
576 }
577
578 onError(op: ErrorEvent) {
579 this.events.error.push(op);
580 }
581
582 doMoreLikeThis(id: string | number) {
583 this.prefetchMoreLikeThis(id, true);
584 }
585
586 getNamespace() {
587 return '';
588 }
589
590 prefetchMoreLikeThis(id: string | number, prefetch: boolean) {
591 const self = this;
592
593 if (!self.mltCache[id]) {
594 const callback = 'cb_' + requestId++;
595
596 const qb =
597 new SolrQueryBuilder(
598 () => new QueryBeingBuilt('', null)
599 ).moreLikeThis(
600 'mlt', // TODO - configurable
601 self.solrConfig.primaryKey,
602 id
603 ).fl(self.solrConfig.fields).jsonp(
604 callback
605 );
606
607 const url = self.solrConfig.url + self.solrConfig.core + '/' + qb.buildSolrUrl();
608
609 fetchJsonp(url, {
610 jsonpCallbackFunction: callback
611 }).then(
612 (data) => {
613 data.json().then(
614 (responseData) => {
615 const mlt = responseData.response.docs;
616 if (prefetch) {
617 self.events.mlt.map(
618 (event) => {
619 event(Immutable(mlt));
620 }
621 );
622
623 mlt.map(
624 (doc) => {
625 self.prefetchMoreLikeThis(
626 doc[this.solrConfig.primaryKey],
627 false
628 );
629 }
630 );
631 }
632
633 responseData.response.docs.map(
634 (doc) => self.getCache[id] = responseData.doc
635 );
636
637 self.mltCache[id] = responseData.response.docs;
638 }
639 ).catch(
640 (error) => {
641 self.events.error.map(
642 (event) => event(error)
643 );
644 }
645 );
646 }
647 );
648 } else {
649 self.events.mlt.map(
650 (event) => {
651 event(Immutable(this.mltCache[id]));
652 }
653 );
654 }
655 }
656
657 onMoreLikeThis(op: (v: T[]) => void) {
658 this.events.mlt.push(op);
659 }
660
661 onGet(op: GetEvent<T>) {
662 this.events.get.push(op);
663 }
664
665 doGet(id: string | number) {
666 const self = this;
667 const callback = 'cb_' + requestId++;
668
669 const qb =
670 new SolrQueryBuilder(() => new QueryBeingBuilt('', null)).get(
671 id
672 ).fl(this.solrConfig.fields).jsonp(
673 callback
674 );
675
676 const url = this.solrConfig.url + this.solrConfig.core + '/' + qb.buildSolrUrl();
677
678 if (!this.getCache[id]) {
679 fetchJsonp(url, {
680 jsonpCallbackFunction: callback
681 }).then(
682 (data) => {
683 data.json().then(
684 (responseData) => {
685 self.events.get.map(
686 (event) => event(Immutable(responseData.doc))
687 );
688
689 this.getCache[id] = responseData.doc;
690 }
691 ).catch(
692 (error) => {
693 self.events.error.map(
694 (event) => event(error)
695 );
696 }
697 );
698 }
699 );
700 } else {
701 self.events.get.map(
702 (event) => event(Immutable(this.getCache[id]))
703 );
704 }
705 }
706
707 doQuery(desiredQuery: GenericSolrQuery, cb?: (qb: SolrQueryBuilder<{}>) => SolrQueryBuilder<{}>) {
708 const self = this;
709
710 // this lets you make one master datastore and refine off it,
711 // and if no controls need it, it won't be run
712
713 if (self.events.query.length > 0) {
714 const callback = 'cb_' + (requestId++);
715 // this lets the provided query override rows/boost
716 const query: GenericSolrQuery =
717 mergeQuery(desiredQuery, this.query);
718
719 let qb =
720 new SolrQueryBuilder(
721 () => new QueryBeingBuilt('', null),
722 );
723
724 qb = qb.select().q(
725 this.solrConfig.defaultSearchFields,
726 query.query
727 );
728
729 if (this.solrConfig.qt) {
730 qb = qb.qt(this.solrConfig.qt);
731 }
732
733 qb = qb.fl(this.solrConfig.fields).rows(
734 query.rows || this.solrConfig.pageSize
735 );
736
737 if (this.solrConfig.fq) {
738 qb = qb.fq(this.solrConfig.fq[0], [this.solrConfig.fq[1]]);
739 }
740
741 if (query.boost) {
742 qb = qb.bq(query.boost);
743 }
744
745 _.map(
746 this.events.facet,
747 (v, k) => {
748 qb = qb.requestFacet(k);
749 }
750 );
751
752 if (cb) {
753 qb = cb(qb);
754 }
755
756 qb = qb.jsonp(
757 callback
758 );
759
760 const url = this.solrConfig.url + this.solrConfig.core + '/' + qb.buildSolrUrl();
761
762 fetchJsonp(url, {
763 jsonpCallbackFunction: callback,
764 timeout: 30000
765 }).then(
766 (data) => {
767 this.currentParameters = qb.buildCurrentParameters();
768
769 data.json().then(
770 (responseData) => {
771 self.events.query.map(
772 (event) =>
773 event(
774 Immutable(responseData.response.docs),
775 Immutable({
776 numFound: responseData.response.numFound,
777 start: responseData.response.start,
778 pageSize: query.rows || 10
779 })
780 )
781 );
782
783 const facetCounts = responseData.facet_counts;
784 if (facetCounts) {
785 const facetFields = facetCounts.facet_fields;
786 if (facetFields) {
787 _.map(
788 self.events.facet,
789 (events, k) => {
790 if (facetFields[k]) {
791 const previousValues = (this.currentParameters.facets || {})[k];
792
793 const facetLabels = facetFields[k].filter( (v, i) => i % 2 === 0 );
794 const facetLabelCount = facetFields[k].filter( (v, i) => i % 2 === 1 );
795 const facetSelections = facetLabels.map(
796 (value) => _.includes(previousValues, value)
797 );
798
799 events.map(
800 (event) => {
801 event(
802 _.zipWith(facetLabels, facetLabelCount, facetSelections).map(
803 (facetData: [string, number, boolean]) => {
804 return {
805 value: facetData[0],
806 count: facetData[1],
807 checked: facetData[2]
808 };
809 }
810 ));
811 }
812 );
813 }
814 }
815 );
816 }
817 }
818
819 }
820 ).catch(
821 (error) => {
822 self.events.error.map(
823 (event) => event(error)
824 );
825 }
826 );
827 }
828 );
829 }
830
831 this.refinements.map(
832 (refinement) => refinement.doQuery(desiredQuery, cb)
833 );
834 }
835
836 doUpdate(id: string | number, attr: string, value: string) {
837 const self = this;
838
839 const op = {
840 id: id,
841 };
842
843 op[attr] = { set: value };
844
845 const url = this.solrConfig.url + this.solrConfig.core + '/' + 'update?commit=true';
846
847 const http = new XMLHttpRequest();
848 const params = JSON.stringify(op);
849
850 http.open('POST', url, true);
851
852 http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
853
854 delete this.getCache[id];
855
856 http.onreadystatechange = function() {
857 if (http.readyState === 4) {
858 // trigger UI re-render - don't care if success or failure
859 // because we might have succeeded but got a CORS error
860 self.doGet(id);
861 }
862 };
863
864 http.send(params);
865 }
866
867 doExport() {
868 const query = this.getCurrentParameters();
869
870 let qb =
871 new SolrQueryBuilder(
872 () => new QueryBeingBuilt('', null),
873 ).select().q(
874 this.solrConfig.defaultSearchFields,
875 query.query || '*'
876 ).fl(this.solrConfig.fields)
877 .rows(
878 2147483647
879 );
880
881 if (this.solrConfig.fq) {
882 qb = qb.fq(this.solrConfig.fq[0], [this.solrConfig.fq[1]]);
883 }
884
885 _.map(
886 this.events.facet,
887 (v, k) => {
888 qb = qb.requestFacet(k);
889 }
890 );
891
892 qb = qb.export();
893
894 const url = this.solrConfig.url + this.solrConfig.core + '/' + qb.buildSolrUrl();
895 window.open(url, '_blank');
896 }
897
898 next(op: (event: SolrQueryBuilder<T>) => SolrQueryBuilder<T>) {
899 const self = this;
900
901 const qb =
902 op(
903 new SolrQueryBuilder<T>(
904 () => new QueryBeingBuilt('', null)
905 )
906 ).fl(self.solrConfig.fields);
907
908 const url = self.solrConfig.url + self.solrConfig.core + '/select?' + qb.buildSolrUrl();
909
910 fetchJsonp(url).then(
911 (data) => {
912 data.json().catch(
913 (error) => {
914 self.events.error.map(
915 (event) => event(error)
916 );
917 }
918 ).then(
919 (responseData) => {
920 self.events.get.map(
921 (event) => event(Immutable(responseData.response.docs))
922 );
923 }
924 );
925 }
926 );
927 }
928
929 stateTransition(newState: SearchParams) {
930 if (newState.type === 'QUERY') {
931 this.doQuery(
932 {
933 rows: this.solrConfig.pageSize,
934 query: newState.query || '*',
935 boost: newState.boost
936 },
937 (qb: SolrQueryBuilder<{}>) => {
938 let response = qb.start(
939 newState.start || 0
940 );
941
942 _.map(
943 newState.facets,
944 (values: string[], k: string) => {
945 if (values.length > 0) {
946 response =
947 response.fq(
948 k, values
949 );
950 }
951 }
952 );
953
954 return response;
955 }
956 );
957 } else {
958 throw 'INVALID STATE TRANSITION: ' + JSON.stringify(newState);
959 }
960 }
961
962 getCurrentParameters(): SearchParams {
963 return this.currentParameters;
964 }
965}
966
967// TODO: I want a way to auto-generate these from Solr management APIs
968// TODO: This thing should provide some reflection capability so the
969// auto-registered version can be used to bind controls through
970// a properties picker UI
971class DataStore {
972 cores: { [ keys: string ]: SolrCore<object> } = {};
973
974 clearEvents() {
975 _.map(
976 this.cores,
977 (v: SolrCore<object>, k) => v.clearEvents()
978 );
979 }
980
981 registerCore<T extends object>(config: SolrConfig): SolrCore<T> {
982 // Check if this exists - Solr URL + core should be enough
983 let key = config.url;
984
985 if (!_.endsWith(key, '/')) {
986 key += '/';
987 }
988
989 key += config.core;
990
991 if (!this.cores[key]) {
992 this.cores[key] = new SolrCore<T>(config);
993 }
994
995 return (this.cores[key] as SolrCore<T>);
996 }
997}
998
999class AutoConfiguredDataStore extends DataStore {
1000 private core: SolrCore<object> & SolrGet<object> & SolrQuery<object>;
1001 private facets: string[];
1002 private fields: string[];
1003
1004 getCore() {
1005 return this.core;
1006 }
1007
1008 getFacets() {
1009 return this.facets;
1010 }
1011
1012 getFields() {
1013 return this.fields;
1014 }
1015
1016 autoconfigure<T extends object>(
1017 config: SolrConfig,
1018 complete: () => void,
1019 useFacet: (facet: string) => boolean
1020 ): void {
1021 const callback = 'cb_autoconfigure_' + config.core;
1022 useFacet = useFacet || ((facet: string) => true);
1023
1024 let qb =
1025 new SolrQueryBuilder(
1026 () => new QueryBeingBuilt('', null),
1027 ).schema().jsonp(
1028 callback
1029 );
1030
1031 const url = config.url + config.core + '/' + qb.buildSolrUrl();
1032
1033 fetchJsonp(url, {
1034 jsonpCallbackFunction: callback
1035 }).then(
1036 (data) => {
1037 data.json().then(
1038 (responseData: SolrSchemaDefinition) => {
1039 // TODO cache aggressively
1040 const fields =
1041 _.toPairs(responseData.fields).filter(
1042 ([fieldName, fieldDef]) => {
1043 return fieldDef.docs > 0 && fieldDef.schema.match(/I.S............../);
1044 }
1045 ).map(
1046 ([fieldName, fieldDef]) => fieldName
1047 );
1048
1049 const defaultSearchFields =
1050 _.toPairs(responseData.fields).filter(
1051 ([fieldName, fieldDef]) => {
1052 return fieldDef.docs > 0 && fieldDef.schema.match(/I................/);
1053 }
1054 ).map(
1055 ([fieldName, fieldDef]) => fieldName
1056 );
1057
1058 const coreConfig = _.extend(
1059 {},
1060 {
1061 primaryKey: 'id',
1062 fields: fields,
1063 defaultSearchFields: defaultSearchFields,
1064 pageSize: 50,
1065 prefix: config.core
1066 },
1067 config
1068 );
1069
1070 this.core = new SolrCore<T>(coreConfig);
1071 this.fields = fields;
1072 this.facets = fields.filter(useFacet);
1073
1074 this.core.registerFacet(this.facets)(_.identity);
1075 complete();
1076 }
1077 );
1078 }
1079 );
1080 }
1081}
1082
1083type SingleComponent<T> =
1084 (data: T, index?: number) => object | null | undefined;
1085
1086export {
1087 ErrorEvent,
1088 UrlParams,
1089 QueryParam,
1090 HighlightQuery,
1091 NamespacedUrlParam,
1092 UrlFragment,
1093 PaginationData,
1094 SavedSearch,
1095 SearchParams,
1096 QueryBeingBuilt,
1097 SolrQueryBuilder,
1098 SingleComponent,
1099 MoreLikeThisEvent,
1100 GetEvent,
1101 GenericSolrQuery,
1102 QueryEvent,
1103 FacetEvent,
1104 SolrConfig,
1105 SolrGet,
1106 SolrUpdate,
1107 SolrMoreLikeThis,
1108 SolrQuery,
1109 SolrHighlight,
1110 SolrTransitions,
1111 SolrCore,
1112 SolrSchemaFieldDefinition,
1113 SolrSchemaDefinition,
1114 SolrSchema,
1115 DataStore,
1116 AutoConfiguredDataStore
1117};
\No newline at end of file