UNPKG

@natlibfi/marc-record

Version:

MARC record implementation in JavaScript

278 lines (277 loc) 8.56 kB
import createDebugLogger from "debug"; import MarcRecordError from "./error.js"; import { fieldOrderComparator } from "./marcFieldSort.js"; import { clone, validateRecord, validateField } from "./utils.js"; export { default as MarcRecordError } from "./error.js"; const debug = createDebugLogger("@natlibfi/marc-record"); const debugDev = debug.extend("dev"); const validationOptionsDefaults = { strict: false, noFailValidation: false, fields: true, subfields: true, subfieldValues: true, controlFieldValues: true, leader: false, characters: false, noControlCharacters: false, noAdditionalProperties: false }; let globalValidationOptions = { ...validationOptionsDefaults }; export class MarcRecord { static setValidationOptions(options) { globalValidationOptions = { ...validationOptionsDefaults, ...options }; } static getValidationOptions() { return clone(globalValidationOptions); } constructor(record, validationOptions = {}) { this._validationOptions = validationOptions; if (record) { const recordClone = clone(record); recordClone.leader = recordClone.leader || ""; recordClone.fields = recordClone.fields || []; recordClone.fields.filter(({ subfields }) => subfields).forEach((field) => { field.ind1 = field.ind1 || " "; field.ind2 = field.ind2 || " "; }); this.leader = recordClone.leader; this.fields = recordClone.fields; this._validationErrors = validateRecord(recordClone, { ...globalValidationOptions, ...this._validationOptions }); if (!this._validationOptions.noFailValidation) { delete this._validationErrors; return; } debugDev(`${JSON.stringify(this)}`); return; } this.leader = ""; this.fields = []; } getValidationErrors() { debugDev(`getting validationErrors: ${this._validationErrors} <-`); if (!this._validationOptions.noFailValidation) { return []; } return this._validationErrors || []; } get(query) { return this.fields.filter((field) => field.tag.match(query)); } pop(query) { const fields = this.get(query); this.removeFields(fields); return fields; } sortFields() { this.fields.sort(fieldOrderComparator); return this; } removeField(field) { const index = this.fields.indexOf(field); if (index !== -1) { const { fields: keepLastField } = { ...globalValidationOptions, ...this._validationOptions }; if (this.fields.length === 1 && keepLastField) { throw new MarcRecordError("Cannot remove last field"); } this.fields.splice(index, 1); return this; } return this; } removeFields(fields) { fields.forEach((f) => this.removeField(f)); return this; } removeSubfield(subfield, field) { const index = field.subfields.indexOf(subfield); field.subfields.splice(index, 1); if (field.subfields.length === 0) { return this.removeField(field); } return this; } appendField(field) { this.insertField(field, this.fields.length); return this; } appendFields(fields) { fields.forEach((f) => this.appendField(f)); return this; } insertField(field, index) { const newField = Array.isArray(field) ? format(convertFromArray(field)) : format(field); validateField(newField, { ...globalValidationOptions, ...this._validationOptions }); this.fields.splice(index ?? this.findPosition(newField), 0, newField); return this; function format(field2) { const cloned = clone(field2); if ("subfields" in field2) { return { ...cloned, ind1: cloned.ind1 || " ", ind2: cloned.ind2 || " " }; } return cloned; } function convertFromArray(args) { if (field.length === 2) { const [tag2, value] = args; return { tag: tag2, value }; } const [tag, ind1, ind2] = args; const subfields = parseSubfields(args.slice(3)); return { tag, ind1, ind2, subfields }; function parseSubfields(args2, subfields2 = []) { const [code, value] = args2; if (code) { return parseSubfields(args2.slice(2), subfields2.concat({ code, value })); } return subfields2; } } } insertFields(fields) { fields.forEach((f) => this.insertField(f)); return this; } findPosition(fieldA) { const index = this.fields.findIndex((fieldB) => fieldOrderComparator(fieldB, fieldA) > 0); return index < 0 ? this.fields.length : index; } getControlfields() { return this.fields.filter((field) => "value" in field); } getDatafields() { return this.fields.filter((field) => "subfields" in field); } getFields(tag, query) { const fields = this.fields.filter((f) => f.tag === tag); if (typeof query === "string") { return fields.filter((f) => f.value === query); } if (Array.isArray(query)) { return fields.filter((field) => query.every((sfQuery) => field.subfields.some((sf) => sf.code === sfQuery.code && sf.value === sfQuery.value))); } return fields; } containsFieldWithValue(tag, query) { return this.getFields(tag, query).length > 0; } getTypeOfRecord() { return this.leader[6]; } getBibliograpicLevel() { return this.leader[7]; } isBK() { return ["a", "t"].includes(this.getTypeOfRecord()) && !this._bibliographicLevelIsBis(); } isCF() { return this.getTypeOfRecord() === "m"; } isCR() { return ["a", "t"].includes(this.getTypeOfRecord()) && this._bibliographicLevelIsBis(); } isMP() { return ["e", "f"].includes(this.getTypeOfRecord()); } isMU() { return ["c", "d", "i", "j"].includes(this.getTypeOfRecord()); } isMX() { return this.getTypeOfRecord() === "p"; } isVM() { return ["g", "k", "o", "r"].includes(this.getTypeOfRecord()); } getTypeOfMaterial() { if (this.isBK()) { return "BK"; } if (this.isCF()) { return "CF"; } if (this.isCR()) { return "CR"; } if (this.isMP()) { return "MP"; } if (this.isMU()) { return "MU"; } if (this.isMX()) { return "MX"; } if (this.isVM()) { return "VM"; } return false; } _bibliographicLevelIsBis() { return ["b", "i", "s"].includes(this.getBibliograpicLevel()); } equalsTo(record) { return MarcRecord.isEqual(this, record); } toString() { return [].concat( `LDR ${this.leader}`, this.getControlfields().map((f) => `${f.tag} ${f.value}`), this.getDatafields().map(mapDatafield) ).join("\n"); function mapDatafield(f) { return `${f.tag} ${f.ind1}${f.ind2} \u2021${formatSubfields(f)}`; function formatSubfields(field) { return field.subfields.map((sf) => `${sf.code}${sf.value || ""}`).join("\u2021"); } } } toObject() { return Object.entries(clone(this)).filter(([k]) => k.startsWith("_") === false).reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {}); } static fromString(str, validationOptions) { const record = new MarcRecord(void 0, validationOptions); str.split("\n").map((ln) => ({ tag: ln.substr(0, 3), ind1: ln.substr(4, 1), ind2: ln.substr(5, 1), data: ln.substr(7) })).forEach((field) => { const { tag, ind1, ind2, data } = field; if (tag === "LDR") { record.leader = data; return; } if (data.substr(0, 1) === "\u2021") { record.appendField({ tag, ind1, ind2, subfields: parseSubfields(data) }); return; } record.appendField({ tag, value: data }); }); return record; function parseSubfields(str2) { return str2.substr(1).split("\u2021").map((data) => { const code = data.substr(0, 1); const value = data.substr(1); return value ? { code, value } : { code }; }); } } static clone(record, validationOptions = {}) { return new MarcRecord(record, validationOptions); } // This uses a strict string-to-string check but re-orders the object keys beforehand (MARC fields should be in same order, but the instance's properties order doesn't matter) static isEqual(r1, r2) { return JSON.stringify(reorder(r1)) === JSON.stringify(reorder(r2)); function reorder(obj) { return Object.keys(obj).sort().reduce((acc, key) => ({ ...acc, [key]: typeof obj[key] === "object" ? reorder(obj[key]) : obj[key] }), {}); } } } //# sourceMappingURL=index.js.map