Line 0
Link Here
|
|
|
1 |
/** |
2 |
* Koha Cypress Testing Data Insertion Utilities |
3 |
* |
4 |
* This module provides functions to create and manage test data for Cypress tests. |
5 |
* It handles creating complete bibliographic records, patrons, holds, checkouts, |
6 |
* and other Koha objects with proper relationships and dependencies. |
7 |
* |
8 |
* @module insertData |
9 |
*/ |
10 |
|
11 |
const { buildSampleObject, buildSampleObjects } = require("./mockData.js"); |
12 |
const { query } = require("./db.js"); |
13 |
|
14 |
const { apiGet, apiPost } = require("./api-client.js"); |
15 |
|
16 |
/** |
17 |
* Creates a complete bibliographic record with associated items and libraries. |
18 |
* |
19 |
* @async |
20 |
* @function insertSampleBiblio |
21 |
* @param {Object} params - Configuration parameters |
22 |
* @param {number} params.item_count - Number of items to create for this biblio |
23 |
* @param {Object} [params.options] - Additional options |
24 |
* @param {boolean} [params.options.different_libraries] - If true, creates different libraries for each item |
25 |
* @param {string} params.baseUrl - Base URL for API calls |
26 |
* @param {string} params.authHeader - Authorization header for API calls |
27 |
* @returns {Promise<Object>} Created biblio with items, libraries, and item_type |
28 |
* @returns {Object} returns.biblio - The created bibliographic record |
29 |
* @returns {Array<Object>} returns.items - Array of created item records |
30 |
* @returns {Array<Object>} returns.libraries - Array of created library records |
31 |
* @returns {Object} returns.item_type - The created item type record |
32 |
* @example |
33 |
* // Create a biblio with 3 items using the same library |
34 |
* const result = await insertSampleBiblio({ |
35 |
* item_count: 3, |
36 |
* baseUrl: 'http://localhost:8081', |
37 |
* authHeader: 'Basic dGVzdDp0ZXN0' |
38 |
* }); |
39 |
* |
40 |
* @example |
41 |
* // Create a biblio with 2 items using different libraries |
42 |
* const result = await insertSampleBiblio({ |
43 |
* item_count: 2, |
44 |
* options: { different_libraries: true }, |
45 |
* baseUrl: 'http://localhost:8081', |
46 |
* authHeader: 'Basic dGVzdDp0ZXN0' |
47 |
* }); |
48 |
*/ |
49 |
const insertSampleBiblio = async ({ |
50 |
item_count, |
51 |
options, |
52 |
baseUrl, |
53 |
authHeader, |
54 |
}) => { |
55 |
const generatedItemType = await buildSampleObject({ object: "item_type" }); |
56 |
const item_type = await insertObject({ |
57 |
type: "item_type", |
58 |
object: generatedItemType, |
59 |
baseUrl, |
60 |
authHeader, |
61 |
}); |
62 |
|
63 |
let title = "Some boring read"; |
64 |
let author = "Some boring author"; |
65 |
let biblio = { |
66 |
leader: " nam a22 7a 4500", |
67 |
fields: [ |
68 |
{ "005": "20250120101920.0" }, |
69 |
{ |
70 |
245: { |
71 |
ind1: "", |
72 |
ind2: "", |
73 |
subfields: [{ a: title }], |
74 |
}, |
75 |
}, |
76 |
{ |
77 |
100: { |
78 |
ind1: "", |
79 |
ind2: "", |
80 |
subfields: [{ c: author }], |
81 |
}, |
82 |
}, |
83 |
{ |
84 |
942: { |
85 |
ind1: "", |
86 |
ind2: "", |
87 |
subfields: [{ c: item_type.item_type_id }], |
88 |
}, |
89 |
}, |
90 |
], |
91 |
}; |
92 |
let result = await apiPost({ |
93 |
endpoint: "/api/v1/biblios", |
94 |
headers: { |
95 |
"Content-Type": "application/marc-in-json", |
96 |
"x-confirm-not-duplicate": 1, |
97 |
}, |
98 |
body: biblio, |
99 |
baseUrl, |
100 |
authHeader, |
101 |
}); |
102 |
const biblio_id = result.id; |
103 |
// We do not have a route to get a biblio as it is stored in DB |
104 |
// We might need to refine that in the future |
105 |
biblio = { |
106 |
biblio_id, |
107 |
title, |
108 |
author, |
109 |
}; |
110 |
|
111 |
let items = buildSampleObjects({ |
112 |
object: "item", |
113 |
count: item_count, |
114 |
values: { |
115 |
biblio_id, |
116 |
lost_status: 0, |
117 |
withdrawn: 0, |
118 |
damaged_status: 0, |
119 |
not_for_loan_status: 0, |
120 |
restricted_status: 0, |
121 |
new_status: null, |
122 |
issues: 0, |
123 |
checked_out_date: null, |
124 |
item_type_id: item_type.item_type_id, |
125 |
}, |
126 |
}); |
127 |
items = items.map( |
128 |
({ |
129 |
item_id, |
130 |
checkout, |
131 |
transfer, |
132 |
lost_date, |
133 |
withdrawn_date, |
134 |
damaged_date, |
135 |
course_item, |
136 |
_strings, |
137 |
biblio, |
138 |
bundle_host, |
139 |
item_group_item, |
140 |
recall, |
141 |
return_claim, |
142 |
return_claims, |
143 |
serial_item, |
144 |
first_hold, |
145 |
checkouts_count, |
146 |
renewals_count, |
147 |
holds_count, |
148 |
bundle_items_lost_count, |
149 |
analytics_count, |
150 |
effective_not_for_loan_status, |
151 |
effective_item_type_id, |
152 |
home_library, |
153 |
holding_library, |
154 |
bundle_items_not_lost_count, |
155 |
item_type, |
156 |
_status, |
157 |
effective_bookable, |
158 |
in_bundle, |
159 |
cover_image_ids, |
160 |
localuse, |
161 |
...rest |
162 |
}) => rest |
163 |
); |
164 |
let createdItems = []; |
165 |
let libraries = []; |
166 |
let commonLibrary; |
167 |
if (!options || !options.different_libraries) { |
168 |
const generatedLibrary = await buildSampleObject({ object: "library" }); |
169 |
commonLibrary = await insertObject({ |
170 |
type: "library", |
171 |
object: generatedLibrary, |
172 |
baseUrl, |
173 |
authHeader, |
174 |
}); |
175 |
libraries.push(commonLibrary); |
176 |
} |
177 |
for (const item of items) { |
178 |
if (options?.different_libraries) { |
179 |
const generatedLibrary = await buildSampleObject({ |
180 |
object: "library", |
181 |
}); |
182 |
const library = await insertObject({ |
183 |
type: "library", |
184 |
object: generatedLibrary, |
185 |
baseUrl, |
186 |
authHeader, |
187 |
}); |
188 |
libraries.push(library); |
189 |
item.home_library_id = library.library_id; |
190 |
item.holding_library_id = library.library_id; |
191 |
} else { |
192 |
item.home_library_id = commonLibrary.library_id; |
193 |
item.holding_library_id = commonLibrary.library_id; |
194 |
} |
195 |
|
196 |
await apiPost({ |
197 |
endpoint: `/api/v1/biblios/${biblio_id}/items`, |
198 |
body: item, |
199 |
baseUrl, |
200 |
authHeader, |
201 |
}).then(i => createdItems.push(i)); |
202 |
} |
203 |
return { biblio, items: createdItems, libraries, item_type }; |
204 |
}; |
205 |
|
206 |
/** |
207 |
* Creates a hold request for a bibliographic record or item. |
208 |
* |
209 |
* @async |
210 |
* @function insertSampleHold |
211 |
* @param {Object} params - Configuration parameters |
212 |
* @param {Object} [params.item] - Item to place hold on (optional if biblio provided) |
213 |
* @param {Object} [params.biblio] - Biblio to place hold on (optional if item provided) |
214 |
* @param {string} [params.library_id] - Library ID for pickup location (defaults to item's home library) |
215 |
* @param {string} params.baseUrl - Base URL for API calls |
216 |
* @param {string} params.authHeader - Authorization header for API calls |
217 |
* @returns {Promise<Object>} Created hold with associated patron and patron_category |
218 |
* @returns {Object} returns.hold - The created hold record |
219 |
* @returns {Object} returns.patron - The patron who placed the hold |
220 |
* @returns {Object} returns.patron_category - The patron's category |
221 |
* @throws {Error} When neither library_id nor item is provided |
222 |
* @example |
223 |
* // Create a hold on a specific item |
224 |
* const holdResult = await insertSampleHold({ |
225 |
* item: { item_id: 123, home_library_id: 'CPL' }, |
226 |
* baseUrl: 'http://localhost:8081', |
227 |
* authHeader: 'Basic dGVzdDp0ZXN0' |
228 |
* }); |
229 |
* |
230 |
* @example |
231 |
* // Create a biblio-level hold |
232 |
* const holdResult = await insertSampleHold({ |
233 |
* biblio: { biblio_id: 456 }, |
234 |
* library_id: 'CPL', |
235 |
* baseUrl: 'http://localhost:8081', |
236 |
* authHeader: 'Basic dGVzdDp0ZXN0' |
237 |
* }); |
238 |
*/ |
239 |
const insertSampleHold = async ({ |
240 |
item, |
241 |
biblio, |
242 |
library_id, |
243 |
baseUrl, |
244 |
authHeader, |
245 |
}) => { |
246 |
library_id ||= item?.home_library_id; |
247 |
|
248 |
if (!library_id) { |
249 |
throw new Error( |
250 |
"Could not generate sample hold without library_id or item" |
251 |
); |
252 |
} |
253 |
|
254 |
const { patron, patron_category } = await insertSamplePatron({ |
255 |
library: { library_id }, |
256 |
baseUrl, |
257 |
authHeader, |
258 |
}); |
259 |
|
260 |
const generatedHold = buildSampleObject({ |
261 |
object: "hold", |
262 |
values: { |
263 |
patron_id: patron.patron_id, |
264 |
biblio_id: item?.biblio_id || biblio.biblio_id, |
265 |
pickup_library_id: library_id, |
266 |
item_id: item?.item_id || null, |
267 |
}, |
268 |
}); |
269 |
const hold = await insertObject({ |
270 |
type: "hold", |
271 |
object: generatedHold, |
272 |
baseUrl, |
273 |
authHeader, |
274 |
}); |
275 |
return { hold, patron, patron_category }; |
276 |
}; |
277 |
|
278 |
/** |
279 |
* Creates a checkout record with associated biblio, item, and optional patron. |
280 |
* |
281 |
* @async |
282 |
* @function insertSampleCheckout |
283 |
* @param {Object} params - Configuration parameters |
284 |
* @param {Object} [params.patron] - Existing patron to check out to (creates new if not provided) |
285 |
* @param {string} params.baseUrl - Base URL for API calls |
286 |
* @param {string} params.authHeader - Authorization header for API calls |
287 |
* @returns {Promise<Object>} Created checkout with all associated records |
288 |
* @returns {Object} returns.biblio - The bibliographic record |
289 |
* @returns {Array<Object>} returns.items - Array of item records |
290 |
* @returns {Array<Object>} returns.libraries - Array of library records |
291 |
* @returns {Object} returns.item_type - The item type record |
292 |
* @returns {Object} returns.checkout - The checkout record |
293 |
* @returns {Object} [returns.patron] - The patron record (if generated) |
294 |
* @returns {Object} [returns.patron_category] - The patron category (if generated) |
295 |
* @example |
296 |
* // Create a checkout with a new patron |
297 |
* const checkoutResult = await insertSampleCheckout({ |
298 |
* baseUrl: 'http://localhost:8081', |
299 |
* authHeader: 'Basic dGVzdDp0ZXN0' |
300 |
* }); |
301 |
* |
302 |
* @example |
303 |
* // Create a checkout for an existing patron |
304 |
* const checkoutResult = await insertSampleCheckout({ |
305 |
* patron: { patron_id: 123 }, |
306 |
* baseUrl: 'http://localhost:8081', |
307 |
* authHeader: 'Basic dGVzdDp0ZXN0' |
308 |
* }); |
309 |
*/ |
310 |
const insertSampleCheckout = async ({ patron, baseUrl, authHeader }) => { |
311 |
const { biblio, items, libraries, item_type } = await insertSampleBiblio({ |
312 |
item_count: 1, |
313 |
baseUrl, |
314 |
authHeader, |
315 |
}); |
316 |
|
317 |
let generatedPatron; |
318 |
let patronCategory; |
319 |
if (!patron) { |
320 |
generatedPatron = true; |
321 |
const patron_objects = await insertSamplePatron({ |
322 |
library: { library_id: libraries[0].library_id }, |
323 |
baseUrl, |
324 |
authHeader, |
325 |
}); |
326 |
generatedCategory = patron_objects.category; |
327 |
patron = patron_objects.patron; |
328 |
} |
329 |
|
330 |
const generatedCheckout = buildSampleObject({ |
331 |
object: "checkout", |
332 |
values: { |
333 |
patron_id: patron.patron_id, |
334 |
item_id: items[0].item_id, |
335 |
}, |
336 |
}); |
337 |
delete generatedCheckout.external_id; |
338 |
const checkout = await insertObject({ |
339 |
type: "checkout", |
340 |
object: generatedCheckout, |
341 |
baseUrl, |
342 |
authHeader, |
343 |
}); |
344 |
return { |
345 |
biblio, |
346 |
items, |
347 |
libraries, |
348 |
item_type, |
349 |
checkout, |
350 |
...(generatedPatron |
351 |
? { |
352 |
patron, |
353 |
patron_category: generatedCategory, |
354 |
} |
355 |
: {}), |
356 |
}; |
357 |
}; |
358 |
|
359 |
/** |
360 |
* Creates a patron record with associated library and category. |
361 |
* |
362 |
* @async |
363 |
* @function insertSamplePatron |
364 |
* @param {Object} params - Configuration parameters |
365 |
* @param {Object} [params.library] - Library to assign patron to (creates new if not provided) |
366 |
* @param {Object} [params.patron_category] - Patron category to assign (creates new if not provided) |
367 |
* @param {string} params.baseUrl - Base URL for API calls |
368 |
* @param {string} params.authHeader - Authorization header for API calls |
369 |
* @returns {Promise<Object>} Created patron with associated records |
370 |
* @returns {Object} returns.patron - The created patron record |
371 |
* @returns {Object} [returns.library] - The library record (if generated) |
372 |
* @returns {Object} [returns.patron_category] - The patron category record (if generated) |
373 |
* @example |
374 |
* // Create a patron with new library and category |
375 |
* const patronResult = await insertSamplePatron({ |
376 |
* baseUrl: 'http://localhost:8081', |
377 |
* authHeader: 'Basic dGVzdDp0ZXN0' |
378 |
* }); |
379 |
* |
380 |
* @example |
381 |
* // Create a patron for an existing library |
382 |
* const patronResult = await insertSamplePatron({ |
383 |
* library: { library_id: 'CPL' }, |
384 |
* baseUrl: 'http://localhost:8081', |
385 |
* authHeader: 'Basic dGVzdDp0ZXN0' |
386 |
* }); |
387 |
*/ |
388 |
const insertSamplePatron = async ({ |
389 |
library, |
390 |
patron_category, |
391 |
baseUrl, |
392 |
authHeader, |
393 |
}) => { |
394 |
let generatedLibrary; |
395 |
let generatedCategory; |
396 |
if (!library) { |
397 |
generatedLibrary = await buildSampleObject({ object: "library" }); |
398 |
library = await insertLibrary({ |
399 |
library: generatedLibrary, |
400 |
baseUrl, |
401 |
authHeader, |
402 |
}); |
403 |
} |
404 |
if (!patron_category) { |
405 |
generatedCategory = await buildSampleObject({ |
406 |
object: "patron_category", |
407 |
}); |
408 |
query({ |
409 |
sql: "INSERT INTO categories(categorycode, description) VALUES (?, ?)", |
410 |
values: [ |
411 |
generatedCategory.patron_category_id, |
412 |
`description for ${generatedCategory.patron_category_id}`, |
413 |
], |
414 |
}); |
415 |
// FIXME We need /patron_categories/:patron_category_id |
416 |
await apiGet({ |
417 |
endpoint: `/api/v1/patron_categories?q={"me.patron_category_id":"${generatedCategory.patron_category_id}"}`, |
418 |
baseUrl, |
419 |
authHeader, |
420 |
}).then(categories => (patron_category = categories[0])); |
421 |
} |
422 |
|
423 |
let generatedPatron = await buildSampleObject({ |
424 |
object: "patron", |
425 |
values: { |
426 |
library_id: library.library_id, |
427 |
category_id: patron_category.patron_category_id, |
428 |
incorrect_address: null, |
429 |
patron_card_lost: null, |
430 |
}, |
431 |
}); |
432 |
|
433 |
let { |
434 |
patron_id, |
435 |
_strings, |
436 |
anonymized, |
437 |
restricted, |
438 |
expired, |
439 |
extended_attributes, |
440 |
checkouts_count, |
441 |
overdues_count, |
442 |
account_balance, |
443 |
lang, |
444 |
login_attempts, |
445 |
sms_provider_id, |
446 |
...patron |
447 |
} = generatedPatron; |
448 |
delete patron.library; |
449 |
|
450 |
patron = await apiPost({ |
451 |
endpoint: `/api/v1/patrons`, |
452 |
body: patron, |
453 |
baseUrl, |
454 |
authHeader, |
455 |
}); |
456 |
|
457 |
return { |
458 |
patron, |
459 |
...(generatedLibrary ? { library } : {}), |
460 |
...(generatedCategory ? { patron_category } : {}), |
461 |
}; |
462 |
}; |
463 |
|
464 |
/** |
465 |
* Deletes test objects from the database in the correct order to respect foreign key constraints. |
466 |
* |
467 |
* @async |
468 |
* @function deleteSampleObjects |
469 |
* @param {Object|Array<Object>} allObjects - Object(s) to delete, can be single object or array |
470 |
* @returns {Promise<boolean>} True if deletion was successful |
471 |
* @description This function handles cleanup of test data by: |
472 |
* - Accepting single objects or arrays of objects |
473 |
* - Grouping objects by type (holds, checkouts, patrons, items, etc.) |
474 |
* - Deleting in dependency order to avoid foreign key violations |
475 |
* - Supporting all major Koha object types |
476 |
* @example |
477 |
* // Delete a single test result |
478 |
* await deleteSampleObjects(checkoutResult); |
479 |
* |
480 |
* @example |
481 |
* // Delete multiple test results |
482 |
* await deleteSampleObjects([biblioResult, holdResult, checkoutResult]); |
483 |
* |
484 |
* @example |
485 |
* // Delete after creating test data |
486 |
* const biblio = await insertSampleBiblio({ item_count: 2, baseUrl, authHeader }); |
487 |
* const hold = await insertSampleHold({ item: biblio.items[0], baseUrl, authHeader }); |
488 |
* // ... run tests ... |
489 |
* await deleteSampleObjects([biblio, hold]); |
490 |
*/ |
491 |
const deleteSampleObjects = async allObjects => { |
492 |
if (!Array.isArray(allObjects)) { |
493 |
allObjects = [allObjects]; |
494 |
} |
495 |
|
496 |
const pluralMap = { |
497 |
hold: "holds", |
498 |
checkout: "checkouts", |
499 |
old_checkout: "old_checkouts", |
500 |
basket: "baskets", |
501 |
vendor: "vendors", |
502 |
patron: "patrons", |
503 |
item: "items", |
504 |
biblio: "biblios", |
505 |
library: "libraries", |
506 |
item_type: "item_types", |
507 |
}; |
508 |
// Merge by type |
509 |
const mergedObjects = {}; |
510 |
for (const objects of allObjects) { |
511 |
for (const [type, value] of Object.entries(objects)) { |
512 |
let plural = pluralMap?.[type] || type; |
513 |
if (!mergedObjects[plural]) { |
514 |
mergedObjects[plural] = []; |
515 |
} |
516 |
|
517 |
if (Array.isArray(value)) { |
518 |
mergedObjects[plural].push(...value); |
519 |
} else { |
520 |
mergedObjects[plural].push(value); |
521 |
} |
522 |
} |
523 |
} |
524 |
|
525 |
const deletionOrder = [ |
526 |
"holds", |
527 |
"checkouts", |
528 |
"old_checkouts", |
529 |
"baskets", |
530 |
"vendors", |
531 |
"patrons", |
532 |
"items", |
533 |
"biblios", |
534 |
"libraries", |
535 |
"item_types", |
536 |
]; |
537 |
|
538 |
for (const type of deletionOrder) { |
539 |
if (!mergedObjects[type] || mergedObjects[type].length === 0) { |
540 |
continue; |
541 |
} |
542 |
|
543 |
const objects = mergedObjects[type]; |
544 |
let ids = []; |
545 |
switch (type) { |
546 |
case "biblios": |
547 |
ids = objects.map(i => i.biblio_id); |
548 |
await query({ |
549 |
sql: `DELETE FROM biblio WHERE biblionumber IN (${ids.map(() => "?").join(",")})`, |
550 |
values: ids, |
551 |
}); |
552 |
break; |
553 |
case "items": |
554 |
ids = objects.map(i => i.item_id); |
555 |
await query({ |
556 |
sql: `DELETE FROM items WHERE itemnumber IN (${ids.map(() => "?").join(",")})`, |
557 |
values: ids, |
558 |
}); |
559 |
break; |
560 |
case "libraries": |
561 |
ids = objects.map(i => i.library_id); |
562 |
await query({ |
563 |
sql: `DELETE FROM branches WHERE branchcode IN (${ids.map(() => "?").join(",")})`, |
564 |
values: ids, |
565 |
}); |
566 |
break; |
567 |
case "holds": |
568 |
ids = objects.map(i => i.hold_id); |
569 |
await query({ |
570 |
sql: `DELETE FROM reserves WHERE reserve_id IN (${ids.map(() => "?").join(",")})`, |
571 |
values: ids, |
572 |
}); |
573 |
break; |
574 |
case "checkouts": |
575 |
ids = objects.map(i => i.checkout_id); |
576 |
await query({ |
577 |
sql: `DELETE FROM issues WHERE issue_id IN (${ids.map(() => "?").join(",")})`, |
578 |
values: ids, |
579 |
}); |
580 |
break; |
581 |
case "old_checkouts": |
582 |
ids = objects.map(i => i.checkout_id); |
583 |
await query({ |
584 |
sql: `DELETE FROM old_issues WHERE issue_id IN (${ids.map(() => "?").join(",")})`, |
585 |
values: ids, |
586 |
}); |
587 |
break; |
588 |
case "item_types": |
589 |
ids = objects.map(i => i.item_type_id); |
590 |
await query({ |
591 |
sql: `DELETE FROM itemtypes WHERE itemtype IN (${ids.map(() => "?").join(",")})`, |
592 |
values: ids, |
593 |
}); |
594 |
break; |
595 |
case "patrons": |
596 |
ids = objects.map(i => i.patron_id); |
597 |
await query({ |
598 |
sql: `DELETE FROM borrowers WHERE borrowernumber IN (${ids.map(() => "?").join(",")})`, |
599 |
values: ids, |
600 |
}); |
601 |
break; |
602 |
case "baskets": |
603 |
ids = objects.map(i => i.basket_id); |
604 |
await query({ |
605 |
sql: `DELETE FROM aqbasket WHERE basketno IN (${ids.map(() => "?").join(",")})`, |
606 |
values: ids, |
607 |
}); |
608 |
break; |
609 |
case "vendors": |
610 |
ids = objects.map(i => i.id); |
611 |
await query({ |
612 |
sql: `DELETE FROM aqbooksellers WHERE id IN (${ids.map(() => "?").join(",")})`, |
613 |
values: ids, |
614 |
}); |
615 |
break; |
616 |
default: |
617 |
throw Error( |
618 |
`Not implemented yet: cannot deleted object '${type}'` |
619 |
); |
620 |
} |
621 |
} |
622 |
return true; |
623 |
}; |
624 |
|
625 |
/** |
626 |
* Creates a library record via API, filtering out unsupported fields. |
627 |
* |
628 |
* @async |
629 |
* @function insertLibrary |
630 |
* @param {Object} params - Configuration parameters |
631 |
* @param {Object} params.library - Library object to insert |
632 |
* @param {string} params.baseUrl - Base URL for API calls |
633 |
* @param {string} params.authHeader - Authorization header for API calls |
634 |
* @returns {Promise<Object>} Created library record |
635 |
* @private |
636 |
* @description This is a helper function that removes fields not supported by the API |
637 |
* before creating the library record. |
638 |
*/ |
639 |
const insertLibrary = async ({ library, baseUrl, authHeader }) => { |
640 |
const { |
641 |
pickup_items, |
642 |
smtp_server, |
643 |
cash_registers, |
644 |
desks, |
645 |
library_hours, |
646 |
needs_override, |
647 |
...new_library |
648 |
} = library; |
649 |
return apiPost({ |
650 |
endpoint: "/api/v1/libraries", |
651 |
body: new_library, |
652 |
baseUrl, |
653 |
authHeader, |
654 |
}); |
655 |
}; |
656 |
|
657 |
/** |
658 |
* Generic function to insert various types of Koha objects. |
659 |
* |
660 |
* @async |
661 |
* @function insertObject |
662 |
* @param {Object} params - Configuration parameters |
663 |
* @param {string} params.type - Type of object to insert ('library', 'item_type', 'hold', 'checkout', 'vendor', 'basket') |
664 |
* @param {Object} params.object - Object data to insert |
665 |
* @param {string} params.baseUrl - Base URL for API calls |
666 |
* @param {string} params.authHeader - Authorization header for API calls |
667 |
* @returns {Promise<Object|boolean>} Created object or true if successful |
668 |
* @throws {Error} When object type is not supported |
669 |
* @private |
670 |
* @description This is a generic helper function that handles the specifics of creating |
671 |
* different types of Koha objects. Each object type may require different field filtering, |
672 |
* API endpoints, or database operations. |
673 |
* |
674 |
* Supported object types: |
675 |
* - library: Creates library via API |
676 |
* - item_type: Creates item type via database query |
677 |
* - hold: Creates hold via API |
678 |
* - checkout: Creates checkout via API with confirmation token support |
679 |
* - vendor: Creates vendor via API |
680 |
* - basket: Creates basket via database query |
681 |
*/ |
682 |
const insertObject = async ({ type, object, baseUrl, authHeader }) => { |
683 |
if (type == "library") { |
684 |
const keysToKeep = ["library_id", "name"]; |
685 |
const library = Object.fromEntries( |
686 |
Object.entries(object).filter(([key]) => keysToKeep.includes(key)) |
687 |
); |
688 |
return apiPost({ |
689 |
endpoint: "/api/v1/libraries", |
690 |
body: library, |
691 |
baseUrl, |
692 |
authHeader, |
693 |
}); |
694 |
} else if (type == "item_type") { |
695 |
const keysToKeep = ["item_type_id", "description"]; |
696 |
const item_type = Object.fromEntries( |
697 |
Object.entries(object).filter(([key]) => keysToKeep.includes(key)) |
698 |
); |
699 |
return query({ |
700 |
sql: "INSERT INTO itemtypes(itemtype, description) VALUES (?, ?)", |
701 |
values: [item_type.item_type_id, item_type.description], |
702 |
}) |
703 |
.then(result => { |
704 |
// FIXME We need /item_types/:item_type_id |
705 |
return apiGet({ |
706 |
endpoint: `/api/v1/item_types?q={"item_type_id":"${item_type.item_type_id}"}`, |
707 |
baseUrl, |
708 |
authHeader, |
709 |
}); |
710 |
}) |
711 |
.then(item_types => item_types[0]); |
712 |
} else if (type == "hold") { |
713 |
const { |
714 |
hold_id, |
715 |
deleted_biblio_id, |
716 |
item_group_id, |
717 |
desk_id, |
718 |
cancellation_date, |
719 |
cancellation_reason, |
720 |
notes, |
721 |
priority, |
722 |
status, |
723 |
timestamp, |
724 |
waiting_date, |
725 |
expiration_date, |
726 |
lowest_priority, |
727 |
suspended, |
728 |
suspended_until, |
729 |
non_priority, |
730 |
item_type, |
731 |
item_level, |
732 |
cancellation_requested, |
733 |
biblio, |
734 |
deleted_biblio, |
735 |
item, |
736 |
pickup_library, |
737 |
hold_date, |
738 |
...hold |
739 |
} = object; |
740 |
|
741 |
return apiPost({ |
742 |
endpoint: `/api/v1/holds`, |
743 |
body: hold, |
744 |
baseUrl, |
745 |
authHeader, |
746 |
}); |
747 |
} else if (type == "checkout") { |
748 |
const { issuer, patron, ...checkout } = object; |
749 |
|
750 |
let endpoint = "/api/v1/checkouts"; |
751 |
// Force the checkout - we might need a parameter to control this behaviour later |
752 |
await apiGet({ |
753 |
endpoint: `/api/v1/checkouts/availability?item_id=${object.item_id}&patron_id=${object.patron_id}`, |
754 |
baseUrl, |
755 |
authHeader, |
756 |
}).then(result => { |
757 |
if (result.confirmation_token) { |
758 |
endpoint += `?confirmation=${result.confirmation_token}`; |
759 |
} |
760 |
}); |
761 |
|
762 |
return apiPost({ |
763 |
endpoint, |
764 |
body: checkout, |
765 |
baseUrl, |
766 |
authHeader, |
767 |
}); |
768 |
} else if (type == "vendor") { |
769 |
const { |
770 |
id, |
771 |
baskets_count, |
772 |
invoices_count, |
773 |
subscriptions_count, |
774 |
external_id, |
775 |
aliases, |
776 |
baskets, |
777 |
contacts, |
778 |
contracts, |
779 |
interfaces, |
780 |
invoices, |
781 |
...vendor |
782 |
} = object; |
783 |
|
784 |
let endpoint = "/api/v1/acquisitions/vendors"; |
785 |
|
786 |
return apiPost({ |
787 |
endpoint, |
788 |
body: vendor, |
789 |
baseUrl, |
790 |
authHeader, |
791 |
}); |
792 |
} else if (type == "basket") { |
793 |
const keysToKeep = ["name", "vendor_id", "close_date"]; |
794 |
const basket = Object.fromEntries( |
795 |
Object.entries(object).filter(([key]) => keysToKeep.includes(key)) |
796 |
); |
797 |
return query({ |
798 |
sql: "INSERT INTO aqbasket(basketname, booksellerid, closedate) VALUES (?, ?, ?)", |
799 |
values: [basket.name, basket.vendor_id, basket.close_date], |
800 |
}) |
801 |
.then(result => { |
802 |
const basket_id = result.insertId; |
803 |
// FIXME We need /acquisitions/baskets/:basket_id |
804 |
return apiGet({ |
805 |
endpoint: `/api/v1/acquisitions/baskets?q={"basket_id":"${basket_id}"}`, |
806 |
baseUrl, |
807 |
authHeader, |
808 |
}); |
809 |
}) |
810 |
.then(baskets => baskets[0]); |
811 |
} else { |
812 |
throw Error(`Unsupported object type '${type}' to insert`); |
813 |
} |
814 |
|
815 |
return true; |
816 |
}; |
817 |
|
818 |
module.exports = { |
819 |
insertSampleBiblio, |
820 |
insertSampleHold, |
821 |
insertSampleCheckout, |
822 |
insertSamplePatron, |
823 |
insertObject, |
824 |
deleteSampleObjects, |
825 |
}; |