Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Single level of abstraction / Better separation of concerns in query methods #280

Merged
merged 5 commits into from
Aug 30, 2021
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Better separation of concerns: Separate functions for doQuery and `…
…doRequest` rather than separate functions for `get` and `post` (which did almost exactly the same thing).
rudolfbyker committed Aug 30, 2021

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
commit 5531fb5405c18ccb0f15284a633474279d710544
386 changes: 150 additions & 236 deletions lib/solr.ts
Original file line number Diff line number Diff line change
@@ -146,6 +146,81 @@ export class Client {
return pathArray.filter((e) => e).join('/');
}

/**
* Common function for all HTTP requests.
*
* @param path
* Full URL for the request.
* @param method
* HTTP method, like "GET" or "POST".
* @param body
* Optional data for the request body.
* @param bodyContentType
* Optional content type for the request body.
* @param acceptContentType
* The expected content type of the response.
* @param callback
* The function to call when done.
*/
private doRequest(
path: string,
method: string,
body: string | null,
bodyContentType: string | null,
acceptContentType: string,
callback?: CallbackFn
): ClientRequest {
const requestOptions: RequestOptions = {
host: this.options.host,
port: this.options.port,
headers: {},
family: this.options.ipVersion,

// Allow these to override (not merge with) previous values.
...this.options.request,

method,
path,
};

// Now set options that the user should not be able to override.
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);
}
}
if (this.options.authorization) {
requestOptions.headers.authorization = this.options.authorization;
}
if (this.options.agent) {
requestOptions.agent = this.options.agent;
}

// Perform the request and handle results.
const request = pickProtocol(this.options.secure).request(requestOptions);
request.on(
'response',
handleJSONResponse(request, this.options.bigint, callback)
);
request.on('error', function onError(err) {
if (callback) {
callback(err, null);
}
});
if (body) {
request.write(body);
}
request.end();
return request;
}

/**
* Create credential using the basic access authentication method
* @api public
@@ -587,52 +662,20 @@ export class Client {
queryParameters = {};
}

const json = pickJSON(this.options.bigint).stringify(data);
const fullPath =
this.getFullHandlerPath(this.UPDATE_JSON_HANDLER) +
'?' +
querystring.stringify({ ...queryParameters, wt: 'json' });

const requestOptions: RequestOptions = {
host: this.options.host,
port: this.options.port,
method: 'POST',
headers: {
'content-type': 'application/json; charset=utf-8',
'content-length': Buffer.byteLength(json),
accept: 'application/json; charset=utf-8',
},
path: fullPath,
family: this.options.ipVersion,

// Allow these to override (not merge with) previous values.
...this.options.request,
};
if (this.options.agent) {
requestOptions.agent = this.options.agent;
}
if (this.options.authorization) {
if (!requestOptions.headers) {
requestOptions.headers = {};
}
requestOptions.headers.authorization = this.options.authorization;
}

const request = pickProtocol(this.options.secure).request(requestOptions);
request.on(
'response',
handleJSONResponse(request, this.options.bigint, callback)
);
request.on('error', function onError(err) {
if (callback) {
callback(err, null);
}
const path = this.getFullHandlerPath(this.UPDATE_JSON_HANDLER);
const queryString = querystring.stringify({
...queryParameters,
wt: 'json',
});

request.write(json);
request.end();

return request;
return this.doRequest(
`${path}?${queryString}`,
'POST',
pickJSON(this.options.bigint).stringify(data),
'application/json',
'application/json; charset=utf-8',
callback
);
}

/**
@@ -741,154 +784,48 @@ export class Client {
query: Collection | Query | Record<string, any> | string | CallbackFn,
callback?: CallbackFn
): ClientRequest {
let parameters = '';
// Construct the string to use as query (GET) or body (POST).
let data = '';
if (typeof query === 'function') {
callback = query as CallbackFn;
} else if (query instanceof Query || query instanceof Collection) {
parameters += query.build();
data = query.build();
} else if (typeof query === 'object') {
parameters += querystring.stringify(query);
data = querystring.stringify(query);
} else {
// query is a string.
parameters += query;
data = query;
}

const fullPath =
this.getFullHandlerPath(handler) + '?' + parameters + '&wt=json';
const path = this.getFullHandlerPath(handler);
const queryString = data + '&wt=json';

// Decide whether to use GET or POST, based on the length of the data.
// 10 accounts for protocol and special characters like ://, port colon,
// initial slash, etc.
const approxUrlLength =
10 +
Buffer.byteLength(this.options.host) +
(this.options.port + '').length +
Buffer.byteLength(fullPath); // Buffer (10) accounts for protocol and special characters like ://, port colon, and initial slash etc

if (
!(
this.options.get_max_request_entity_size === false ||
approxUrlLength <= this.options.get_max_request_entity_size
)
) {
// Funnel this through a POST because it's too large
return this.post(handler, query, callback);
}

const requestOptions: RequestOptions = {
host: this.options.host,
port: this.options.port,
path: fullPath,
headers: {
accept: 'application/json; charset=utf-8',
},
family: this.options.ipVersion,

// Allow these to override (not merge with) previous values.
...this.options.request,
};
if (this.options.agent) {
requestOptions.agent = this.options.agent;
}
if (this.options.authorization) {
if (!requestOptions.headers) {
requestOptions.headers = {};
}
requestOptions.headers.authorization = this.options.authorization;
}

const request = pickProtocol(this.options.secure).get(requestOptions);
request.on(
'response',
handleJSONResponse(request, this.options.bigint, callback)
);
request.on('error', function (err) {
if (callback) {
callback(err, null);
}
});
return request;
}

/**
* Perform an arbitrary query on a Solr handler (a.k.a. 'path').
*
* This serves the same purpose as the `get` method, except it uses POST to
* transfer the data instead of putting all of the data in the URL.
*
* @param handler
* The name of the handler (or 'path' in Solr terminology).
* @param query
* A function, Query object, Collection object, plain object, or string
* describing the query to perform.
* @param callback
* A function to execute when the Solr server responds or an error occurs.
*/
post(
handler: string,
query: Collection | Query | Record<string, any> | string | CallbackFn,
callback?: CallbackFn
): ClientRequest {
let parameters = '';
if (typeof query === 'function') {
callback = query as CallbackFn;
} else if (query instanceof Query || query instanceof Collection) {
parameters += query.build();
} else if (typeof query === 'object') {
parameters += querystring.stringify(query);
} else {
// query is a string.
parameters += query;
}
let pathArray;

if (handler != 'admin/collections') {
pathArray = [this.options.path, this.options.core, handler + '?wt=json'];
} else {
pathArray = [this.options.path, handler + '?wt=json'];
}

const fullPath = pathArray
.filter(function (element) {
return element;
})
.join('/');

const requestOptions: RequestOptions = {
host: this.options.host,
port: this.options.port,
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
'content-length': Buffer.byteLength(parameters),
accept: 'application/json; charset=utf-8',
},
path: fullPath,
family: this.options.ipVersion,

// Allow these to override (not merge with) previous values.
...this.options.request,
};
if (this.options.agent) {
requestOptions.agent = this.options.agent;
}
if (this.options.authorization) {
if (!requestOptions.headers) {
requestOptions.headers = {};
}
requestOptions.headers.authorization = this.options.authorization;
}

const request = pickProtocol(this.options.secure).request(requestOptions);
request.on(
'response',
handleJSONResponse(request, this.options.bigint, callback)
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(
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',
callback
);
request.on('error', function onError(err) {
if (callback) callback(err, null);
});

request.write(parameters);
request.end();

return request;
}

/**
@@ -943,68 +880,45 @@ export class Client {
return this.doQuery(this.ADMIN_PING_HANDLER, callback);
}

createSchemaField(
/**
* Utility only used in tests.
*
* @param {string} fieldName
* The name of the field to create.
* @param {string} fieldType
* The type of field to create.
* @param {Function} cb
* A callback to run when completed.
*/
private createSchemaField(
fieldName: string,
fieldType: string,
cb: CallbackFn
): void {
const json = JSON.stringify({
'add-field': {
name: fieldName,
type: fieldType,
multiValued: false,
stored: true,
},
});

const callback = (err, result) => {
if (err) {
// ToDo We should handle this in a more robust way in the future, but
// there is a difference between default setup in Solr 5 and Solr 8, so
// some fields already exist in Solr 8. Hence if that's the case, we just
// ignore that.
console.warn(err.message);
}
cb(undefined, result);
};

const fullPath = this.getFullHandlerPath('schema');

const requestOptions: RequestOptions = {
host: this.options.host,
port: this.options.port,
method: 'POST',
headers: {
'content-type': 'application/json; charset=utf-8',
'content-length': Buffer.byteLength(json),
accept: 'application/json; charset=utf-8',
},
path: fullPath,
family: this.options.ipVersion,
};
if (this.options.agent) {
requestOptions.agent = this.options.agent;
}
if (this.options.authorization) {
if (!requestOptions.headers) {
requestOptions.headers = {};
): ClientRequest {
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',
(err, data) => {
if (err) {
// TODO: We should handle this in a more robust way in the future, but
// there is a difference between default setup in Solr 5 and Solr 8,
// so some fields already exist in Solr 8. Hence if that's the case, we
// just ignore that.
console.warn(err.message);
}
cb(undefined, data);
}
requestOptions.headers.authorization = this.options.authorization;
}

const request = pickProtocol(this.options.secure).request(requestOptions);
request.on(
'response',
handleJSONResponse(request, this.options.bigint, callback)
);
request.on('error', function onError(err) {
if (callback) callback(err, null);
});

request.write(json);
request.end();

return request;
}
}