diff --git a/app/Controllers/ExportController.js b/app/Controllers/ExportController.js new file mode 100644 index 00000000..466f4bcf --- /dev/null +++ b/app/Controllers/ExportController.js @@ -0,0 +1,42 @@ +const { getAllGroupVocabulary, getAllLanguagePackageVocabulary } = require('../Services/ExportServiceProvider.js'); +const catchAsync = require('../utils/catchAsync'); +const { getVersion } = require('../Services/InfoServiceProvider'); + +const exportGroup = catchAsync(async (req, res) => { + // get userId from request + const userId = req.user.id; + + // get language package id from params + const { groupId } = req.params; + + const group = await getAllGroupVocabulary(userId, groupId); + const formatted = { + version: getVersion(), + type: 'vocascan/group', + ...group.toJSON(), + }; + res.send(formatted); +}); + +const exportLanguagePackage = catchAsync(async (req, res) => { + // get userId from request + const userId = req.user.id; + + // get language package id from params + const { languagePackageId } = req.params; + const queryStatus = (req.query.queryStatus || 'false') === 'true'; + + const languagePackage = await getAllLanguagePackageVocabulary({ userId, languagePackageId, queryStatus }); + const formatted = { + version: getVersion(), + type: 'vocascan/package', + ...languagePackage.toJSON(), + }; + + res.send(formatted); +}); + +module.exports = { + exportGroup, + exportLanguagePackage, +}; diff --git a/app/Controllers/ImportController.js b/app/Controllers/ImportController.js new file mode 100644 index 00000000..8a8c1367 --- /dev/null +++ b/app/Controllers/ImportController.js @@ -0,0 +1,32 @@ +const { storeGroupVocabulary, storeLanguagePackageVocabulary } = require('../Services/ImportServiceProvider.js'); +const catchAsync = require('../utils/catchAsync'); +const ApiError = require('../utils/ApiError.js'); +const httpStatus = require('http-status'); + +const importVocabs = catchAsync(async (req, res) => { + // get userId from request + const userId = req.user.id; + + const active = (req.query.active || 'true') === 'true'; + const activate = (req.query.activate || 'false') === 'true'; + + const { type } = req.body; + // use different types of import to separate the functions + if (!type) { + throw new ApiError(httpStatus.BAD_REQUEST, 'imported data has no Vocascan structure'); + } else if (type === 'vocascan/package') { + const queryStatus = (req.query.queryStatus || 'false') === 'true'; + await storeLanguagePackageVocabulary(req.body, userId, active, activate, queryStatus); + } else if (type === 'vocascan/group') { + const { languagePackageId } = req.query; + await storeGroupVocabulary(req.body, userId, languagePackageId, active, activate); + } else { + throw new ApiError(httpStatus.BAD_REQUEST, 'imported data type not recognized'); + } + + res.status(204).end(); +}); + +module.exports = { + importVocabs, +}; diff --git a/app/Controllers/VocabularyController.js b/app/Controllers/VocabularyController.js index 35b4aa44..51775852 100644 --- a/app/Controllers/VocabularyController.js +++ b/app/Controllers/VocabularyController.js @@ -11,13 +11,21 @@ const addVocabularyCard = catchAsync(async (req, res) => { // get userId from request const userId = req.user.id; const { name, description, active, translations } = req.body; - const { languagePackageId } = req.params; + const { languagePackageId, groupId } = req.params; // check if user wants to train vocabulary card directly const activate = req.query.activate === 'true'; // create vocabulary card - const vocabularyCard = await createVocabularyCard(req.params, name, description, userId, active, activate); + const vocabularyCard = await createVocabularyCard({ + languagePackageId, + groupId, + name, + description, + userId, + active, + activate, + }); // parse vocabulary card id from response and create translations await createTranslations(translations, userId, languagePackageId, vocabularyCard.id); diff --git a/app/Services/DrawerServiceProvider.js b/app/Services/DrawerServiceProvider.js index 1a85b1ee..a4b08773 100644 --- a/app/Services/DrawerServiceProvider.js +++ b/app/Services/DrawerServiceProvider.js @@ -12,12 +12,18 @@ async function createDrawer(languagePackageId, stage, queryInterval, userId) { return drawer; } -async function createDrawers(drawers, languagePackageId, userId) { - await Promise.all( - drawers.map(async (drawer) => { - await createDrawer(languagePackageId, drawer.stage, drawer.queryInterval, userId); - }) +async function createDrawers(drawers, languagePackageId, userId, transaction) { + const createdDrawers = await Drawer.bulkCreate( + drawers.map((drawer) => ({ + userId, + languagePackageId, + stage: drawer.stage, + queryInterval: drawer.queryInterval, + })), + { transaction } ); + + return createdDrawers; } module.exports = { diff --git a/app/Services/ExportServiceProvider.js b/app/Services/ExportServiceProvider.js new file mode 100644 index 00000000..d2455564 --- /dev/null +++ b/app/Services/ExportServiceProvider.js @@ -0,0 +1,85 @@ +const { VocabularyCard, Translation, Group, LanguagePackage, Drawer } = require('../../database'); + +// get every vocabulary of a group +async function getAllGroupVocabulary(userId, groupId) { + const group = await Group.findOne({ + attributes: ['name', 'description'], + include: [ + { + model: VocabularyCard, + attributes: ['name', 'description'], + include: [ + { + model: Translation, + attributes: ['name'], + }, + ], + }, + ], + where: { + id: groupId, + userId, + }, + }); + return group; +} + +async function getAllLanguagePackageVocabulary({ userId, languagePackageId, queryStatus }) { + // Get user with email from database + const languagePackage = await LanguagePackage.findOne({ + // if groups is true, return groups to every language package + include: queryStatus + ? [ + { + model: Group, + attributes: ['name', 'description'], + include: [ + { + model: VocabularyCard, + attributes: queryStatus ? ['name', 'description', 'drawerId'] : ['name', 'description'], + include: [ + { + model: Translation, + attributes: ['name'], + }, + ], + }, + ], + }, + { + model: Drawer, + attributes: ['id', 'stage', 'queryInterval'], + }, + ] + : [ + { + model: Group, + attributes: ['name', 'description'], + include: [ + { + model: VocabularyCard, + attributes: queryStatus ? ['name', 'description', 'drawerId'] : ['name', 'description'], + include: [ + { + model: Translation, + attributes: ['name'], + }, + ], + }, + ], + }, + ], + attributes: ['name', 'foreignWordLanguage', 'translatedWordLanguage', 'vocabsPerDay', 'rightWords'], + where: { + userId, + id: languagePackageId, + }, + }); + + return languagePackage; +} + +module.exports = { + getAllGroupVocabulary, + getAllLanguagePackageVocabulary, +}; diff --git a/app/Services/ImportServiceProvider.js b/app/Services/ImportServiceProvider.js new file mode 100644 index 00000000..0849c6af --- /dev/null +++ b/app/Services/ImportServiceProvider.js @@ -0,0 +1,184 @@ +const { Group, LanguagePackage, VocabularyCard, Translation, Drawer, sequelize } = require('../../database'); +const ApiError = require('../utils/ApiError.js'); +const httpStatus = require('http-status'); +const { createDrawers } = require('./DrawerServiceProvider.js'); +const { createLanguagePackage } = require('./LanguagePackageServiceProvider.js'); +const { drawers } = require('../utils/constants.js'); + +async function storeGroupVocabulary( + { name, description, VocabularyCards }, + userId, + languagePackageId, + active, + activate +) { + const languagePackage = await LanguagePackage.findOne({ + where: { + userId, + id: languagePackageId, + }, + }); + + if (!languagePackage) { + throw new ApiError(httpStatus.NOT_FOUND, 'language package not found'); + } + + const transaction = await sequelize.transaction(); + + try { + const group = await Group.create( + { + userId, + languagePackageId, + name, + description, + active, + }, + { transaction } + ); + + const drawer = await Drawer.findOne({ + where: { + userId, + languagePackageId, + stage: activate ? 1 : 0, + }, + transaction, + }); + + const yesterday = new Date().setDate(new Date().getDate() - 1); + + await Promise.all( + VocabularyCards.map(async (vocabularyCard) => { + await VocabularyCard.create( + { + userId, + languagePackageId, + groupId: group.id, + drawerId: drawer.id, + name: vocabularyCard.name, + description: vocabularyCard.description, + lastQuery: yesterday, + lastQueryCorrect: yesterday, + active, + Translations: vocabularyCard.Translations.map((translation) => ({ + name: translation.name, + userId, + languagePackageId, + })), + }, + { + transaction, + include: [ + { + model: Translation, + }, + ], + } + ); + }) + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw new ApiError(httpStatus.BAD_REQUEST, 'Error importing vocabs'); + } +} + +async function storeLanguagePackageVocabulary( + { name, foreignWordLanguage, translatedWordLanguage, vocabsPerDay, rightWords, Groups, Drawers }, + userId, + active, + activate, + queryStatus +) { + if (!Drawers && queryStatus) { + throw new ApiError(httpStatus.BAD_REQUEST, 'No exported drawers found'); + } + + const transaction = await sequelize.transaction(); + + try { + // create language Package + const createdLanguagePackage = await createLanguagePackage( + { name, foreignWordLanguage, translatedWordLanguage, vocabsPerDay, rightWords }, + userId, + transaction + ); + + let getDrawer = () => null; + const yesterday = new Date().setDate(new Date().getDate() - 1); + + // store drawers for language package in database + if (queryStatus) { + const createdDrawers = await createDrawers(Drawers, createdLanguagePackage.id, userId, transaction); + const drawerMap = Drawers.reduce( + (_drawerMap, oldDrawer, index) => ({ + ..._drawerMap, + [oldDrawer.id]: createdDrawers[index], + }), + {} + ); + + getDrawer = (oldId) => drawerMap[oldId]; + } else { + const createdDrawers = await createDrawers(drawers, createdLanguagePackage.id, userId, transaction); + const drawer = createdDrawers.find((_drawer) => +_drawer.stage === (activate ? 1 : 0)); + + getDrawer = () => drawer; + } + + await Promise.all( + Groups.map(async (group) => { + await Group.create( + { + userId, + languagePackageId: createdLanguagePackage.id, + name: group.name, + description: group.description, + active, + VocabularyCards: group.VocabularyCards.map((vocabularyCard) => ({ + userId, + languagePackageId: createdLanguagePackage.id, + drawerId: queryStatus ? getDrawer(vocabularyCard.drawerId).id : getDrawer().id, + name: vocabularyCard.name, + description: vocabularyCard.description, + lastQuery: yesterday, + lastQueryCorrect: yesterday, + active, + Translations: vocabularyCard.Translations.map((translation) => ({ + name: translation.name, + userId, + languagePackageId: createdLanguagePackage.id, + })), + })), + }, + { + transaction, + include: [ + { + model: VocabularyCard, + include: [ + { + model: Translation, + }, + ], + }, + ], + } + ); + }) + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw new ApiError(httpStatus.BAD_REQUEST, 'Error importing vocabs'); + } +} + +module.exports = { + storeGroupVocabulary, + storeLanguagePackageVocabulary, +}; diff --git a/app/Services/LanguagePackageServiceProvider.js b/app/Services/LanguagePackageServiceProvider.js index 7aef2d2a..8dc9663c 100644 --- a/app/Services/LanguagePackageServiceProvider.js +++ b/app/Services/LanguagePackageServiceProvider.js @@ -7,17 +7,21 @@ const httpStatus = require('http-status'); // create language package async function createLanguagePackage( { name, foreignWordLanguage, translatedWordLanguage, vocabsPerDay, rightWords }, - userId + userId, + transaction ) { try { - const languagePackage = await LanguagePackage.create({ - userId, - name, - foreignWordLanguage, - translatedWordLanguage, - vocabsPerDay, - rightWords, - }); + const languagePackage = await LanguagePackage.create( + { + userId, + name, + foreignWordLanguage, + translatedWordLanguage, + vocabsPerDay, + rightWords, + }, + { transaction } + ); return deleteKeysFromObject(['userId', 'createdAt', 'updatedAt'], languagePackage.toJSON()); } catch (error) { diff --git a/app/Services/VocabularyServiceProvider.js b/app/Services/VocabularyServiceProvider.js index de614f79..2f1fbed6 100644 --- a/app/Services/VocabularyServiceProvider.js +++ b/app/Services/VocabularyServiceProvider.js @@ -4,18 +4,32 @@ const ApiError = require('../utils/ApiError.js'); const httpStatus = require('http-status'); // create language package -async function createVocabularyCard({ languagePackageId, groupId }, name, description, userId, active, activate) { +async function createVocabularyCard({ + languagePackageId, + groupId, + name, + description, + userId, + active, + activate, + drawerId, +}) { // if activate = false store vocabulary card in drawer 0 directly - // select drawer id depending on the activate state - const drawer = await Drawer.findOne({ - attributes: ['id'], - where: { - stage: activate ? 1 : 0, - languagePackageId, - userId, - }, - }); + // select drawer id depending on the activate and drawerId state + let drawer = {}; + if (drawerId) { + drawer.id = drawerId; + } else { + drawer = await Drawer.findOne({ + attributes: ['id'], + where: { + stage: activate ? 1 : 0, + languagePackageId, + userId, + }, + }); + } if (!drawer) { throw new ApiError(httpStatus.NOT_FOUND, 'no drawer found due to wrong language package id'); diff --git a/routes/api.js b/routes/api.js index a142c1cb..9186458e 100644 --- a/routes/api.js +++ b/routes/api.js @@ -13,6 +13,8 @@ const LanguageController = require('../app/Controllers/LanguageController.js'); const DocsController = require('../app/Controllers/DocsController.js'); const StatsController = require('../app/Controllers/StatsController.js'); const InfoController = require('../app/Controllers/InfoController.js'); +const ImportController = require('../app/Controllers/ImportController.js'); +const ExportController = require('../app/Controllers/ExportController.js'); const router = express.Router(); @@ -62,6 +64,11 @@ router.patch('/vocabulary/:vocabularyId', ProtectMiddleware, QueryController.che // Language router.get('/language', ProtectMiddleware, LanguageController.sendLanguages); +// Import / Export +router.get('/group/:groupId/export', ProtectMiddleware, ExportController.exportGroup); +router.get('/languagePackage/:languagePackageId/export', ProtectMiddleware, ExportController.exportLanguagePackage); +router.post('/import', ProtectMiddleware, ImportController.importVocabs); + // Docs router.get('/swagger.json', DocsController.document); router.use('/swagger', DocsController.swagger);