diff --git a/public/api/Dictionary.php b/public/api/Dictionary.php index b960220..c0f8075 100644 --- a/public/api/Dictionary.php +++ b/public/api/Dictionary.php @@ -189,11 +189,30 @@ WHERE dictionary=$dictionary"; return array(); } + public function getDeletedWords ($user, $dictionary) { + $query = "SELECT deleted_words.* FROM deleted_words JOIN dictionaries ON id = dictionary WHERE dictionary=$dictionary AND user=$user"; + $results = $this->db->query($query)->fetchAll(); + if ($results) { + return array_map(function ($row) { + return array( + 'id' => intval($row['word_id']), + 'deletedOn' => intval($row['deleted_on']), + ); + }, $results); + } + return array(); + } + public function setWords ($user, $dictionary, $words = array()) { $query = 'INSERT INTO words (dictionary, word_id, name, pronunciation, part_of_speech, definition, details, last_updated, created_on) VALUES '; $params = array(); $word_ids = array(); + $most_recent_word_update = 0; foreach($words as $word) { + $last_updated = is_null($word['lastUpdated']) ? $word['createdOn'] : $word['lastUpdated']; + if ($most_recent_word_update < $last_updated) { + $most_recent_word_update = $last_updated; + } $word_ids[] = $word['id']; $query .= "(?, ?, ?, ?, ?, ?, ?, ?, ?), "; $params[] = $dictionary; @@ -203,7 +222,7 @@ WHERE dictionary=$dictionary"; $params[] = $word['partOfSpeech']; $params[] = $word['definition']; $params[] = $word['details']; - $params[] = is_null($word['lastUpdated']) ? $word['createdOn'] : $word['lastUpdated']; + $params[] = $last_updated; $params[] = $word['createdOn']; } $query = trim($query, ', ') . ' ON DUPLICATE KEY UPDATE @@ -216,28 +235,42 @@ last_updated=VALUES(last_updated)'; $results = $this->db->execute($query, $params); - if ($results) { - $database_words = $this->getWords($user, $dictionary); - $database_ids = array_map(function($database_word) { return $database_word['id']; }, $database_words); - $words_to_delete = array_filter($database_ids, function($database_id) use($word_ids) { return !in_array($database_id, $word_ids); }); - if ($words_to_delete) { - $delete_results = $this->deleteWords($dictionary, $words_to_delete); - return $delete_results; - } - } + // if ($results) { + // $database_words = $this->getWords($user, $dictionary); + // $database_ids = array_map(function($database_word) { return $database_word['id']; }, $database_words); + // $words_to_delete = array_filter($database_ids, function($database_id) use($word_ids) { return !in_array($database_id, $word_ids); }); + // if ($words_to_delete) { + // $delete_results = $this->deleteWords($dictionary, $words_to_delete); + // return $delete_results; + // } + // } return $results; } public function deleteWords ($dictionary, $word_ids) { - $query = 'DELETE FROM words WHERE dictionary=? AND word_id IN ('; - $params = array($dictionary); + $insert_query = 'INSERT INTO deleted_words (dictionary, word_id, deleted_on) VALUES '; + $insert_params = array(); + $delete_query = 'DELETE FROM words WHERE dictionary=? AND word_id IN ('; + $delete_params = array($dictionary); foreach($word_ids as $word_id) { - $query .= '?, '; - $params[] = $word_id; + $insert_query .= "(?, ?, ?), "; + $insert_params[] = $dictionary; + $insert_params[] = $word_id; + $insert_params[] = time(); + + $delete_query .= '?, '; + $delete_params[] = $word_id; } - $query = trim($query, ', ') . ')'; - $results = $this->db->execute($query, $params); - return $results; + + $insert_query = trim($insert_query, ', ') . ' ON DUPLICATE KEY UPDATE deleted_on=VALUES(deleted_on)'; + $delete_query = trim($delete_query, ', ') . ')'; + + $insert_results = $this->db->execute($insert_query, $insert_params); + if ($insert_results) { + $delete_results = $this->db->execute($delete_query, $delete_params); + return $delete_results; + } + return $insert_results; } } \ No newline at end of file diff --git a/public/api/User.php b/public/api/User.php index 1c8e696..c14a9a2 100644 --- a/public/api/User.php +++ b/public/api/User.php @@ -171,6 +171,7 @@ VALUES (?, ?, ?, ?, ?, ?)'; return array( 'details' => $this->dictionary->getDetails($user, $dictionary), 'words' => $this->dictionary->getWords($user, $dictionary), + 'deletedWords' => $this->dictionary->getDeletedWords($user, $dictionary), ); } return false; @@ -212,13 +213,13 @@ VALUES (?, ?, ?, ?, ?, ?)'; return false; } - public function deleteWordFromCurrentDictionary ($token, $word_id) { + public function deleteWordsFromCurrentDictionary ($token, $word_ids) { // Useful even for just one word $user_data = $this->token->decode($token); if ($user_data !== false) { $dictionary = $user_data->dictionary; $user = $user_data->id; - $deleted_word = $this->dictionary->deleteWords($dictionary, array($word_id)); + $deleted_word = $this->dictionary->deleteWords($dictionary, $word_ids); if ($deleted_word) { return true; } diff --git a/public/api/index.php b/public/api/index.php index 6ac92a0..4af0c82 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -247,7 +247,7 @@ switch ($action) { case 'delete-word': { if ($token !== false && isset($request['word'])) { $user = new User(); - $delete_word_success = $user->deleteWordFromCurrentDictionary($token, $request['word']); + $delete_word_success = $user->deleteWordsFromCurrentDictionary($token, array($request['word'])); if ($delete_word_success !== false) { return Response::json(array( 'data' => 'Deleted successfully', @@ -264,6 +264,26 @@ switch ($action) { 'error' => true, ), 400); } + case 'delete-words': { + if ($token !== false && isset($request['words'])) { + $user = new User(); + $delete_word_success = $user->deleteWordsFromCurrentDictionary($token, $request['words']); + if ($delete_word_success !== false) { + return Response::json(array( + 'data' => 'Deleted successfully', + 'error' => false, + ), 200); + } + return Response::json(array( + 'data' => 'Could not delete words: invalid token', + 'error' => true, + ), 401); + } + return Response::json(array( + 'data' => 'Could not delete words: required data missing', + 'error' => true, + ), 400); + } default: { return Response::html('Hi!'); diff --git a/public/api/structure.sql b/public/api/structure.sql index b42c654..2059396 100644 --- a/public/api/structure.sql +++ b/public/api/structure.sql @@ -7,6 +7,12 @@ SET time_zone = "+00:00"; /*!40101 SET NAMES utf8mb4 */; +CREATE TABLE IF NOT EXISTS `deleted_words` ( + `dictionary` int(11) NOT NULL, + `word_id` int(11) NOT NULL, + `deleted_on` int(11) NOT NULL +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + CREATE TABLE IF NOT EXISTS `dictionaries` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user` int(11) NOT NULL, diff --git a/src/managers/DictionaryData.js b/src/managers/DictionaryData.js index 4444f8e..ac420d1 100644 --- a/src/managers/DictionaryData.js +++ b/src/managers/DictionaryData.js @@ -312,6 +312,10 @@ class DictionaryData { return wordDb.words.orderBy('name').toArray(); } + get deletedWordsPromise () { + return wordDb.deletedWords.toArray(); + } + wordsWithPartOfSpeech (partOfSpeech) { let words = wordDb.words.where('partOfSpeech'); diff --git a/src/managers/Updater.js b/src/managers/Updater.js index 30f71dd..13ba23e 100644 --- a/src/managers/Updater.js +++ b/src/managers/Updater.js @@ -101,6 +101,13 @@ export class Updater { }, response => console.log(response)); } + sendDeletedWords (words) { + return request('delete-words', { + token: store.get('LexicongaToken'), + words, + }, response => console.log(response)); + } + sync () { return request('get-current-dictionary', { token: store.get('LexicongaToken'), @@ -110,7 +117,7 @@ export class Updater { console.error(data); } else { this.compareDetails(data.details); - this.compareWords(data.words); + this.compareWords(data.words, data.deletedWords); } }); } @@ -129,10 +136,11 @@ export class Updater { } } - compareWords (externalWords) { + compareWords (externalWords, deletedWords) { const wordsToSend = []; const wordsToAdd = []; const wordsToUpdate = []; + const wordsToDelete = []; const localWordsPromise = this.dictionary.wordsPromise.then(localWords => { externalWords.forEach(externalWord => { if (externalWord.lastUpdated) { @@ -151,24 +159,61 @@ export class Updater { // Find words not in external database and add them to send. localWords.forEach(localWord => { if (localWord.lastUpdated) { - const wordAlreadyChecked = externalWords.some(word => word.id === localWord.id); - if (!wordAlreadyChecked) { - wordsToSend.push(localWord); + const wordDeleted = deletedWords.some(word => word.id === localWord.id); + if (wordDeleted) { + wordsToDelete.push(localWord); + } else { + const wordAlreadyChecked = externalWords.some(word => word.id === localWord.id); + if (!wordAlreadyChecked) { + wordsToSend.push(localWord); + } } } }); - wordsToAdd.forEach(newWord => { - new Word(newWord).create(); + return { + wordsToAdd, + wordsToUpdate, + wordsToDelete, + wordsToSend, + }; + }).then(processedWords => { + let { + wordsToAdd, + wordsToUpdate, + wordsToDelete, + wordsToSend, + } = processedWords; + + return this.dictionary.deletedWordsPromise.then(localDeletedWords => { + wordsToAdd = wordsToAdd.filter(word => !localDeletedWords.some(deleted => deleted.id === word.id)); + wordsToUpdate = wordsToUpdate.filter(word => !localDeletedWords.some(deleted => deleted.id === word.id)); + wordsToSend = wordsToSend.filter(word => !localDeletedWords.some(deleted => deleted.id === word.id)); + const deletedWordsToSend = localDeletedWords.filter(local => !deletedWords.some(remote => remote.id === local.id)); + + wordsToAdd.forEach(newWord => { + new Word(newWord).create(); + }); + wordsToUpdate.forEach(updatedWord => { + new Word(updatedWord).update(); + }); + wordsToDelete.forEach(deletedWord => { + // Remove words deleted on server from local dictionary + new Word(deletedWord).delete(deletedWord.id, true); + }); + if (wordsToSend.length > 0) { + this.sendWords(wordsToSend); + } + if (deletedWordsToSend.length > 0) { + this.sendDeletedWords(deletedWordsToSend.map(deletedWord => deletedWord.id)); + } + }).catch(error => { + console.error(error); }); - wordsToUpdate.forEach(updatedWord => { - new Word(updatedWord).update(); - }); - if (wordsToSend.length > 0) { - this.sendWords(wordsToSend); - } }).then(() => { - this.app.updateDisplayedWords(() => console.log('synced words')); + this.app.updateDisplayedWords(() => console.log('synced words')); + }).catch(error => { + console.error(error); }); } } \ No newline at end of file diff --git a/src/managers/Word.js b/src/managers/Word.js index aa620ae..cc64a99 100644 --- a/src/managers/Word.js +++ b/src/managers/Word.js @@ -42,10 +42,15 @@ export class Word { create () { this.createdOn = this.createdOn ? this.createdOn : timestampInSeconds(); + let addPromise; // Delete id if it exists to allow creation of new word. - if (this.hasOwnProperty('id')) delete this.id; + if (this.hasOwnProperty('id')) { + addPromise = wordDb.words.put(this); + } else { + addPromise = wordDb.words.add(this); + } - return wordDb.words.add(this) + return addPromise .then((id) => { this.id = id; console.log('Word added successfully'); @@ -69,14 +74,26 @@ export class Word { }); } - delete (wordId) { + delete (wordId, skipSend = false) { return wordDb.words.delete(wordId) .then(() => { console.log('Word deleted successfully'); - request('delete-word', { - token: store.get('LexicongaToken'), - word: wordId, - }, response => console.log(response)); + if (!skipSend) { + request('delete-word', { + token: store.get('LexicongaToken'), + word: wordId, + }, response => console.log(response)); + } + wordDb.deletedWords.add({ + id: wordId, + deletedOn: timestampInSeconds(), + }) + .then(() => { + console.log('Word added to deleted list'); + }) + .catch(error => { + console.error(error); + }); }) .catch(error => { console.error(error); diff --git a/src/managers/WordDatabase.js b/src/managers/WordDatabase.js index 047b611..9ccbae5 100644 --- a/src/managers/WordDatabase.js +++ b/src/managers/WordDatabase.js @@ -6,6 +6,10 @@ const db = new Dexie('Lexiconga'); db.version(1).stores({ words: '++id, name, partOfSpeech, createdOn, lastUpdated', }); +db.version(2).stores({ + words: '++id, name, partOfSpeech, createdOn, lastUpdated', + deletedWords: 'id', +}); if (['emptydb', 'donotsave'].includes(process.env.NODE_ENV)) { db.words.clear();