@natlibfi/melinda-record-matching 5.1.4 → 5.1.5-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/candidate-search/index.js +2 -0
  2. package/dist/candidate-search/index.js.map +2 -2
  3. package/dist/candidate-search/query-list/bib.js +16 -3
  4. package/dist/candidate-search/query-list/bib.js.map +2 -2
  5. package/dist/match-detection/features/bib/title.js +51 -5
  6. package/dist/match-detection/features/bib/title.js.map +3 -3
  7. package/dist/matching-utils.js +39 -0
  8. package/dist/matching-utils.js.map +2 -2
  9. package/package.json +9 -11
  10. package/src/candidate-search/index.js +3 -0
  11. package/src/candidate-search/query-list/bib.js +24 -4
  12. package/src/match-detection/features/bib/title.js +62 -7
  13. package/src/matching-utils.js +50 -0
  14. package/{src → test}/candidate-search/index.test.js +3 -1
  15. package/{src → test}/candidate-search/query-list/auth.test.js +1 -1
  16. package/{src → test}/candidate-search/query-list/bib.test.js +1 -1
  17. package/{src → test}/index.test.js +10 -5
  18. package/{src → test}/match-detection/features/auth/index.test.js +1 -1
  19. package/{src → test}/match-detection/features/bib/index.test.js +1 -1
  20. package/{src → test}/match-detection/index.test.js +2 -2
  21. package/dist/candidate-search/index.test.js +0 -89
  22. package/dist/candidate-search/index.test.js.map +0 -7
  23. package/dist/candidate-search/query-list/auth.test.js +0 -31
  24. package/dist/candidate-search/query-list/auth.test.js.map +0 -7
  25. package/dist/candidate-search/query-list/bib.test.js +0 -31
  26. package/dist/candidate-search/query-list/bib.test.js.map +0 -7
  27. package/dist/index.test.js +0 -69
  28. package/dist/index.test.js.map +0 -7
  29. package/dist/match-detection/features/auth/index.test.js +0 -37
  30. package/dist/match-detection/features/auth/index.test.js.map +0 -7
  31. package/dist/match-detection/features/bib/index.test.js +0 -37
  32. package/dist/match-detection/features/bib/index.test.js.map +0 -7
  33. package/dist/match-detection/index.test.js +0 -40
  34. package/dist/match-detection/index.test.js.map +0 -7
@@ -4,6 +4,7 @@ import {MarcRecord} from '@natlibfi/marc-record';
4
4
 
5
5
  import generateQueryList from './query-list/index.js';
6
6
  import chooseQueries from './choose-queries.js';
7
+ import {preferenceSortRecords} from '../matching-utils.js';
7
8
 
8
9
  export {searchTypes} from './query-list/index.js';
9
10
 
@@ -96,6 +97,8 @@ export default async ({record, searchSpec, url, maxCandidates, maxRecordsPerRequ
96
97
  }
97
98
  debug(`Query ${queryOffset} ${query} done.`);
98
99
  debug(`There are (${queryList.length - (queryOffset + 1)} queries left)`);
100
+
101
+ records.sort(preferenceSortRecords);
99
102
  return {records, queryOffset: queryOffset + 1, queriesLeft: queryList.length - (queryOffset + 1), totalRecords: newTotalRecords, query, searchCounter: newSearchCounter, queryCandidateCounter: newQueryCandidateCounter, queryCounter: newQueryCounter, maxedQueries: newMaxedQueries};
100
103
  }
101
104
 
@@ -435,7 +435,8 @@ export function bibStandardIdentifiers(record) {
435
435
 
436
436
  // DEVELOP: should we query also f015 and f028?
437
437
 
438
- const fields = record.get(/^(?<def>020|022|024)$/u);
438
+ // const fields = record.get(/^(?<def>020|022|024)$/u);
439
+ const fields = record.get(/^(?<def>015|020|022|024|028)$/u);
439
440
  const identifiers = [].concat(...fields.map(toIdentifiers));
440
441
  const uniqueIdentifiers = [...new Set(identifiers)];
441
442
 
@@ -454,10 +455,10 @@ export function bibStandardIdentifiers(record) {
454
455
  const issnIsbnReqExp = (/^[A-Za-z0-9-]+$/u);
455
456
  const otherIdReqExp = (/^[A-Za-z0-9-:]+$/u);
456
457
 
457
- if (tag === '022') {
458
+ if (tag === '015') { // TODO: test (does this use the right index etc.)
458
459
  return subfields
459
- .filter(sub => ['a', 'z', 'y'].includes(sub.code) && testStringOrNumber(sub.value) && issnIsbnReqExp.test(String(sub.value)))
460
- .map(({value}) => String(value));
460
+ .filter(sf => sf.code === 'a' && sf.value.match(/^[A-Z0-9\-]+$/ui))
461
+ .map(sf => sf.value)
461
462
  }
462
463
 
463
464
  if (tag === '020') {
@@ -466,6 +467,25 @@ export function bibStandardIdentifiers(record) {
466
467
  .map(({value}) => String(value));
467
468
  }
468
469
 
470
+ if (tag === '022') {
471
+ return subfields
472
+ .filter(sub => ['a', 'z', 'y'].includes(sub.code) && testStringOrNumber(sub.value) && issnIsbnReqExp.test(String(sub.value)))
473
+ .map(({value}) => String(value));
474
+ }
475
+
476
+ if (tag === '028') { // TODO: test
477
+ const a = subfields.find(sf => sf.code === 'a');
478
+ if (a) {
479
+ const b = subfields.find(sf => sf.code === 'b');
480
+ // TODO: normalize?
481
+ if (b) {
482
+ return [a.value, `${b.value} ${a.value}`];
483
+ }
484
+ return [a.value];
485
+ }
486
+ }
487
+
488
+ // Default seems to be 024:
469
489
  return subfields
470
490
  .filter(sub => ['a', 'z'].includes(sub.code) && testStringOrNumber(sub.value) && otherIdReqExp.test(String(sub.value)))
471
491
  .map(({value}) => String(value));
@@ -40,7 +40,10 @@ export default ({threshold = 0.9} = {}) => ({
40
40
  // - note: combined with decomposing unicode diacritics this normalizes both 'saa' and 'sää' as 'saa'
41
41
  // - we could precompose the Finnish letters back to avoid this
42
42
  // - see validator normalize-utf8-diacritics for details
43
- .replace(/[^\p{Letter}\p{Number}&]/gu, '')
43
+ .replace(/[^\p{Letter}\p{Number}& ]/gu, '') // 2026-07-01: keep ' '. We need it for splitToBaseAndNumber()
44
+ .replace(/\s+/ug, ' ')
45
+ .replace(/ $/u, '')
46
+ .replace(/^ /u, '')
44
47
  .toLowerCase();
45
48
  }
46
49
 
@@ -52,11 +55,12 @@ export default ({threshold = 0.9} = {}) => ({
52
55
 
53
56
  const [aa, ab, an, ap] = a;
54
57
  const [ba, bb, bn, bp] = b;
58
+ debugData(`COMPARE:\n['${aa}', '${ab}', '${an}', '${ap}'] VS\n['${ba}', '${bb}', '${bn}', '${bp}']`);
55
59
 
56
60
  const aFull = toFullTitle(a);
57
61
  const bFull = toFullTitle(b);
58
62
 
59
- debug(`COMPARE:\n '${aFull}' vs \n '${bFull}'`);
63
+ // debugData(`JOINED COMPARE:\n '${aFull}' vs \n '${bFull}'`);
60
64
 
61
65
  if (isEmpty(aa) || isEmpty(ba)) {
62
66
  return -1.0;
@@ -66,6 +70,11 @@ export default ({threshold = 0.9} = {}) => ({
66
70
  return TITLE_MAX;
67
71
  }
68
72
 
73
+ // MELKEHITYS-3496-ish: move part of $a to $n:
74
+ const [altAa, altAn, altBa, altBn] = reconstructTitleAndNumber(aa, an, ba, bn);
75
+ if (altAa) {
76
+ return compare2([altAa, ab, altAn, ap], [altBa, bb, altBn, bp]);
77
+ }
69
78
 
70
79
  if (ab && ab !== '' && ab === bb) { // Remove $b from equation (MELKEHITYS-3494)
71
80
  debug(`Ignore \$b ${ab}`);
@@ -79,7 +88,7 @@ export default ({threshold = 0.9} = {}) => ({
79
88
  debug(`Ignore \$p ${ap}`);
80
89
  return compare2([aa, ab, an, ''], [ba, bb, bn, '']);
81
90
  }
82
- // There seems to be wobble between $b and $p
91
+ // There seems to be wobble between $b and $p:
83
92
  if (!isEmpty(ab) && ab === bp) {
84
93
  return compare2([aa, '', an, ap], [ba, bb, bn, '']);
85
94
  }
@@ -103,7 +112,7 @@ export default ({threshold = 0.9} = {}) => ({
103
112
  return -1.0;
104
113
  }
105
114
 
106
- const [distance, maxLength, correctness] = doLevenshtein(aFull, bFull);
115
+ const [distance, maxLength, correctness] = doLevenshtein(aFull.replace(/ /ug, ''), bFull.replace(/ /ug, ''));
107
116
 
108
117
  debug(`'${aFull}' vs '${bFull}': Max length = ${maxLength}, distance = ${distance}, correctness = ${correctness}`);
109
118
 
@@ -122,16 +131,16 @@ export default ({threshold = 0.9} = {}) => ({
122
131
  return compare2([aa, '', an, ap], [ba, bb.substring(ab.length), bn, bp]);
123
132
  }
124
133
  }
125
-
134
+
126
135
  // Try the same without $p:
127
136
  if (localXor(ap, bp)) {
128
137
  const result = compare2([aa, ab, an, ''], [ba, bb, bn, '']);
129
138
  return result > 0.0 ? result * 0.8 : result;
130
139
  }
131
-
140
+
132
141
  if (aa === ba && an === bn) {
133
- // No $n: Don't score $a vs $ab. Return 0, and let other match detectors decide
134
142
  const candScore = TITLE_MAX/2;
143
+ // $b is missing from one:
135
144
  if (ap === bp && localXor(ab, bb)) {
136
145
  debug(`Handle omitted \$b using x ${candScore}`);
137
146
  return candScore;
@@ -142,6 +151,17 @@ export default ({threshold = 0.9} = {}) => ({
142
151
  }
143
152
  }
144
153
 
154
+ if (an === bn && ap === bp ) {
155
+ // $b-less $a is other record's $b, see MELKEHITYS-3485 for motivation (though we skip 246 and 490 here):
156
+ const candScore = TITLE_MAX/2;
157
+ if (aa === bb && !ab) {
158
+ return candScore;
159
+ }
160
+ if (ba === ab && !bb) {
161
+ return candScore;
162
+ }
163
+ }
164
+
145
165
  return -0.5; // Not likely
146
166
  }
147
167
 
@@ -174,6 +194,41 @@ export default ({threshold = 0.9} = {}) => ({
174
194
  return str1.length > str2.length ? str1.length : str2.length;
175
195
  }
176
196
 
197
+ function reconstructTitleAndNumber(a1, n1, a2, n2) {
198
+ if (!n1 && n2) {
199
+ const [altA1, altN1] = splitToBaseAndNumber(a1);
200
+ if (altN1) {
201
+ debugData(`A: Reconstruct:\n $a '${a1}' =>\n $a '${altA1}' +\n $n '${altN1}`);
202
+ return [altA1, altN1, a2, n2];
203
+ }
204
+ }
205
+ if (n1 && !n2) {
206
+ const [altA2, altN2] = splitToBaseAndNumber(a2);
207
+ if (altN2) {
208
+ debugData(`B: Reconstruct $a '${a2}' as $a '${altA2}' and $n '${altN2}`);
209
+ return [a1, n1, altA2, altN2];
210
+ }
211
+
212
+ }
213
+ return [null, null, null, null]; // Fail
214
+ }
215
+
216
+
217
+ function splitToBaseAndNumber(val) {
218
+ const words = val.split(' ');
219
+ const len = words.length;
220
+ if (len < 2 || !words[len-1].match(/^[1-9][0-9]*$/)) {
221
+ return [val, null];
222
+ }
223
+ if (len > 2 && words[len-2].match(/^(del|osa|vol|volume)$/u)) { // NB! "vol." has lost it's '.' during normalization!
224
+ const number = `${words[len-2]} ${words[len-1]}`;
225
+ return [words.slice(0, len-2).join(' '), number];
226
+ }
227
+
228
+ const number = words.pop();
229
+ return [words.join(' '), number];
230
+ }
231
+
177
232
  }
178
233
  });
179
234
 
@@ -129,3 +129,53 @@ export function stringBefore(string, substring) {
129
129
  }
130
130
  return string.substring(0, pos);
131
131
  }
132
+
133
+ // We sort records here (put FIKKA and low-cased records first, ref MELINDA-3492)
134
+ // NB! this only sorts the records the current set. If the best stuff would come in a later set and this set contains good enough record, we'll never reach the best...
135
+ // Still this would alleviate the issue when maxMatches === 1...
136
+ export function preferenceSortRecords(x, y) {
137
+ debug(`sortRecords: RECORD COMPARISON\n${JSON.stringify(x)} vs\n${JSON.stringify(y)}`);
138
+ // Prefer the record with a FIKKA LOW tag:
139
+ const xFikka = hasLow(x.record, 'FIKKA');
140
+ const yFikka = hasLow(y.record, 'FIKKA');
141
+
142
+ if (xFikka && !yFikka) {
143
+ debug('sortRecords: prefer FIKKA');
144
+ return -1;
145
+ }
146
+ if (yFikka && !xFikka) {
147
+ return 1;
148
+ }
149
+
150
+ // Prefer downcased:
151
+ const xAllCaps = hasAllCapsTitle(x.record);
152
+ const yAllCaps = hasAllCapsTitle(y.record);
153
+
154
+ if (!xAllCaps && yAllCaps) {
155
+ debug('sortRecords: prefer non-all caps');
156
+ return -1; // x is better
157
+ }
158
+ if (!yAllCaps && xAllCaps) {
159
+ return 1; // y is better
160
+ }
161
+ // Default:
162
+ return 0;
163
+
164
+ function hasLow(record, lowTag) {
165
+ return record.fields?.some(f => f.tag === 'LOW' && f.subfields?.some(sf => sf.code === 'a' && sf.value === lowTag));
166
+ }
167
+
168
+ function hasAllCapsTitle(record) {
169
+ const f245 = record.fields?.find(f => f.tag === '245');
170
+ if (!f245) {
171
+ debugData(`WARNING: No f245 found for ALL CAPS check`);
172
+ return false;
173
+ }
174
+
175
+ const title = f245.subfields.filter(sf => ['a', 'b', 'c'].includes(sf.code)).map(sf => sf.value).join(' ');
176
+ if (title.match(/[A-Z]/u) && !title.match(/[a-z]/u)) {
177
+ return true;
178
+ }
179
+ return false;
180
+ }
181
+ }
@@ -4,11 +4,13 @@ import {READERS} from '@natlibfi/fixura';
4
4
  import generateTests from '@natlibfi/fixugen-http-client';
5
5
  import {MarcRecord} from '@natlibfi/marc-record';
6
6
  import {Error as MatchingError} from '@natlibfi/melinda-commons';
7
- import createSearchInterface, {CandidateSearchError} from './index.js';
7
+ import createSearchInterface, {CandidateSearchError} from '../../src/candidate-search/index.js';
8
8
  import createDebugLogger from 'debug';
9
9
 
10
10
  const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:test');
11
11
 
12
+ // NOTE: it seems that "only" does not work when using generateTests from @natlibfi/fixugen-http-client'
13
+
12
14
  describe('candidate-search', () => {
13
15
  generateTests({
14
16
  callback,
@@ -4,7 +4,7 @@ import createDebugLogger from 'debug';
4
4
  import generateTests from '@natlibfi/fixugen';
5
5
  import {READERS} from '@natlibfi/fixura';
6
6
  import {MarcRecord} from '@natlibfi/marc-record';
7
- import * as generators from './auth.js';
7
+ import * as generators from '../../../src/candidate-search/query-list/auth.js';
8
8
 
9
9
  const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query-list:auth-test');
10
10
  const debugData = debug.extend('data');
@@ -4,7 +4,7 @@ import createDebugLogger from 'debug';
4
4
  import generateTests from '@natlibfi/fixugen';
5
5
  import {READERS} from '@natlibfi/fixura';
6
6
  import {MarcRecord} from '@natlibfi/marc-record';
7
- import * as generators from './bib.js';
7
+ import * as generators from '../../../src/candidate-search/query-list/bib.js';
8
8
 
9
9
  const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:bibHostComponents:test');
10
10
  const debugData = debug.extend('data');
@@ -4,12 +4,14 @@ import createDebugLogger from 'debug';
4
4
  import {READERS} from '@natlibfi/fixura';
5
5
  import generateTests from '@natlibfi/fixugen-http-client';
6
6
  import {MarcRecord} from '@natlibfi/marc-record';
7
- import createMatchInterface, {matchDetection} from './index.js';
7
+ import createMatchInterface, {matchDetection} from '../src/index.js';
8
8
 
9
9
  const debug = createDebugLogger('@natlibfi/melinda-record-matching:index:test');
10
10
  const debugData = debug.extend('data');
11
11
 
12
- describe('INDEX', () => {
12
+ // NOTE: it seems that "only" does not work when using generateTests from @natlibfi/fixugen-http-client'
13
+
14
+ describe('INDEX-FOO', () => {
13
15
  generateTests({
14
16
  callback,
15
17
  path: [import.meta.dirname, '..', 'test-fixtures', 'index'],
@@ -21,12 +23,15 @@ describe('INDEX', () => {
21
23
 
22
24
  async function callback({getFixture, options, expectedMatchStatus, expectedStopReason, expectedFailures, expectedCandidateCount}) {
23
25
 
26
+ debug(`Running tests from indezx.test.js`);
27
+
24
28
  const record = new MarcRecord(getFixture('inputRecord.json'), {subfieldValues: false});
25
29
  const expectedMatches = getFixture('expectedMatches.json');
26
30
  const expectedNonMatches = getFixture('expectedNonMatches.json') || [];
27
31
 
28
-
29
- const match = createMatchInterface(formatOptions());
32
+ debugData(`Options: ${JSON.stringify(options)}`);
33
+ debugData(`Formatted options: ${JSON.stringify(formatOptions(options))}`);
34
+ const match = await createMatchInterface(formatOptions(options));
30
35
  const {matches, matchStatus, nonMatches, conversionFailures, candidateCount} = await match({record});
31
36
  debugData(`Matches: ${matches.length}, Status: ${matchStatus.status}/${matchStatus.stopReason}, NonMatches: ${nonMatches ? nonMatches.length : 'not returned'}, ConversionFailures: ${conversionFailures ? conversionFailures.length : 'not returned'}`);
32
37
 
@@ -44,7 +49,7 @@ describe('INDEX', () => {
44
49
  assert.deepStrictEqual(conversionFailures, expectedFailures);
45
50
  }
46
51
 
47
- function formatOptions() {
52
+ function formatOptions(options) {
48
53
  const contextFeatures = matchDetection.features[options.detection.strategy.type];
49
54
 
50
55
  return {
@@ -5,7 +5,7 @@ import createDebugLogger from 'debug';
5
5
  import generateTests from '@natlibfi/fixugen';
6
6
  import {READERS} from '@natlibfi/fixura';
7
7
  import {MarcRecord} from '@natlibfi/marc-record';
8
- import * as features from './index.js';
8
+ import * as features from '../../../../src/match-detection/features/auth/index.js';
9
9
 
10
10
 
11
11
  const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features/auth:test');
@@ -5,7 +5,7 @@ import createDebugLogger from 'debug';
5
5
  import generateTests from '@natlibfi/fixugen';
6
6
  import {READERS} from '@natlibfi/fixura';
7
7
  import {MarcRecord} from '@natlibfi/marc-record';
8
- import * as features from './index.js';
8
+ import * as features from '../../../../src/match-detection/features/bib/index.js';
9
9
 
10
10
 
11
11
  const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features/bib:test');
@@ -6,8 +6,8 @@ import generateTests from '@natlibfi/fixugen';
6
6
  import {READERS} from '@natlibfi/fixura';
7
7
  import {MarcRecord} from '@natlibfi/marc-record';
8
8
 
9
- import createDetectionInterface from './index.js';
10
- import * as features from './features/index.js';
9
+ import createDetectionInterface from '../../src/match-detection/index.js';
10
+ import * as features from '../../src/match-detection/features/index.js';
11
11
 
12
12
  const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:test');
13
13
  const debugData = debug.extend('data');
@@ -1,89 +0,0 @@
1
- import assert from "node:assert";
2
- import { describe } from "node:test";
3
- import { READERS } from "@natlibfi/fixura";
4
- import generateTests from "@natlibfi/fixugen-http-client";
5
- import { MarcRecord } from "@natlibfi/marc-record";
6
- import { Error as MatchingError } from "@natlibfi/melinda-commons";
7
- import createSearchInterface, { CandidateSearchError } from "./index.js";
8
- import createDebugLogger from "debug";
9
- const debug = createDebugLogger("@natlibfi/melinda-record-matching:candidate-search:test");
10
- describe("candidate-search", () => {
11
- generateTests({
12
- callback,
13
- path: [import.meta.dirname, "..", "..", "test-fixtures", "candidate-search", "index"],
14
- recurse: false,
15
- fixura: {
16
- reader: READERS.JSON
17
- }
18
- });
19
- async function callback({ getFixture, factoryOptions, searchOptions, expectedFactoryError = false, expectedSearchError = false }) {
20
- const url = "http://foo.bar";
21
- if (expectedFactoryError) {
22
- debug(`We're expecting an error`);
23
- if (expectedFactoryError.isCandidateSearchError) {
24
- try {
25
- const result = await createSearchInterface({ ...formatFactoryOptions(), url });
26
- debug(result);
27
- } catch (err) {
28
- assert.equal(err instanceof CandidateSearchError, true);
29
- assert.match(err.message, new RegExp(expectedFactoryError.message));
30
- } finally {
31
- return;
32
- }
33
- }
34
- try {
35
- const result = await createSearchInterface({ ...formatFactoryOptions(), url });
36
- debug(result);
37
- } catch (err) {
38
- assert.equal(err instanceof Error, true);
39
- assert.match(err.message, new RegExp(expectedFactoryError.message));
40
- } finally {
41
- return;
42
- }
43
- }
44
- const { search } = await createSearchInterface({ ...formatFactoryOptions(), url });
45
- await iterate({ searchOptions, expectedSearchError });
46
- function formatFactoryOptions() {
47
- debug(`Using factoryOptions: ${JSON.stringify(factoryOptions)}`);
48
- return {
49
- ...factoryOptions,
50
- maxRecordsPerRequest: factoryOptions.maxRecordsPerRequest || 1,
51
- maxServerResults: factoryOptions.maxServerResults || void 0,
52
- record: new MarcRecord(factoryOptions.record, { subfieldValues: false })
53
- };
54
- }
55
- async function iterate({ searchOptions: searchOptions2, expectedSearchError: expectedSearchError2, expectedErrorStatus, count = 1 }) {
56
- const expectedResults = getFixture(`expectedResults${count}.json`);
57
- if (expectedSearchError2) {
58
- try {
59
- await search(searchOptions2);
60
- throw new Error("Expected an error");
61
- } catch (err) {
62
- debug(`Got an error: ${err}`);
63
- assert(err instanceof Error);
64
- const errorMessage = err instanceof MatchingError ? err.payload.message : err.message;
65
- const errorStatus = err instanceof MatchingError ? err.status : void 0;
66
- debug(`errorMessage: ${errorMessage}, errorStatus: ${errorStatus}`);
67
- assert.match(errorMessage, new RegExp(expectedSearchError2, "u"));
68
- if (expectedErrorStatus) {
69
- assert.equal(errorStatus, expectedErrorStatus);
70
- return;
71
- }
72
- return;
73
- }
74
- }
75
- if (!expectedSearchError2) {
76
- const results = await search(searchOptions2);
77
- assert.deepStrictEqual(formatResults(results), expectedResults);
78
- }
79
- function formatResults(results) {
80
- debug(results);
81
- return {
82
- ...results,
83
- records: results.records.map(({ record, id }) => ({ id, record: record.toObject() }))
84
- };
85
- }
86
- }
87
- }
88
- });
89
- //# sourceMappingURL=index.test.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../src/candidate-search/index.test.js"],
4
- "sourcesContent": ["import assert from 'node:assert';\nimport {describe} from 'node:test';\nimport {READERS} from '@natlibfi/fixura';\nimport generateTests from '@natlibfi/fixugen-http-client';\nimport {MarcRecord} from '@natlibfi/marc-record';\nimport {Error as MatchingError} from '@natlibfi/melinda-commons';\nimport createSearchInterface, {CandidateSearchError} from './index.js';\nimport createDebugLogger from 'debug';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:test');\n\ndescribe('candidate-search', () => {\n generateTests({\n callback,\n path: [import.meta.dirname, '..', '..', 'test-fixtures', 'candidate-search', 'index'],\n recurse: false,\n fixura: {\n reader: READERS.JSON\n }\n });\n\n async function callback({getFixture, factoryOptions, searchOptions, expectedFactoryError = false, expectedSearchError = false}) {\n const url = 'http://foo.bar';\n\n if (expectedFactoryError) {\n debug(`We're expecting an error`);\n if (expectedFactoryError.isCandidateSearchError) {\n try {\n const result = await createSearchInterface({...formatFactoryOptions(), url});\n debug(result);\n } catch (err) {\n assert.equal(err instanceof CandidateSearchError, true);\n assert.match(err.message, new RegExp(expectedFactoryError.message));\n } finally {\n return;\n }\n }\n\n try {\n const result = await createSearchInterface({...formatFactoryOptions(), url});\n debug(result);\n } catch (err) {\n assert.equal(err instanceof Error, true);\n assert.match(err.message, new RegExp(expectedFactoryError.message));\n } finally {\n return;\n }\n }\n\n const {search} = await createSearchInterface({...formatFactoryOptions(), url});\n await iterate({searchOptions, expectedSearchError});\n\n function formatFactoryOptions() {\n debug(`Using factoryOptions: ${JSON.stringify(factoryOptions)}`);\n return {\n ...factoryOptions,\n maxRecordsPerRequest: factoryOptions.maxRecordsPerRequest || 1,\n maxServerResults: factoryOptions.maxServerResults || undefined,\n record: new MarcRecord(factoryOptions.record, {subfieldValues: false})\n };\n }\n\n async function iterate({searchOptions, expectedSearchError, expectedErrorStatus, count = 1}) {\n const expectedResults = getFixture(`expectedResults${count}.json`);\n\n if (expectedSearchError) {\n try {\n await search(searchOptions);\n throw new Error('Expected an error');\n } catch (err) {\n debug(`Got an error: ${err}`);\n assert(err instanceof Error);\n const errorMessage = err instanceof MatchingError ? err.payload.message : err.message;\n const errorStatus = err instanceof MatchingError ? err.status : undefined;\n debug(`errorMessage: ${errorMessage}, errorStatus: ${errorStatus}`);\n assert.match(errorMessage, new RegExp(expectedSearchError, 'u'));\n\n if (expectedErrorStatus) {\n assert.equal(errorStatus, expectedErrorStatus);\n return;\n }\n return;\n }\n }\n\n if (!expectedSearchError) {\n const results = await search(searchOptions);\n assert.deepStrictEqual(formatResults(results), expectedResults);\n }\n\n function formatResults(results) {\n debug(results);\n return {\n ...results,\n records: results.records.map(({record, id}) => ({id, record: record.toObject()}))\n };\n }\n }\n }\n});\n"],
5
- "mappings": "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",
6
- "names": ["searchOptions", "expectedSearchError"]
7
- }
@@ -1,31 +0,0 @@
1
- import assert from "node:assert";
2
- import { describe } from "node:test";
3
- import createDebugLogger from "debug";
4
- import generateTests from "@natlibfi/fixugen";
5
- import { READERS } from "@natlibfi/fixura";
6
- import { MarcRecord } from "@natlibfi/marc-record";
7
- import * as generators from "./auth.js";
8
- const debug = createDebugLogger("@natlibfi/melinda-record-matching:candidate-search:query-list:auth-test");
9
- const debugData = debug.extend("data");
10
- describe("candidate-search/query-list/auth/", () => {
11
- generateTests({
12
- path: [import.meta.dirname, "..", "..", "..", "test-fixtures", "candidate-search", "query-list", "auth"],
13
- useMetadataFile: true,
14
- fixura: {
15
- reader: READERS.JSON
16
- },
17
- callback: ({ type, inputRecord, expectedQuery, expectedQueryListType }) => {
18
- const generate = generators[type];
19
- const record = new MarcRecord(inputRecord, { subfieldValues: false });
20
- const result = generate(record);
21
- debugData(`Result: ${JSON.stringify(result)}`);
22
- if (result.queryListType) {
23
- assert.deepStrictEqual(result.queryList, expectedQuery);
24
- assert.equal(result.queryListType, expectedQueryListType);
25
- return;
26
- }
27
- assert.deepStrictEqual(result, expectedQuery);
28
- }
29
- });
30
- });
31
- //# sourceMappingURL=auth.test.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/candidate-search/query-list/auth.test.js"],
4
- "sourcesContent": ["import assert from 'node:assert';\nimport {describe} from 'node:test';\nimport createDebugLogger from 'debug';\nimport generateTests from '@natlibfi/fixugen';\nimport {READERS} from '@natlibfi/fixura';\nimport {MarcRecord} from '@natlibfi/marc-record';\nimport * as generators from './auth.js';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query-list:auth-test');\nconst debugData = debug.extend('data');\n\ndescribe('candidate-search/query-list/auth/', () => {\n generateTests({\n path: [import.meta.dirname, '..', '..', '..', 'test-fixtures', 'candidate-search', 'query-list', 'auth'],\n useMetadataFile: true,\n fixura: {\n reader: READERS.JSON\n },\n callback: ({type, inputRecord, expectedQuery, expectedQueryListType}) => {\n const generate = generators[type];\n const record = new MarcRecord(inputRecord, {subfieldValues: false});\n\n const result = generate(record);\n debugData(`Result: ${JSON.stringify(result)}`);\n\n if (result.queryListType) {\n assert.deepStrictEqual(result.queryList, expectedQuery);\n assert.equal(result.queryListType, expectedQueryListType);\n return;\n }\n assert.deepStrictEqual(result, expectedQuery);\n }\n });\n});\n"],
5
- "mappings": "AAAA,OAAO,YAAY;AACnB,SAAQ,gBAAe;AACvB,OAAO,uBAAuB;AAC9B,OAAO,mBAAmB;AAC1B,SAAQ,eAAc;AACtB,SAAQ,kBAAiB;AACzB,YAAY,gBAAgB;AAE5B,MAAM,QAAQ,kBAAkB,yEAAyE;AACzG,MAAM,YAAY,MAAM,OAAO,MAAM;AAErC,SAAS,qCAAqC,MAAM;AAClD,gBAAc;AAAA,IACZ,MAAM,CAAC,YAAY,SAAS,MAAM,MAAM,MAAM,iBAAiB,oBAAoB,cAAc,MAAM;AAAA,IACvG,iBAAiB;AAAA,IACjB,QAAQ;AAAA,MACN,QAAQ,QAAQ;AAAA,IAClB;AAAA,IACA,UAAU,CAAC,EAAC,MAAM,aAAa,eAAe,sBAAqB,MAAM;AACvE,YAAM,WAAW,WAAW,IAAI;AAChC,YAAM,SAAS,IAAI,WAAW,aAAa,EAAC,gBAAgB,MAAK,CAAC;AAElE,YAAM,SAAS,SAAS,MAAM;AAC9B,gBAAU,WAAW,KAAK,UAAU,MAAM,CAAC,EAAE;AAE7C,UAAI,OAAO,eAAe;AACxB,eAAO,gBAAgB,OAAO,WAAW,aAAa;AACtD,eAAO,MAAM,OAAO,eAAe,qBAAqB;AACxD;AAAA,MACF;AACA,aAAO,gBAAgB,QAAQ,aAAa;AAAA,IAC9C;AAAA,EACF,CAAC;AACH,CAAC;",
6
- "names": []
7
- }
@@ -1,31 +0,0 @@
1
- import assert from "node:assert";
2
- import { describe } from "node:test";
3
- import createDebugLogger from "debug";
4
- import generateTests from "@natlibfi/fixugen";
5
- import { READERS } from "@natlibfi/fixura";
6
- import { MarcRecord } from "@natlibfi/marc-record";
7
- import * as generators from "./bib.js";
8
- const debug = createDebugLogger("@natlibfi/melinda-record-matching:candidate-search:query:bibHostComponents:test");
9
- const debugData = debug.extend("data");
10
- describe("candidate-search/query-list/bib/", () => {
11
- generateTests({
12
- path: [import.meta.dirname, "..", "..", "..", "test-fixtures", "candidate-search", "query-list", "bib"],
13
- useMetadataFile: true,
14
- fixura: {
15
- reader: READERS.JSON
16
- },
17
- callback: ({ type, inputRecord, expectedQuery, expectedQueryListType }) => {
18
- const generate = generators[type];
19
- const record = new MarcRecord(inputRecord, { subfieldValues: false });
20
- const result = generate(record);
21
- debugData(`Result: ${JSON.stringify(result)}`);
22
- if (result.queryListType) {
23
- assert.deepStrictEqual(result.queryList, expectedQuery);
24
- assert.equal(result.queryListType, expectedQueryListType);
25
- return;
26
- }
27
- assert.deepStrictEqual(result, expectedQuery);
28
- }
29
- });
30
- });
31
- //# sourceMappingURL=bib.test.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/candidate-search/query-list/bib.test.js"],
4
- "sourcesContent": ["import assert from 'node:assert';\nimport {describe} from 'node:test';\nimport createDebugLogger from 'debug';\nimport generateTests from '@natlibfi/fixugen';\nimport {READERS} from '@natlibfi/fixura';\nimport {MarcRecord} from '@natlibfi/marc-record';\nimport * as generators from './bib.js';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:bibHostComponents:test');\nconst debugData = debug.extend('data');\n\ndescribe('candidate-search/query-list/bib/', () => {\n generateTests({\n path: [import.meta.dirname, '..', '..', '..', 'test-fixtures', 'candidate-search', 'query-list', 'bib'],\n useMetadataFile: true,\n fixura: {\n reader: READERS.JSON\n },\n callback: ({type, inputRecord, expectedQuery, expectedQueryListType}) => {\n const generate = generators[type];\n const record = new MarcRecord(inputRecord, {subfieldValues: false});\n\n const result = generate(record);\n debugData(`Result: ${JSON.stringify(result)}`);\n\n if (result.queryListType) {\n assert.deepStrictEqual(result.queryList, expectedQuery);\n assert.equal(result.queryListType, expectedQueryListType);\n return;\n }\n assert.deepStrictEqual(result, expectedQuery);\n }\n });\n});\n"],
5
- "mappings": "AAAA,OAAO,YAAY;AACnB,SAAQ,gBAAe;AACvB,OAAO,uBAAuB;AAC9B,OAAO,mBAAmB;AAC1B,SAAQ,eAAc;AACtB,SAAQ,kBAAiB;AACzB,YAAY,gBAAgB;AAE5B,MAAM,QAAQ,kBAAkB,iFAAiF;AACjH,MAAM,YAAY,MAAM,OAAO,MAAM;AAErC,SAAS,oCAAoC,MAAM;AACjD,gBAAc;AAAA,IACZ,MAAM,CAAC,YAAY,SAAS,MAAM,MAAM,MAAM,iBAAiB,oBAAoB,cAAc,KAAK;AAAA,IACtG,iBAAiB;AAAA,IACjB,QAAQ;AAAA,MACN,QAAQ,QAAQ;AAAA,IAClB;AAAA,IACA,UAAU,CAAC,EAAC,MAAM,aAAa,eAAe,sBAAqB,MAAM;AACvE,YAAM,WAAW,WAAW,IAAI;AAChC,YAAM,SAAS,IAAI,WAAW,aAAa,EAAC,gBAAgB,MAAK,CAAC;AAElE,YAAM,SAAS,SAAS,MAAM;AAC9B,gBAAU,WAAW,KAAK,UAAU,MAAM,CAAC,EAAE;AAE7C,UAAI,OAAO,eAAe;AACxB,eAAO,gBAAgB,OAAO,WAAW,aAAa;AACtD,eAAO,MAAM,OAAO,eAAe,qBAAqB;AACxD;AAAA,MACF;AACA,aAAO,gBAAgB,QAAQ,aAAa;AAAA,IAC9C;AAAA,EACF,CAAC;AACH,CAAC;",
6
- "names": []
7
- }
@@ -1,69 +0,0 @@
1
- import assert from "node:assert";
2
- import { describe } from "node:test";
3
- import createDebugLogger from "debug";
4
- import { READERS } from "@natlibfi/fixura";
5
- import generateTests from "@natlibfi/fixugen-http-client";
6
- import { MarcRecord } from "@natlibfi/marc-record";
7
- import createMatchInterface, { matchDetection } from "./index.js";
8
- const debug = createDebugLogger("@natlibfi/melinda-record-matching:index:test");
9
- const debugData = debug.extend("data");
10
- describe("INDEX", () => {
11
- generateTests({
12
- callback,
13
- path: [import.meta.dirname, "..", "test-fixtures", "index"],
14
- recurse: false,
15
- fixura: {
16
- reader: READERS.JSON
17
- }
18
- });
19
- async function callback({ getFixture, options, expectedMatchStatus, expectedStopReason, expectedFailures, expectedCandidateCount }) {
20
- const record = new MarcRecord(getFixture("inputRecord.json"), { subfieldValues: false });
21
- const expectedMatches = getFixture("expectedMatches.json");
22
- const expectedNonMatches = getFixture("expectedNonMatches.json") || [];
23
- const match = createMatchInterface(formatOptions());
24
- const { matches, matchStatus, nonMatches, conversionFailures, candidateCount } = await match({ record });
25
- debugData(`Matches: ${matches.length}, Status: ${matchStatus.status}/${matchStatus.stopReason}, NonMatches: ${nonMatches ? nonMatches.length : "not returned"}, ConversionFailures: ${conversionFailures ? conversionFailures.length : "not returned"}`);
26
- assert.equal(matchStatus.status, expectedMatchStatus);
27
- assert.equal(matchStatus.stopReason, expectedStopReason);
28
- assert.equal(candidateCount, expectedCandidateCount);
29
- const formattedMatchResult = formatRecordResults(matches);
30
- assert.deepStrictEqual(formattedMatchResult, expectedMatches);
31
- const formattedNonMatchResult = formatRecordResults(nonMatches);
32
- assert.deepStrictEqual(formattedNonMatchResult, expectedNonMatches);
33
- if (expectedFailures) {
34
- assert.deepStrictEqual(conversionFailures, expectedFailures);
35
- }
36
- function formatOptions() {
37
- const contextFeatures = matchDetection.features[options.detection.strategy.type];
38
- return {
39
- ...options,
40
- search: {
41
- ...options.search
42
- },
43
- detection: {
44
- ...options.detect,
45
- strategy: options.detection.strategy.features.map((v) => contextFeatures[v]())
46
- }
47
- };
48
- }
49
- function formatRecordResults(matches2) {
50
- if (matches2) {
51
- debugData(JSON.stringify(matches2));
52
- return matches2.map((match2) => ({
53
- ...match2,
54
- candidate: formatCandidate(match2.candidate)
55
- }));
56
- }
57
- return [];
58
- }
59
- function formatCandidate({ id, record: record2 }) {
60
- const newId = id;
61
- const newRecord = record2;
62
- return {
63
- id: newId,
64
- record: newRecord.toObject()
65
- };
66
- }
67
- }
68
- });
69
- //# sourceMappingURL=index.test.js.map