diff --git a/src/controllers/collectionController.js b/src/controllers/collectionController.js new file mode 100644 index 0000000..8636273 --- /dev/null +++ b/src/controllers/collectionController.js @@ -0,0 +1,38 @@ +const { setAmountOwned } = require('../models/collectionModel'); + +async function updateAmountOwned(req, res) { + try { + const { card_id, set_id, rarity_id, amount_owned } = req.body; + + if ( + card_id == null || + set_id == null || + rarity_id == null || + amount_owned == null + ) { + return res.status(400).json({ error: 'Missing required fields' }); + } + + if (amount_owned < 0) { + return res.status(400).json({ error: 'Amount cannot be negative' }); + } + + const result = await setAmountOwned({ + card_id, + set_id, + rarity_id, + amount_owned + }); + + res.json({ + message: 'Amount updated', + ...result + }); + + } catch (err) { + console.error('Error updating amount:', err); + res.status(500).json({ error: 'Failed to update amount owned' }); + } +} + +module.exports = { updateAmountOwned }; \ No newline at end of file diff --git a/src/controllers/importController.js b/src/controllers/importController.js index c58dcd4..de2b718 100644 --- a/src/controllers/importController.js +++ b/src/controllers/importController.js @@ -1,172 +1,164 @@ -const { - upsertCard -} = require('../models/cardModel'); -const { - upsertRarity, - getRarityId -} = require('../models/rarityModel'); +const {upsertCard} = require('../models/cardModel'); +const {upsertRarity,getRarityId} = require('../models/rarityModel'); +const { upsertSet } = require('../models/setModel'); const { insertCardSetRarity } = require('../models/cardSetRarityModel'); const { insertCardImage } = require('../models/cardImageModel'); -const { fetchAllCards } = require('../services/ygoproService'); +const { fetchAllCards, fetchAllSets } = require('../services/ygoproService'); const db = require('../config/db'); const BATCH_SIZE = 50; +async function importSetsInternal() { + const sets = await fetchAllSets(); + let added = 0; + + for (let i = 0; i < sets.length; i += BATCH_SIZE) { + const batch = sets.slice(i, i + BATCH_SIZE); + await Promise.all(batch.map(async (set) => { + await upsertSet(set); + added++; + })); + } + + return { + added, + total: sets.length + }; +} + +// Express handler for /sets endpoint async function importSets(req, res) { try { - const sets = await fetchAllSets(); - let added = 0; - - // Parallelized batch insert - for (let i = 0; i < sets.length; i += BATCH_SIZE) { - const batch = sets.slice(i, i + BATCH_SIZE); - await Promise.all(batch.map(async (set) => { - await upsertSet(set); - added++; - })); - } - - res.json({ - added, - total: sets.length - }); + const result = await importSetsInternal(); + res.json(result); } catch (err) { console.error('Error importing sets:', err); res.status(500).json({ error: 'Failed to import sets' }); } } -async function importCards(req, res) { +async function importCardsInternal() { const startTime = Date.now(); + const cards = await fetchAllCards(); + const totalCards = cards.length; - try { - const cards = await fetchAllCards(); - const totalCards = cards.length; + let addedCards = 0, addedImages = 0, addedRarities = 0; - let addedCards = 0, addedImages = 0, addedRarities = 0; + const rarityCache = {}; + const setCache = {}; - // Cache sets and rarities to reduce repeated DB lookups - const rarityCache = {}; - const setCache = {}; + const [setsRows] = await db.execute('SELECT id, set_name FROM sets'); + for (const row of setsRows) { + setCache[row.set_name] = row.id; + } - // Preload sets into cache - const [setsRows] = await db.execute('SELECT id, set_code FROM sets'); - for (const row of setsRows) { - setCache[row.set_code] = row.id; - } + const [rarityRows] = await db.execute('SELECT id, rarity_name FROM rarities'); + for (const row of rarityRows) { + rarityCache[row.rarity_name] = row.id; + } - // Preload rarities into cache - const [rarityRows] = await db.execute('SELECT id, rarity_name FROM rarities'); - for (const row of rarityRows) { - rarityCache[row.rarity_name] = row.id; - } + for (let i = 0; i < totalCards; i += BATCH_SIZE) { + const batch = cards.slice(i, i + BATCH_SIZE); + const conn = await db.getConnection(); + try { + await conn.beginTransaction(); - // Process cards in batches - for (let i = 0; i < totalCards; i += BATCH_SIZE) { - const batch = cards.slice(i, i + BATCH_SIZE); + await Promise.all(batch.map(async (card) => { + await conn.execute(` + INSERT INTO cards + (id, name, card_type, frame_type, level, race, attribute, link_val, tcg_date, ocg_date) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + name = VALUES(name), + card_type = VALUES(card_type), + frame_type = VALUES(frame_type), + level = VALUES(level), + race = VALUES(race), + attribute = VALUES(attribute), + link_val = VALUES(link_val), + tcg_date = VALUES(tcg_date), + ocg_date = VALUES(ocg_date) + `, [ + card.id, + card.name, + card.type, + card.frameType, + card.level || null, + card.race || null, + card.attribute || null, + card.linkval || null, + card.tcg_date || null, + card.ocg_date || null + ]); + addedCards++; - // Wrap batch in a transaction - const conn = await db.getConnection(); - try { - await conn.beginTransaction(); - - await Promise.all(batch.map(async (card) => { - // Upsert card - await conn.execute(` - INSERT INTO cards - (id, name, card_type, frame_type, level, race, attribute, link_val, tcg_date, ocg_date) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - name = VALUES(name), - card_type = VALUES(card_type), - frame_type = VALUES(frame_type), - level = VALUES(level), - race = VALUES(race), - attribute = VALUES(attribute), - link_val = VALUES(link_val), - tcg_date = VALUES(tcg_date), - ocg_date = VALUES(ocg_date) - `, [ - card.id, - card.name, - card.type, - card.frameType, - card.level || null, - card.race || null, - card.attribute || null, - card.linkval || null, - card.tcg_date || null, - card.ocg_date || null - ]); - addedCards++; - - // Process rarities and card_sets_rarity - if (Array.isArray(card.card_sets)) { - for (const set of card.card_sets) { - // Upsert rarity if not cached - if (!rarityCache[set.set_rarity]) { - await conn.execute(` - INSERT INTO rarities (rarity_name, rarity_code) - VALUES (?, ?) - ON DUPLICATE KEY UPDATE rarity_code = VALUES(rarity_code) - `, [set.set_rarity, set.set_rarity_code]); - const [rows] = await conn.execute('SELECT id FROM rarities WHERE rarity_name = ? LIMIT 1', [set.set_rarity]); - rarityCache[set.set_rarity] = rows[0].id; - addedRarities++; - } - const rarity_id = rarityCache[set.set_rarity]; - - // Get set_id from cache - const set_id = setCache[set.set_code]; - if (set_id) { - await conn.execute(` - INSERT INTO card_sets_rarity (card_id, set_id, rarity_id, card_set_code) - VALUES (?, ?, ?, ?) - ON DUPLICATE KEY UPDATE card_set_code = VALUES(card_set_code) - `, [card.id, set_id, rarity_id, set.set_code]); - } - } - } - - // Process card images - if (Array.isArray(card.card_images)) { - for (const img of card.card_images) { + if (Array.isArray(card.card_sets)) { + for (const set of card.card_sets) { + if (!rarityCache[set.set_rarity]) { await conn.execute(` - INSERT INTO card_images (card_id, image_url) + INSERT INTO rarities (rarity_name, rarity_code) VALUES (?, ?) - `, [card.id, img.image_url]); - addedImages++; + ON DUPLICATE KEY UPDATE rarity_code = VALUES(rarity_code) + `, [set.set_rarity, set.set_rarity_code]); + const [rows] = await conn.execute('SELECT id FROM rarities WHERE rarity_name = ? LIMIT 1', [set.set_rarity]); + rarityCache[set.set_rarity] = rows[0].id; + addedRarities++; + } + const rarity_id = rarityCache[set.set_rarity]; + const set_id = setCache[set.set_name]; + if (set_id) { + await conn.execute(` + INSERT INTO card_sets_rarity (card_id, set_id, rarity_id, card_set_code) + VALUES (?, ?, ?, ?) + ON DUPLICATE KEY UPDATE card_set_code = VALUES(card_set_code) + `, [card.id, set_id, rarity_id, set.set_code]); } } - })); + } - await conn.commit(); - } catch (err) { - await conn.rollback(); - throw err; - } finally { - conn.release(); - } + if (Array.isArray(card.card_images)) { + for (const img of card.card_images) { + await conn.execute(` + INSERT INTO card_images (card_id, image_url) + VALUES (?, ?) + `, [card.id, img.image_url]); + addedImages++; + } + } + })); - // Release batch memory - batch.length = 0; + await conn.commit(); + } catch (err) { + await conn.rollback(); + throw err; + } finally { + conn.release(); } - const endTime = Date.now(); - const durationSeconds = ((endTime - startTime) / 1000).toFixed(2); + batch.length = 0; + } - res.json({ - total_cards: totalCards, - cards_added: addedCards, - images_added: addedImages, - rarities_added: addedRarities, - duration_seconds: parseFloat(durationSeconds) - }); + const durationSeconds = ((Date.now() - startTime) / 1000).toFixed(2); + return { + total_cards: totalCards, + cards_added: addedCards, + images_added: addedImages, + rarities_added: addedRarities, + duration_seconds: parseFloat(durationSeconds) + }; +} + +// Express handler for /cards +async function importCards(req, res) { + try { + const result = await importCardsInternal(); + res.json(result); } catch (err) { console.error('Error importing cards:', err); res.status(500).json({ error: 'Failed to import cards' }); } } -module.exports = { importCards, importSets }; \ No newline at end of file + +module.exports = { importCards, importCardsInternal, importSets, importSetsInternal }; \ No newline at end of file diff --git a/src/controllers/newVersionController.js b/src/controllers/newVersionController.js new file mode 100644 index 0000000..87a8c0d --- /dev/null +++ b/src/controllers/newVersionController.js @@ -0,0 +1,42 @@ +const { fetchDatabaseVersion } = require('../services/ygoproService'); +const { getLocalDBVersion, setLocalDBVersion } = require('../models/dbVersionModel'); +const { importSetsInternal } = require('./importController'); +const { importCardsInternal } = require('./importController'); + +async function importFullDatabase(req, res) { + const startTime = Date.now(); + + try { + const remoteVersion = await fetchDatabaseVersion(); + const localVersion = await getLocalDBVersion(); + + if (localVersion && localVersion.database_version === remoteVersion.database_version) { + return res.json({ + message: 'Database is already up to date.', + version: remoteVersion.database_version + }); + } + + const setsStats = await importSetsInternal(); + + const cardsStats = await importCardsInternal(); + + await setLocalDBVersion(remoteVersion.database_version, remoteVersion.last_update); + + const totalDuration = ((Date.now() - startTime) / 1000).toFixed(2); + + res.json({ + message: 'Database import completed successfully.', + version: remoteVersion.database_version, + sets: setsStats, + cards: cardsStats, + duration_seconds: parseFloat(totalDuration) + }); + + } catch (err) { + console.error('Error in full database import:', err); + res.status(500).json({ error: 'Failed to import database' }); + } +} + +module.exports = { importFullDatabase }; \ No newline at end of file diff --git a/src/index.js b/src/index.js index 61d73ac..05c5c4c 100644 --- a/src/index.js +++ b/src/index.js @@ -10,4 +10,7 @@ app.use('/import', importRoutes); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); -}); \ No newline at end of file +}); + +const collectionRoutes = require('./routes/collectionRoutes'); +app.use('/collection', collectionRoutes); \ No newline at end of file diff --git a/src/models/collectionModel.js b/src/models/collectionModel.js new file mode 100644 index 0000000..971018e --- /dev/null +++ b/src/models/collectionModel.js @@ -0,0 +1,14 @@ +const db = require('../config/db'); + +async function setAmountOwned({ card_id, set_id, rarity_id, amount_owned }) { + await db.execute(` + INSERT INTO card_sets_rarity (card_id, set_id, rarity_id, amount_owned) + VALUES (?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + amount_owned = VALUES(amount_owned) + `, [card_id, set_id, rarity_id, amount_owned]); + + return { card_id, set_id, rarity_id, amount_owned }; +} + +module.exports = { setAmountOwned }; \ No newline at end of file diff --git a/src/models/dbVersionModel.js b/src/models/dbVersionModel.js new file mode 100644 index 0000000..9c328b7 --- /dev/null +++ b/src/models/dbVersionModel.js @@ -0,0 +1,17 @@ +const db = require('../config/db'); + +async function getLocalDBVersion() { + const [rows] = await db.execute( + 'SELECT database_version, last_update FROM db_version ORDER BY last_update DESC LIMIT 1' + ); + return rows.length ? rows[0] : null; +} + +async function setLocalDBVersion(version, date) { + await db.execute( + 'INSERT INTO db_version (database_version, last_update) VALUES (?, ?)', + [version ?? null, date ?? null] + ); +} + +module.exports = { getLocalDBVersion, setLocalDBVersion }; \ No newline at end of file diff --git a/src/routes/collectionRoutes.js b/src/routes/collectionRoutes.js new file mode 100644 index 0000000..76623f9 --- /dev/null +++ b/src/routes/collectionRoutes.js @@ -0,0 +1,7 @@ +const express = require('express'); +const router = express.Router(); +const { updateAmountOwned } = require('../controllers/collectionController'); + +router.put('/amount', updateAmountOwned); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/importRoutes.js b/src/routes/importRoutes.js index d3bff93..602d341 100644 --- a/src/routes/importRoutes.js +++ b/src/routes/importRoutes.js @@ -1,8 +1,10 @@ const express = require('express'); const router = express.Router(); const { importSets, importCards} = require('../controllers/importController'); +const { importFullDatabase } = require('../controllers/newVersionController'); router.post('/sets', importSets); router.post('/cards', importCards); +router.post('/full-import', importFullDatabase); module.exports = router; \ No newline at end of file diff --git a/src/services/ygoproService.js b/src/services/ygoproService.js index 4e39aaa..8d98a21 100644 --- a/src/services/ygoproService.js +++ b/src/services/ygoproService.js @@ -22,12 +22,29 @@ async function fetchAllSets() { async function fetchAllCards() { try { const response = await axios.get(`${API_BASE}/cardinfo.php?misc=yes`); - return response.data.data; // array of card objects + return response.data.data; } catch (err) { console.error('Error fetching cards from YGOPRODeck:', err.message); throw err; } } +async function fetchDatabaseVersion() { + const response = await axios.get(`${API_BASE}/checkDBVer.php`); + const data = response.data; -module.exports = { fetchAllCards, fetchAllSets }; \ No newline at end of file + // Handle array response from API + const versionInfo = Array.isArray(data) ? data[0] : data; + + if (!versionInfo || !versionInfo.database_version || !versionInfo.last_update) { + throw new Error('Invalid database version response from YGOPRODeck API'); + } + + return { + database_version: versionInfo.database_version, + last_update: versionInfo.last_update + }; +} + + +module.exports = { fetchAllCards, fetchAllSets, fetchDatabaseVersion }; \ No newline at end of file