diff options
Diffstat (limited to 'ext/bg/js/database.js')
| -rw-r--r-- | ext/bg/js/database.js | 297 | 
1 files changed, 297 insertions, 0 deletions
| diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js new file mode 100644 index 00000000..7ad7d410 --- /dev/null +++ b/ext/bg/js/database.js @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2016  Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <http://www.gnu.org/licenses/>. + */ + + +class Database { +    constructor() { +        this.db = null; +        this.tagMetaCache = {}; +    } + +    prepare() { +        if (this.db !== null) { +            return Promise.reject('database already initialized'); +        } + +        this.db = new Dexie('dict'); +        this.db.version(1).stores({ +            terms: '++id,dictionary,expression,reading', +            kanji: '++,dictionary,character', +            tagMeta: '++,dictionary', +            dictionaries: '++,title,version', +        }); + +        return this.db.open(); +    } + +    purge() { +        if (this.db === null) { +            return Promise.reject('database not initialized'); +        } + +        this.db.close(); +        return this.db.delete().then(() => { +            this.db = null; +            this.tagMetaCache = {}; +            return this.prepare(); +        }); +    } + +    findTerm(term, dictionaries) { +        if (this.db === null) { +            return Promise.reject('database not initialized'); +        } + +        const results = []; +        return this.db.terms.where('expression').equals(term).or('reading').equals(term).each(row => { +            if (dictionaries.includes(row.dictionary)) { +                results.push({ +                    expression: row.expression, +                    reading: row.reading, +                    tags: splitField(row.tags), +                    rules: splitField(row.rules), +                    glossary: row.glossary, +                    score: row.score, +                    dictionary: row.dictionary, +                    id: row.id +                }); +            } +        }).then(() => { +            return this.cacheTagMeta(dictionaries); +        }).then(() => { +            for (const result of results) { +                result.tagMeta = this.tagMetaCache[result.dictionary] || {}; +            } + +            return results; +        }); +    } + +    findKanji(kanji, dictionaries) { +        if (this.db === null) { +            return Promise.reject('database not initialized'); +        } + +        const results = []; +        return this.db.kanji.where('character').equals(kanji).each(row => { +            if (dictionaries.includes(row.dictionary)) { +                results.push({ +                    character: row.character, +                    onyomi: splitField(row.onyomi), +                    kunyomi: splitField(row.kunyomi), +                    tags: splitField(row.tags), +                    glossary: row.meanings, +                    dictionary: row.dictionary +                }); +            } +        }).then(() => { +            return this.cacheTagMeta(dictionaries); +        }).then(() => { +            for (const result of results) { +                result.tagMeta = this.tagMetaCache[result.dictionary] || {}; +            } + +            return results; +        }); +    } + +    cacheTagMeta(dictionaries) { +        if (this.db === null) { +            return Promise.reject('database not initialized'); +        } + +        const promises = []; +        for (const dictionary of dictionaries) { +            if (this.tagMetaCache[dictionary]) { +                continue; +            } + +            const tagMeta = {}; +            promises.push( +                this.db.tagMeta.where('dictionary').equals(dictionary).each(row => { +                    tagMeta[row.name] = {category: row.category, notes: row.notes, order: row.order}; +                }).then(() => { +                    this.tagMetaCache[dictionary] = tagMeta; +                }) +            ); +        } + +        return Promise.all(promises); +    } + +    getDictionaries() { +        if (this.db === null) { +            return Promise.reject('database not initialized'); +        } + +        return this.db.dictionaries.toArray(); +    } + +    deleteDictionary(title, callback) { +        if (this.db === null) { +            return Promise.reject('database not initialized'); +        } + +        return this.db.dictionaries.where('title').equals(title).first(info => { +            if (!info) { +                return; +            } + +            let termCounter = Promise.resolve(0); +            if (info.hasTerms) { +                termCounter = this.db.terms.where('dictionary').equals(title).count(); +            } + +            let kanjiCounter = Promise.resolve(0); +            if (info.hasKanji) { +                kanjiCounter = this.db.kanji.where('dictionary').equals(title).count(); +            } + +            return Promise.all([termCounter, kanjiCounter]).then(([termCount, kanjiCount]) => { +                const rowLimit = 500; +                const totalCount = termCount + kanjiCount; +                let deletedCount = 0; + +                let termDeleter = Promise.resolve(); +                if (info.hasTerms) { +                    const termDeleterFunc = () => { +                        return this.db.terms.where('dictionary').equals(title).limit(rowLimit).delete().then(count => { +                            if (count === 0) { +                                return Promise.resolve(); +                            } + +                            deletedCount += count; +                            if (callback) { +                                callback(totalCount, deletedCount); +                            } + +                            return termDeleterFunc(); +                        }); +                    }; + +                    termDeleter = termDeleterFunc(); +                } + +                let kanjiDeleter = Promise.resolve(); +                if (info.hasKanji) { +                    const kanjiDeleterFunc = () => { +                        return this.db.kanji.where('dictionary').equals(title).limit(rowLimit).delete().then(count => { +                            if (count === 0) { +                                return Promise.resolve(); +                            } + +                            deletedCount += count; +                            if (callback) { +                                callback(totalCount, deletedCount); +                            } + +                            return kanjiDeleterFunc(); +                        }); +                    }; + +                    kanjiDeleter = kanjiDeleterFunc(); +                } + +                return Promise.all([termDeleter, kanjiDeleter]); +            }); +        }).then(() => { +            return this.db.tagMeta.where('dictionary').equals(title).delete(); +        }).then(() => { +            return this.db.dictionaries.where('title').equals(title).delete(); +        }).then(() => { +            delete this.cacheTagMeta[title]; +        }); +    } + +    importDictionary(indexUrl, callback) { +        if (this.db === null) { +            return Promise.reject('database not initialized'); +        } + +        let summary = null; +        const indexLoaded = (title, version, tagMeta, hasTerms, hasKanji) => { +            summary = {title, hasTerms, hasKanji, version}; +            return this.db.dictionaries.where('title').equals(title).count().then(count => { +                if (count > 0) { +                    return Promise.reject(`dictionary "${title}" is already imported`); +                } + +                return this.db.dictionaries.add({title, version, hasTerms, hasKanji}).then(() => { +                    const rows = []; +                    for (const tag in tagMeta || {}) { +                        const meta = tagMeta[tag]; +                        const row = sanitizeTag({ +                            name: tag, +                            category: meta.category, +                            notes: meta.notes, +                            order: meta.order, +                            dictionary: title +                        }); + +                        rows.push(row); +                    } + +                    return this.db.tagMeta.bulkAdd(rows); +                }); +            }); +        }; + +        const termsLoaded = (title, entries, total, current) => { +            const rows = []; +            for (const [expression, reading, tags, rules, score, ...glossary] of entries) { +                rows.push({ +                    expression, +                    reading, +                    tags, +                    rules, +                    score, +                    glossary, +                    dictionary: title +                }); +            } + +            return this.db.terms.bulkAdd(rows).then(() => { +                if (callback) { +                    callback(total, current, indexUrl); +                } +            }); +        }; + +        const kanjiLoaded = (title, entries, total, current)  => { +            const rows = []; +            for (const [character, onyomi, kunyomi, tags, ...meanings] of entries) { +                rows.push({ +                    character, +                    onyomi, +                    kunyomi, +                    tags, +                    meanings, +                    dictionary: title +                }); +            } + +            return this.db.kanji.bulkAdd(rows).then(() => { +                if (callback) { +                    callback(total, current, indexUrl); +                } +            }); +        }; + +        return importJsonDb(indexUrl, indexLoaded, termsLoaded, kanjiLoaded).then(() => summary); +    } +} |