UNPKG

@natlibfi/sru-client

Version:

SRU Javascript client library

201 lines (200 loc) 8.16 kB
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