@natlibfi/sru-client
Version:
SRU Javascript client library
201 lines (200 loc) • 8.16 kB
JavaScript
import httpStatus from "http-status";
import { EventEmitter } from "events";
import { Parser as XMLParser, Builder as XMLBuilder } from "xml2js";
import createDebugLogger from "debug";
import { promisify } from "util";
import { MARCXML } from "@natlibfi/marc-record-serializers";
export class SruSearchError extends Error {
}
const setTimeoutPromise = promisify(setTimeout);
export const metadataFormats = {
object: "object",
string: "string",
marcJson: "marcJson"
};
export default ({
url: baseUrl,
recordSchema: recordSchemaDefault,
version = "2.0",
maxRecordsPerRequest = 1e3,
metadataFormat = metadataFormats.string,
// Renamed from 'recordFormat' in v7
retrieveAll = true
}) => {
const debug = createDebugLogger("@natlibfi/sru-client");
const debugData = debug.extend("data");
debug(retrieveAll);
const formatRecord = createFormatter();
class Emitter extends EventEmitter {
constructor(...args) {
super(args);
}
}
return { searchRetrieve };
function searchRetrieve(query, { startRecord = 1, recordSchema: recordSchemaArg } = {}) {
const recordSchema = recordSchemaArg || recordSchemaDefault;
const iteration = 1;
const emitter = new Emitter();
iterate(startRecord, iteration);
return emitter;
async function iterate(startRecord2, iteration2) {
try {
await processRequest(startRecord2);
} catch (err) {
debug(JSON.stringify(err));
return emitter.emit("error", err);
}
async function processRequest(startRecord3) {
const url = generateUrl({ operation: "searchRetrieve", query, startRecord: startRecord3, recordSchema, version, maximumRecords: maxRecordsPerRequest });
debug(`Sending request-${iteration2}: ${url}`);
const response = await fetch(url, { headers: { "Cache-control": "max-age=0, must-revalidate" } });
debugData(response.status);
debugData(JSON.stringify(response));
if (response.status === httpStatus.OK) {
const { records, error, nextRecordOffset, totalNumberOfRecords } = await parsePayload(response);
const numberOfRecords = Array.isArray(records) ? records.length : 0;
const endRecord = isNaN(nextRecordOffset) ? totalNumberOfRecords : nextRecordOffset - 1;
debug(`Request-${iteration2} got records ${startRecord3}-${endRecord} (${numberOfRecords}) out of total ${totalNumberOfRecords}.`);
if (error) {
debug(`SRU received error: ${error}`);
throw new SruSearchError(error);
}
if (iteration2 === 1) {
debugData(`Emitting total: ${totalNumberOfRecords}`);
emitter.emit("total", totalNumberOfRecords);
}
if (records) {
await emitRecords(records);
if (typeof nextRecordOffset === "number") {
if (retrieveAll === true) {
debug(`Continuing (retrieveAll is true) with next searchRetrieve starting from ${nextRecordOffset}`);
return iterate(nextRecordOffset, iteration2 + 1);
}
debug(`Stopping (retrievaAll is false), there are still records to retrieve starting from ${nextRecordOffset}`);
return emitter.emit("end", nextRecordOffset);
}
debug(`Stopping, no more records to retrieve: ${nextRecordOffset}`);
return emitter.emit("end");
}
return emitter.emit("end");
}
const { status } = response;
const message = await response.text();
debug(`SRU non-OK response status: ${status}, message: ${message} `);
throw new Error(`Unexpected response ${status}: ${message}`);
async function parsePayload(response2) {
const payload = await parse();
debugData(JSON.stringify(payload));
const [error] = pathParser(payload, "zs:searchRetrieveResponse/zs:diagnostics/0/diag:diagnostic/0/diag:message") || [];
if (error) {
debug(`SRU response status was ${response2.status}, but response contained an error ${error}`);
return { error };
}
const totalNumberOfRecords = Number(pathParser(payload, "zs:searchRetrieveResponse/zs:numberOfRecords/0"));
debug(`Total number of records: ${totalNumberOfRecords}`);
if (totalNumberOfRecords === 0) {
return { totalNumberOfRecords };
}
const records = pathParser(payload, "zs:searchRetrieveResponse/zs:records/0/zs:record") || [];
const lastOffset = Number(pathParser(records.slice(-1), "0/zs:recordPosition/0"));
if (lastOffset === totalNumberOfRecords) {
return { records, totalNumberOfRecords };
}
return { records, nextRecordOffset: lastOffset + 1, totalNumberOfRecords };
async function parse() {
const payload2 = await response2.text();
return new Promise((resolve, reject) => {
new XMLParser().parseString(payload2, (err, obj) => {
if (err) {
return reject(new Error(`Error parsing XML: ${err}, input: ${payload2}`));
}
return resolve(obj);
});
});
}
}
async function emitRecords(records, promises = []) {
const [record, ...rest] = records;
if (record !== void 0) {
promises.push(formatAndEmitRecord(pathParser(record, "zs:recordData/0")));
return emitRecords(rest, promises);
}
await Promise.all(promises);
async function formatAndEmitRecord(record2) {
const formatedRecord = await formatRecord(record2);
emitter.emit("record", formatedRecord);
}
}
function generateUrl(params) {
const formatted = Object.entries(params).filter(([, value]) => value).reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
const searchParams = new URLSearchParams(formatted);
return `${baseUrl}?${searchParams.toString()}`;
}
function pathParser(value, path) {
const pathArray = `${path}`.split("/");
return parse(pathArray, value);
function parse(pathArray2, value2) {
const [pathPart, ...restOfPathArray] = pathArray2;
if (pathPart === void 0) {
return value2;
}
if (value2 && typeof value2 === "object") {
if (pathPart in value2) {
return parse(restOfPathArray, value2[pathPart]);
}
const [, tag] = pathPart.split(":");
if (tag in value2) {
return parse(restOfPathArray, value2[tag]);
}
return void 0;
}
return pathParser(restOfPathArray, value2);
}
}
}
}
}
function createFormatter() {
if (metadataFormat === metadataFormats.object) {
return (metadata) => metadata;
}
if (metadataFormat === metadataFormats.string) {
const builder = new XMLBuilder({
xmldec: {
version: "1.0",
encoding: "UTF-8",
standalone: false
},
renderOpts: {
pretty: true,
indent: " "
}
});
return (metadata) => {
const [[key, value]] = Object.entries(metadata);
return builder.buildObject({ [key]: value[0] });
};
}
if (metadataFormat === metadataFormats.marcJson) {
const builder = new XMLBuilder({
xmldec: {
version: "1.0",
encoding: "UTF-8",
standalone: false
},
renderOpts: {
pretty: true,
indent: " "
}
});
return async (metadata) => {
const [[key, value]] = Object.entries(metadata);
const xmlString = builder.buildObject({ [key]: value[0] });
const record = await MARCXML.from(xmlString, { subfieldValues: false });
return record;
};
}
throw new Error(`Invalid record format: ${metadataFormat}`);
}
};
//# sourceMappingURL=index.js.map