@natlibfi/melinda-rest-api-commons 6.0.1 → 6.2.0-alpha.0
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.
- package/.github/workflows/melinda-node-tests-and-publish.yml +4 -2
- package/README.md +82 -0
- package/dist/amqp.d.ts +41 -0
- package/dist/amqp.d.ts.map +1 -0
- package/dist/amqp.js +242 -218
- package/dist/amqp.js.map +1 -7
- package/dist/constants.d.ts +49 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +38 -38
- package/dist/constants.js.map +1 -7
- package/dist/helpers/conversion.d.ts +8 -0
- package/dist/helpers/conversion.d.ts.map +1 -0
- package/dist/helpers/conversion.js +70 -65
- package/dist/helpers/conversion.js.map +1 -7
- package/dist/helpers/fix-constants.d.ts +76 -0
- package/dist/helpers/fix-constants.d.ts.map +1 -0
- package/dist/helpers/fix-constants.js +41 -41
- package/dist/helpers/fix-constants.js.map +1 -7
- package/dist/helpers/fix-handle-tempurns.d.ts +3 -0
- package/dist/helpers/fix-handle-tempurns.d.ts.map +1 -0
- package/dist/helpers/fix-handle-tempurns.js +73 -71
- package/dist/helpers/fix-handle-tempurns.js.map +1 -7
- package/dist/helpers/fix-strip-f884s.d.ts +10 -0
- package/dist/helpers/fix-strip-f884s.d.ts.map +1 -0
- package/dist/helpers/fix-strip-f884s.js +163 -151
- package/dist/helpers/fix-strip-f884s.js.map +1 -7
- package/dist/helpers/fix.d.ts +6 -0
- package/dist/helpers/fix.d.ts.map +1 -0
- package/dist/helpers/fix.js +133 -117
- package/dist/helpers/fix.js.map +1 -7
- package/dist/helpers/validation.d.ts +9 -0
- package/dist/helpers/validation.d.ts.map +1 -0
- package/dist/helpers/validation.js +14 -16
- package/dist/helpers/validation.js.map +1 -7
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -7
- package/dist/mongo.d.ts +165 -0
- package/dist/mongo.d.ts.map +1 -0
- package/dist/mongo.js +603 -464
- package/dist/mongo.js.map +1 -7
- package/dist/mongoError.d.ts +2 -0
- package/dist/mongoError.d.ts.map +1 -0
- package/dist/mongoError.js +19 -0
- package/dist/mongoError.js.map +1 -0
- package/dist/mongoLog.d.ts +50 -0
- package/dist/mongoLog.d.ts.map +1 -0
- package/dist/mongoLog.js +282 -172
- package/dist/mongoLog.js.map +1 -7
- package/dist/utils.d.ts +36 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +118 -87
- package/dist/utils.js.map +1 -7
- package/eslint.config.ts +66 -0
- package/package.json +23 -25
- package/test/amqp-error-branches.test.ts +551 -0
- package/{tests/amqp.healthtest.js → test/amqp.healthtest.ts} +19 -7
- package/{tests/amqp.tests.localtest.js → test/amqp.tests.localtest.ts} +66 -52
- package/{tests/helpers/conversion.test.js → test/helpers/conversion.test.ts} +39 -11
- package/test/helpers/fix-edge.test.ts +201 -0
- package/test/helpers/fix.test.ts +65 -0
- package/{tests/helpers/validation.test.js → test/helpers/validation.test.ts} +13 -8
- package/{tests/mongo-add-blob-size.test.js → test/mongo-add-blob-size.test.ts} +14 -15
- package/{tests/mongo-check-and-check-state.test.js → test/mongo-check-and-check-state.test.ts} +9 -9
- package/{tests/mongo-check-and-set-import-job-state.test.js → test/mongo-check-and-set-import-job-state.test.ts} +9 -10
- package/{tests/mongo-check-timeout.test.js → test/mongo-check-timeout.test.ts} +6 -9
- package/{tests/mongo-create-bulk.test.js → test/mongo-create-bulk.test.ts} +6 -7
- package/{tests/mongo-create-prio.test.js → test/mongo-create-prio.test.ts} +6 -6
- package/test/mongo-error-branches.test.ts +883 -0
- package/{tests/mongo-get-one.test.js → test/mongo-get-one.test.ts} +6 -6
- package/{tests/mongo-get-stream.test.js → test/mongo-get-stream.test.ts} +10 -9
- package/{tests/mongo-push-ids.test.js → test/mongo-push-ids.test.ts} +6 -6
- package/{tests/mongo-push-messages.test.js → test/mongo-push-messages.test.ts} +6 -6
- package/{tests/mongo-query-by-id.test.js → test/mongo-query-by-id.test.ts} +6 -9
- package/{tests/mongo-query.test.js → test/mongo-query.test.ts} +6 -7
- package/{tests/mongo-read-content.test.js → test/mongo-read-content.test.ts} +16 -9
- package/{tests/mongo-remove.test.js → test/mongo-remove.test.ts} +6 -8
- package/{tests/mongo-set-blob-size.test.js → test/mongo-set-blob-size.test.ts} +6 -6
- package/{tests/mongo-set-import-job-state.test.js → test/mongo-set-import-job-state.test.ts} +6 -6
- package/{tests/mongo-set-operation.test.js → test/mongo-set-operation.test.ts} +6 -6
- package/{tests/mongo-set-operations.test.js → test/mongo-set-operations.test.ts} +6 -6
- package/{tests/mongo-set-state.test.js → test/mongo-set-state.test.ts} +6 -6
- package/test/mongoLog-protect.test.ts +182 -0
- package/test/mongoLog.test.ts +191 -0
- package/{tests/testUtils.js → test/testUtils.ts} +49 -15
- package/test/utils.test.ts +360 -0
- package/tsconfig.json +62 -0
- package/tsconfig.test.json +17 -0
- package/dist/helpers/fix-utils.js +0 -39
- package/dist/helpers/fix-utils.js.map +0 -7
- package/eslint.config.js +0 -53
- package/tests/helpers/fix.test.js +0 -41
- package/tests/mongoLog.test.js +0 -84
|
@@ -3,8 +3,11 @@ import createDebugLogger from 'debug';
|
|
|
3
3
|
import {promisify} from 'util';
|
|
4
4
|
import {READERS} from '@natlibfi/fixura';
|
|
5
5
|
import generateTests from '@natlibfi/fixugen';
|
|
6
|
+
import type {CallbackArgs} from '@natlibfi/fixugen';
|
|
6
7
|
import {Error as ApiError} from '@natlibfi/melinda-commons';
|
|
7
|
-
import {
|
|
8
|
+
import type {GetMessage, Message} from 'amqplib';
|
|
9
|
+
import {amqpFactory} from '../src/index.ts';
|
|
10
|
+
import type {AmqpOperator, QueueItemMessagePayload, QueueItemRecordPayload, SendToQueueParams} from '../src/amqp.ts';
|
|
8
11
|
|
|
9
12
|
const setTimeoutPromise = promisify(setTimeout);
|
|
10
13
|
const debug = createDebugLogger('@natlibfi/melinda-rest-api-commons:amqp:test');
|
|
@@ -18,8 +21,7 @@ const testData = {
|
|
|
18
21
|
|
|
19
22
|
debug(`Testing amqp against a local AMQP instance - we'll need some kind of mockup for this`);
|
|
20
23
|
const amqpUrl = 'amqp://127.0.0.1:5672/';
|
|
21
|
-
|
|
22
|
-
let amqpOperator;
|
|
24
|
+
let amqpOperator: AmqpOperator;
|
|
23
25
|
|
|
24
26
|
generateTests({
|
|
25
27
|
callback,
|
|
@@ -48,8 +50,15 @@ generateTests({
|
|
|
48
50
|
}
|
|
49
51
|
});
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
/**
|
|
54
|
+
* One fixture case. See the fixture metadata files in test-fixtures/amqp
|
|
55
|
+
* for the possible keys (getFixture, expectToFail, expectedErrorStatus,
|
|
56
|
+
* ackMessages, nackMessages, checkNextMessage, checkQueueStyle, queue,
|
|
57
|
+
* sentToQueue, expectedMessageCount, removeQueue, expectedErrorMessage,
|
|
58
|
+
* expectApiError, expectFalseCheckResult).
|
|
59
|
+
*/
|
|
60
|
+
async function callback(callbackParams: CallbackArgs) {
|
|
61
|
+
const {
|
|
53
62
|
getFixture,
|
|
54
63
|
expectToFail = false,
|
|
55
64
|
expectedErrorStatus = '',
|
|
@@ -57,14 +66,16 @@ async function callback({
|
|
|
57
66
|
nackMessages = false,
|
|
58
67
|
checkNextMessage = false,
|
|
59
68
|
checkQueueStyle = false,
|
|
60
|
-
queue = testData.queue,
|
|
69
|
+
queue: queueFromFixture = testData.queue,
|
|
61
70
|
sentToQueue,
|
|
62
71
|
expectedMessageCount,
|
|
63
72
|
removeQueue = false,
|
|
64
73
|
expectedErrorMessage = undefined,
|
|
65
74
|
expectApiError = false,
|
|
66
75
|
expectFalseCheckResult = false
|
|
67
|
-
}
|
|
76
|
+
} = callbackParams;
|
|
77
|
+
// the fixture contract passes the queue name as a string
|
|
78
|
+
const queue = queueFromFixture as string;
|
|
68
79
|
try {
|
|
69
80
|
await sendMessagesToQueue();
|
|
70
81
|
|
|
@@ -79,10 +90,13 @@ async function callback({
|
|
|
79
90
|
assert.equal(messageCount, expectedMessageCount);
|
|
80
91
|
|
|
81
92
|
if (checkNextMessage) {
|
|
82
|
-
const {headers, records} = await queueCheckNextMessage();
|
|
93
|
+
const {headers, records} = (await queueCheckNextMessage()) as QueueItemRecordPayload;
|
|
83
94
|
const nextMessage = {headers, records};
|
|
84
95
|
const expectedNextMessage = getFixture('expectedMessage.json');
|
|
85
|
-
assert.
|
|
96
|
+
assert.deepStrictEqual(
|
|
97
|
+
JSON.parse(JSON.stringify(nextMessage)),
|
|
98
|
+
expectedNextMessage,
|
|
99
|
+
);
|
|
86
100
|
return;
|
|
87
101
|
}
|
|
88
102
|
|
|
@@ -94,15 +108,19 @@ async function callback({
|
|
|
94
108
|
testErrors(error);
|
|
95
109
|
}
|
|
96
110
|
|
|
97
|
-
|
|
111
|
+
/**
|
|
112
|
+
* @param error
|
|
113
|
+
*/
|
|
114
|
+
function testErrors(error: unknown) {
|
|
98
115
|
const isApiError = error instanceof ApiError;
|
|
99
116
|
if (expectApiError) {
|
|
100
117
|
assert.equal(isApiError, true);
|
|
101
118
|
debug(`We expected and got an ApiError`);
|
|
102
|
-
|
|
119
|
+
const apiError = error as ApiError;
|
|
120
|
+
assert.equal(apiError.status, expectedErrorStatus);
|
|
103
121
|
|
|
104
122
|
if (expectedErrorMessage) {
|
|
105
|
-
assert.equal(
|
|
123
|
+
assert.equal(apiError.payload, expectedErrorMessage);
|
|
106
124
|
return;
|
|
107
125
|
}
|
|
108
126
|
|
|
@@ -110,26 +128,26 @@ async function callback({
|
|
|
110
128
|
}
|
|
111
129
|
assert.equal(isApiError, false);
|
|
112
130
|
debug(`We expected and got an non-ApiError`);
|
|
113
|
-
|
|
131
|
+
const maybeError = error as Error;
|
|
132
|
+
debug(`Error is a ${maybeError.constructor.name}`);
|
|
114
133
|
}
|
|
115
134
|
|
|
116
135
|
function queueRemoval() {
|
|
117
136
|
if (removeQueue) {
|
|
118
|
-
return amqpOperator.removeQueue(removeQueue);
|
|
137
|
+
return amqpOperator.removeQueue(removeQueue as string);
|
|
119
138
|
}
|
|
120
139
|
|
|
121
140
|
return;
|
|
122
141
|
}
|
|
123
142
|
|
|
124
|
-
function queueCheckNextMessage() {
|
|
143
|
+
function queueCheckNextMessage(): Promise<QueueItemRecordPayload> | false {
|
|
125
144
|
if (checkNextMessage) {
|
|
126
|
-
return amqpOperator.consumeOne(testData.queue, true)
|
|
145
|
+
return amqpOperator.consumeOne(testData.queue, true) as Promise<QueueItemRecordPayload>;
|
|
127
146
|
}
|
|
128
147
|
|
|
129
148
|
return false;
|
|
130
149
|
}
|
|
131
150
|
|
|
132
|
-
// eslint-disable-next-line max-statements
|
|
133
151
|
async function checkQueue() {
|
|
134
152
|
debug(`checkQueueStyle: ${checkQueueStyle}`);
|
|
135
153
|
|
|
@@ -137,33 +155,29 @@ async function callback({
|
|
|
137
155
|
const result = await amqpOperator.checkQueue({queue, style: 'one', toRecord: true, purge: false});
|
|
138
156
|
|
|
139
157
|
if (expectFalseCheckResult) {
|
|
140
|
-
|
|
158
|
+
assert.strictEqual(result, false);
|
|
141
159
|
return;
|
|
142
160
|
}
|
|
143
161
|
|
|
144
|
-
|
|
145
|
-
assert.equal(Object.hasOwn(
|
|
146
|
-
assert.equal(Object.hasOwn(
|
|
147
|
-
assert.equal(
|
|
148
|
-
assert.equal(
|
|
162
|
+
const recordResult = result as QueueItemRecordPayload;
|
|
163
|
+
assert.equal(Object.hasOwn(recordResult, 'headers'), true);
|
|
164
|
+
assert.equal(Object.hasOwn(recordResult, 'records'), true);
|
|
165
|
+
assert.equal(Object.hasOwn(recordResult, 'messages'), true);
|
|
166
|
+
assert.equal(recordResult.records.length, 1);
|
|
167
|
+
assert.equal(recordResult.messages.length, 1);
|
|
149
168
|
|
|
150
169
|
return;
|
|
151
170
|
}
|
|
152
171
|
|
|
153
172
|
if (checkQueueStyle === 'oneNotRecord') {
|
|
154
173
|
const result = await amqpOperator.checkQueue({queue, style: 'one', toRecord: false, purge: false});
|
|
155
|
-
const {fields} = result;
|
|
174
|
+
const {fields} = result as GetMessage;
|
|
156
175
|
|
|
157
176
|
if (expectFalseCheckResult) {
|
|
158
|
-
|
|
177
|
+
assert.strictEqual(result, false);
|
|
159
178
|
return;
|
|
160
179
|
}
|
|
161
180
|
|
|
162
|
-
assert.equal(Object.hasOwn(result, 'deliveryTag'), true);
|
|
163
|
-
assert.equal(Object.hasOwn(result, 'exchange'), true);
|
|
164
|
-
assert.equal(Object.hasOwn(result, 'messageCount'), true);
|
|
165
|
-
assert.equal(Object.hasOwn(result, 'redelivered'), true);
|
|
166
|
-
assert.equal(Object.hasOwn(result, 'routingKey'), true);
|
|
167
181
|
assert.equal(typeof fields.deliveryTag, 'number');
|
|
168
182
|
assert.equal(typeof fields.messageCount, 'number');
|
|
169
183
|
assert.equal(typeof fields.redelivered, 'boolean');
|
|
@@ -176,17 +190,21 @@ async function callback({
|
|
|
176
190
|
const result = await amqpOperator.checkQueue({queue, style: 'basic', toRecord: true, purge: false});
|
|
177
191
|
|
|
178
192
|
if (expectFalseCheckResult) {
|
|
179
|
-
|
|
193
|
+
assert.strictEqual(result, false);
|
|
180
194
|
return;
|
|
181
195
|
}
|
|
182
196
|
|
|
183
|
-
|
|
184
|
-
assert.equal(Object.hasOwn(
|
|
185
|
-
assert.equal(Object.hasOwn(
|
|
186
|
-
assert.equal(
|
|
187
|
-
assert.equal(
|
|
197
|
+
const recordResult = result as QueueItemRecordPayload;
|
|
198
|
+
assert.equal(Object.hasOwn(recordResult, 'headers'), true);
|
|
199
|
+
assert.equal(Object.hasOwn(recordResult, 'records'), true);
|
|
200
|
+
assert.equal(Object.hasOwn(recordResult, 'messages'), true);
|
|
201
|
+
assert.equal(recordResult.records.length, 3);
|
|
202
|
+
assert.equal(recordResult.messages.length, 3);
|
|
188
203
|
const expectedResult = getFixture('expectedResult.json');
|
|
189
|
-
assert.deepStrictEqual(
|
|
204
|
+
assert.deepStrictEqual(
|
|
205
|
+
JSON.parse(JSON.stringify(recordResult.records)),
|
|
206
|
+
expectedResult,
|
|
207
|
+
);
|
|
190
208
|
|
|
191
209
|
return;
|
|
192
210
|
}
|
|
@@ -195,21 +213,17 @@ async function callback({
|
|
|
195
213
|
const result = await amqpOperator.checkQueue({queue, style: 'basic', toRecord: false, purge: false});
|
|
196
214
|
|
|
197
215
|
if (expectFalseCheckResult) {
|
|
198
|
-
|
|
216
|
+
assert.strictEqual(result, false);
|
|
199
217
|
return;
|
|
200
218
|
}
|
|
201
219
|
|
|
202
|
-
|
|
203
|
-
assert.equal(Object.hasOwn(
|
|
204
|
-
assert.equal(
|
|
220
|
+
const messageResult = result as QueueItemMessagePayload;
|
|
221
|
+
assert.equal(Object.hasOwn(messageResult, 'headers'), true);
|
|
222
|
+
assert.equal(Object.hasOwn(messageResult, 'messages'), true);
|
|
223
|
+
assert.equal(messageResult.messages.length, 3);
|
|
205
224
|
|
|
206
|
-
const [
|
|
207
|
-
|
|
208
|
-
assert.equal(Object.hasOwn(result, 'deliveryTag'), true);
|
|
209
|
-
assert.equal(Object.hasOwn(result, 'exchange'), true);
|
|
210
|
-
assert.equal(Object.hasOwn(result, 'messageCount'), true);
|
|
211
|
-
assert.equal(Object.hasOwn(result, 'redelivered'), true);
|
|
212
|
-
assert.equal(Object.hasOwn(result, 'routingKey'), true);
|
|
225
|
+
const [firstMessage] = messageResult.messages as [Message, ...Message[]];
|
|
226
|
+
const {fields} = firstMessage;
|
|
213
227
|
|
|
214
228
|
assert.equal(typeof fields.deliveryTag, 'number');
|
|
215
229
|
assert.equal(typeof fields.messageCount, 'number');
|
|
@@ -233,14 +247,14 @@ async function callback({
|
|
|
233
247
|
async function ackOrNackMessages() {
|
|
234
248
|
if (ackMessages) {
|
|
235
249
|
const message = await amqpOperator.consumeOne(testData.queue, false);
|
|
236
|
-
await amqpOperator.ackMessages([message]);
|
|
250
|
+
await amqpOperator.ackMessages([message as GetMessage]);
|
|
237
251
|
|
|
238
252
|
return setTimeoutPromise(10);
|
|
239
253
|
}
|
|
240
254
|
|
|
241
255
|
if (nackMessages) {
|
|
242
256
|
const message = await amqpOperator.consumeOne(testData.queue, false);
|
|
243
|
-
await amqpOperator.nackMessages([message]);
|
|
257
|
+
await amqpOperator.nackMessages([message as GetMessage]);
|
|
244
258
|
|
|
245
259
|
return setTimeoutPromise(10);
|
|
246
260
|
}
|
|
@@ -250,7 +264,7 @@ async function callback({
|
|
|
250
264
|
|
|
251
265
|
async function sendMessagesToQueue() {
|
|
252
266
|
if (sentToQueue) {
|
|
253
|
-
const messages = getFixture('input.json');
|
|
267
|
+
const messages = getFixture('input.json') as SendToQueueParams[];
|
|
254
268
|
// {queue, correlationId, headers, data}
|
|
255
269
|
|
|
256
270
|
const promises = messages.map(message => amqpOperator.sendToQueue(message));
|
|
@@ -277,7 +291,7 @@ function testHeathCheck() {
|
|
|
277
291
|
await testHealthCheckLoopErroring(amqpOperator2);
|
|
278
292
|
} catch (error) {
|
|
279
293
|
debug(error);
|
|
280
|
-
|
|
294
|
+
assert.strictEqual(error, '');
|
|
281
295
|
} finally {
|
|
282
296
|
amqpOperator2.closeConnection();
|
|
283
297
|
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
1
|
import assert from 'node:assert';
|
|
4
2
|
import {describe, it} from 'node:test';
|
|
5
3
|
import fixtureFactory from '@natlibfi/fixura';
|
|
6
4
|
import {MarcRecord} from '@natlibfi/marc-record';
|
|
5
|
+
import type {MarcRecordObject} from '@natlibfi/marc-record';
|
|
7
6
|
import {Error as ConversionError} from '@natlibfi/melinda-commons';
|
|
8
|
-
import createConversionService from '../../src/helpers/conversion.
|
|
9
|
-
import {CONVERSION_FORMATS} from '../../src/constants.
|
|
7
|
+
import createConversionService from '../../src/helpers/conversion.ts';
|
|
8
|
+
import {CONVERSION_FORMATS} from '../../src/constants.ts';
|
|
10
9
|
|
|
11
10
|
describe('services/conversion', () => {
|
|
12
11
|
const {getFixture} = fixtureFactory({
|
|
@@ -20,11 +19,11 @@ describe('services/conversion', () => {
|
|
|
20
19
|
});
|
|
21
20
|
const conversionService = createConversionService();
|
|
22
21
|
|
|
23
|
-
const marcRecord = new MarcRecord(JSON.parse(getFixture({components: ['json1']})), {subfieldValues: false});
|
|
24
|
-
const marcXml = getFixture({components: ['marcxml1']});
|
|
25
|
-
const iso2709 = getFixture({components: ['iso2709_1']});
|
|
26
|
-
const json = getFixture({components: ['json1']});
|
|
27
|
-
const alephSeq = getFixture({components: ['aleph_sequential']});
|
|
22
|
+
const marcRecord = new MarcRecord(JSON.parse(getFixture({components: ['json1']}) as string) as MarcRecordObject, {subfieldValues: false});
|
|
23
|
+
const marcXml = getFixture({components: ['marcxml1']}) as string;
|
|
24
|
+
const iso2709 = getFixture({components: ['iso2709_1']}) as string;
|
|
25
|
+
const json = getFixture({components: ['json1']}) as string;
|
|
26
|
+
const alephSeq = getFixture({components: ['aleph_sequential']}) as string;
|
|
28
27
|
|
|
29
28
|
|
|
30
29
|
describe('factory', () => {
|
|
@@ -38,11 +37,16 @@ describe('services/conversion', () => {
|
|
|
38
37
|
|
|
39
38
|
describe('#serialize', () => {
|
|
40
39
|
it('Should throw because of unsupported format', () => {
|
|
40
|
+
let threw = false;
|
|
41
41
|
try {
|
|
42
|
-
|
|
42
|
+
// the undefined inputs are the point of this test: serialize must
|
|
43
|
+
// throw a ConversionError for an unsupported format, not a TypeError
|
|
44
|
+
conversionService.serialize(undefined as never, undefined as never);
|
|
43
45
|
} catch (error) {
|
|
46
|
+
threw = true;
|
|
44
47
|
assert(error instanceof ConversionError);
|
|
45
48
|
}
|
|
49
|
+
assert.equal(threw, true);
|
|
46
50
|
});
|
|
47
51
|
|
|
48
52
|
it('Should serialize to MARCXML', () => {
|
|
@@ -65,15 +69,36 @@ describe('services/conversion', () => {
|
|
|
65
69
|
assert.equal(data, alephSeq);
|
|
66
70
|
});
|
|
67
71
|
|
|
72
|
+
it('Should rewrap a plain (non-ConversionError) serializer error into a ConversionError with 400', () => {
|
|
73
|
+
// a plain {} makes MARCXML.to throw a TypeError, which serialize must
|
|
74
|
+
// rewrap into a ConversionError 400 (not propagate the raw TypeError)
|
|
75
|
+
let threw = false;
|
|
76
|
+
try {
|
|
77
|
+
conversionService.serialize({} as never, CONVERSION_FORMATS.MARCXML);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
threw = true;
|
|
80
|
+
assert(error instanceof ConversionError);
|
|
81
|
+
assert.equal(error.status, 400);
|
|
82
|
+
// the payload is a string here: 'Error while serializing record. ...'
|
|
83
|
+
assert.ok((error.payload as string).includes('Error while serializing record.'));
|
|
84
|
+
}
|
|
85
|
+
assert.equal(threw, true);
|
|
86
|
+
});
|
|
87
|
+
|
|
68
88
|
});
|
|
69
89
|
|
|
70
90
|
describe('#unserialize', () => {
|
|
71
91
|
it('Should throw because of unsupported format', () => {
|
|
92
|
+
let threw = false;
|
|
72
93
|
try {
|
|
73
|
-
|
|
94
|
+
// the undefined inputs are the point of this test: unserialize must
|
|
95
|
+
// throw a ConversionError for an unsupported format, not a TypeError
|
|
96
|
+
conversionService.unserialize(undefined as never, undefined as never);
|
|
74
97
|
} catch (error) {
|
|
98
|
+
threw = true;
|
|
75
99
|
assert(error instanceof ConversionError);
|
|
76
100
|
}
|
|
101
|
+
assert.equal(threw, true);
|
|
77
102
|
});
|
|
78
103
|
|
|
79
104
|
it('Should unserialize from MARCXML', async () => {
|
|
@@ -98,11 +123,14 @@ describe('services/conversion', () => {
|
|
|
98
123
|
|
|
99
124
|
|
|
100
125
|
it('Should throw because the record could not be unserialized', () => {
|
|
126
|
+
let threw = false;
|
|
101
127
|
try {
|
|
102
128
|
conversionService.unserialize('', CONVERSION_FORMATS.JSON);
|
|
103
129
|
} catch (error) {
|
|
130
|
+
threw = true;
|
|
104
131
|
assert(error instanceof ConversionError);
|
|
105
132
|
}
|
|
133
|
+
assert.equal(threw, true);
|
|
106
134
|
});
|
|
107
135
|
});
|
|
108
136
|
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Edge-case tests for the MARC fix helpers.
|
|
2
|
+
|
|
3
|
+
import assert from 'node:assert';
|
|
4
|
+
import {test} from 'node:test';
|
|
5
|
+
import {MarcRecord} from '@natlibfi/marc-record';
|
|
6
|
+
import type {MarcField} from '@natlibfi/marc-record';
|
|
7
|
+
import {fixRecord} from '../../src/helpers/fix.ts';
|
|
8
|
+
import {BIB_STRIP_F884S_SETTINGS, BIB_F035_TO_SID} from '../../src/helpers/fix-constants.ts';
|
|
9
|
+
|
|
10
|
+
// a Melinda f884 (tag 884, $5 MELINDA, $a + $g present) that
|
|
11
|
+
// has NO $k subfield. findSource/findHash used to call `sfK.replace(...)` on
|
|
12
|
+
// undefined and crash the whole fixRecord call. Now a missing $k is treated
|
|
13
|
+
// as an empty string: the field gets NO_SOURCE (source) and the all-zero
|
|
14
|
+
// sentinel (hash), and the merged date range is applied to its $g.
|
|
15
|
+
test('fixRecord handles Melinda f884 without $k subfield without crashing', () => {
|
|
16
|
+
const record = new MarcRecord({
|
|
17
|
+
leader: '03085cam a22006858i 4500',
|
|
18
|
+
fields: [
|
|
19
|
+
{tag: '001', value: '000000001'},
|
|
20
|
+
{tag: '003', value: 'FI-MELINDA'},
|
|
21
|
+
{tag: '005', value: '20220818041047.0'},
|
|
22
|
+
{
|
|
23
|
+
// Melinda f884 WITHOUT $k
|
|
24
|
+
tag: '884',
|
|
25
|
+
ind1: ' ',
|
|
26
|
+
ind2: ' ',
|
|
27
|
+
subfields: [
|
|
28
|
+
{code: 'a', value: 'ONIX3 to MARC transformation'},
|
|
29
|
+
{code: 'g', value: '20220101'},
|
|
30
|
+
{code: '5', value: 'MELINDA'}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
// A second Melinda f884 with $k so that the
|
|
35
|
+
// "not enough Melinda f884s" early return does not trigger
|
|
36
|
+
tag: '884',
|
|
37
|
+
ind1: ' ',
|
|
38
|
+
ind2: ' ',
|
|
39
|
+
subfields: [
|
|
40
|
+
{code: 'a', value: 'ONIX3 to MARC transformation'},
|
|
41
|
+
{code: 'g', value: '20000718'},
|
|
42
|
+
{
|
|
43
|
+
code: 'k',
|
|
44
|
+
value: 'SRC_4:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
|
45
|
+
},
|
|
46
|
+
{code: '5', value: 'MELINDA'}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}, {subfieldValues: false});
|
|
51
|
+
|
|
52
|
+
// Fixed behaviour: fixRecord must not throw
|
|
53
|
+
const result = fixRecord(record, BIB_STRIP_F884S_SETTINGS);
|
|
54
|
+
|
|
55
|
+
// Both Melinda f884s survive (they are not identical, so uniq keeps both)
|
|
56
|
+
// these are data fields: mirrors the JSDoc baseline MarcField[] cast
|
|
57
|
+
const f884s = (result.fields as MarcField[]).filter((f) => f.tag === '884');
|
|
58
|
+
assert.equal(f884s.length, 2);
|
|
59
|
+
|
|
60
|
+
// The field WITHOUT $k still has its $a, $g and $5 MELINDA
|
|
61
|
+
const noK = f884s.find((f) => !f.subfields.some((s) => s.code === 'k'));
|
|
62
|
+
assert.ok(noK, 'field without $k should survive');
|
|
63
|
+
assert.deepEqual(
|
|
64
|
+
noK.subfields.map((s) => s.code).sort(),
|
|
65
|
+
['5', 'a', 'g']
|
|
66
|
+
);
|
|
67
|
+
const gNoK = noK.subfields.find((s) => s.code === 'g')?.value;
|
|
68
|
+
const aNoK = noK.subfields.find((s) => s.code === 'a')?.value;
|
|
69
|
+
assert.equal(aNoK, 'ONIX3 to MARC transformation');
|
|
70
|
+
assert.equal(gNoK, '20220101');
|
|
71
|
+
|
|
72
|
+
// The field WITH $k keeps its $a, $g, $k and $5 MELINDA
|
|
73
|
+
const withK = f884s.find((f) => f.subfields.some((s) => s.code === 'k'));
|
|
74
|
+
assert.ok(withK, 'field with $k should survive');
|
|
75
|
+
assert.deepEqual(
|
|
76
|
+
withK.subfields.map((s) => s.code).sort(),
|
|
77
|
+
['5', 'a', 'g', 'k']
|
|
78
|
+
);
|
|
79
|
+
assert.equal(withK.subfields.find((s) => s.code === 'g')?.value, '20000718');
|
|
80
|
+
assert.equal(withK.subfields.find((s) => s.code === 'k')?.value,
|
|
81
|
+
'SRC_4:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// stripF884s early return: fewer than 2 Melinda f884s (0 or 1) means the
|
|
85
|
+
// record is returned unchanged (the merge/uniq pipeline never runs).
|
|
86
|
+
test('stripF884s returns the record unchanged when it has no Melinda f884s', () => {
|
|
87
|
+
const record = new MarcRecord({
|
|
88
|
+
leader: '03085cam a22006858i 4500',
|
|
89
|
+
fields: [
|
|
90
|
+
{tag: '001', value: '000000001'},
|
|
91
|
+
{tag: '035', ind1: ' ', ind2: ' ', subfields: [{code: 'a', value: '(FI-BTJ)12345'}]}
|
|
92
|
+
]
|
|
93
|
+
}, {subfieldValues: false});
|
|
94
|
+
|
|
95
|
+
const result = fixRecord(record, BIB_STRIP_F884S_SETTINGS);
|
|
96
|
+
assert.deepStrictEqual(result, record.toObject());
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('stripF884s returns the record unchanged when it has a single Melinda f884', () => {
|
|
100
|
+
const record = new MarcRecord({
|
|
101
|
+
leader: '03085cam a22006858i 4500',
|
|
102
|
+
fields: [
|
|
103
|
+
{tag: '001', value: '000000001'},
|
|
104
|
+
{
|
|
105
|
+
// a single Melinda f884: fewer than 2, so the field must survive
|
|
106
|
+
// untouched (no date-range merge, no removeFields/insertFields)
|
|
107
|
+
tag: '884',
|
|
108
|
+
ind1: ' ',
|
|
109
|
+
ind2: ' ',
|
|
110
|
+
subfields: [
|
|
111
|
+
{code: 'a', value: 'ONIX3 to MARC transformation'},
|
|
112
|
+
{code: 'g', value: '20220101'},
|
|
113
|
+
{
|
|
114
|
+
code: 'k',
|
|
115
|
+
value: 'SRC_4:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
|
116
|
+
},
|
|
117
|
+
{code: '5', value: 'MELINDA'}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
}, {subfieldValues: false});
|
|
122
|
+
|
|
123
|
+
const result = fixRecord(record, BIB_STRIP_F884S_SETTINGS);
|
|
124
|
+
assert.deepStrictEqual(result, record.toObject());
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// generateMissingSIDs: an existing SID field is only counted when its $b is
|
|
128
|
+
// in fSIDFilters AND it has a $c. Anything else maps to undefined and is
|
|
129
|
+
// filtered out, so the SID is (re)generated from the f035.
|
|
130
|
+
test('fixRecord ignores an existing SID field whose $b is not in fSIDFilters and generates the SID from the f035', () => {
|
|
131
|
+
const record = new MarcRecord({
|
|
132
|
+
leader: '03085cam a22006858i 4500',
|
|
133
|
+
fields: [
|
|
134
|
+
{tag: '001', value: '000000001'},
|
|
135
|
+
{tag: '035', ind1: ' ', ind2: ' ', subfields: [{code: 'a', value: '(FI-BTJ)12345'}]},
|
|
136
|
+
{
|
|
137
|
+
// $b 'FI-KV' is NOT in fSIDFilters -> the field must be ignored,
|
|
138
|
+
// so the f035 (FI-BTJ)12345 still yields a generated SID
|
|
139
|
+
tag: 'SID',
|
|
140
|
+
ind1: ' ',
|
|
141
|
+
ind2: ' ',
|
|
142
|
+
subfields: [
|
|
143
|
+
{code: 'c', value: '12345'},
|
|
144
|
+
{code: 'b', value: 'FI-KV'}
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
}, {subfieldValues: false});
|
|
149
|
+
|
|
150
|
+
const result = fixRecord(record, {generateMissingSIDs: BIB_F035_TO_SID});
|
|
151
|
+
// these are data fields: mirrors the JSDoc baseline MarcField[] cast
|
|
152
|
+
const sids = (result.fields as MarcField[]).filter((f) => f.tag === 'SID');
|
|
153
|
+
assert.equal(sids.length, 2);
|
|
154
|
+
const generated = sids.find((f) => f.subfields.some((s) => s.code === 'b' && s.value === 'FI-BTJ'));
|
|
155
|
+
assert.ok(generated, 'a new SID with $b FI-BTJ should have been generated');
|
|
156
|
+
assert.equal(generated.subfields.find((s) => s.code === 'c')?.value, '12345');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('fixRecord ignores an existing SID field with a matching $b but missing $c and generates the SID from the f035', () => {
|
|
160
|
+
const record = new MarcRecord({
|
|
161
|
+
leader: '03085cam a22006858i 4500',
|
|
162
|
+
fields: [
|
|
163
|
+
{tag: '001', value: '000000001'},
|
|
164
|
+
{tag: '035', ind1: ' ', ind2: ' ', subfields: [{code: 'a', value: '(FI-BTJ)12345'}]},
|
|
165
|
+
{
|
|
166
|
+
// $b 'FI-BTJ' IS in fSIDFilters, but there is no $c -> the field
|
|
167
|
+
// must be ignored, so the f035 still yields a generated SID
|
|
168
|
+
tag: 'SID',
|
|
169
|
+
ind1: ' ',
|
|
170
|
+
ind2: ' ',
|
|
171
|
+
subfields: [
|
|
172
|
+
{code: 'b', value: 'FI-BTJ'}
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
}, {subfieldValues: false});
|
|
177
|
+
|
|
178
|
+
const result = fixRecord(record, {generateMissingSIDs: BIB_F035_TO_SID});
|
|
179
|
+
// these are data fields: mirrors the JSDoc baseline MarcField[] cast
|
|
180
|
+
const sids = (result.fields as MarcField[]).filter((f) => f.tag === 'SID');
|
|
181
|
+
assert.equal(sids.length, 2);
|
|
182
|
+
const generated = sids.find((f) => f.subfields.some((s) => s.code === 'c'));
|
|
183
|
+
assert.ok(generated, 'a generated SID with $c should exist');
|
|
184
|
+
assert.equal(generated.subfields.find((s) => s.code === 'b')?.value, 'FI-BTJ');
|
|
185
|
+
assert.equal(generated.subfields.find((s) => s.code === 'c')?.value, '12345');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('fixRecord adds no SID when the f035 $a matches no f035Filter', () => {
|
|
189
|
+
const record = new MarcRecord({
|
|
190
|
+
leader: '03085cam a22006858i 4500',
|
|
191
|
+
fields: [
|
|
192
|
+
{tag: '001', value: '000000001'},
|
|
193
|
+
// $a does not match any f035Filter in BIB_F035_TO_SID, so
|
|
194
|
+
// f035ToSidInfo maps the field to undefined and nothing is generated
|
|
195
|
+
{tag: '035', ind1: ' ', ind2: ' ', subfields: [{code: 'a', value: '(NOT-IN-FILTERS)999'}]}
|
|
196
|
+
]
|
|
197
|
+
}, {subfieldValues: false});
|
|
198
|
+
|
|
199
|
+
const result = fixRecord(record, {generateMissingSIDs: BIB_F035_TO_SID});
|
|
200
|
+
assert.equal(result.fields.filter((f) => f.tag === 'SID').length, 0);
|
|
201
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import createDebugLogger from 'debug';
|
|
3
|
+
import {MarcRecord} from '@natlibfi/marc-record';
|
|
4
|
+
import type {MarcRecordObject} from '@natlibfi/marc-record';
|
|
5
|
+
import {READERS} from '@natlibfi/fixura';
|
|
6
|
+
import generateTests, {type CallbackArgs} from '@natlibfi/fixugen';
|
|
7
|
+
import {fixRecord} from '../../src/helpers/fix.ts';
|
|
8
|
+
import * as fixSettings from '../../src/helpers/fix-constants.ts';
|
|
9
|
+
import type {FixSettings} from '../../src/helpers/fix-constants.ts';
|
|
10
|
+
// import inspect from 'util';
|
|
11
|
+
|
|
12
|
+
const debug = createDebugLogger('@natlibfi/melinda-rest-api-commons:fix:test');
|
|
13
|
+
const debugData = debug.extend('data');
|
|
14
|
+
|
|
15
|
+
// the settings names usable via metadata.json
|
|
16
|
+
type SettingsKey = 'BIB_FORMAT_SETTINGS' | 'BIB_PREVALIDATION_FIX_SETTINGS' | 'BIB_POSTMERGE_FIX_SETTINGS' | 'BIB_PREIMPORT_FIX_SETTINGS' | 'AUTNAME_PREIMPORT_FIX_SETTINGS' | 'BIB_HANDLE_TEMP_URNS_SETTINGS' | 'BIB_STRIP_F884S_SETTINGS' | 'BIB_STRIP_F984S_SETTINGS' | 'BIB_STRIP_MELINDA_TEMP_SUBFIELDS_SETTINGS';
|
|
17
|
+
|
|
18
|
+
generateTests({
|
|
19
|
+
callback,
|
|
20
|
+
path: [import.meta.dirname, '..', '..', 'test-fixtures', 'fix'],
|
|
21
|
+
recurse: true,
|
|
22
|
+
useMetadataFile: true,
|
|
23
|
+
fixura: {
|
|
24
|
+
failWhenNotFound: false,
|
|
25
|
+
reader: READERS.JSON
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function callback(callbackParams: CallbackArgs) {
|
|
30
|
+
const {getFixture, settings = undefined} = callbackParams;
|
|
31
|
+
if (
|
|
32
|
+
settings !== undefined
|
|
33
|
+
&& typeof settings !== 'object'
|
|
34
|
+
&& (fixSettings[settings as SettingsKey]) === undefined
|
|
35
|
+
) {
|
|
36
|
+
throw new Error(`There are no settings ${settings} defined!`);
|
|
37
|
+
}
|
|
38
|
+
let runSettings: FixSettings | undefined = (fixSettings[settings as SettingsKey]) || (settings as FixSettings);
|
|
39
|
+
// metadata.json is plain JSON, so f035Filters may contain string
|
|
40
|
+
// patterns which fix.js cannot use as-is; convert them to RegExp
|
|
41
|
+
if (runSettings && runSettings.generateMissingSIDs) {
|
|
42
|
+
runSettings = {
|
|
43
|
+
...runSettings,
|
|
44
|
+
generateMissingSIDs: {
|
|
45
|
+
...runSettings.generateMissingSIDs,
|
|
46
|
+
f035Filters: (runSettings.generateMissingSIDs.f035Filters || []).map(
|
|
47
|
+
(filter): RegExp => (typeof filter === 'string' ? new RegExp(filter) : filter)
|
|
48
|
+
),
|
|
49
|
+
fSIDFilters: runSettings.generateMissingSIDs.fSIDFilters
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const record = new MarcRecord(getFixture('record.json') as MarcRecordObject, {subfieldValues: false});
|
|
55
|
+
const expectedRecord = getFixture('expectedRecord.json');
|
|
56
|
+
|
|
57
|
+
debug(`Running fixer with settings ${settings}.`);
|
|
58
|
+
debugData(JSON.stringify(runSettings, null, 2));
|
|
59
|
+
|
|
60
|
+
const resultRecord = fixRecord(record, runSettings);
|
|
61
|
+
debugData(`Result: ${JSON.stringify(resultRecord)}`);
|
|
62
|
+
debugData(`Expected: ${JSON.stringify(expectedRecord)}`);
|
|
63
|
+
assert.deepStrictEqual(resultRecord, expectedRecord);
|
|
64
|
+
|
|
65
|
+
}
|