import * as querystring from 'querystring' ; import * as JSONStream from 'JSONStream' ; import * as duplexer from 'duplexer' ; import { Query } from './query' ; import { Collection } from './collection' ; import * as versionUtils from './utils/version' ; import { AddResponse , CommonResponse , FullSolrClientParams , JsonResponseData , ResourceOptions , SolrClientParams , UndiciRequestOptions , } from './types' ; import { Duplex } from 'stream' ; import { request , Client as UndiciClient } from 'undici' ; const oldRequest = require ( 'request' ) ; const format = require ( './utils/format' ) ; const JSONbig = require ( 'json-bigint' ) ; export type SearchResult < SolrDocument > = { docs : SolrDocument [ ] ; numFound : number ; numFoundExact : boolean ; start : number ; } ; export type SearchResponse < SolrDocument > = { debug ? : Record < string , any > ; /** If the query defined a `cursorMark` parameter then this field will be present */ nextCursorMark ? : string ; response : SearchResult < SolrDocument > ; responseHeader : { QTime : 0 ; params ? : Record < string , any > ; status : number ; } ; } ; param bigint return function pickJSON ( bigint ) { return bigint ? JSONbig : JSON ; } export function createClient ( options = { } ) { return new Client ( options ) ; } /**
 * Solr client.
 */ export class Client { private readonly options : FullSolrClientParams ; private readonly UPDATE_JSON_HANDLER : string ; private readonly UPDATE_HANDLER : string ; private readonly TERMS_HANDLER : string ; private readonly SPELL_HANDLER : string ; private readonly REAL_TIME_GET_HANDLER : string ; private readonly ADMIN_PING_HANDLER : string ; private readonly COLLECTIONS_HANDLER : string ; private readonly SELECT_HANDLER : string ; private readonly undiciClient : UndiciClient ; constructor ( options = { } ) { this . options = { host : options . host || '127.0.0.1' , port : options . port === 0 ? 0 : options . port || '8983' , core : options . core || '' , path : options . path || '/solr' , tls : options . tls , secure : options . secure || false , bigint : options . bigint || false , get_max_request_entity_size : options . get_max_request_entity_size || false , solrVersion : options . solrVersion || versionUtils . Solr3_2 , ipVersion : options . ipVersion == 6 ? 6 : 4 , request : options . request || null , } ; this . UPDATE_JSON_HANDLER = versionUtils . version ( this . options . solrVersion ) >= versionUtils . Solr4_0 ? 'update' : 'update/json' ; this . UPDATE_HANDLER = 'update' ; this . SELECT_HANDLER = 'select' ; this . COLLECTIONS_HANDLER = 'admin/collections' ; this . ADMIN_PING_HANDLER = 'admin/ping' ; this . REAL_TIME_GET_HANDLER = 'get' ; this . SPELL_HANDLER = 'spell' ; this . TERMS_HANDLER = 'terms' ; const urlPrefix = this . options . host . startsWith ( 'http' ) ? '' : this . options . tls ? 'https://' : 'http://' ; this . undiciClient = new UndiciClient ( `${ urlPrefix }${ this . options . host }:${ this . options . port }` , { connect : this . options . tls , } ) ; } get solrVersion ( ) : number { return this . options . solrVersion ; } param handler returns private getFullHandlerPath ( handler ) { let pathArray ; if ( handler === this . COLLECTIONS_HANDLER ) { pathArray = [ this . options . path , handler ] ; } else { pathArray = [ this . options . path , this . options . core , handler ] ; } return pathArray . filter ( ( e ) => e ) . join ( '/' ) ; } param path param method param body param bodyContentType param acceptContentType returns private async doRequest < T = JsonResponseData > ( path , method , body , bodyContentType , acceptContentType ) { const protocol = this . options . secure ? 'https' : 'http' ; const url = `${ protocol }://${ this . options . host }:${ this . options . port }${ path }` ; const requestOptions = { ... this . options . request , method , } ; if ( ! requestOptions . headers ) { requestOptions . headers = { } ; } requestOptions . headers [ 'accept' ] = acceptContentType ; if ( method === 'POST' ) { if ( bodyContentType ) { requestOptions . headers [ 'content-type' ] = bodyContentType ; } if ( body ) { requestOptions . headers [ 'content-length' ] = Buffer . byteLength ( body ) ; requestOptions . body = body ; } } if ( this . options . authorization ) { requestOptions . headers [ 'authorization' ] = this . options . authorization ; } const response = await this . undiciClient . request ( { path : url , ... requestOptions , } ) ; const text = await response . body . text ( ) ; if ( response . statusCode < 200 || response . statusCode > 299 ) { throw new Error ( `Request HTTP error ${ response . statusCode }: ${ text }` ) ; } return pickJSON ( this . options . bigint ) . parse ( text ) ; } /**
     * Create credential using the basic access authentication method
     */ basicAuth ( username , password ) { this . options . authorization = 'Basic ' + Buffer . from ( username + ':' + password ) . toString ( 'base64' ) ; return this ; } /**
     * Remove authorization header
     */ unauth ( ) { delete this . options . authorization ; return this ; } param docs param queryParameters add ( docs , queryParameters ? ) { docs = format . dateISOify ( docs ) ; docs = Array . isArray ( docs ) ? docs : [ docs ] ; return this . update < AddResponse > ( docs , queryParameters ) ; } /**
     * Updates a document or a list of documents.
     *
     * This function is a clone of `add` and it was added to give more clarity on the availability of atomic updates.
     */ atomicUpdate = Client . prototype . add ; param ids param query realTimeGet < T > ( ids , query = { } ) { ids = Array . isArray ( ids ) ? ids : [ ids ] ; if ( typeof query === 'object' ) { query [ 'ids' ] = ids . join ( ',' ) ; } return this . doQuery ( this . REAL_TIME_GET_HANDLER , query ) ; } /**
     * Add the remote resource located at the given path `options.path` into
     * the Solr database.
     */ addRemoteResource ( options ) { options . parameters = options . parameters || { } ; options . format = options . format === 'xml' ? '' : options . format || '' ; options . parameters . commit = options . parameters . commit === undefined ? false : options . parameters . commit ; options . parameters [ 'stream.contentType' ] = options . contentType || 'text/plain;charset=utf-8' ; if ( options . path . match ( /^https?:\/\// ) ) { options . parameters [ 'stream.url' ] = options . path ; } else { options . parameters [ 'stream.file' ] = options . path ; } const handler = this . UPDATE_HANDLER + '/' + options . format . toLowerCase ( ) ; const query = querystring . stringify ( options . parameters ) ; return this . doQuery ( handler , query ) ; } /**
     * Create a writable/readable `Stream` to add documents into the Solr database.
     */ createAddStream ( options = { } ) { const path = [ this . options . path , this . options . core , this . UPDATE_JSON_HANDLER + '?' + querystring . stringify ( { ... options , wt : 'json' } ) , ] . filter ( function ( element ) { return element ; } ) . join ( '/' ) ; const headers = { 'content-type' : 'application/json' , charset : 'utf-8' , } ; if ( this . options . authorization ) { headers [ 'authorization' ] = this . options . authorization ; } const protocol = this . options . secure ? 'https' : 'http' ; const optionsRequest = { url : protocol + '://' + this . options . host + ':' + this . options . port + path , method : 'POST' , headers : headers , } ; const jsonStreamStringify = JSONStream . stringify ( ) ; const postRequest = oldRequest ( optionsRequest ) ; jsonStreamStringify . pipe ( postRequest ) ; return duplexer ( jsonStreamStringify , postRequest ) ; } /**
     * Commit last added and removed documents, that means your documents are now indexed.
     */ commit ( options ? ) { return this . update ( { commit : options || { } , } ) ; } /**
     * Call Lucene's IndexWriter.prepareCommit, the changes won't be visible in the index.
     */ prepareCommit ( ) { return this . update ( { } , { prepareCommit : true } ) ; } /**
     * Soft commit all changes
     */ softCommit ( ) { return this . update ( { } , { softCommit : true } ) ; } /**
     * Delete documents based on the given `field` and `text`.
     */ delete ( field , text , options ? ) { return this . update ( { delete : { query : field + ':' + format . escapeSpecialChars ( format . dateISOify ( text ) ) , } , } , options ) ; } /**
     * Delete a range of documents based on the given `field`, `start` and `stop` arguments.
     */ deleteByRange ( field , start , stop , options ? ) { start = format . dateISOify ( start ) ; stop = format . dateISOify ( stop ) ; return this . deleteByQuery ( `${ field }:[${ start } TO ${ stop }]` , options ) ; } param id param options deleteByID ( id , options ? ) { return this . update ( { delete : { id : id , } , } , options ) ; } /**
     * Delete documents matching the given `query`.
     */ deleteByQuery ( query , options ? ) { return this . update ( { delete : { query : query , } , } , options ) ; } /**
     * Delete all documents.
     */ deleteAll ( options ? ) { return this . deleteByQuery ( '*:*' , options ) ; } /**
     * Optimize the index.
     */ optimize ( options ) { return this . update ( { optimize : options || { } , } ) ; } /**
     * Rollback all add/delete commands made since the last commit.
     */ rollback ( ) { return this . update ( { rollback : { } , } ) ; } param data param queryParameters update < T > ( data , queryParameters ? ) { const path = this . getFullHandlerPath ( this . UPDATE_JSON_HANDLER ) ; const queryString = querystring . stringify ( { ... queryParameters , wt : 'json' , } ) ; return this . doRequest < T > ( `${ path }?${ queryString }` , 'POST' , pickJSON ( this . options . bigint ) . stringify ( data ) , 'application/json' , 'application/json; charset=utf-8' ) ; } /**
     * Search documents matching the `query`
     */ search < T > ( query ) { return this . doQuery < SearchResponse < T > > ( this . SELECT_HANDLER , query ) ; } /**
     * Execute an Admin Collections task on `collection`
     */ executeCollection ( collection ) { return this . doQuery ( this . COLLECTIONS_HANDLER , collection ) ; } /**
     * Search for all documents.
     */ searchAll ( ) { return this . search ( 'q=*' ) ; } /**
     * Search documents matching the `query`, with spellchecking enabled.
     */ spell ( query ) { return this . doQuery ( this . SPELL_HANDLER , query ) ; } /**
     * Terms search.
     *
     * Provides access to the indexed terms in a field and the number of documents that match each term.
     */ termsSearch ( query ) { return this . doQuery ( this . TERMS_HANDLER , query ) ; } param handler param query async doQuery < T > ( handler , query ) { let data ; if ( query instanceof Query || query instanceof Collection ) { data = query . build ( ) ; } else if ( typeof query === 'object' ) { data = querystring . stringify ( query ) ; } else { data = query ; } const path = this . getFullHandlerPath ( handler ) ; const queryString = data ? data + '&wt=json' : 'wt=json' ; const approxUrlLength = 10 + Buffer . byteLength ( this . options . host ) + this . options . port . toString ( ) . length + Buffer . byteLength ( path ) + 1 + Buffer . byteLength ( queryString ) ; const method = this . options . get_max_request_entity_size === false || approxUrlLength <= this . options . get_max_request_entity_size ? 'GET' : 'POST' ; return this . doRequest < T > ( method === 'GET' ? `${ path }?${ queryString }` : path , method , method === 'POST' ? data : null , method === 'POST' ? 'application/x-www-form-urlencoded; charset=utf-8' : null , 'application/json; charset=utf-8' ) ; } /**
     * Create an instance of `Query`
     */ query ( ) { return new Query ( this . options ) ; } /**
     * Create an instance of `Collection`.
     */ collection ( ) { return new Collection ( ) ; } /**
     * Expose `format.escapeSpecialChars` from `Client.escapeSpecialChars`
     */ escapeSpecialChars = format . escapeSpecialChars ; /**
     * Ping the Solr server.
     */ ping ( ) { return this . doQuery ( this . ADMIN_PING_HANDLER , '' ) ; } param { string } fieldName param { string } fieldType public createSchemaField ( fieldName , fieldType ) { return this . doRequest ( this . getFullHandlerPath ( 'schema' ) , 'POST' , pickJSON ( this . options . bigint ) . stringify ( { 'add-field' : { name : fieldName , type : fieldType , multiValued : false , stored : true , } , } ) , 'application/json' , 'application/json; charset=utf-8' ) . catch ( ( err ) => { console . warn ( err . message ) ; return { } ; } ) ; } }