mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-06 18:18:53 +08:00
CSV import as a table and db connection rework
- Add csv to existing db #32 - [RFE] Simplify working with temporary tables #53
This commit is contained in:
15
package-lock.json
generated
15
package-lock.json
generated
@@ -1,17 +1,16 @@
|
||||
{
|
||||
"name": "sqliteviz",
|
||||
"version": "1.0.0",
|
||||
"version": "0.13.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sqliteviz",
|
||||
"version": "1.0.0",
|
||||
"version": "0.13.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"codemirror": "^5.57.0",
|
||||
"core-js": "^3.6.5",
|
||||
"debounce": "^1.2.0",
|
||||
"nanoid": "^3.1.12",
|
||||
"papaparse": "^5.3.0",
|
||||
"plotly.js": "^1.58.4",
|
||||
@@ -6748,11 +6747,6 @@
|
||||
"integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/debounce": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz",
|
||||
"integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
@@ -29304,11 +29298,6 @@
|
||||
"integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=",
|
||||
"dev": true
|
||||
},
|
||||
"debounce": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz",
|
||||
"integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"dependencies": {
|
||||
"codemirror": "^5.57.0",
|
||||
"core-js": "^3.6.5",
|
||||
"debounce": "^1.2.0",
|
||||
"nanoid": "^3.1.12",
|
||||
"papaparse": "^5.3.0",
|
||||
"plotly.js": "^1.58.4",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.rounded-bg {
|
||||
padding: 40px 5px 5px;
|
||||
padding: 35px 5px 5px;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
@@ -36,7 +36,7 @@
|
||||
}
|
||||
table {
|
||||
min-width: 100%;
|
||||
margin-top: -40px;
|
||||
margin-top: -35px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
thead th, .fixed-header {
|
||||
@@ -56,7 +56,7 @@ tbody td {
|
||||
border-right: 1px solid var(--color-border-light);
|
||||
}
|
||||
td, th, .fixed-header {
|
||||
padding: 12px 24px;
|
||||
padding: 8px 24px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
381
src/components/CsvImport/index.vue
Normal file
381
src/components/CsvImport/index.vue
Normal file
@@ -0,0 +1,381 @@
|
||||
<template>
|
||||
<modal
|
||||
:name="dialogName"
|
||||
classes="dialog"
|
||||
height="auto"
|
||||
width="80%"
|
||||
scrollable
|
||||
:clickToClose="false"
|
||||
>
|
||||
<div class="dialog-header">
|
||||
CSV import
|
||||
<close-icon @click="cancelCsvImport" :disabled="disableDialog"/>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<text-field
|
||||
label="Table name"
|
||||
v-model="tableName"
|
||||
width="484px"
|
||||
:disabled="disableDialog"
|
||||
:error-msg="tableNameError"
|
||||
id="csv-table-name"
|
||||
/>
|
||||
<div class="chars">
|
||||
<delimiter-selector
|
||||
v-model="delimiter"
|
||||
width="210px"
|
||||
class="char-input"
|
||||
@input="previewCsv"
|
||||
:disabled="disableDialog"
|
||||
/>
|
||||
<text-field
|
||||
label="Quote char"
|
||||
hint="The character used to quote fields."
|
||||
v-model="quoteChar"
|
||||
width="93px"
|
||||
:disabled="disableDialog"
|
||||
class="char-input"
|
||||
id="quote-char"
|
||||
/>
|
||||
<text-field
|
||||
label="Escape char"
|
||||
hint='The character used to escape the quote character within a field (e.g. "column with ""quotes"" in text").'
|
||||
max-hint-width="242px"
|
||||
v-model="escapeChar"
|
||||
width="93px"
|
||||
:disabled="disableDialog"
|
||||
class="char-input"
|
||||
id="escape-char"
|
||||
/>
|
||||
</div>
|
||||
<check-box
|
||||
@click="header = $event"
|
||||
:init="true"
|
||||
label="Use first row as column headers"
|
||||
:disabled="disableDialog"
|
||||
/>
|
||||
<sql-table
|
||||
v-if="previewData && (previewData.values.length > 0 || previewData.columns.length > 0)"
|
||||
:data-set="previewData"
|
||||
height="160"
|
||||
class="preview-table"
|
||||
:preview="true"
|
||||
/>
|
||||
<div v-else class="no-data">No data</div>
|
||||
<logs
|
||||
class="import-csv-errors"
|
||||
:messages="importCsvMessages"
|
||||
/>
|
||||
</div>
|
||||
<div class="dialog-buttons-container">
|
||||
<button
|
||||
class="secondary"
|
||||
:disabled="disableDialog"
|
||||
@click="cancelCsvImport"
|
||||
id="csv-cancel"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
v-show="!importCsvCompleted"
|
||||
class="primary"
|
||||
:disabled="disableDialog"
|
||||
@click="loadFromCsv(file)"
|
||||
id="csv-import"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
<button
|
||||
v-show="importCsvCompleted"
|
||||
class="primary"
|
||||
:disabled="disableDialog"
|
||||
@click="finish"
|
||||
id="csv-finish"
|
||||
>
|
||||
Finish
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import csv from './csv'
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
import TextField from '@/components/TextField'
|
||||
import DelimiterSelector from './DelimiterSelector'
|
||||
import CheckBox from '@/components/CheckBox'
|
||||
import SqlTable from '@/components/SqlTable'
|
||||
import Logs from '@/components/Logs'
|
||||
import time from '@/lib/utils/time'
|
||||
import fIo from '@/lib/utils/fileIo'
|
||||
|
||||
export default {
|
||||
name: 'CsvImport',
|
||||
components: {
|
||||
CloseIcon,
|
||||
TextField,
|
||||
DelimiterSelector,
|
||||
CheckBox,
|
||||
SqlTable,
|
||||
Logs
|
||||
},
|
||||
props: ['file', 'db', 'dialogName'],
|
||||
data () {
|
||||
return {
|
||||
disableDialog: false,
|
||||
tableName: '',
|
||||
delimiter: '',
|
||||
quoteChar: '"',
|
||||
escapeChar: '"',
|
||||
header: true,
|
||||
importCsvCompleted: false,
|
||||
importCsvMessages: [],
|
||||
previewData: null,
|
||||
addedTable: null,
|
||||
tableNameError: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
quoteChar () {
|
||||
this.previewCsv()
|
||||
},
|
||||
|
||||
escapeChar () {
|
||||
this.previewCsv()
|
||||
},
|
||||
|
||||
header () {
|
||||
this.previewCsv()
|
||||
},
|
||||
tableName: time.debounce(function () {
|
||||
this.tableNameError = ''
|
||||
if (!this.tableName) {
|
||||
return
|
||||
}
|
||||
this.db.validateTableName(this.tableName)
|
||||
.catch(err => {
|
||||
this.tableNameError = err.message + '. Try another table name.'
|
||||
})
|
||||
}, 400)
|
||||
},
|
||||
methods: {
|
||||
cancelCsvImport () {
|
||||
if (!this.disableDialog) {
|
||||
if (this.addedTable) {
|
||||
this.db.execute(`DROP TABLE "${this.addedTable}"`)
|
||||
this.db.refreshSchema()
|
||||
}
|
||||
this.$modal.hide(this.dialogName)
|
||||
this.$emit('cancel')
|
||||
}
|
||||
},
|
||||
reset () {
|
||||
this.header = true
|
||||
this.quoteChar = '"'
|
||||
this.escapeChar = '"'
|
||||
this.delimiter = ''
|
||||
this.tableName = ''
|
||||
this.disableDialog = false
|
||||
this.importCsvCompleted = false
|
||||
this.importCsvMessages = []
|
||||
this.previewData = null
|
||||
this.addedTable = null
|
||||
this.tableNameError = ''
|
||||
},
|
||||
open () {
|
||||
this.tableName = this.db.sanitizeTableName(fIo.getFileName(this.file))
|
||||
this.$modal.show(this.dialogName)
|
||||
},
|
||||
async previewCsv () {
|
||||
this.importCsvCompleted = false
|
||||
const config = {
|
||||
preview: 3,
|
||||
quoteChar: this.quoteChar || '"',
|
||||
escapeChar: this.escapeChar,
|
||||
header: this.header,
|
||||
delimiter: this.delimiter
|
||||
}
|
||||
try {
|
||||
const start = new Date()
|
||||
const parseResult = await csv.parse(this.file, config)
|
||||
const end = new Date()
|
||||
this.previewData = parseResult.data
|
||||
this.delimiter = parseResult.delimiter
|
||||
|
||||
// In parseResult.messages we can get parse errors
|
||||
this.importCsvMessages = parseResult.messages || []
|
||||
|
||||
if (!parseResult.hasErrors) {
|
||||
this.importCsvMessages.push({
|
||||
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
this.importCsvMessages = [{
|
||||
message: err,
|
||||
type: 'error'
|
||||
}]
|
||||
}
|
||||
},
|
||||
async loadFromCsv (file) {
|
||||
if (!this.tableName) {
|
||||
this.tableNameError = "Table name can't be empty"
|
||||
return
|
||||
}
|
||||
|
||||
this.disableDialog = true
|
||||
const config = {
|
||||
quoteChar: this.quoteChar || '"',
|
||||
escapeChar: this.escapeChar,
|
||||
header: this.header,
|
||||
delimiter: this.delimiter
|
||||
}
|
||||
const parseCsvMsg = {
|
||||
message: 'Parsing CSV...',
|
||||
type: 'info'
|
||||
}
|
||||
this.importCsvMessages.push(parseCsvMsg)
|
||||
const parseCsvLoadingIndicator = setTimeout(() => { parseCsvMsg.type = 'loading' }, 1000)
|
||||
|
||||
const importMsg = {
|
||||
message: 'Importing CSV into a SQLite database...',
|
||||
type: 'info'
|
||||
}
|
||||
let importLoadingIndicator = null
|
||||
|
||||
const updateProgress = progress => {
|
||||
this.$set(importMsg, 'progress', progress)
|
||||
}
|
||||
const progressCounterId = this.db.createProgressCounter(updateProgress)
|
||||
|
||||
try {
|
||||
let start = new Date()
|
||||
const parseResult = await csv.parse(this.file, config)
|
||||
let end = new Date()
|
||||
|
||||
if (!parseResult.hasErrors) {
|
||||
const rowCount = parseResult.data.values.length
|
||||
let period = time.getPeriod(start, end)
|
||||
parseCsvMsg.type = 'success'
|
||||
|
||||
if (parseResult.messages.length > 0) {
|
||||
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
||||
parseCsvMsg.message = `${rowCount} rows are parsed in ${period}.`
|
||||
} else {
|
||||
// Inform about csv parsing success
|
||||
parseCsvMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
|
||||
}
|
||||
|
||||
// Loading indicator for csv parsing is not needed anymore
|
||||
clearTimeout(parseCsvLoadingIndicator)
|
||||
|
||||
// Add info about import start
|
||||
this.importCsvMessages.push(importMsg)
|
||||
|
||||
// Show import progress after 1 second
|
||||
importLoadingIndicator = setTimeout(() => {
|
||||
importMsg.type = 'loading'
|
||||
}, 1000)
|
||||
|
||||
// Add table
|
||||
start = new Date()
|
||||
await this.db.addTableFromCsv(this.tableName, parseResult.data, progressCounterId)
|
||||
end = new Date()
|
||||
|
||||
this.addedTable = this.tableName
|
||||
// Inform about import success
|
||||
period = time.getPeriod(start, end)
|
||||
importMsg.message = `Importing CSV into a SQLite database is completed in ${period}.`
|
||||
importMsg.type = 'success'
|
||||
|
||||
// Loading indicator for import is not needed anymore
|
||||
clearTimeout(importLoadingIndicator)
|
||||
|
||||
this.importCsvCompleted = true
|
||||
} else {
|
||||
parseCsvMsg.message = 'Parsing ended with errors.'
|
||||
parseCsvMsg.type = 'info'
|
||||
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
||||
}
|
||||
} catch (err) {
|
||||
if (parseCsvMsg.type === 'loading') {
|
||||
parseCsvMsg.type = 'info'
|
||||
}
|
||||
|
||||
if (importMsg.type === 'loading') {
|
||||
importMsg.type = 'info'
|
||||
}
|
||||
|
||||
this.importCsvMessages.push({
|
||||
message: err,
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
|
||||
clearTimeout(parseCsvLoadingIndicator)
|
||||
clearTimeout(importLoadingIndicator)
|
||||
this.db.deleteProgressCounter(progressCounterId)
|
||||
this.disableDialog = false
|
||||
},
|
||||
async finish () {
|
||||
this.$modal.hide(this.dialogName)
|
||||
const stmt = [
|
||||
'/*',
|
||||
` * Your CSV file has been imported into ${this.addedTable} table.`,
|
||||
' * You can run this SQL query to make all CSV records available for charting.',
|
||||
' */',
|
||||
`SELECT * FROM "${this.addedTable}"`
|
||||
].join('\n')
|
||||
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
||||
this.$store.commit('setCurrentTabId', tabId)
|
||||
this.importCsvCompleted = false
|
||||
this.$emit('finish')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-body {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.chars {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin: 24px 0 20px;
|
||||
}
|
||||
.char-input {
|
||||
margin-right: 44px;
|
||||
}
|
||||
.preview-table {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.import-csv-errors {
|
||||
height: 136px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.no-data {
|
||||
margin-top: 32px;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
border: 1px solid var(--color-border-light);
|
||||
box-sizing: border-box;
|
||||
height: 147px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-base);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* https://github.com/euvl/vue-js-modal/issues/623 */
|
||||
>>> .vm--modal {
|
||||
max-width: 1152px;
|
||||
margin: auto;
|
||||
left: 0 !important;
|
||||
}
|
||||
</style>
|
||||
259
src/components/DbUploader.vue
Normal file
259
src/components/DbUploader.vue
Normal file
@@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<div class="db-uploader-container" :style="{ width }">
|
||||
<change-db-icon v-if="type === 'small'" @click.native="browse"/>
|
||||
<div v-if="type === 'illustrated'" class="drop-area-container">
|
||||
<div
|
||||
class="drop-area"
|
||||
@dragover.prevent="state = 'dragover'"
|
||||
@dragleave.prevent="state=''"
|
||||
@drop.prevent="drop"
|
||||
@click="browse"
|
||||
>
|
||||
<div class="text">
|
||||
Drop the database or CSV file here or click to choose a file from your computer.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="type === 'illustrated'" id="img-container">
|
||||
<img id="drop-file-top-img" :src="require('@/assets/images/top.svg')" />
|
||||
<img
|
||||
id="left-arm-img"
|
||||
:class="{'swing': state === 'dragover'}"
|
||||
:src="require('@/assets/images/leftArm.svg')"
|
||||
/>
|
||||
<img
|
||||
id="file-img"
|
||||
ref="fileImg"
|
||||
:class="{
|
||||
'swing': state === 'dragover',
|
||||
'fly': state === 'dropping',
|
||||
'hidden': state === 'dropped'
|
||||
}"
|
||||
:src="require('@/assets/images/file.png')"
|
||||
/>
|
||||
<img id="drop-file-bottom-img" :src="require('@/assets/images/bottom.svg')" />
|
||||
<img id="body-img" :src="require('@/assets/images/body.svg')" />
|
||||
<img
|
||||
id="right-arm-img"
|
||||
:class="{'swing': state === 'dragover'}"
|
||||
:src="require('@/assets/images/rightArm.svg')"
|
||||
/>
|
||||
</div>
|
||||
<div id="error" class="error"></div>
|
||||
|
||||
<!--Parse csv dialog -->
|
||||
<csv-import
|
||||
ref="addCsv"
|
||||
:file="file"
|
||||
:db="newDb"
|
||||
dialog-name="importFromCsv"
|
||||
@cancel="cancelCsvImport"
|
||||
@finish="finish"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import fIo from '@/lib/utils/fileIo'
|
||||
import ChangeDbIcon from '@/components/svg/changeDb'
|
||||
import database from '@/lib/database'
|
||||
import CsvImport from '@/components/CsvImport'
|
||||
|
||||
export default {
|
||||
name: 'DbUploader',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'small',
|
||||
validator: (value) => {
|
||||
return ['illustrated', 'small'].includes(value)
|
||||
}
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'unset'
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ChangeDbIcon,
|
||||
CsvImport
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
state: '',
|
||||
animationPromise: Promise.resolve(),
|
||||
file: null,
|
||||
newDb: null
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.type === 'illustrated') {
|
||||
this.animationPromise = new Promise((resolve) => {
|
||||
this.$refs.fileImg.addEventListener('animationend', event => {
|
||||
if (event.animationName.startsWith('fly')) {
|
||||
this.state = 'dropped'
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancelCsvImport () {
|
||||
if (this.newDb) {
|
||||
this.newDb.shutDown()
|
||||
this.newDb = null
|
||||
}
|
||||
},
|
||||
|
||||
async finish () {
|
||||
this.$store.commit('setDb', this.newDb)
|
||||
if (this.$route.path !== '/editor') {
|
||||
this.$router.push('/editor')
|
||||
}
|
||||
},
|
||||
|
||||
loadDb (file) {
|
||||
return Promise.all([this.newDb.loadDb(file), this.animationPromise])
|
||||
.then(this.finish)
|
||||
},
|
||||
|
||||
async checkFile (file) {
|
||||
this.state = 'dropping'
|
||||
this.newDb = database.getNewDatabase()
|
||||
|
||||
if (fIo.isDatabase(file)) {
|
||||
this.loadDb(file)
|
||||
} else {
|
||||
this.file = file
|
||||
await this.$nextTick()
|
||||
const csvImport = this.$refs.addCsv
|
||||
csvImport.reset()
|
||||
return Promise.all([csvImport.previewCsv(), this.animationPromise])
|
||||
.then(csvImport.open)
|
||||
}
|
||||
},
|
||||
browse () {
|
||||
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
|
||||
.then(this.checkFile)
|
||||
},
|
||||
|
||||
drop (event) {
|
||||
this.checkFile(event.dataTransfer.files[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.db-uploader-container {
|
||||
position: relative;
|
||||
}
|
||||
.drop-area-container {
|
||||
display: inline-block;
|
||||
border: 1px dashed var(--color-border);
|
||||
padding: 8px;
|
||||
border-radius: var(--border-radius-big);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.drop-area {
|
||||
background-color: var(--color-bg-light-3);
|
||||
border-radius: var(--border-radius-big);
|
||||
color: var(--color-text-base);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#img-container {
|
||||
position: absolute;
|
||||
top: 54px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
width: 450px;
|
||||
height: 338px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#drop-file-top-img {
|
||||
width: 450px;
|
||||
height: 175px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
#drop-file-bottom-img {
|
||||
width: 450px;
|
||||
height: 167px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
#body-img {
|
||||
width: 74px;
|
||||
position: absolute;
|
||||
top: 94.05px;
|
||||
left: 46px;
|
||||
}
|
||||
#right-arm-img {
|
||||
width: 106px;
|
||||
position: absolute;
|
||||
top: 110.05px;
|
||||
left: 78px;
|
||||
}
|
||||
#left-arm-img {
|
||||
width: 114px;
|
||||
position: absolute;
|
||||
top: 69.05px;
|
||||
left: 69px;
|
||||
}
|
||||
#file-img {
|
||||
width: 125px;
|
||||
position: absolute;
|
||||
top: 15.66px;
|
||||
left: 152px;
|
||||
}
|
||||
|
||||
.swing {
|
||||
animation: swing ease-in-out 0.6s infinite alternate;
|
||||
}
|
||||
#left-arm-img.swing {
|
||||
transform-origin: 9px 83px;
|
||||
}
|
||||
#right-arm-img.swing {
|
||||
transform-origin: 0 56px;
|
||||
}
|
||||
#file-img.swing {
|
||||
transform-origin: -74px 139px;
|
||||
}
|
||||
@keyframes swing {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(-7deg); }
|
||||
}
|
||||
|
||||
#file-img.fly {
|
||||
animation: fly ease-in-out 1s 1 normal;
|
||||
transform-origin: center center;
|
||||
}
|
||||
@keyframes fly {
|
||||
100% {
|
||||
transform: rotate(360deg) scale(0.5);
|
||||
top: 183px;
|
||||
left: 225px;
|
||||
}
|
||||
}
|
||||
|
||||
#file-img.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,558 +0,0 @@
|
||||
<template>
|
||||
<div class="db-uploader-container" :style="{ width }">
|
||||
<change-db-icon v-if="type === 'small'" @click.native="browse"/>
|
||||
<div v-if="type === 'illustrated'" class="drop-area-container">
|
||||
<div
|
||||
class="drop-area"
|
||||
@dragover.prevent="state = 'dragover'"
|
||||
@dragleave.prevent="state=''"
|
||||
@drop.prevent="drop"
|
||||
@click="browse"
|
||||
>
|
||||
<div class="text">
|
||||
Drop the database or CSV file here or click to choose a file from your computer.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="type === 'illustrated'" id="img-container">
|
||||
<img id="drop-file-top-img" :src="require('@/assets/images/top.svg')" />
|
||||
<img
|
||||
id="left-arm-img"
|
||||
:class="{'swing': state === 'dragover'}"
|
||||
:src="require('@/assets/images/leftArm.svg')"
|
||||
/>
|
||||
<img
|
||||
id="file-img"
|
||||
ref="fileImg"
|
||||
:class="{
|
||||
'swing': state === 'dragover',
|
||||
'fly': state === 'dropping',
|
||||
'hidden': state === 'dropped'
|
||||
}"
|
||||
:src="require('@/assets/images/file.png')"
|
||||
/>
|
||||
<img id="drop-file-bottom-img" :src="require('@/assets/images/bottom.svg')" />
|
||||
<img id="body-img" :src="require('@/assets/images/body.svg')" />
|
||||
<img
|
||||
id="right-arm-img"
|
||||
:class="{'swing': state === 'dragover'}"
|
||||
:src="require('@/assets/images/rightArm.svg')"
|
||||
/>
|
||||
</div>
|
||||
<div id="error" class="error"></div>
|
||||
|
||||
<!--Parse csv dialog -->
|
||||
<modal
|
||||
name="parse"
|
||||
classes="dialog"
|
||||
scrollable
|
||||
height="auto"
|
||||
width="60%"
|
||||
:clickToClose="false"
|
||||
>
|
||||
<div class="dialog-header">
|
||||
Import CSV
|
||||
<close-icon @click="cancelCsvImport" :disabled="disableDialog"/>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<div class="chars">
|
||||
<delimiter-selector
|
||||
v-model="delimiter"
|
||||
width="210px"
|
||||
class="char-input"
|
||||
@input="previewCSV"
|
||||
:disabled="disableDialog"
|
||||
/>
|
||||
<text-field
|
||||
label="Quote char"
|
||||
hint="The character used to quote fields."
|
||||
v-model="quoteChar"
|
||||
width="93px"
|
||||
:disabled="disableDialog"
|
||||
class="char-input"
|
||||
id="quote-char"
|
||||
/>
|
||||
<text-field
|
||||
label="Escape char"
|
||||
hint='The character used to escape the quote character within a field (e.g. "column with ""quotes"" in text").'
|
||||
max-hint-width="242px"
|
||||
v-model="escapeChar"
|
||||
width="93px"
|
||||
:disabled="disableDialog"
|
||||
class="char-input"
|
||||
id="escape-char"
|
||||
/>
|
||||
</div>
|
||||
<check-box
|
||||
@click="header = $event"
|
||||
:init="true"
|
||||
label="Use first row as column headers"
|
||||
:disabled="disableDialog"
|
||||
/>
|
||||
<sql-table
|
||||
v-if="previewData"
|
||||
:data-set="previewData"
|
||||
height="160"
|
||||
class="preview-table"
|
||||
:preview="true"
|
||||
/>
|
||||
<div v-if="!previewData" class="no-data">No data</div>
|
||||
<logs
|
||||
class="import-csv-errors"
|
||||
:messages="importCsvMessages"
|
||||
/>
|
||||
</div>
|
||||
<div class="dialog-buttons-container">
|
||||
<button
|
||||
class="secondary"
|
||||
:disabled="disableDialog"
|
||||
@click="cancelCsvImport"
|
||||
id="csv-cancel"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
v-show="!importCsvCompleted"
|
||||
class="primary"
|
||||
:disabled="disableDialog"
|
||||
@click="loadFromCsv(file)"
|
||||
id="csv-import"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
<button
|
||||
v-show="importCsvCompleted"
|
||||
class="primary"
|
||||
:disabled="disableDialog"
|
||||
@click="finish"
|
||||
id="csv-finish"
|
||||
>
|
||||
Finish
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import fIo from '@/lib/utils/fileIo'
|
||||
import csv from './csv'
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
import TextField from '@/components/TextField'
|
||||
import DelimiterSelector from './DelimiterSelector'
|
||||
import CheckBox from '@/components/CheckBox'
|
||||
import SqlTable from '@/components/SqlTable'
|
||||
import Logs from '@/components/Logs'
|
||||
import ChangeDbIcon from '@/components/svg/changeDb'
|
||||
import time from '@/lib/utils/time'
|
||||
import database from '@/lib/database'
|
||||
|
||||
export default {
|
||||
name: 'DbUploader',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'small',
|
||||
validator: (value) => {
|
||||
return ['illustrated', 'small'].includes(value)
|
||||
}
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'unset'
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ChangeDbIcon,
|
||||
TextField,
|
||||
DelimiterSelector,
|
||||
CloseIcon,
|
||||
CheckBox,
|
||||
SqlTable,
|
||||
Logs
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
state: '',
|
||||
animationPromise: Promise.resolve(),
|
||||
file: null,
|
||||
schema: null,
|
||||
delimiter: '',
|
||||
quoteChar: '"',
|
||||
escapeChar: '"',
|
||||
header: true,
|
||||
previewData: null,
|
||||
importCsvMessages: [],
|
||||
disableDialog: false,
|
||||
importCsvCompleted: false,
|
||||
newDb: null
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.type === 'illustrated') {
|
||||
this.animationPromise = new Promise((resolve) => {
|
||||
this.$refs.fileImg.addEventListener('animationend', event => {
|
||||
if (event.animationName.startsWith('fly')) {
|
||||
this.state = 'dropped'
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
quoteChar () {
|
||||
this.previewCSV()
|
||||
},
|
||||
|
||||
escapeChar () {
|
||||
this.previewCSV()
|
||||
},
|
||||
|
||||
header () {
|
||||
this.previewCSV()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancelCsvImport () {
|
||||
if (!this.disableDialog) {
|
||||
this.$modal.hide('parse')
|
||||
if (this.newDb) {
|
||||
this.newDb.shutDown()
|
||||
this.newDb = null
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async finish () {
|
||||
this.$store.commit('setDb', this.newDb)
|
||||
this.$store.commit('saveSchema', this.schema)
|
||||
if (this.importCsvCompleted) {
|
||||
this.$modal.hide('parse')
|
||||
const stmt = [
|
||||
'/*',
|
||||
' * Your CSV file has been imported into csv_import table.',
|
||||
' * You can run this SQL query to make all CSV records available for charting.',
|
||||
' */',
|
||||
'SELECT * FROM csv_import'
|
||||
].join('\n')
|
||||
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
||||
this.$store.commit('setCurrentTabId', tabId)
|
||||
this.importCsvCompleted = false
|
||||
}
|
||||
if (this.$route.path !== '/editor') {
|
||||
this.$router.push('/editor')
|
||||
}
|
||||
},
|
||||
|
||||
async previewCSV () {
|
||||
this.importCsvCompleted = false
|
||||
const config = {
|
||||
preview: 3,
|
||||
quoteChar: this.quoteChar || '"',
|
||||
escapeChar: this.escapeChar,
|
||||
header: this.header,
|
||||
delimiter: this.delimiter
|
||||
}
|
||||
try {
|
||||
const start = new Date()
|
||||
const parseResult = await csv.parse(this.file, config)
|
||||
const end = new Date()
|
||||
this.previewData = parseResult.data
|
||||
this.delimiter = parseResult.delimiter
|
||||
|
||||
// In parseResult.messages we can get parse errors
|
||||
this.importCsvMessages = parseResult.messages || []
|
||||
|
||||
if (!parseResult.hasErrors) {
|
||||
this.importCsvMessages.push({
|
||||
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
this.importCsvMessages = [{
|
||||
message: err,
|
||||
type: 'error'
|
||||
}]
|
||||
}
|
||||
},
|
||||
|
||||
loadDb (file) {
|
||||
this.newDb = database.getNewDatabase()
|
||||
return Promise.all([this.newDb.loadDb(file), this.animationPromise])
|
||||
.then(([schema]) => {
|
||||
this.schema = schema
|
||||
this.finish()
|
||||
})
|
||||
},
|
||||
|
||||
async loadFromCsv (file) {
|
||||
this.disableDialog = true
|
||||
const config = {
|
||||
quoteChar: this.quoteChar || '"',
|
||||
escapeChar: this.escapeChar,
|
||||
header: this.header,
|
||||
delimiter: this.delimiter
|
||||
}
|
||||
const parseCsvMsg = {
|
||||
message: 'Parsing CSV...',
|
||||
type: 'info'
|
||||
}
|
||||
this.importCsvMessages.push(parseCsvMsg)
|
||||
const parseCsvLoadingIndicator = setTimeout(() => { parseCsvMsg.type = 'loading' }, 1000)
|
||||
|
||||
const importMsg = {
|
||||
message: 'Importing CSV into a SQLite database...',
|
||||
type: 'info'
|
||||
}
|
||||
let importLoadingIndicator = null
|
||||
|
||||
const updateProgress = progress => {
|
||||
this.$set(importMsg, 'progress', progress)
|
||||
}
|
||||
this.newDb = database.getNewDatabase()
|
||||
const progressCounterId = this.newDb.createProgressCounter(updateProgress)
|
||||
|
||||
try {
|
||||
let start = new Date()
|
||||
const parseResult = await csv.parse(this.file, config)
|
||||
let end = new Date()
|
||||
|
||||
if (!parseResult.hasErrors) {
|
||||
const rowCount = parseResult.data.values.length
|
||||
let period = time.getPeriod(start, end)
|
||||
parseCsvMsg.type = 'success'
|
||||
|
||||
if (parseResult.messages.length > 0) {
|
||||
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
||||
parseCsvMsg.message = `${rowCount} rows are parsed in ${period}.`
|
||||
} else {
|
||||
// Inform about csv parsing success
|
||||
parseCsvMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
|
||||
}
|
||||
|
||||
// Loading indicator for csv parsing is not needed anymore
|
||||
clearTimeout(parseCsvLoadingIndicator)
|
||||
|
||||
// Add info about import start
|
||||
this.importCsvMessages.push(importMsg)
|
||||
|
||||
// Show import progress after 1 second
|
||||
importLoadingIndicator = setTimeout(() => {
|
||||
importMsg.type = 'loading'
|
||||
}, 1000)
|
||||
|
||||
// Create db with csv table and get schema
|
||||
const name = file.name.replace(/\.[^.]+$/, '')
|
||||
start = new Date()
|
||||
this.schema = await this.newDb.importDb(name, parseResult.data, progressCounterId)
|
||||
end = new Date()
|
||||
|
||||
// Inform about import success
|
||||
period = time.getPeriod(start, end)
|
||||
importMsg.message = `Importing CSV into a SQLite database is completed in ${period}.`
|
||||
importMsg.type = 'success'
|
||||
|
||||
// Loading indicator for import is not needed anymore
|
||||
clearTimeout(importLoadingIndicator)
|
||||
|
||||
this.importCsvCompleted = true
|
||||
} else {
|
||||
parseCsvMsg.message = 'Parsing ended with errors.'
|
||||
parseCsvMsg.type = 'info'
|
||||
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
||||
}
|
||||
} catch (err) {
|
||||
if (parseCsvMsg.type === 'loading') {
|
||||
parseCsvMsg.type = 'info'
|
||||
}
|
||||
|
||||
if (importMsg.type === 'loading') {
|
||||
importMsg.type = 'info'
|
||||
}
|
||||
|
||||
this.importCsvMessages.push({
|
||||
message: err,
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
|
||||
clearTimeout(parseCsvLoadingIndicator)
|
||||
clearTimeout(importLoadingIndicator)
|
||||
this.newDb.deleteProgressCounter(progressCounterId)
|
||||
this.disableDialog = false
|
||||
},
|
||||
|
||||
async checkFile (file) {
|
||||
this.state = 'dropping'
|
||||
if (fIo.isDatabase(file)) {
|
||||
this.loadDb(file)
|
||||
} else {
|
||||
this.file = file
|
||||
this.header = true
|
||||
this.quoteChar = '"'
|
||||
this.escapeChar = '"'
|
||||
this.delimiter = ''
|
||||
return Promise.all([this.previewCSV(), this.animationPromise])
|
||||
.then(() => {
|
||||
this.$modal.show('parse')
|
||||
})
|
||||
}
|
||||
},
|
||||
browse () {
|
||||
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
|
||||
.then(this.checkFile)
|
||||
},
|
||||
|
||||
drop (event) {
|
||||
this.checkFile(event.dataTransfer.files[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.db-uploader-container {
|
||||
position: relative;
|
||||
}
|
||||
.drop-area-container {
|
||||
display: inline-block;
|
||||
border: 1px dashed var(--color-border);
|
||||
padding: 8px;
|
||||
border-radius: var(--border-radius-big);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.drop-area {
|
||||
background-color: var(--color-bg-light-3);
|
||||
border-radius: var(--border-radius-big);
|
||||
color: var(--color-text-base);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#img-container {
|
||||
position: absolute;
|
||||
top: 54px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
width: 450px;
|
||||
height: 338px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#drop-file-top-img {
|
||||
width: 450px;
|
||||
height: 175px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
#drop-file-bottom-img {
|
||||
width: 450px;
|
||||
height: 167px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
#body-img {
|
||||
width: 74px;
|
||||
position: absolute;
|
||||
top: 94.05px;
|
||||
left: 46px;
|
||||
}
|
||||
#right-arm-img {
|
||||
width: 106px;
|
||||
position: absolute;
|
||||
top: 110.05px;
|
||||
left: 78px;
|
||||
}
|
||||
#left-arm-img {
|
||||
width: 114px;
|
||||
position: absolute;
|
||||
top: 69.05px;
|
||||
left: 69px;
|
||||
}
|
||||
#file-img {
|
||||
width: 125px;
|
||||
position: absolute;
|
||||
top: 15.66px;
|
||||
left: 152px;
|
||||
}
|
||||
|
||||
.swing {
|
||||
animation: swing ease-in-out 0.6s infinite alternate;
|
||||
}
|
||||
#left-arm-img.swing {
|
||||
transform-origin: 9px 83px;
|
||||
}
|
||||
#right-arm-img.swing {
|
||||
transform-origin: 0 56px;
|
||||
}
|
||||
#file-img.swing {
|
||||
transform-origin: -74px 139px;
|
||||
}
|
||||
@keyframes swing {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(-7deg); }
|
||||
}
|
||||
|
||||
#file-img.fly {
|
||||
animation: fly ease-in-out 1s 1 normal;
|
||||
transform-origin: center center;
|
||||
}
|
||||
@keyframes fly {
|
||||
100% {
|
||||
transform: rotate(360deg) scale(0.5);
|
||||
top: 183px;
|
||||
left: 225px;
|
||||
}
|
||||
}
|
||||
|
||||
#file-img.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Parse CSV dialog */
|
||||
.chars {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.char-input {
|
||||
margin-right: 44px;
|
||||
}
|
||||
.preview-table {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.import-csv-errors {
|
||||
height: 160px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
.no-data {
|
||||
margin-top: 32px;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
border: 1px solid var(--color-border-light);
|
||||
box-sizing: border-box;
|
||||
height: 160px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-base);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -39,13 +39,15 @@ export default class Sql {
|
||||
return this.db.exec(sql, params)
|
||||
}
|
||||
|
||||
import (columns, values, progressCounterId, progressCallback, chunkSize = 1500) {
|
||||
this.createDb()
|
||||
this.db.exec(dbUtils.getCreateStatement(columns, values))
|
||||
import (tabName, columns, values, progressCounterId, progressCallback, chunkSize = 1500) {
|
||||
if (this.db === null) {
|
||||
this.createDb()
|
||||
}
|
||||
this.db.exec(dbUtils.getCreateStatement(tabName, columns, values))
|
||||
const chunks = dbUtils.generateChunks(values, chunkSize)
|
||||
const chunksAmount = Math.ceil(values.length / chunkSize)
|
||||
let count = 0
|
||||
const insertStr = dbUtils.getInsertStmt(columns)
|
||||
const insertStr = dbUtils.getInsertStmt(tabName, columns)
|
||||
const insertStmt = this.db.prepare(insertStr)
|
||||
|
||||
progressCallback({ progress: 0, id: progressCounterId })
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import sqliteParser from 'sqlite-parser'
|
||||
|
||||
export default {
|
||||
* generateChunks (arr, size) {
|
||||
const count = Math.ceil(arr.length / size)
|
||||
@@ -9,14 +11,14 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
getInsertStmt (columns) {
|
||||
getInsertStmt (tabName, columns) {
|
||||
const colList = `"${columns.join('", "')}"`
|
||||
const params = columns.map(() => '?').join(', ')
|
||||
return `INSERT INTO csv_import (${colList}) VALUES (${params});`
|
||||
return `INSERT INTO "${tabName}" (${colList}) VALUES (${params});`
|
||||
},
|
||||
|
||||
getCreateStatement (columns, values) {
|
||||
let result = 'CREATE table csv_import('
|
||||
getCreateStatement (tabName, columns, values) {
|
||||
let result = `CREATE table "${tabName}"(`
|
||||
columns.forEach((col, index) => {
|
||||
// Get the first row of values to determine types
|
||||
const value = values[0][index]
|
||||
@@ -40,5 +42,49 @@ export default {
|
||||
})
|
||||
result = result.replace(/,\s$/, ');')
|
||||
return result
|
||||
},
|
||||
|
||||
getAst (sql) {
|
||||
// There is a bug is sqlite-parser
|
||||
// It throws an error if tokenizer has an arguments:
|
||||
// https://github.com/codeschool/sqlite-parser/issues/59
|
||||
const fixedSql = sql
|
||||
.replace(/(tokenize=[^,]+)"tokenchars=.+?"/, '$1')
|
||||
.replace(/(tokenize=[^,]+)"remove_diacritics=.+?"/, '$1')
|
||||
.replace(/(tokenize=[^,]+)"separators=.+?"/, '$1')
|
||||
.replace(/tokenize=.+?(,|\))/, 'tokenize=unicode61$1')
|
||||
|
||||
return sqliteParser(fixedSql)
|
||||
},
|
||||
|
||||
/*
|
||||
* Return an array of columns with name and type. E.g.:
|
||||
* [
|
||||
* { name: 'id', type: 'INTEGER' },
|
||||
* { name: 'title', type: 'NVARCHAR(30)' },
|
||||
* ]
|
||||
*/
|
||||
getColumns (sql) {
|
||||
const columns = []
|
||||
const ast = this.getAst(sql)
|
||||
|
||||
const columnDefinition = ast.statement[0].format === 'table'
|
||||
? ast.statement[0].definition
|
||||
: ast.statement[0].result.args.expression // virtual table
|
||||
|
||||
columnDefinition.forEach(item => {
|
||||
if (item.variant === 'column' && ['identifier', 'definition'].includes(item.type)) {
|
||||
let type = item.datatype ? item.datatype.variant : 'N/A'
|
||||
if (item.datatype && item.datatype.args) {
|
||||
type = type + '(' + item.datatype.args.expression[0].value
|
||||
if (item.datatype.args.expression.length === 2) {
|
||||
type = type + ', ' + item.datatype.args.expression[1].value
|
||||
}
|
||||
type = type + ')'
|
||||
}
|
||||
columns.push({ name: item.name, type: type })
|
||||
}
|
||||
})
|
||||
return columns
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,18 @@ function processMsg (sql) {
|
||||
switch (data && data.action) {
|
||||
case 'open':
|
||||
return sql.open(data.buffer)
|
||||
case 'reopen':
|
||||
return sql.open(sql.export())
|
||||
case 'exec':
|
||||
return sql.exec(data.sql, data.params)
|
||||
case 'import':
|
||||
return sql.import(data.columns, data.values, data.progressCounterId, postMessage)
|
||||
return sql.import(
|
||||
data.tabName,
|
||||
data.columns,
|
||||
data.values,
|
||||
data.progressCounterId,
|
||||
postMessage
|
||||
)
|
||||
case 'export':
|
||||
return sql.export()
|
||||
case 'close':
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import sqliteParser from 'sqlite-parser'
|
||||
import stms from './_statements'
|
||||
import fu from '@/lib/utils/fileIo'
|
||||
// We can import workers like so because of worker-loader:
|
||||
// https://webpack.js.org/loaders/worker-loader/
|
||||
@@ -20,6 +20,8 @@ export default {
|
||||
let progressCounterIds = 0
|
||||
class Database {
|
||||
constructor (worker) {
|
||||
this.dbName = null
|
||||
this.schema = null
|
||||
this.worker = worker
|
||||
this.pw = new PromiseWorker(worker)
|
||||
|
||||
@@ -50,19 +52,20 @@ class Database {
|
||||
delete this.importProgresses[id]
|
||||
}
|
||||
|
||||
async importDb (name, data, progressCounterId) {
|
||||
async addTableFromCsv (tabName, data, progressCounterId) {
|
||||
const result = await this.pw.postMessage({
|
||||
action: 'import',
|
||||
columns: data.columns,
|
||||
values: data.values,
|
||||
progressCounterId
|
||||
progressCounterId,
|
||||
tabName
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
|
||||
return await this.getSchema(name)
|
||||
this.dbName = this.dbName || 'database'
|
||||
this.refreshSchema()
|
||||
}
|
||||
|
||||
async loadDb (file) {
|
||||
@@ -73,11 +76,11 @@ class Database {
|
||||
throw new Error(res.error)
|
||||
}
|
||||
|
||||
const dbName = file ? file.name.replace(/\.[^.]+$/, '') : 'database'
|
||||
return this.getSchema(dbName)
|
||||
this.dbName = file ? fu.getFileName(file) : 'database'
|
||||
this.refreshSchema()
|
||||
}
|
||||
|
||||
async getSchema (name) {
|
||||
async refreshSchema () {
|
||||
const getSchemaSql = `
|
||||
SELECT name, sql
|
||||
FROM sqlite_master
|
||||
@@ -90,19 +93,17 @@ class Database {
|
||||
result.values.forEach(item => {
|
||||
parsedSchema.push({
|
||||
name: item[0],
|
||||
columns: getColumns(item[1])
|
||||
columns: stms.getColumns(item[1])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Return db name and schema
|
||||
return {
|
||||
dbName: name,
|
||||
schema: parsedSchema
|
||||
}
|
||||
// Refresh schema
|
||||
this.schema = parsedSchema
|
||||
}
|
||||
|
||||
async execute (commands) {
|
||||
await this.pw.postMessage({ action: 'reopen' })
|
||||
const results = await this.pw.postMessage({ action: 'exec', sql: commands })
|
||||
|
||||
if (results.error) {
|
||||
@@ -120,48 +121,27 @@ class Database {
|
||||
}
|
||||
fu.exportToFile(data, fileName)
|
||||
}
|
||||
}
|
||||
|
||||
function getAst (sql) {
|
||||
// There is a bug is sqlite-parser
|
||||
// It throws an error if tokenizer has an arguments:
|
||||
// https://github.com/codeschool/sqlite-parser/issues/59
|
||||
const fixedSql = sql
|
||||
.replace(/(tokenize=[^,]+)"tokenchars=.+?"/, '$1')
|
||||
.replace(/(tokenize=[^,]+)"remove_diacritics=.+?"/, '$1')
|
||||
.replace(/(tokenize=[^,]+)"separators=.+?"/, '$1')
|
||||
.replace(/tokenize=.+?(,|\))/, 'tokenize=unicode61$1')
|
||||
|
||||
return sqliteParser(fixedSql)
|
||||
}
|
||||
|
||||
/*
|
||||
* Return an array of columns with name and type. E.g.:
|
||||
* [
|
||||
* { name: 'id', type: 'INTEGER' },
|
||||
* { name: 'title', type: 'NVARCHAR(30)' },
|
||||
* ]
|
||||
*/
|
||||
function getColumns (sql) {
|
||||
const columns = []
|
||||
const ast = getAst(sql)
|
||||
|
||||
const columnDefinition = ast.statement[0].format === 'table'
|
||||
? ast.statement[0].definition
|
||||
: ast.statement[0].result.args.expression // virtual table
|
||||
|
||||
columnDefinition.forEach(item => {
|
||||
if (item.variant === 'column' && ['identifier', 'definition'].includes(item.type)) {
|
||||
let type = item.datatype ? item.datatype.variant : 'N/A'
|
||||
if (item.datatype && item.datatype.args) {
|
||||
type = type + '(' + item.datatype.args.expression[0].value
|
||||
if (item.datatype.args.expression.length === 2) {
|
||||
type = type + ', ' + item.datatype.args.expression[1].value
|
||||
}
|
||||
type = type + ')'
|
||||
}
|
||||
columns.push({ name: item.name, type: type })
|
||||
async validateTableName (name) {
|
||||
if (name.startsWith('sqlite_')) {
|
||||
throw new Error("Table name can't start with sqlite_")
|
||||
}
|
||||
})
|
||||
return columns
|
||||
|
||||
if (/[^\w]/.test(name)) {
|
||||
throw new Error('Table name can contain only letters, digits and underscores')
|
||||
}
|
||||
|
||||
if (/^(\d)/.test(name)) {
|
||||
throw new Error("Table name can't start with a digit")
|
||||
}
|
||||
|
||||
await this.execute(`BEGIN; CREATE TABLE "${name}"(id); ROLLBACK;`)
|
||||
}
|
||||
|
||||
sanitizeTableName (tabName) {
|
||||
return tabName
|
||||
.replace(/[^\w]/g, '_') // replace everything that is not letter, digit or _ with _
|
||||
.replace(/^(\d)/, '_$1') // add _ at beginning if starts with digit
|
||||
.replace(/_{2,}/g, '_') // replace multiple _ with one _
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ export default {
|
||||
: /\.(db|sqlite(3)?)+$/.test(file.name)
|
||||
},
|
||||
|
||||
getFileName (file) {
|
||||
return file.name.replace(/\.[^.]+$/, '')
|
||||
},
|
||||
|
||||
exportToFile (str, fileName, type = 'octet/stream') {
|
||||
// Create downloader
|
||||
const downloader = document.createElement('a')
|
||||
|
||||
@@ -3,5 +3,13 @@ export default {
|
||||
const diff = end.getTime() - start.getTime()
|
||||
const seconds = diff / 1000
|
||||
return seconds.toFixed(3) + 's'
|
||||
},
|
||||
|
||||
debounce (func, ms) {
|
||||
let timeout
|
||||
return function () {
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(() => func.apply(this, arguments), ms)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,6 @@ export default {
|
||||
}
|
||||
state.db = db
|
||||
},
|
||||
saveSchema (state, { dbName, schema }) {
|
||||
state.dbName = dbName
|
||||
state.schema = schema
|
||||
},
|
||||
|
||||
updateTab (state, { index, name, id, query, chart, isUnsaved }) {
|
||||
const tab = state.tabs[index]
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
export default {
|
||||
schema: null,
|
||||
dbFile: null,
|
||||
dbName: null,
|
||||
tabs: [],
|
||||
currentTab: null,
|
||||
currentTabId: null,
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
</div>
|
||||
<db-uploader id="db-edit" type="small" />
|
||||
<export-icon tooltip="Export database" @click="exportToFile"/>
|
||||
<add-table-icon @click="addCsv"/>
|
||||
</div>
|
||||
<div v-show="schemaVisible" class="schema">
|
||||
<table-description
|
||||
@@ -19,15 +20,26 @@
|
||||
:columns="table.columns"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!--Parse csv dialog -->
|
||||
<csv-import
|
||||
ref="addCsv"
|
||||
:file="file"
|
||||
:db="$store.state.db"
|
||||
dialog-name="addCsv"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import fIo from '@/lib/utils/fileIo'
|
||||
import TableDescription from './TableDescription'
|
||||
import TextField from '@/components/TextField'
|
||||
import TreeChevron from '@/components/svg/treeChevron'
|
||||
import DbUploader from '@/components/DbUploader'
|
||||
import ExportIcon from '@/components/svg/export'
|
||||
import AddTableIcon from '@/components/svg/addTable'
|
||||
import CsvImport from '@/components/CsvImport'
|
||||
|
||||
export default {
|
||||
name: 'Schema',
|
||||
@@ -36,33 +48,44 @@ export default {
|
||||
TextField,
|
||||
TreeChevron,
|
||||
DbUploader,
|
||||
ExportIcon
|
||||
ExportIcon,
|
||||
AddTableIcon,
|
||||
CsvImport
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
schemaVisible: true,
|
||||
filter: null
|
||||
filter: null,
|
||||
file: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
schema () {
|
||||
if (!this.$store.state.schema) {
|
||||
if (!this.$store.state.db.schema) {
|
||||
return []
|
||||
}
|
||||
|
||||
return !this.filter
|
||||
? this.$store.state.schema
|
||||
: this.$store.state.schema.filter(
|
||||
? this.$store.state.db.schema
|
||||
: this.$store.state.db.schema.filter(
|
||||
table => table.name.toUpperCase().indexOf(this.filter.toUpperCase()) !== -1
|
||||
)
|
||||
},
|
||||
dbName () {
|
||||
return this.$store.state.dbName
|
||||
return this.$store.state.db.dbName
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
exportToFile () {
|
||||
this.$store.state.db.export(`${this.dbName}.sqlite`)
|
||||
},
|
||||
async addCsv () {
|
||||
this.file = await fIo.getFileFromUser('.csv')
|
||||
await this.$nextTick()
|
||||
const csvImport = this.$refs.addCsv
|
||||
csvImport.reset()
|
||||
await csvImport.previewCsv()
|
||||
csvImport.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,15 +17,15 @@ export function getHints (cm, options) {
|
||||
const hintOptions = {
|
||||
get tables () {
|
||||
const tables = {}
|
||||
if (store.state.schema) {
|
||||
store.state.schema.forEach(table => {
|
||||
if (store.state.db.schema) {
|
||||
store.state.db.schema.forEach(table => {
|
||||
tables[table.name] = table.columns.map(column => column.name)
|
||||
})
|
||||
}
|
||||
return tables
|
||||
},
|
||||
get defaultTable () {
|
||||
const schema = store.state.schema
|
||||
const schema = store.state.db.schema
|
||||
return schema && schema.length === 1 ? schema[0].name : null
|
||||
},
|
||||
completeSingle: false,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<script>
|
||||
import showHint, { showHintOnDemand } from './hint'
|
||||
import { debounce } from 'debounce'
|
||||
import time from '@/lib/utils/time'
|
||||
import { codemirror } from 'vue-codemirror'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import 'codemirror/mode/sql/sql.js'
|
||||
@@ -39,7 +39,7 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChange: debounce(showHint, 400),
|
||||
onChange: time.debounce(showHint, 400),
|
||||
focus () {
|
||||
this.$refs.cm.codemirror.focus()
|
||||
}
|
||||
|
||||
@@ -120,14 +120,13 @@ export default {
|
||||
const start = new Date()
|
||||
this.result = await state.db.execute(this.query + ';')
|
||||
this.time = time.getPeriod(start, new Date())
|
||||
const schema = await state.db.getSchema(state.dbName)
|
||||
this.$store.commit('saveSchema', schema)
|
||||
} catch (err) {
|
||||
this.error = {
|
||||
type: 'error',
|
||||
message: err
|
||||
}
|
||||
}
|
||||
state.db.refreshSchema()
|
||||
this.isGettingResults = false
|
||||
},
|
||||
handleResize () {
|
||||
@@ -143,12 +142,12 @@ export default {
|
||||
calculateTableHeight () {
|
||||
const bottomPane = this.$refs.bottomPane
|
||||
// 88 - view swittcher height
|
||||
// 42 - table footer width
|
||||
// 30 - desirable space after the table
|
||||
// 34 - table footer width
|
||||
// 12 - desirable space after the table
|
||||
// 5 - padding-bottom of rounded table container
|
||||
// 40 - height of table header
|
||||
const freeSpace = bottomPane.offsetHeight - 88 - 42 - 30 - 5 - 40
|
||||
this.tableViewHeight = freeSpace - (freeSpace % 40)
|
||||
// 35 - height of table header
|
||||
const freeSpace = bottomPane.offsetHeight - 88 - 34 - 12 - 5 - 35
|
||||
this.tableViewHeight = freeSpace - (freeSpace % 35)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,11 +30,10 @@ export default {
|
||||
Tabs
|
||||
},
|
||||
async beforeRouteEnter (to, from, next) {
|
||||
if (!store.state.schema) {
|
||||
if (!store.state.db) {
|
||||
const newDb = database.getNewDatabase()
|
||||
const newSchema = await newDb.loadDb()
|
||||
await newDb.loadDb()
|
||||
store.commit('setDb', newDb)
|
||||
store.commit('saveSchema', newSchema)
|
||||
const stmt = [
|
||||
'/*',
|
||||
' * Your database is empty. In order to start building charts',
|
||||
|
||||
@@ -100,7 +100,7 @@ export default {
|
||||
}
|
||||
},
|
||||
runDisabled () {
|
||||
return this.currentQuery && (!this.$store.state.schema || !this.currentQuery.query)
|
||||
return this.currentQuery && (!this.$store.state.db || !this.currentQuery.query)
|
||||
}
|
||||
},
|
||||
created () {
|
||||
|
||||
@@ -1,172 +1,26 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import Vuex from 'vuex'
|
||||
import { shallowMount, mount } from '@vue/test-utils'
|
||||
import DbUploader from '@/components/DbUploader'
|
||||
import fu from '@/lib/utils/fileIo'
|
||||
import database from '@/lib/database'
|
||||
import csv from '@/components/DbUploader/csv'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import CsvImport from '@/components/CsvImport'
|
||||
import csv from '@/components/CsvImport/csv'
|
||||
|
||||
describe('DbUploader.vue', () => {
|
||||
describe('CsvImport.vue', () => {
|
||||
let state = {}
|
||||
let mutations = {}
|
||||
let store = {}
|
||||
let place
|
||||
|
||||
beforeEach(() => {
|
||||
// mock store state and mutations
|
||||
state = {}
|
||||
mutations = {
|
||||
saveSchema: sinon.stub(),
|
||||
setDb: sinon.stub()
|
||||
}
|
||||
store = new Vuex.Store({ state, mutations })
|
||||
|
||||
place = document.createElement('div')
|
||||
document.body.appendChild(place)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
place.remove()
|
||||
})
|
||||
|
||||
it('loads db on click and redirects to /editor', async () => {
|
||||
// mock getting a file from user
|
||||
const file = { name: 'test.db' }
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock db loading
|
||||
const schema = {}
|
||||
const db = {
|
||||
loadDb: sinon.stub().resolves(schema)
|
||||
}
|
||||
sinon.stub(database, 'getNewDatabase').returns(db)
|
||||
|
||||
// mock router
|
||||
const $router = { push: sinon.stub() }
|
||||
const $route = { path: '/' }
|
||||
|
||||
// mount the component
|
||||
const wrapper = shallowMount(DbUploader, {
|
||||
attachTo: place,
|
||||
store,
|
||||
mocks: { $router, $route },
|
||||
propsData: {
|
||||
type: 'illustrated'
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
|
||||
await db.loadDb.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(mutations.saveSchema.calledOnceWith(state, schema)).to.equal(true)
|
||||
expect($router.push.calledOnceWith('/editor')).to.equal(true)
|
||||
wrapper.destroy()
|
||||
})
|
||||
|
||||
it('loads db on drop and redirects to /editor', async () => {
|
||||
// mock db loading
|
||||
const schema = {}
|
||||
const db = {
|
||||
loadDb: sinon.stub().resolves(schema)
|
||||
}
|
||||
sinon.stub(database, 'getNewDatabase').returns(db)
|
||||
|
||||
// mock router
|
||||
const $router = { push: sinon.stub() }
|
||||
const $route = { path: '/' }
|
||||
|
||||
// mount the component
|
||||
const wrapper = shallowMount(DbUploader, {
|
||||
attachTo: place,
|
||||
store,
|
||||
mocks: { $router, $route },
|
||||
propsData: {
|
||||
type: 'illustrated'
|
||||
}
|
||||
})
|
||||
|
||||
// mock a file dropped by a user
|
||||
const file = { name: 'test.db' }
|
||||
const dropData = { dataTransfer: new DataTransfer() }
|
||||
Object.defineProperty(dropData.dataTransfer, 'files', {
|
||||
value: [file],
|
||||
writable: false
|
||||
})
|
||||
|
||||
await wrapper.find('.drop-area').trigger('drop', dropData)
|
||||
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
|
||||
await db.loadDb.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(mutations.saveSchema.calledOnceWith(state, schema)).to.equal(true)
|
||||
expect($router.push.calledOnceWith('/editor')).to.equal(true)
|
||||
wrapper.destroy()
|
||||
})
|
||||
|
||||
it("doesn't redirect if already on /editor", async () => {
|
||||
// mock getting a file from user
|
||||
const file = { name: 'test.db' }
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock db loading
|
||||
const schema = {}
|
||||
const db = {
|
||||
loadDb: sinon.stub().resolves(schema)
|
||||
}
|
||||
sinon.stub(database, 'getNewDatabase').returns(db)
|
||||
|
||||
// mock router
|
||||
const $router = { push: sinon.stub() }
|
||||
const $route = { path: '/editor' }
|
||||
|
||||
// mount the component
|
||||
const wrapper = shallowMount(DbUploader, {
|
||||
attachTo: place,
|
||||
store,
|
||||
mocks: { $router, $route },
|
||||
propsData: {
|
||||
type: 'illustrated'
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await db.loadDb.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
expect($router.push.called).to.equal(false)
|
||||
wrapper.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('DbUploader.vue import CSV', () => {
|
||||
let state = {}
|
||||
let mutations = {}
|
||||
let actions = {}
|
||||
const newTabId = 1
|
||||
let mutations = {}
|
||||
let store = {}
|
||||
let place
|
||||
|
||||
// mock router
|
||||
const $router = { }
|
||||
const $route = { path: '/' }
|
||||
|
||||
let clock
|
||||
let wrapper
|
||||
const newTabId = 1
|
||||
const file = { name: 'my data.csv' }
|
||||
|
||||
beforeEach(() => {
|
||||
// mock getting a file from user
|
||||
sinon.stub(fu, 'getFileFromUser').resolves({ type: 'text/csv', name: 'foo.csv' })
|
||||
|
||||
clock = sinon.useFakeTimers()
|
||||
|
||||
// mock store state and mutations
|
||||
state = {}
|
||||
mutations = {
|
||||
saveSchema: sinon.stub(),
|
||||
setDb: sinon.stub(),
|
||||
setCurrentTabId: sinon.stub()
|
||||
}
|
||||
@@ -175,29 +29,32 @@ describe('DbUploader.vue import CSV', () => {
|
||||
}
|
||||
store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
$router.push = sinon.stub()
|
||||
|
||||
place = document.createElement('div')
|
||||
document.body.appendChild(place)
|
||||
const db = {
|
||||
sanitizeTableName: sinon.stub().returns('my_data'),
|
||||
addTableFromCsv: sinon.stub().resolves(),
|
||||
createProgressCounter: sinon.stub().returns(1),
|
||||
deleteProgressCounter: sinon.stub(),
|
||||
validateTableName: sinon.stub().resolves(),
|
||||
execute: sinon.stub().resolves(),
|
||||
refreshSchema: sinon.stub().resolves()
|
||||
}
|
||||
|
||||
// mount the component
|
||||
wrapper = mount(DbUploader, {
|
||||
attachTo: place,
|
||||
wrapper = mount(CsvImport, {
|
||||
store,
|
||||
mocks: { $router, $route },
|
||||
propsData: {
|
||||
type: 'illustrated'
|
||||
file,
|
||||
dialogName: 'addCsv',
|
||||
db
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
wrapper.destroy()
|
||||
place.remove()
|
||||
})
|
||||
|
||||
it('shows parse dialog if gets csv file', async () => {
|
||||
it('previews', async () => {
|
||||
sinon.stub(csv, 'parse').resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
@@ -216,12 +73,11 @@ describe('DbUploader.vue import CSV', () => {
|
||||
}]
|
||||
})
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
wrapper.vm.previewCsv()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('[data-modal="parse"]').exists()).to.equal(true)
|
||||
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(true)
|
||||
expect(wrapper.find('#csv-table-name input').element.value).to.equal('my_data')
|
||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.value).to.equal('|')
|
||||
expect(wrapper.find('#quote-char input').element.value).to.equal('"')
|
||||
expect(wrapper.find('#escape-char input').element.value).to.equal('"')
|
||||
@@ -252,10 +108,9 @@ describe('DbUploader.vue import CSV', () => {
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.open()
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
parse.onCall(1).resolves({
|
||||
@@ -362,17 +217,18 @@ describe('DbUploader.vue import CSV', () => {
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
let resolveParsing
|
||||
parse.onCall(1).returns(new Promise(resolve => {
|
||||
resolveParsing = resolve
|
||||
}))
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// "Parsing CSV..." in the logs
|
||||
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(1).text())
|
||||
@@ -430,12 +286,16 @@ describe('DbUploader.vue import CSV', () => {
|
||||
messages: []
|
||||
})
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
let resolveImport
|
||||
wrapper.vm.db.addTableFromCsv.onCall(0).returns(new Promise(resolve => {
|
||||
resolveImport = resolve
|
||||
}))
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
@@ -454,6 +314,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||
await resolveImport()
|
||||
})
|
||||
|
||||
it('parsing is completed with notes', async () => {
|
||||
@@ -488,12 +349,16 @@ describe('DbUploader.vue import CSV', () => {
|
||||
}]
|
||||
})
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
let resolveImport
|
||||
wrapper.vm.db.addTableFromCsv.onCall(0).returns(new Promise(resolve => {
|
||||
resolveImport = resolve
|
||||
}))
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
@@ -514,6 +379,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||
await resolveImport()
|
||||
})
|
||||
|
||||
it('parsing is completed with errors', async () => {
|
||||
@@ -548,12 +414,11 @@ describe('DbUploader.vue import CSV', () => {
|
||||
}]
|
||||
})
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
@@ -604,19 +469,14 @@ describe('DbUploader.vue import CSV', () => {
|
||||
})
|
||||
|
||||
let resolveImport = sinon.stub()
|
||||
const newDb = {
|
||||
importDb: sinon.stub().resolves(new Promise(resolve => { resolveImport = resolve })),
|
||||
createProgressCounter: sinon.stub().returns(1),
|
||||
deleteProgressCounter: sinon.stub()
|
||||
}
|
||||
sinon.stub(database, 'getNewDatabase').returns(newDb)
|
||||
wrapper.vm.db.addTableFromCsv = sinon.stub()
|
||||
.resolves(new Promise(resolve => { resolveImport = resolve }))
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
@@ -641,11 +501,11 @@ describe('DbUploader.vue import CSV', () => {
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||
expect(newDb.importDb.getCall(0).args[0]).to.equal('foo') // file name
|
||||
expect(wrapper.vm.db.addTableFromCsv.getCall(0).args[0]).to.equal('foo') // table name
|
||||
|
||||
// After resolving - loading indicator is not shown
|
||||
await resolveImport()
|
||||
await newDb.importDb.returnValues[0]
|
||||
await wrapper.vm.db.addTableFromCsv.returnValues[0]
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(false)
|
||||
@@ -678,20 +538,11 @@ describe('DbUploader.vue import CSV', () => {
|
||||
messages: []
|
||||
})
|
||||
|
||||
const schema = {}
|
||||
const newDb = {
|
||||
importDb: sinon.stub().resolves(schema),
|
||||
createProgressCounter: sinon.stub().returns(1),
|
||||
deleteProgressCounter: sinon.stub()
|
||||
}
|
||||
sinon.stub(database, 'getNewDatabase').returns(newDb)
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
@@ -739,19 +590,13 @@ describe('DbUploader.vue import CSV', () => {
|
||||
messages: []
|
||||
})
|
||||
|
||||
const newDb = {
|
||||
importDb: sinon.stub().rejects(new Error('fail')),
|
||||
createProgressCounter: sinon.stub().returns(1),
|
||||
deleteProgressCounter: sinon.stub()
|
||||
}
|
||||
sinon.stub(database, 'getNewDatabase').returns(newDb)
|
||||
wrapper.vm.db.addTableFromCsv = sinon.stub().rejects(new Error('fail'))
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
@@ -773,7 +618,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
})
|
||||
|
||||
it('import final', async () => {
|
||||
it('import finish', async () => {
|
||||
sinon.stub(csv, 'parse').resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
@@ -786,33 +631,20 @@ describe('DbUploader.vue import CSV', () => {
|
||||
messages: []
|
||||
})
|
||||
|
||||
const schema = {}
|
||||
const newDb = {
|
||||
importDb: sinon.stub().resolves(schema),
|
||||
createProgressCounter: sinon.stub().returns(1),
|
||||
deleteProgressCounter: sinon.stub()
|
||||
}
|
||||
sinon.stub(database, 'getNewDatabase').returns(newDb)
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-finish').trigger('click')
|
||||
|
||||
expect(mutations.setDb.calledOnceWith(state, newDb)).to.equal(true)
|
||||
expect(mutations.saveSchema.calledOnceWith(state, schema)).to.equal(true)
|
||||
expect(actions.addTab.calledOnce).to.equal(true)
|
||||
await actions.addTab.returnValues[0]
|
||||
expect(mutations.setCurrentTabId.calledOnceWith(state, newTabId)).to.equal(true)
|
||||
expect($router.push.calledOnceWith('/editor')).to.equal(true)
|
||||
expect(wrapper.find('[data-modal="parse"]').exists()).to.equal(false)
|
||||
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
|
||||
expect(wrapper.emitted('finish')).to.have.lengthOf(1)
|
||||
})
|
||||
|
||||
it('import cancel', async () => {
|
||||
@@ -828,79 +660,47 @@ describe('DbUploader.vue import CSV', () => {
|
||||
messages: []
|
||||
})
|
||||
|
||||
const schema = {}
|
||||
const newDb = {
|
||||
importDb: sinon.stub().resolves(schema),
|
||||
createProgressCounter: sinon.stub().returns(1),
|
||||
deleteProgressCounter: sinon.stub(),
|
||||
shutDown: sinon.stub()
|
||||
}
|
||||
sinon.stub(database, 'getNewDatabase').returns(newDb)
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.previewCsv()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-cancel').trigger('click')
|
||||
|
||||
expect(mutations.setDb.called).to.equal(false)
|
||||
expect(mutations.saveSchema.called).to.equal(false)
|
||||
expect(actions.addTab.called).to.equal(false)
|
||||
expect(mutations.setCurrentTabId.called).to.equal(false)
|
||||
expect($router.push.called).to.equal(false)
|
||||
expect(newDb.shutDown.calledOnce).to.equal(true)
|
||||
expect(wrapper.find('[data-modal="parse"]').exists()).to.equal(false)
|
||||
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
|
||||
expect(wrapper.vm.db.execute.calledOnceWith('DROP TABLE "my_data"')).to.equal(true)
|
||||
expect(wrapper.vm.db.refreshSchema.calledOnce).to.equal(true)
|
||||
expect(wrapper.emitted('cancel')).to.have.lengthOf(1)
|
||||
})
|
||||
|
||||
it("doesn't open new tab when load db after importing CSV", async () => {
|
||||
fu.getFileFromUser.onCall(0).resolves({ type: 'text/csv', name: 'foo.csv' })
|
||||
fu.getFileFromUser.onCall(1).resolves({ type: 'application/x-sqlite3', name: 'bar.sqlite3' })
|
||||
sinon.stub(csv, 'parse').resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
const schema = {}
|
||||
const newDb = {
|
||||
importDb: sinon.stub().resolves(schema),
|
||||
createProgressCounter: sinon.stub().returns(1),
|
||||
deleteProgressCounter: sinon.stub(),
|
||||
loadDb: sinon.stub().resolves()
|
||||
}
|
||||
sinon.stub(database, 'getNewDatabase').returns(newDb)
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
it('checks table name', async () => {
|
||||
sinon.stub(csv, 'parse').resolves()
|
||||
await wrapper.vm.previewCsv()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await clock.tick(400)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('#csv-table-name .text-field-error').text()).to.equal('')
|
||||
|
||||
wrapper.vm.db.validateTableName = sinon.stub().rejects(new Error('this is a bad table name'))
|
||||
await wrapper.find('#csv-table-name input').setValue('bar')
|
||||
await clock.tick(400)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('#csv-table-name .text-field-error').text())
|
||||
.to.equal('this is a bad table name. Try another table name.')
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('')
|
||||
await clock.tick(400)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('#csv-table-name .text-field-error').text()).to.equal('')
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-finish').trigger('click')
|
||||
|
||||
expect(actions.addTab.calledOnce).to.equal(true)
|
||||
await actions.addTab.returnValues[0]
|
||||
expect(mutations.setCurrentTabId.calledOnceWith(state, newTabId)).to.equal(true)
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await newDb.loadDb.returnValues[0]
|
||||
expect(actions.addTab.calledOnce).to.equal(true)
|
||||
expect(mutations.setCurrentTabId.calledOnce).to.equal(true)
|
||||
expect(wrapper.find('#csv-table-name .text-field-error').text())
|
||||
.to.equal("Table name can't be empty")
|
||||
expect(wrapper.vm.db.addTableFromCsv.called).to.equal(false)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from 'chai'
|
||||
import { mount, shallowMount } from '@vue/test-utils'
|
||||
import DelimiterSelector from '@/components/DbUploader/DelimiterSelector'
|
||||
import DelimiterSelector from '@/components/CsvImport/DelimiterSelector'
|
||||
|
||||
describe('DelimiterSelector', async () => {
|
||||
it('shows the name of value', async () => {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import csv from '@/components/DbUploader/csv'
|
||||
import csv from '@/components/CsvImport/csv'
|
||||
import Papa from 'papaparse'
|
||||
|
||||
describe('csv.js', () => {
|
||||
199
tests/components/DbUploader.spec.js
Normal file
199
tests/components/DbUploader.spec.js
Normal file
@@ -0,0 +1,199 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import Vuex from 'vuex'
|
||||
import { shallowMount, mount } from '@vue/test-utils'
|
||||
import DbUploader from '@/components/DbUploader'
|
||||
import fu from '@/lib/utils/fileIo'
|
||||
import database from '@/lib/database'
|
||||
|
||||
describe('DbUploader.vue', () => {
|
||||
let state = {}
|
||||
let mutations = {}
|
||||
let store = {}
|
||||
let place
|
||||
|
||||
beforeEach(() => {
|
||||
// mock store state and mutations
|
||||
state = {}
|
||||
mutations = {
|
||||
setDb: sinon.stub()
|
||||
}
|
||||
store = new Vuex.Store({ state, mutations })
|
||||
|
||||
place = document.createElement('div')
|
||||
document.body.appendChild(place)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
place.remove()
|
||||
})
|
||||
|
||||
it('loads db on click and redirects to /editor', async () => {
|
||||
// mock getting a file from user
|
||||
const file = { name: 'test.db' }
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock db loading
|
||||
const db = {
|
||||
loadDb: sinon.stub().resolves()
|
||||
}
|
||||
sinon.stub(database, 'getNewDatabase').returns(db)
|
||||
|
||||
// mock router
|
||||
const $router = { push: sinon.stub() }
|
||||
const $route = { path: '/' }
|
||||
|
||||
// mount the component
|
||||
const wrapper = shallowMount(DbUploader, {
|
||||
attachTo: place,
|
||||
store,
|
||||
mocks: { $router, $route },
|
||||
propsData: {
|
||||
type: 'illustrated'
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
|
||||
await db.loadDb.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
expect($router.push.calledOnceWith('/editor')).to.equal(true)
|
||||
wrapper.destroy()
|
||||
})
|
||||
|
||||
it('loads db on drop and redirects to /editor', async () => {
|
||||
// mock db loading
|
||||
const db = {
|
||||
loadDb: sinon.stub().resolves()
|
||||
}
|
||||
sinon.stub(database, 'getNewDatabase').returns(db)
|
||||
|
||||
// mock router
|
||||
const $router = { push: sinon.stub() }
|
||||
const $route = { path: '/' }
|
||||
|
||||
// mount the component
|
||||
const wrapper = shallowMount(DbUploader, {
|
||||
attachTo: place,
|
||||
store,
|
||||
mocks: { $router, $route },
|
||||
propsData: {
|
||||
type: 'illustrated'
|
||||
}
|
||||
})
|
||||
|
||||
// mock a file dropped by a user
|
||||
const file = { name: 'test.db' }
|
||||
const dropData = { dataTransfer: new DataTransfer() }
|
||||
Object.defineProperty(dropData.dataTransfer, 'files', {
|
||||
value: [file],
|
||||
writable: false
|
||||
})
|
||||
|
||||
await wrapper.find('.drop-area').trigger('drop', dropData)
|
||||
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
|
||||
await db.loadDb.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
expect($router.push.calledOnceWith('/editor')).to.equal(true)
|
||||
wrapper.destroy()
|
||||
})
|
||||
|
||||
it("doesn't redirect if already on /editor", async () => {
|
||||
// mock getting a file from user
|
||||
const file = { name: 'test.db' }
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock db loading
|
||||
const db = {
|
||||
loadDb: sinon.stub().resolves()
|
||||
}
|
||||
sinon.stub(database, 'getNewDatabase').returns(db)
|
||||
|
||||
// mock router
|
||||
const $router = { push: sinon.stub() }
|
||||
const $route = { path: '/editor' }
|
||||
|
||||
// mount the component
|
||||
const wrapper = shallowMount(DbUploader, {
|
||||
attachTo: place,
|
||||
store,
|
||||
mocks: { $router, $route },
|
||||
propsData: {
|
||||
type: 'illustrated'
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await db.loadDb.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
expect($router.push.called).to.equal(false)
|
||||
wrapper.destroy()
|
||||
})
|
||||
|
||||
it('shows parse dialog if gets csv file', async () => {
|
||||
// mock getting a file from user
|
||||
const file = { name: 'test.csv' }
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock router
|
||||
const $router = { push: sinon.stub() }
|
||||
const $route = { path: '/editor' }
|
||||
|
||||
// mount the component
|
||||
const wrapper = mount(DbUploader, {
|
||||
attachTo: place,
|
||||
store,
|
||||
mocks: { $router, $route },
|
||||
propsData: {
|
||||
type: 'illustrated'
|
||||
}
|
||||
})
|
||||
|
||||
const CsvImport = wrapper.vm.$refs.addCsv
|
||||
sinon.stub(CsvImport, 'reset')
|
||||
sinon.stub(CsvImport, 'previewCsv').resolves()
|
||||
sinon.stub(CsvImport, 'open')
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(CsvImport.reset.calledOnce).to.equal(true)
|
||||
await wrapper.vm.animationPromise
|
||||
expect(CsvImport.previewCsv.calledOnce).to.equal(true)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(CsvImport.open.calledOnce).to.equal(true)
|
||||
wrapper.destroy()
|
||||
})
|
||||
|
||||
it('deletes temporary db if CSV import is canceled', async () => {
|
||||
// mock getting a file from user
|
||||
const file = { name: 'test.csv' }
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock router
|
||||
const $router = { push: sinon.stub() }
|
||||
const $route = { path: '/editor' }
|
||||
|
||||
// mount the component
|
||||
const wrapper = mount(DbUploader, {
|
||||
store,
|
||||
mocks: { $router, $route },
|
||||
propsData: {
|
||||
type: 'illustrated'
|
||||
}
|
||||
})
|
||||
|
||||
const CsvImport = wrapper.vm.$refs.addCsv
|
||||
sinon.stub(CsvImport, 'reset')
|
||||
sinon.stub(CsvImport, 'previewCsv').resolves()
|
||||
sinon.stub(CsvImport, 'open')
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
await CsvImport.$emit('cancel')
|
||||
expect(wrapper.vm.newDb).to.equal(null)
|
||||
})
|
||||
})
|
||||
@@ -74,8 +74,8 @@ describe('_sql.js', () => {
|
||||
const progressCallback = sinon.stub()
|
||||
const progressCounterId = 1
|
||||
const sql = await Sql.build()
|
||||
sql.import(data.columns, data.values, progressCounterId, progressCallback, 2)
|
||||
const result = sql.exec('SELECT * from csv_import')
|
||||
sql.import('foo', data.columns, data.values, progressCounterId, progressCallback, 2)
|
||||
const result = sql.exec('SELECT * from foo')
|
||||
expect(result).to.have.lengthOf(1)
|
||||
expect(result[0].columns).to.eql(['id', 'name'])
|
||||
expect(result[0].values).to.have.lengthOf(4)
|
||||
@@ -135,7 +135,7 @@ describe('_sql.js', () => {
|
||||
expect(sql.db.db).to.equal(null)
|
||||
})
|
||||
|
||||
it('overwrites', async () => {
|
||||
it('adds', async () => {
|
||||
const sql = await Sql.build()
|
||||
sql.exec(`
|
||||
CREATE TABLE test (
|
||||
@@ -160,12 +160,11 @@ describe('_sql.js', () => {
|
||||
[4, 'Ron Weasley']
|
||||
]
|
||||
}
|
||||
// rewrite the database by import
|
||||
sql.import(data.columns, data.values, 1, sinon.stub(), 2)
|
||||
result = sql.exec('SELECT * from csv_import')
|
||||
// import adds table
|
||||
sql.import('foo', data.columns, data.values, 1, sinon.stub(), 2)
|
||||
result = sql.exec('SELECT * from foo')
|
||||
expect(result[0].values).to.have.lengthOf(4)
|
||||
|
||||
// test table oesn't exists anymore: the db was overwritten
|
||||
expect(() => { sql.exec('SELECT * from test') }).to.throw('no such table: test')
|
||||
result = sql.exec('SELECT * from test')
|
||||
expect(result[0].values).to.have.lengthOf(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { expect } from 'chai'
|
||||
import dbUtils from '@/lib/database/_statements'
|
||||
import stmts from '@/lib/database/_statements'
|
||||
|
||||
describe('_statements.js', () => {
|
||||
it('generateChunks', () => {
|
||||
const arr = ['1', '2', '3', '4', '5']
|
||||
const size = 2
|
||||
const chunks = dbUtils.generateChunks(arr, size)
|
||||
const chunks = stmts.generateChunks(arr, size)
|
||||
const output = []
|
||||
for (const chunk of chunks) {
|
||||
output.push(chunk)
|
||||
@@ -17,8 +17,8 @@ describe('_statements.js', () => {
|
||||
|
||||
it('getInsertStmt', () => {
|
||||
const columns = ['id', 'name']
|
||||
expect(dbUtils.getInsertStmt(columns))
|
||||
.to.equal('INSERT INTO csv_import ("id", "name") VALUES (?, ?);')
|
||||
expect(stmts.getInsertStmt('foo', columns))
|
||||
.to.equal('INSERT INTO "foo" ("id", "name") VALUES (?, ?);')
|
||||
})
|
||||
|
||||
it('getCreateStatement', () => {
|
||||
@@ -27,8 +27,36 @@ describe('_statements.js', () => {
|
||||
[1, 'foo', true, new Date()],
|
||||
[2, 'bar', false, new Date()]
|
||||
]
|
||||
expect(dbUtils.getCreateStatement(columns, values)).to.equal(
|
||||
'CREATE table csv_import("id" REAL, "name" TEXT, "isAdmin" INTEGER, "startDate" TEXT);'
|
||||
expect(stmts.getCreateStatement('foo', columns, values)).to.equal(
|
||||
'CREATE table "foo"("id" REAL, "name" TEXT, "isAdmin" INTEGER, "startDate" TEXT);'
|
||||
)
|
||||
})
|
||||
|
||||
it('getColumns', () => {
|
||||
const sql = `CREATE TABLE test (
|
||||
col1,
|
||||
col2 integer,
|
||||
col3 decimal(5,2),
|
||||
col4 varchar(30)
|
||||
)`
|
||||
expect(stmts.getColumns(sql)).to.eql([
|
||||
{ name: 'col1', type: 'N/A' },
|
||||
{ name: 'col2', type: 'integer' },
|
||||
{ name: 'col3', type: 'decimal(5, 2)' },
|
||||
{ name: 'col4', type: 'varchar(30)' }
|
||||
])
|
||||
})
|
||||
|
||||
it('getColumns with virtual table', async () => {
|
||||
const sql = `
|
||||
CREATE VIRTUAL TABLE test_virtual USING fts4(
|
||||
col1, col2,
|
||||
notindexed=col1, notindexed=col2,
|
||||
tokenize=unicode61 "tokenchars=.+#")
|
||||
`
|
||||
expect(stmts.getColumns(sql)).to.eql([
|
||||
{ name: 'col1', type: 'N/A' },
|
||||
{ name: 'col2', type: 'N/A' }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -27,58 +27,39 @@ describe('database.js', () => {
|
||||
const tempDb = new SQL.Database()
|
||||
tempDb.run(`CREATE TABLE test (
|
||||
col1,
|
||||
col2 integer,
|
||||
col3 decimal(5,2),
|
||||
col4 varchar(30)
|
||||
col2 integer
|
||||
)`)
|
||||
|
||||
const data = tempDb.export()
|
||||
const buffer = new Blob([data])
|
||||
buffer.name = 'foo.sqlite'
|
||||
|
||||
const { schema, dbName } = await db.loadDb(buffer)
|
||||
expect(dbName).to.equal('foo')
|
||||
sinon.spy(db, 'refreshSchema')
|
||||
|
||||
await db.loadDb(buffer)
|
||||
await db.refreshSchema.returnValues[0]
|
||||
const schema = db.schema
|
||||
expect(db.dbName).to.equal('foo')
|
||||
expect(schema).to.have.lengthOf(1)
|
||||
expect(schema[0].name).to.equal('test')
|
||||
|
||||
expect(schema[0].columns[0].name).to.equal('col1')
|
||||
expect(schema[0].columns[0].type).to.equal('N/A')
|
||||
|
||||
expect(schema[0].columns[1].name).to.equal('col2')
|
||||
expect(schema[0].columns[1].type).to.equal('integer')
|
||||
expect(schema[0].columns[2].name).to.equal('col3')
|
||||
expect(schema[0].columns[2].type).to.equal('decimal(5, 2)')
|
||||
expect(schema[0].columns[3].name).to.equal('col4')
|
||||
expect(schema[0].columns[3].type).to.equal('varchar(30)')
|
||||
})
|
||||
|
||||
it('creates schema with virtual table', async () => {
|
||||
const SQL = await getSQL
|
||||
const tempDb = new SQL.Database()
|
||||
tempDb.run(`
|
||||
CREATE VIRTUAL TABLE test_virtual USING fts4(
|
||||
col1, col2,
|
||||
notindexed=col1, notindexed=col2,
|
||||
tokenize=unicode61 "tokenchars=.+#")
|
||||
`)
|
||||
it('creates empty db with name database', async () => {
|
||||
sinon.spy(db, 'refreshSchema')
|
||||
|
||||
const data = tempDb.export()
|
||||
const buffer = new Blob([data])
|
||||
buffer.name = 'foo.sqlite'
|
||||
|
||||
const { schema } = await db.loadDb(buffer)
|
||||
expect(schema[0].name).to.equal('test_virtual')
|
||||
expect(schema[0].columns[0].name).to.equal('col1')
|
||||
expect(schema[0].columns[0].type).to.equal('N/A')
|
||||
expect(schema[0].columns[1].name).to.equal('col2')
|
||||
expect(schema[0].columns[1].type).to.equal('N/A')
|
||||
await db.loadDb()
|
||||
await db.refreshSchema.returnValues[0]
|
||||
expect(db.dbName).to.equal('database')
|
||||
})
|
||||
|
||||
it('loadDb throws errors', async () => {
|
||||
const SQL = await getSQL
|
||||
const tempDb = new SQL.Database()
|
||||
tempDb.run('CREATE TABLE test (col1, col2)')
|
||||
|
||||
const data = tempDb.export()
|
||||
const buffer = new Blob([data])
|
||||
const buffer = new Blob([])
|
||||
buffer.name = 'foo.sqlite'
|
||||
|
||||
sinon.stub(db.pw, 'postMessage').resolves({ error: new Error('foo') })
|
||||
@@ -136,7 +117,7 @@ describe('database.js', () => {
|
||||
await expect(db.execute('SELECT * from foo')).to.be.rejectedWith(/^no such table: foo$/)
|
||||
})
|
||||
|
||||
it('creates db', async () => {
|
||||
it('adds table from csv', async () => {
|
||||
const data = {
|
||||
columns: ['id', 'name', 'faculty'],
|
||||
values: [
|
||||
@@ -146,16 +127,19 @@ describe('database.js', () => {
|
||||
}
|
||||
const progressHandler = sinon.spy()
|
||||
const progressCounterId = db.createProgressCounter(progressHandler)
|
||||
const { dbName, schema } = await db.importDb('foo', data, progressCounterId)
|
||||
expect(dbName).to.equal('foo')
|
||||
expect(schema).to.have.lengthOf(1)
|
||||
expect(schema[0].name).to.equal('csv_import')
|
||||
expect(schema[0].columns).to.have.lengthOf(3)
|
||||
expect(schema[0].columns[0]).to.eql({ name: 'id', type: 'real' })
|
||||
expect(schema[0].columns[1]).to.eql({ name: 'name', type: 'text' })
|
||||
expect(schema[0].columns[2]).to.eql({ name: 'faculty', type: 'text' })
|
||||
sinon.spy(db, 'refreshSchema')
|
||||
|
||||
const result = await db.execute('SELECT * from csv_import')
|
||||
await db.addTableFromCsv('foo', data, progressCounterId)
|
||||
await db.refreshSchema.returnValues[0]
|
||||
expect(db.dbName).to.equal('database')
|
||||
expect(db.schema).to.have.lengthOf(1)
|
||||
expect(db.schema[0].name).to.equal('foo')
|
||||
expect(db.schema[0].columns).to.have.lengthOf(3)
|
||||
expect(db.schema[0].columns[0]).to.eql({ name: 'id', type: 'real' })
|
||||
expect(db.schema[0].columns[1]).to.eql({ name: 'name', type: 'text' })
|
||||
expect(db.schema[0].columns[2]).to.eql({ name: 'faculty', type: 'text' })
|
||||
|
||||
const result = await db.execute('SELECT * from foo')
|
||||
expect(result.columns).to.eql(data.columns)
|
||||
expect(result.values).to.eql(data.values)
|
||||
|
||||
@@ -164,7 +148,7 @@ describe('database.js', () => {
|
||||
expect(progressHandler.secondCall.calledWith(100)).to.equal(true)
|
||||
})
|
||||
|
||||
it('importDb throws errors', async () => {
|
||||
it('addTableFromCsv throws errors', async () => {
|
||||
const data = {
|
||||
columns: ['id', 'name'],
|
||||
values: [
|
||||
@@ -174,7 +158,7 @@ describe('database.js', () => {
|
||||
}
|
||||
const progressHandler = sinon.stub()
|
||||
const progressCounterId = db.createProgressCounter(progressHandler)
|
||||
await expect(db.importDb('foo', data, progressCounterId))
|
||||
await expect(db.addTableFromCsv('foo', data, progressCounterId))
|
||||
.to.be.rejectedWith('column index out of range')
|
||||
})
|
||||
|
||||
@@ -242,4 +226,23 @@ describe('database.js', () => {
|
||||
expect(result.values).to.have.lengthOf(1)
|
||||
expect(result.values[0]).to.eql([1, 'Harry Potter'])
|
||||
})
|
||||
|
||||
it('sanitizeTableName', () => {
|
||||
let name = 'foo[]bar'
|
||||
expect(db.sanitizeTableName(name)).to.equal('foo_bar')
|
||||
|
||||
name = '1 foo(01.05.2020)'
|
||||
expect(db.sanitizeTableName(name)).to.equal('_1_foo_01_05_2020_')
|
||||
})
|
||||
|
||||
it('validateTableName', async () => {
|
||||
await db.execute('CREATE TABLE foo(id)')
|
||||
await expect(db.validateTableName('foo')).to.be.rejectedWith('table "foo" already exists')
|
||||
await expect(db.validateTableName('1foo'))
|
||||
.to.be.rejectedWith("Table name can't start with a digit")
|
||||
await expect(db.validateTableName('foo(05.08.2020)'))
|
||||
.to.be.rejectedWith('Table name can contain only letters, digits and underscores')
|
||||
await expect(db.validateTableName('sqlite_foo'))
|
||||
.to.be.rejectedWith("Table name can't start with sqlite_")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect } from 'chai'
|
||||
import fu from '@/lib/utils/fileIo'
|
||||
import fIo from '@/lib/utils/fileIo'
|
||||
import sinon from 'sinon'
|
||||
|
||||
describe('fileIo.js', () => {
|
||||
@@ -15,7 +15,7 @@ describe('fileIo.js', () => {
|
||||
sinon.spy(URL, 'revokeObjectURL')
|
||||
sinon.spy(window, 'Blob')
|
||||
|
||||
fu.exportToFile('foo', 'foo.txt')
|
||||
fIo.exportToFile('foo', 'foo.txt')
|
||||
|
||||
expect(document.createElement.calledOnceWith('a')).to.equal(true)
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('fileIo.js', () => {
|
||||
sinon.spy(URL, 'revokeObjectURL')
|
||||
sinon.spy(window, 'Blob')
|
||||
|
||||
fu.exportToFile('foo', 'foo.html', 'text/html')
|
||||
fIo.exportToFile('foo', 'foo.html', 'text/html')
|
||||
|
||||
expect(document.createElement.calledOnceWith('a')).to.equal(true)
|
||||
|
||||
@@ -71,7 +71,7 @@ describe('fileIo.js', () => {
|
||||
|
||||
setTimeout(() => { spyInput.dispatchEvent(new Event('change')) })
|
||||
|
||||
const data = await fu.importFile()
|
||||
const data = await fIo.importFile()
|
||||
expect(data).to.equal('foo')
|
||||
expect(document.createElement.calledOnceWith('input')).to.equal(true)
|
||||
expect(spyInput.type).to.equal('file')
|
||||
@@ -82,13 +82,13 @@ describe('fileIo.js', () => {
|
||||
it('readFile', () => {
|
||||
sinon.spy(window, 'fetch')
|
||||
|
||||
fu.readFile('./foo.bar')
|
||||
fIo.readFile('./foo.bar')
|
||||
expect(window.fetch.calledOnceWith('./foo.bar')).to.equal(true)
|
||||
})
|
||||
|
||||
it('readAsArrayBuffer resolves', async () => {
|
||||
const blob = new Blob(['foo'])
|
||||
const buffer = await fu.readAsArrayBuffer(blob)
|
||||
const buffer = await fIo.readAsArrayBuffer(blob)
|
||||
|
||||
const uint8Array = new Uint8Array(buffer)
|
||||
const text = new TextDecoder().decode(uint8Array)
|
||||
@@ -103,29 +103,34 @@ describe('fileIo.js', () => {
|
||||
sinon.stub(window, 'FileReader').returns(r)
|
||||
|
||||
const blob = new Blob(['foo'])
|
||||
await expect(fu.readAsArrayBuffer(blob)).to.be.rejectedWith('Problem parsing input file.')
|
||||
await expect(fIo.readAsArrayBuffer(blob)).to.be.rejectedWith('Problem parsing input file.')
|
||||
})
|
||||
|
||||
it('isDatabase', () => {
|
||||
let file = { type: 'application/vnd.sqlite3' }
|
||||
expect(fu.isDatabase(file)).to.equal(true)
|
||||
expect(fIo.isDatabase(file)).to.equal(true)
|
||||
|
||||
file = { type: 'application/x-sqlite3' }
|
||||
expect(fu.isDatabase(file)).to.equal(true)
|
||||
expect(fIo.isDatabase(file)).to.equal(true)
|
||||
|
||||
file = { type: '', name: 'test.db' }
|
||||
expect(fu.isDatabase(file)).to.equal(true)
|
||||
expect(fIo.isDatabase(file)).to.equal(true)
|
||||
|
||||
file = { type: '', name: 'test.sqlite' }
|
||||
expect(fu.isDatabase(file)).to.equal(true)
|
||||
expect(fIo.isDatabase(file)).to.equal(true)
|
||||
|
||||
file = { type: '', name: 'test.sqlite3' }
|
||||
expect(fu.isDatabase(file)).to.equal(true)
|
||||
expect(fIo.isDatabase(file)).to.equal(true)
|
||||
|
||||
file = { type: '', name: 'test.csv' }
|
||||
expect(fu.isDatabase(file)).to.equal(false)
|
||||
expect(fIo.isDatabase(file)).to.equal(false)
|
||||
|
||||
file = { type: 'text', name: 'test.db' }
|
||||
expect(fu.isDatabase(file)).to.equal(false)
|
||||
expect(fIo.isDatabase(file)).to.equal(false)
|
||||
})
|
||||
|
||||
it('getFileName', () => {
|
||||
expect(fIo.getFileName({ name: 'foo.csv' })).to.equal('foo')
|
||||
expect(fIo.getFileName({ name: 'foo.bar.db' })).to.equal('foo.bar')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@ import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import mutations from '@/store/mutations'
|
||||
const {
|
||||
saveSchema,
|
||||
updateTab,
|
||||
deleteTab,
|
||||
setCurrentTabId,
|
||||
@@ -24,25 +23,6 @@ describe('mutations', () => {
|
||||
expect(oldDb.shutDown.calledOnce).to.equal(true)
|
||||
})
|
||||
|
||||
it('saveSchema', () => {
|
||||
const state = {}
|
||||
|
||||
const schema = [
|
||||
{
|
||||
name: 'table1',
|
||||
columns: [
|
||||
{ name: 'id', type: 'INTEGER' }
|
||||
]
|
||||
}
|
||||
]
|
||||
saveSchema(state, {
|
||||
dbName: 'test',
|
||||
schema
|
||||
})
|
||||
expect(state.dbName).to.equal('test')
|
||||
expect(state.schema).to.eql(schema)
|
||||
})
|
||||
|
||||
it('updateTab (save)', () => {
|
||||
const tab = {
|
||||
id: 1,
|
||||
|
||||
@@ -4,6 +4,9 @@ import { mount, createLocalVue } from '@vue/test-utils'
|
||||
import Vuex from 'vuex'
|
||||
import Schema from '@/views/Main/Editor/Schema'
|
||||
import TableDescription from '@/views/Main/Editor/Schema/TableDescription'
|
||||
import database from '@/lib/database'
|
||||
import fIo from '@/lib/utils/fileIo'
|
||||
import csv from '@/components/CsvImport/csv'
|
||||
|
||||
const localVue = createLocalVue()
|
||||
localVue.use(Vuex)
|
||||
@@ -16,7 +19,9 @@ describe('Schema.vue', () => {
|
||||
it('Renders DB name on initial', () => {
|
||||
// mock store state
|
||||
const state = {
|
||||
dbName: 'fooDB'
|
||||
db: {
|
||||
dbName: 'fooDB'
|
||||
}
|
||||
}
|
||||
const store = new Vuex.Store({ state })
|
||||
|
||||
@@ -31,7 +36,9 @@ describe('Schema.vue', () => {
|
||||
it('Schema visibility is toggled when click on DB name', async () => {
|
||||
// mock store state
|
||||
const state = {
|
||||
dbName: 'fooDB'
|
||||
db: {
|
||||
dbName: 'fooDB'
|
||||
}
|
||||
}
|
||||
const store = new Vuex.Store({ state })
|
||||
|
||||
@@ -48,30 +55,32 @@ describe('Schema.vue', () => {
|
||||
it('Schema filter', async () => {
|
||||
// mock store state
|
||||
const state = {
|
||||
dbName: 'fooDB',
|
||||
schema: [
|
||||
{
|
||||
name: 'foo',
|
||||
columns: [
|
||||
{ name: 'id', type: 'INTEGER' },
|
||||
{ name: 'title', type: 'NVARCHAR(24)' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'bar',
|
||||
columns: [
|
||||
{ name: 'id', type: 'INTEGER' },
|
||||
{ name: 'price', type: 'INTEGER' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'foobar',
|
||||
columns: [
|
||||
{ name: 'id', type: 'INTEGER' },
|
||||
{ name: 'price', type: 'INTEGER' }
|
||||
]
|
||||
}
|
||||
]
|
||||
db: {
|
||||
dbName: 'fooDB',
|
||||
schema: [
|
||||
{
|
||||
name: 'foo',
|
||||
columns: [
|
||||
{ name: 'id', type: 'INTEGER' },
|
||||
{ name: 'title', type: 'NVARCHAR(24)' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'bar',
|
||||
columns: [
|
||||
{ name: 'id', type: 'INTEGER' },
|
||||
{ name: 'price', type: 'INTEGER' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'foobar',
|
||||
columns: [
|
||||
{ name: 'id', type: 'INTEGER' },
|
||||
{ name: 'price', type: 'INTEGER' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
const store = new Vuex.Store({ state })
|
||||
|
||||
@@ -101,15 +110,69 @@ describe('Schema.vue', () => {
|
||||
|
||||
it('exports db', async () => {
|
||||
const state = {
|
||||
dbName: 'fooDB',
|
||||
db: {
|
||||
dbName: 'fooDB',
|
||||
export: sinon.stub().resolves()
|
||||
}
|
||||
}
|
||||
const store = new Vuex.Store({ state })
|
||||
const wrapper = mount(Schema, { store, localVue })
|
||||
|
||||
await wrapper.findComponent({ name: 'export-icon' }).trigger('click')
|
||||
await wrapper.findComponent({ name: 'export-icon' }).find('svg').trigger('click')
|
||||
expect(state.db.export.calledOnceWith('fooDB'))
|
||||
})
|
||||
|
||||
it('adds table', async () => {
|
||||
const file = { name: 'test.csv' }
|
||||
sinon.stub(fIo, 'getFileFromUser').resolves(file)
|
||||
|
||||
sinon.stub(csv, 'parse').resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
const state = {
|
||||
db: database.getNewDatabase()
|
||||
}
|
||||
state.db.dbName = 'db'
|
||||
state.db.execute('CREATE TABLE foo(id)')
|
||||
state.db.refreshSchema()
|
||||
sinon.spy(state.db, 'refreshSchema')
|
||||
|
||||
const store = new Vuex.Store({ state })
|
||||
const wrapper = mount(Schema, { store, localVue })
|
||||
sinon.spy(wrapper.vm.$refs.addCsv, 'previewCsv')
|
||||
sinon.spy(wrapper.vm, 'addCsv')
|
||||
sinon.spy(wrapper.vm.$refs.addCsv, 'loadFromCsv')
|
||||
|
||||
await wrapper.findComponent({ name: 'add-table-icon' }).find('svg').trigger('click')
|
||||
await wrapper.vm.$refs.addCsv.previewCsv.returnValues[0]
|
||||
await wrapper.vm.addCsv.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(true)
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.vm.$refs.addCsv.loadFromCsv.returnValues[0]
|
||||
await wrapper.find('#csv-finish').trigger('click')
|
||||
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
|
||||
await state.db.refreshSchema.returnValues[0]
|
||||
|
||||
expect(wrapper.vm.$store.state.db.schema).to.eql([
|
||||
{ name: 'test', columns: [{ name: 'col1', type: 'real' }, { name: 'col2', type: 'text' }] },
|
||||
{ name: 'foo', columns: [{ name: 'id', type: 'N/A' }] }
|
||||
])
|
||||
|
||||
const res = await wrapper.vm.$store.state.db.execute('select * from test')
|
||||
expect(res).to.eql({
|
||||
columns: ['col1', 'col2'],
|
||||
values: [[1, 'foo']]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,6 +7,5 @@ describe('SqlEditor.vue', () => {
|
||||
const wrapper = mount(SqlEditor)
|
||||
await wrapper.findComponent({ name: 'codemirror' }).vm.$emit('input', 'SELECT * FROM foo')
|
||||
expect(wrapper.emitted('input')[0]).to.eql(['SELECT * FROM foo'])
|
||||
// Take a pause to keep proper state in debounced '@/views/Main/Editor/Tabs/Tab/SqlEditor/hint'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,22 +11,24 @@ describe('hint.js', () => {
|
||||
|
||||
it('Calculates table list for hint', () => {
|
||||
// mock store state
|
||||
const schema = [
|
||||
{
|
||||
name: 'foo',
|
||||
columns: [
|
||||
{ name: 'fooId', type: 'INTEGER' },
|
||||
{ name: 'name', type: 'NVARCHAR(20)' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'bar',
|
||||
columns: [
|
||||
{ name: 'barId', type: 'INTEGER' }
|
||||
]
|
||||
}
|
||||
]
|
||||
sinon.stub(state, 'schema').value(schema)
|
||||
const db = {
|
||||
schema: [
|
||||
{
|
||||
name: 'foo',
|
||||
columns: [
|
||||
{ name: 'fooId', type: 'INTEGER' },
|
||||
{ name: 'name', type: 'NVARCHAR(20)' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'bar',
|
||||
columns: [
|
||||
{ name: 'barId', type: 'INTEGER' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
sinon.stub(state, 'db').value(db)
|
||||
|
||||
// mock showHint and editor
|
||||
sinon.stub(CM, 'showHint')
|
||||
@@ -52,16 +54,18 @@ describe('hint.js', () => {
|
||||
|
||||
it('Add default table if there is only one table in schema', () => {
|
||||
// mock store state
|
||||
const schema = [
|
||||
{
|
||||
name: 'foo',
|
||||
columns: [
|
||||
{ name: 'fooId', type: 'INTEGER' },
|
||||
{ name: 'name', type: 'NVARCHAR(20)' }
|
||||
]
|
||||
}
|
||||
]
|
||||
sinon.stub(state, 'schema').value(schema)
|
||||
const db = {
|
||||
schema: [
|
||||
{
|
||||
name: 'foo',
|
||||
columns: [
|
||||
{ name: 'fooId', type: 'INTEGER' },
|
||||
{ name: 'name', type: 'NVARCHAR(20)' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
sinon.stub(state, 'db').value(db)
|
||||
|
||||
// mock showHint and editor
|
||||
sinon.stub(CM, 'showHint')
|
||||
@@ -190,7 +194,7 @@ describe('hint.js', () => {
|
||||
|
||||
it('tables is empty object when schema is null', () => {
|
||||
// mock store state
|
||||
sinon.stub(state, 'schema').value(null)
|
||||
sinon.stub(state, 'db').value({ schema: null })
|
||||
|
||||
// mock showHint and editor
|
||||
sinon.stub(CM, 'showHint')
|
||||
|
||||
@@ -182,7 +182,8 @@ describe('Tab.vue', () => {
|
||||
const state = {
|
||||
currentTabId: 1,
|
||||
db: {
|
||||
execute: sinon.stub().rejects(new Error('There is no table foo'))
|
||||
execute: sinon.stub().rejects(new Error('There is no table foo')),
|
||||
refreshSchema: sinon.stub().resolves()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +222,7 @@ describe('Tab.vue', () => {
|
||||
currentTabId: 1,
|
||||
db: {
|
||||
execute: sinon.stub().resolves(result),
|
||||
getSchema: sinon.stub().resolves({ dbName: '', schema: [] })
|
||||
refreshSchema: sinon.stub().resolves()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,36 +254,17 @@ describe('Tab.vue', () => {
|
||||
columns: ['id', 'name'],
|
||||
values: []
|
||||
}
|
||||
const newSchema = {
|
||||
dbName: 'fooDb',
|
||||
schema: [
|
||||
{
|
||||
name: 'foo',
|
||||
columns: [
|
||||
{ name: 'id', type: 'INTEGER' },
|
||||
{ name: 'title', type: 'NVARCHAR(30)' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'bar',
|
||||
columns: [
|
||||
{ name: 'a', type: 'N/A' },
|
||||
{ name: 'b', type: 'N/A' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// mock store state
|
||||
const state = {
|
||||
currentTabId: 1,
|
||||
dbName: 'fooDb',
|
||||
db: {
|
||||
execute: sinon.stub().resolves(result),
|
||||
getSchema: sinon.stub().resolves(newSchema)
|
||||
refreshSchema: sinon.stub().resolves()
|
||||
}
|
||||
}
|
||||
|
||||
sinon.spy(mutations, 'saveSchema')
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
|
||||
// mount the component
|
||||
@@ -300,7 +282,6 @@ describe('Tab.vue', () => {
|
||||
})
|
||||
|
||||
await wrapper.vm.execute()
|
||||
expect(state.db.getSchema.calledOnceWith('fooDb')).to.equal(true)
|
||||
expect(mutations.saveSchema.calledOnceWith(state, newSchema)).to.equal(true)
|
||||
expect(state.db.refreshSchema.calledOnce).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('MainMenu.vue', () => {
|
||||
const state = {
|
||||
currentTab: { query: '', execute: sinon.stub() },
|
||||
tabs: [{}],
|
||||
schema: []
|
||||
db: {}
|
||||
}
|
||||
const store = new Vuex.Store({ state })
|
||||
const $route = { path: '/editor' }
|
||||
@@ -49,7 +49,7 @@ describe('MainMenu.vue', () => {
|
||||
const state = {
|
||||
currentTab: null,
|
||||
tabs: [{}],
|
||||
schema: []
|
||||
db: {}
|
||||
}
|
||||
const store = new Vuex.Store({ state })
|
||||
const $route = { path: '/editor' }
|
||||
@@ -65,11 +65,11 @@ describe('MainMenu.vue', () => {
|
||||
expect(wrapper.find('#create-btn').isVisible()).to.equal(true)
|
||||
})
|
||||
|
||||
it('Run is disabled if there is no schema or no query', async () => {
|
||||
it('Run is disabled if there is no db or no query', async () => {
|
||||
const state = {
|
||||
currentTab: { query: 'SELECT * FROM foo', execute: sinon.stub() },
|
||||
tabs: [{}],
|
||||
schema: null
|
||||
db: null
|
||||
}
|
||||
const store = new Vuex.Store({ state })
|
||||
const $route = { path: '/editor' }
|
||||
@@ -82,7 +82,7 @@ describe('MainMenu.vue', () => {
|
||||
const vm = wrapper.vm
|
||||
expect(wrapper.find('#run-btn').element.disabled).to.equal(true)
|
||||
|
||||
await vm.$set(state, 'schema', [])
|
||||
await vm.$set(state, 'db', {})
|
||||
expect(wrapper.find('#run-btn').element.disabled).to.equal(false)
|
||||
|
||||
await vm.$set(state.currentTab, 'query', '')
|
||||
@@ -97,7 +97,7 @@ describe('MainMenu.vue', () => {
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ isUnsaved: true }],
|
||||
schema: null
|
||||
db: {}
|
||||
}
|
||||
const store = new Vuex.Store({ state })
|
||||
const $route = { path: '/editor' }
|
||||
@@ -122,7 +122,7 @@ describe('MainMenu.vue', () => {
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ isUnsaved: true }],
|
||||
schema: null
|
||||
db: {}
|
||||
}
|
||||
const newQueryId = 1
|
||||
const actions = {
|
||||
@@ -156,7 +156,7 @@ describe('MainMenu.vue', () => {
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ isUnsaved: true }],
|
||||
schema: null
|
||||
db: {}
|
||||
}
|
||||
const newQueryId = 1
|
||||
const actions = {
|
||||
@@ -191,7 +191,7 @@ describe('MainMenu.vue', () => {
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ isUnsaved: true }],
|
||||
schema: []
|
||||
db: {}
|
||||
}
|
||||
const store = new Vuex.Store({ state })
|
||||
const $route = { path: '/editor' }
|
||||
@@ -212,14 +212,14 @@ describe('MainMenu.vue', () => {
|
||||
expect(state.currentTab.execute.calledTwice).to.equal(true)
|
||||
|
||||
// Running is disabled and route path is editor
|
||||
await wrapper.vm.$set(state, 'schema', null)
|
||||
await wrapper.vm.$set(state, 'db', null)
|
||||
document.dispatchEvent(ctrlR)
|
||||
expect(state.currentTab.execute.calledTwice).to.equal(true)
|
||||
document.dispatchEvent(metaR)
|
||||
expect(state.currentTab.execute.calledTwice).to.equal(true)
|
||||
|
||||
// Running is enabled and route path is not editor
|
||||
await wrapper.vm.$set(state, 'schema', [])
|
||||
await wrapper.vm.$set(state, 'db', {})
|
||||
await wrapper.vm.$set($route, 'path', '/my-queries')
|
||||
document.dispatchEvent(ctrlR)
|
||||
expect(state.currentTab.execute.calledTwice).to.equal(true)
|
||||
@@ -236,7 +236,7 @@ describe('MainMenu.vue', () => {
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ isUnsaved: true }],
|
||||
schema: []
|
||||
db: {}
|
||||
}
|
||||
const store = new Vuex.Store({ state })
|
||||
const $route = { path: '/editor' }
|
||||
@@ -257,14 +257,14 @@ describe('MainMenu.vue', () => {
|
||||
expect(state.currentTab.execute.calledTwice).to.equal(true)
|
||||
|
||||
// Running is disabled and route path is editor
|
||||
await wrapper.vm.$set(state, 'schema', null)
|
||||
await wrapper.vm.$set(state, 'db', null)
|
||||
document.dispatchEvent(ctrlEnter)
|
||||
expect(state.currentTab.execute.calledTwice).to.equal(true)
|
||||
document.dispatchEvent(metaEnter)
|
||||
expect(state.currentTab.execute.calledTwice).to.equal(true)
|
||||
|
||||
// Running is enabled and route path is not editor
|
||||
await wrapper.vm.$set(state, 'schema', [])
|
||||
await wrapper.vm.$set(state, 'db', {})
|
||||
await wrapper.vm.$set($route, 'path', '/my-queries')
|
||||
document.dispatchEvent(ctrlEnter)
|
||||
expect(state.currentTab.execute.calledTwice).to.equal(true)
|
||||
@@ -280,7 +280,7 @@ describe('MainMenu.vue', () => {
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ isUnsaved: true }],
|
||||
schema: []
|
||||
db: {}
|
||||
}
|
||||
const store = new Vuex.Store({ state })
|
||||
const $route = { path: '/editor' }
|
||||
@@ -315,7 +315,7 @@ describe('MainMenu.vue', () => {
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ isUnsaved: true }],
|
||||
schema: []
|
||||
db: {}
|
||||
}
|
||||
const store = new Vuex.Store({ state })
|
||||
const $route = { path: '/editor' }
|
||||
@@ -360,7 +360,7 @@ describe('MainMenu.vue', () => {
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ id: 1, name: 'foo', isUnsaved: true }],
|
||||
schema: []
|
||||
db: {}
|
||||
}
|
||||
const mutations = {
|
||||
updateTab: sinon.stub()
|
||||
@@ -411,7 +411,7 @@ describe('MainMenu.vue', () => {
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ id: 1, name: null, tempName: 'Untitled', isUnsaved: true }],
|
||||
schema: []
|
||||
db: {}
|
||||
}
|
||||
const mutations = {
|
||||
updateTab: sinon.stub()
|
||||
@@ -456,7 +456,7 @@ describe('MainMenu.vue', () => {
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ id: 1, name: null, tempName: 'Untitled', isUnsaved: true }],
|
||||
schema: []
|
||||
db: {}
|
||||
}
|
||||
const mutations = {
|
||||
updateTab: sinon.stub()
|
||||
@@ -528,7 +528,7 @@ describe('MainMenu.vue', () => {
|
||||
view: 'chart'
|
||||
},
|
||||
tabs: [{ id: 1, name: 'foo', isUnsaved: true, isPredefined: true }],
|
||||
schema: []
|
||||
db: {}
|
||||
}
|
||||
const mutations = {
|
||||
updateTab: sinon.stub()
|
||||
@@ -607,7 +607,7 @@ describe('MainMenu.vue', () => {
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ id: 1, name: null, tempName: 'Untitled', isUnsaved: true }],
|
||||
schema: []
|
||||
db: {}
|
||||
}
|
||||
const mutations = {
|
||||
updateTab: sinon.stub()
|
||||
|
||||
Reference in New Issue
Block a user