UNPKG

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