Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d07506266c | ||
|
|
cea1d40797 | ||
|
|
0f2dc9f11e | ||
|
|
23250259eb | ||
|
|
fb930028de | ||
|
|
1ff4adf95c | ||
|
|
78cdb3809c | ||
|
|
3a6628cab9 | ||
|
|
418809d27d | ||
|
|
f9edeafd40 | ||
|
|
a37ed93306 | ||
|
|
cf4b83f7d4 | ||
|
|
2abd42c9c3 | ||
|
|
1251c542cb | ||
|
|
ac89259924 | ||
|
|
179ff8b1e1 | ||
|
|
99a10225a3 | ||
|
|
c96deb5766 | ||
|
|
700970e1cc | ||
|
|
e2be61e2cf | ||
|
|
9c2c8f3692 | ||
|
|
414a116f94 | ||
|
|
3e503f85a9 | ||
|
|
88257bfcf6 | ||
|
|
bdcc494138 | ||
|
|
d750541c80 | ||
|
|
75f743ff9e | ||
|
|
8a9f4b3c0a | ||
|
|
77468d34ae | ||
|
|
a0577ec0ce | ||
|
|
e7d1398546 | ||
|
|
aa52048d51 | ||
|
|
33913f8f5c | ||
|
|
51eb7a543c | ||
|
|
a3fb38b23c | ||
|
|
3bb40b4eb7 | ||
|
|
6864bf84f8 | ||
|
|
9f1b3823f6 | ||
|
|
7574f529c3 | ||
|
|
653f8eff7b | ||
|
|
9b3dda6cff | ||
|
|
d94604ebfb | ||
|
|
16868ef430 | ||
|
|
b162c7043e | ||
|
|
8e856063b8 | ||
|
|
8684b4cef9 | ||
|
|
bcaebd4840 | ||
|
|
4619461af8 | ||
|
|
9fff1d699a | ||
|
|
5ab19c3fae | ||
|
|
cc483f4720 | ||
|
|
a07f2d3d99 | ||
|
|
b9844b8696 | ||
|
|
464bff3db8 | ||
|
|
00e434e142 | ||
|
|
5d6280abec | ||
|
|
7a39e905b9 | ||
|
|
297ea2c18a | ||
|
|
1f2327a724 |
17
.github/workflows/config.grenrc.js
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
dataSource: 'milestones',
|
||||
ignoreIssuesWith: [
|
||||
'wontfix',
|
||||
'duplicate'
|
||||
],
|
||||
milestoneMatch: 'v{{tag_name}}',
|
||||
template: {
|
||||
issue: '- {{name}} [{{text}}]({{url}})',
|
||||
changelogTitle: "",
|
||||
release: "{{body}}",
|
||||
},
|
||||
groupBy: {
|
||||
'Enhancements': ["enhancement", "internal"],
|
||||
'Bug fixes': ["bug"]
|
||||
}
|
||||
}
|
||||
24
.github/workflows/main.yml
vendored
@@ -15,6 +15,10 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10.x
|
||||
|
||||
- name: Update npm
|
||||
run: npm install -g npm@7
|
||||
|
||||
- name: npm install and build
|
||||
run: |
|
||||
npm install
|
||||
@@ -25,16 +29,26 @@ jobs:
|
||||
cd dist
|
||||
zip -9 -r dist.zip . -x "js/*.map"
|
||||
|
||||
- name: Create Release Notes
|
||||
run: |
|
||||
npm install github-release-notes@0.16.0 -g
|
||||
gren changelog --generate --config="/.github/workflows/config.grenrc.js"
|
||||
env:
|
||||
GREN_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "dist/dist.zip"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
bodyFile: "CHANGELOG.md"
|
||||
|
||||
- name: Deploy 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@3.6.2
|
||||
uses: JamesIves/github-pages-deploy-action@4.1.1
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BRANCH: build # The branch the action should deploy to.
|
||||
FOLDER: dist/ # The folder the action should deploy.
|
||||
CLEAN: false # Automatically remove deleted files from the deploy branch
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: build # The branch the action should deploy to.
|
||||
folder: dist/ # The folder the action should deploy.
|
||||
clean: true # Automatically remove deleted files from the deploy branch
|
||||
clean-exclude: .nojekyll
|
||||
|
||||
|
||||
8
.github/workflows/test.yml
vendored
@@ -4,6 +4,9 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -21,11 +24,14 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y chromium-browser firefox
|
||||
|
||||
- name: Update npm
|
||||
run: npm install -g npm@7
|
||||
|
||||
- name: Install the project
|
||||
run: npm install
|
||||
|
||||
- name: Run lint
|
||||
run: npm run lint
|
||||
run: npm run lint -- --no-fix
|
||||
|
||||
- name: Run karma tests
|
||||
run: npm run test
|
||||
|
||||
25
README.md
@@ -4,20 +4,20 @@
|
||||
|
||||
# sqliteviz
|
||||
|
||||
Sqliteviz is a single-page PWA for fully client-side visualisation of SQLite databases or CSV files.
|
||||
Sqliteviz is a single-page offline-first PWA for fully client-side visualisation of SQLite databases or CSV files.
|
||||
|
||||
This application allows to:
|
||||
- run SQL queries in SQLite database and create all kinds of charts based on result set
|
||||
- import CSV file into SQLite database and visualize imported data
|
||||
- save queries and chart settings
|
||||
With sqliteviz you can:
|
||||
- run SQL queries against a SQLite database and create [Plotly][11] charts based on the result sets
|
||||
- import a CSV file into a SQLite database and visualize imported data
|
||||
- manage queries and chart settings and run them against different databases
|
||||
- import/export queries and chart settings to/from a JSON file
|
||||
- manipulate saved queries (rename, duplicate, delete)
|
||||
- set predefined queries available for all users of sqliteviz on your server (read more about predefind queries on [Wiki][10])
|
||||
- export modified SQLite database
|
||||
- use it offline
|
||||
- export a modified SQLite database
|
||||
- use it offline from your OS application menu like any other desktop app
|
||||
|
||||
## Get started
|
||||
The latest release of sqliteviz is running on [Github pages][6]. The simplest way to start is to use sqliteviz there.
|
||||
https://user-images.githubusercontent.com/24638357/117355518-fa332680-aeb2-11eb-8a69-fbcea4f7aeb0.mp4
|
||||
|
||||
## Quickstart
|
||||
The latest release of sqliteviz is deployed on GitHub Pages at [lana-k.github.io/sqliteviz][6].
|
||||
|
||||
## Wiki
|
||||
For user documentation, check out sqliteviz [Wiki][7].
|
||||
@@ -33,8 +33,9 @@ It is built on top of [react-chart-editor][3], [sql.js][4] and [Vue-Codemirror][
|
||||
[3]: https://github.com/plotly/react-chart-editor
|
||||
[4]: https://github.com/sql-js/sql.js
|
||||
[5]: https://github.com/vuejs/vue
|
||||
[6]: https://lana-k.github.io/sqliteviz
|
||||
[6]: https://lana-k.github.io/sqliteviz/
|
||||
[7]: https://github.com/lana-k/sqliteviz/wiki
|
||||
[8]: https://github.com/surmon-china/vue-codemirror#readme
|
||||
[9]: https://www.papaparse.com/
|
||||
[10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries
|
||||
[11]: https://github.com/plotly/plotly.js
|
||||
|
||||
@@ -141,7 +141,7 @@ module.exports = function (config) {
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.worker\.js$/,
|
||||
test: /worker\.js$/,
|
||||
loader: 'worker-loader'
|
||||
},
|
||||
{
|
||||
|
||||
9
lib/sql-js/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
||||
FROM emscripten/emsdk:2.0.24
|
||||
|
||||
WORKDIR /tmp/build
|
||||
|
||||
COPY configure.py .
|
||||
RUN python3.8 configure.py
|
||||
|
||||
COPY build.py .
|
||||
RUN python3.8 build.py
|
||||
73
lib/sql-js/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# SQLite WebAssembly build
|
||||
|
||||
This directory contains Docker-based build script, `make.sh`, that builds
|
||||
a custom version of [sql.js][1]. It allows sqliteviz to have more recent
|
||||
version of SQLite build with a number of useful extensions.
|
||||
|
||||
`Makefile` from [sql.js][1] is rewritten as more comprehensible `configure.py`
|
||||
and `build.py` Python scripts that run in `emscripten/emsdk` Docker container.
|
||||
|
||||
## Extension
|
||||
|
||||
SQLite [amalgamation][2] extensions included:
|
||||
|
||||
1. [FTS5][4] -- virtual table module that provides full-text search
|
||||
functionality
|
||||
|
||||
SQLite [miscellaneous extensions][3] included:
|
||||
|
||||
1. `generate_series` table-valued [series function][6] ([series.c][7])
|
||||
2. `transitive_closure` virtual table for
|
||||
[Querying Tree Structures in SQLite][11] ([closure.c][8])
|
||||
3. `uuid`, `uuid_str` and `uuid_blob` RFC-4122 UUID functions ([uuid.c][9])
|
||||
4. `regexp` (hence `REGEXP` operator) and `regexpi` functions ([regexp.c][10])
|
||||
|
||||
SQLite 3rd party extensions included:
|
||||
|
||||
1. [pivot_vtab][5] -- a pivot virtual table
|
||||
|
||||
To ease the step to have working clone locally, the build is committed into
|
||||
the repository.
|
||||
|
||||
## Build method
|
||||
|
||||
Basically it's extended amalgamation and `SQLITE_EXTRA_INIT` concisely
|
||||
described in [this message from SQLite Forum][12]:
|
||||
|
||||
> Simply append it to the end of the amalgamation file. The real problem is
|
||||
> how you get the init function called. The easiest way (to me at any rate) is
|
||||
> to append a function (after the extensions you want to add are all appended)
|
||||
> that adds the init function for each extension to the auto extension list
|
||||
> for new connections, and set the pre-processor symbol SQLITE_EXTRA_INIT to
|
||||
> the name of this function. [...]
|
||||
>
|
||||
> An example `SQLITE_EXTRA_INIT` function looks like this:
|
||||
>
|
||||
> ```
|
||||
> int core_init(const char* dummy)
|
||||
> {
|
||||
> int nErr = 0;
|
||||
>
|
||||
> nErr += sqlite3_auto_extension((void*)sqlite3_autobusy_init);
|
||||
> nErr += sqlite3_auto_extension((void*)sqlite3_ipaddress_init);
|
||||
>
|
||||
> return nErr ? SQLITE_ERROR : SQLITE_OK;
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> so you would then define `SQLITE_EXTRA_INIT=core_init` when compiling the
|
||||
> amalgamation code and the extensions would thereafter be automatically
|
||||
> initialized on each connection.
|
||||
|
||||
[1]: https://github.com/sql-js/sql.js
|
||||
[2]: https://sqlite.org/amalgamation.html
|
||||
[3]: https://sqlite.org/src/dir?ci=trunk&name=ext/misc
|
||||
[4]: https://sqlite.org/fts5.html
|
||||
[5]: https://github.com/jakethaw/pivot_vtab
|
||||
[6]: https://sqlite.org/series.html
|
||||
[7]: https://sqlite.org/src/file/ext/misc/series.c
|
||||
[8]: https://sqlite.org/src/file/ext/misc/closure.c
|
||||
[9]: https://sqlite.org/src/file/ext/misc/uuid.c
|
||||
[10]: https://sqlite.org/src/file/ext/misc/regexp.c
|
||||
[11]: https://charlesleifer.com/blog/querying-tree-structures-in-sqlite-using-python-and-the-transitive-closure-extension/
|
||||
[12]: https://sqlite.org/forum/forumpost/6ad7d4f4bebe5e06?raw
|
||||
83
lib/sql-js/build.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
cflags = (
|
||||
'-O2',
|
||||
'-DSQLITE_OMIT_LOAD_EXTENSION',
|
||||
'-DSQLITE_DISABLE_LFS',
|
||||
'-DSQLITE_ENABLE_FTS3',
|
||||
'-DSQLITE_ENABLE_FTS3_PARENTHESIS',
|
||||
'-DSQLITE_ENABLE_FTS5',
|
||||
'-DSQLITE_ENABLE_JSON1',
|
||||
'-DSQLITE_THREADSAFE=0',
|
||||
'-DSQLITE_ENABLE_NORMALIZE',
|
||||
'-DSQLITE_EXTRA_INIT=extra_init',
|
||||
'-DSQLITE_DEFAULT_MEMSTATUS=0',
|
||||
'-DSQLITE_USE_ALLOCA',
|
||||
)
|
||||
emflags = (
|
||||
# Base
|
||||
'--memory-init-file', '0',
|
||||
'-s', 'RESERVED_FUNCTION_POINTERS=64',
|
||||
'-s', 'ALLOW_TABLE_GROWTH=1',
|
||||
'-s', 'SINGLE_FILE=0',
|
||||
# WASM
|
||||
'-s', 'WASM=1',
|
||||
'-s', 'ALLOW_MEMORY_GROWTH=1',
|
||||
# Optimisation
|
||||
'-s', 'INLINING_LIMIT=50',
|
||||
'-O3',
|
||||
'-flto',
|
||||
'--closure', '1',
|
||||
# sql.js
|
||||
'-s', 'EXPORTED_FUNCTIONS=@src/sqljs/exported_functions.json',
|
||||
'-s', 'EXPORTED_RUNTIME_METHODS=@src/sqljs/exported_runtime_methods.json',
|
||||
'--pre-js', 'src/sqljs/api.js',
|
||||
)
|
||||
|
||||
|
||||
def build(src: Path, dst: Path):
|
||||
out = Path('out')
|
||||
out.mkdir()
|
||||
|
||||
logging.info('Building LLVM bitcode for sqlite3.c')
|
||||
subprocess.check_call([
|
||||
'emcc',
|
||||
*cflags,
|
||||
'-c', src / 'sqlite3.c',
|
||||
'-o', out / 'sqlite3.bc',
|
||||
])
|
||||
logging.info('Building LLVM bitcode for extension-functions.c')
|
||||
subprocess.check_call([
|
||||
'emcc',
|
||||
*cflags,
|
||||
'-c', src / 'extension-functions.c',
|
||||
'-o', out / 'extension-functions.bc',
|
||||
])
|
||||
|
||||
logging.info('Building WASM from bitcode')
|
||||
subprocess.check_call([
|
||||
'emcc',
|
||||
*emflags,
|
||||
out / 'sqlite3.bc',
|
||||
out / 'extension-functions.bc',
|
||||
'-o', out / 'sql-wasm.js',
|
||||
])
|
||||
|
||||
logging.info('Post-processing build and copying to dist')
|
||||
(out / 'sql-wasm.wasm').rename(dst / 'sql-wasm.wasm')
|
||||
with (dst / 'sql-wasm.js').open('w') as f:
|
||||
f.write((src / 'sqljs' / 'shell-pre.js').read_text())
|
||||
f.write((out / 'sql-wasm.js').read_text())
|
||||
f.write((src / 'sqljs' / 'shell-post.js').read_text())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(level='INFO', format='%(asctime)s %(levelname)s %(name)s %(message)s')
|
||||
|
||||
src = Path('src')
|
||||
dst = Path('dist')
|
||||
dst.mkdir()
|
||||
build(src, dst)
|
||||
105
lib/sql-js/configure.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import logging
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import zipfile
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from urllib import request
|
||||
|
||||
|
||||
amalgamation_url = 'https://sqlite.org/2021/sqlite-amalgamation-3360000.zip'
|
||||
|
||||
# Extension-functions
|
||||
# ===================
|
||||
# It breaks amalgamation if appended as other extension because it redefines
|
||||
# several functions, so build it separately. Note that sql.js registers these
|
||||
# extension functions by calling ``registerExtensionFunctions`` itself.
|
||||
contrib_functions_url = 'https://sqlite.org/contrib/download/extension-functions.c?get=25'
|
||||
|
||||
extension_urls = (
|
||||
# Miscellaneous extensions
|
||||
# ========================
|
||||
('https://sqlite.org/src/raw/c6bd5d24?at=series.c', 'sqlite3_series_init'),
|
||||
('https://sqlite.org/src/raw/dbfd8543?at=closure.c', 'sqlite3_closure_init'),
|
||||
('https://sqlite.org/src/raw/5bb2264c?at=uuid.c', 'sqlite3_uuid_init'),
|
||||
('https://sqlite.org/src/raw/5853b0e5?at=regexp.c', 'sqlite3_regexp_init'),
|
||||
# Third-party extension
|
||||
# =====================
|
||||
('https://github.com/jakethaw/pivot_vtab/raw/08ab0797/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
|
||||
)
|
||||
|
||||
sqljs_url = 'https://github.com/sql-js/sql.js/archive/refs/tags/v1.5.0.zip'
|
||||
|
||||
|
||||
def _generate_extra_init_c_function(init_function_names):
|
||||
auto_ext_calls = '\n'.join([
|
||||
'nErr += sqlite3_auto_extension((void*){});'.format(init_fn)
|
||||
for init_fn in init_function_names
|
||||
])
|
||||
return '''
|
||||
int extra_init(const char* dummy)
|
||||
{
|
||||
int nErr = 0;
|
||||
%s
|
||||
return nErr ? SQLITE_ERROR : SQLITE_OK;
|
||||
}
|
||||
''' % auto_ext_calls
|
||||
|
||||
|
||||
def _get_amalgamation(tgt: Path):
|
||||
logging.info('Downloading and extracting SQLite amalgamation %s', amalgamation_url)
|
||||
archive = zipfile.ZipFile(BytesIO(request.urlopen(amalgamation_url).read()))
|
||||
archive_root_dir = zipfile.Path(archive, archive.namelist()[0])
|
||||
for zpath in archive_root_dir.iterdir():
|
||||
with zpath.open() as fr, (tgt / zpath.name).open('wb') as fw:
|
||||
shutil.copyfileobj(fr, fw)
|
||||
|
||||
|
||||
def _get_contrib_functions(tgt: Path):
|
||||
request.urlretrieve(contrib_functions_url, tgt / 'extension-functions.c')
|
||||
|
||||
|
||||
def _get_extensions(tgt: Path):
|
||||
init_functions = []
|
||||
sqlite3_c = tgt / 'sqlite3.c'
|
||||
with sqlite3_c.open('ab') as f:
|
||||
for url, init_fn in extension_urls:
|
||||
logging.info('Downloading and appending to amalgamation %s', url)
|
||||
with request.urlopen(url) as resp:
|
||||
shutil.copyfileobj(resp, f)
|
||||
init_functions.append(init_fn)
|
||||
|
||||
logging.info('Appending SQLITE_EXTRA_INIT to amalgamation')
|
||||
f.write(_generate_extra_init_c_function(init_functions).encode())
|
||||
|
||||
|
||||
def _get_sqljs(tgt: Path):
|
||||
logging.info('Downloading and extracting sql.js %s', sqljs_url)
|
||||
archive = zipfile.ZipFile(BytesIO(request.urlopen(sqljs_url).read()))
|
||||
archive_root_dir = zipfile.Path(archive, archive.namelist()[0])
|
||||
(tgt / 'sqljs').mkdir()
|
||||
for zpath in (archive_root_dir / 'src').iterdir():
|
||||
with zpath.open() as fr, (tgt / 'sqljs' / zpath.name).open('wb') as fw:
|
||||
shutil.copyfileobj(fr, fw)
|
||||
|
||||
|
||||
def configure(tgt: Path):
|
||||
_get_amalgamation(tgt)
|
||||
_get_contrib_functions(tgt)
|
||||
_get_extensions(tgt)
|
||||
_get_sqljs(tgt)
|
||||
|
||||
subprocess.check_call(['emcc', '--version'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if sys.version_info < (3, 8):
|
||||
print('Python 3.8 or higher is expected', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
logging.basicConfig(level='INFO', format='%(asctime)s %(levelname)s %(name)s %(message)s')
|
||||
|
||||
src = Path('src')
|
||||
src.mkdir()
|
||||
configure(src)
|
||||
201
lib/sql-js/dist/sql-wasm.js
vendored
Normal file
@@ -0,0 +1,201 @@
|
||||
|
||||
// We are modularizing this manually because the current modularize setting in Emscripten has some issues:
|
||||
// https://github.com/kripken/emscripten/issues/5820
|
||||
// In addition, When you use emcc's modularization, it still expects to export a global object called `Module`,
|
||||
// which is able to be used/called before the WASM is loaded.
|
||||
// The modularization below exports a promise that loads and resolves to the actual sql.js module.
|
||||
// That way, this module can't be used before the WASM is finished loading.
|
||||
|
||||
// We are going to define a function that a user will call to start loading initializing our Sql.js library
|
||||
// However, that function might be called multiple times, and on subsequent calls, we don't actually want it to instantiate a new instance of the Module
|
||||
// Instead, we want to return the previously loaded module
|
||||
|
||||
// TODO: Make this not declare a global if used in the browser
|
||||
var initSqlJsPromise = undefined;
|
||||
|
||||
var initSqlJs = function (moduleConfig) {
|
||||
|
||||
if (initSqlJsPromise){
|
||||
return initSqlJsPromise;
|
||||
}
|
||||
// If we're here, we've never called this function before
|
||||
initSqlJsPromise = new Promise(function (resolveModule, reject) {
|
||||
|
||||
// We are modularizing this manually because the current modularize setting in Emscripten has some issues:
|
||||
// https://github.com/kripken/emscripten/issues/5820
|
||||
|
||||
// The way to affect the loading of emcc compiled modules is to create a variable called `Module` and add
|
||||
// properties to it, like `preRun`, `postRun`, etc
|
||||
// We are using that to get notified when the WASM has finished loading.
|
||||
// Only then will we return our promise
|
||||
|
||||
// If they passed in a moduleConfig object, use that
|
||||
// Otherwise, initialize Module to the empty object
|
||||
var Module = typeof moduleConfig !== 'undefined' ? moduleConfig : {};
|
||||
|
||||
// EMCC only allows for a single onAbort function (not an array of functions)
|
||||
// So if the user defined their own onAbort function, we remember it and call it
|
||||
var originalOnAbortFunction = Module['onAbort'];
|
||||
Module['onAbort'] = function (errorThatCausedAbort) {
|
||||
reject(new Error(errorThatCausedAbort));
|
||||
if (originalOnAbortFunction){
|
||||
originalOnAbortFunction(errorThatCausedAbort);
|
||||
}
|
||||
};
|
||||
|
||||
Module['postRun'] = Module['postRun'] || [];
|
||||
Module['postRun'].push(function () {
|
||||
// When Emscripted calls postRun, this promise resolves with the built Module
|
||||
resolveModule(Module);
|
||||
});
|
||||
|
||||
// There is a section of code in the emcc-generated code below that looks like this:
|
||||
// (Note that this is lowercase `module`)
|
||||
// if (typeof module !== 'undefined') {
|
||||
// module['exports'] = Module;
|
||||
// }
|
||||
// When that runs, it's going to overwrite our own modularization export efforts in shell-post.js!
|
||||
// The only way to tell emcc not to emit it is to pass the MODULARIZE=1 or MODULARIZE_INSTANCE=1 flags,
|
||||
// but that carries with it additional unnecessary baggage/bugs we don't want either.
|
||||
// So, we have three options:
|
||||
// 1) We undefine `module`
|
||||
// 2) We remember what `module['exports']` was at the beginning of this function and we restore it later
|
||||
// 3) We write a script to remove those lines of code as part of the Make process.
|
||||
//
|
||||
// Since those are the only lines of code that care about module, we will undefine it. It's the most straightforward
|
||||
// of the options, and has the side effect of reducing emcc's efforts to modify the module if its output were to change in the future.
|
||||
// That's a nice side effect since we're handling the modularization efforts ourselves
|
||||
module = undefined;
|
||||
|
||||
// The emcc-generated code and shell-post.js code goes below,
|
||||
// meaning that all of it runs inside of this promise. If anything throws an exception, our promise will abort
|
||||
|
||||
var e;e||(e=typeof Module !== 'undefined' ? Module : {});null;
|
||||
e.onRuntimeInitialized=function(){function a(h,l){this.Ra=h;this.db=l;this.Qa=1;this.lb=[]}function b(h,l){this.db=l;l=ba(h)+1;this.eb=ca(l);if(null===this.eb)throw Error("Unable to allocate memory for the SQL string");k(h,n,this.eb,l);this.jb=this.eb;this.$a=this.pb=null}function c(h){this.filename="dbfile_"+(4294967295*Math.random()>>>0);if(null!=h){var l=this.filename,p=l?r("//"+l):"/";l=da(!0,!0);p=ea(p,(void 0!==l?l:438)&4095|32768,0);if(h){if("string"===typeof h){for(var q=Array(h.length),B=
|
||||
0,ha=h.length;B<ha;++B)q[B]=h.charCodeAt(B);h=q}fa(p,l|146);q=v(p,577);ka(q,h,0,h.length,0,void 0);la(q);fa(p,l)}}this.handleError(g(this.filename,d));this.db=x(d,"i32");ic(this.db);this.fb={};this.Xa={}}var d=y(4),f=e.cwrap,g=f("sqlite3_open","number",["string","number"]),m=f("sqlite3_close_v2","number",["number"]),t=f("sqlite3_exec","number",["number","string","number","number","number"]),w=f("sqlite3_changes","number",["number"]),u=f("sqlite3_prepare_v2","number",["number","string","number","number",
|
||||
"number"]),C=f("sqlite3_sql","string",["number"]),H=f("sqlite3_normalized_sql","string",["number"]),aa=f("sqlite3_prepare_v2","number",["number","number","number","number","number"]),jc=f("sqlite3_bind_text","number",["number","number","number","number","number"]),rb=f("sqlite3_bind_blob","number",["number","number","number","number","number"]),kc=f("sqlite3_bind_double","number",["number","number","number"]),lc=f("sqlite3_bind_int","number",["number","number","number"]),mc=f("sqlite3_bind_parameter_index",
|
||||
"number",["number","string"]),nc=f("sqlite3_step","number",["number"]),oc=f("sqlite3_errmsg","string",["number"]),pc=f("sqlite3_column_count","number",["number"]),qc=f("sqlite3_data_count","number",["number"]),rc=f("sqlite3_column_double","number",["number","number"]),sc=f("sqlite3_column_text","string",["number","number"]),tc=f("sqlite3_column_blob","number",["number","number"]),uc=f("sqlite3_column_bytes","number",["number","number"]),vc=f("sqlite3_column_type","number",["number","number"]),wc=
|
||||
f("sqlite3_column_name","string",["number","number"]),xc=f("sqlite3_reset","number",["number"]),yc=f("sqlite3_clear_bindings","number",["number"]),zc=f("sqlite3_finalize","number",["number"]),Ac=f("sqlite3_create_function_v2","number","number string number number number number number number number".split(" ")),Bc=f("sqlite3_value_type","number",["number"]),Cc=f("sqlite3_value_bytes","number",["number"]),Dc=f("sqlite3_value_text","string",["number"]),Ec=f("sqlite3_value_blob","number",["number"]),
|
||||
Fc=f("sqlite3_value_double","number",["number"]),Gc=f("sqlite3_result_double","",["number","number"]),sb=f("sqlite3_result_null","",["number"]),Hc=f("sqlite3_result_text","",["number","string","number","number"]),Ic=f("sqlite3_result_blob","",["number","number","number","number"]),Jc=f("sqlite3_result_int","",["number","number"]),tb=f("sqlite3_result_error","",["number","string","number"]),ic=f("RegisterExtensionFunctions","number",["number"]);a.prototype.bind=function(h){if(!this.Ra)throw"Statement closed";
|
||||
this.reset();return Array.isArray(h)?this.Bb(h):null!=h&&"object"===typeof h?this.Cb(h):!0};a.prototype.step=function(){if(!this.Ra)throw"Statement closed";this.Qa=1;var h=nc(this.Ra);switch(h){case 100:return!0;case 101:return!1;default:throw this.db.handleError(h);}};a.prototype.Hb=function(h){null==h&&(h=this.Qa,this.Qa+=1);return rc(this.Ra,h)};a.prototype.Ib=function(h){null==h&&(h=this.Qa,this.Qa+=1);return sc(this.Ra,h)};a.prototype.getBlob=function(h){null==h&&(h=this.Qa,this.Qa+=1);var l=
|
||||
uc(this.Ra,h);h=tc(this.Ra,h);for(var p=new Uint8Array(l),q=0;q<l;q+=1)p[q]=z[h+q];return p};a.prototype.get=function(h){null!=h&&this.bind(h)&&this.step();h=[];for(var l=qc(this.Ra),p=0;p<l;p+=1)switch(vc(this.Ra,p)){case 1:case 2:h.push(this.Hb(p));break;case 3:h.push(this.Ib(p));break;case 4:h.push(this.getBlob(p));break;default:h.push(null)}return h};a.prototype.getColumnNames=function(){for(var h=[],l=pc(this.Ra),p=0;p<l;p+=1)h.push(wc(this.Ra,p));return h};a.prototype.getAsObject=function(h){h=
|
||||
this.get(h);for(var l=this.getColumnNames(),p={},q=0;q<l.length;q+=1)p[l[q]]=h[q];return p};a.prototype.getSQL=function(){return C(this.Ra)};a.prototype.getNormalizedSQL=function(){return H(this.Ra)};a.prototype.run=function(h){null!=h&&this.bind(h);this.step();return this.reset()};a.prototype.Fb=function(h,l){null==l&&(l=this.Qa,this.Qa+=1);h=ma(h);var p=na(h);this.lb.push(p);this.db.handleError(jc(this.Ra,l,p,h.length-1,0))};a.prototype.Ab=function(h,l){null==l&&(l=this.Qa,this.Qa+=1);var p=na(h);
|
||||
this.lb.push(p);this.db.handleError(rb(this.Ra,l,p,h.length,0))};a.prototype.Eb=function(h,l){null==l&&(l=this.Qa,this.Qa+=1);this.db.handleError((h===(h|0)?lc:kc)(this.Ra,l,h))};a.prototype.Db=function(h){null==h&&(h=this.Qa,this.Qa+=1);rb(this.Ra,h,0,0,0)};a.prototype.tb=function(h,l){null==l&&(l=this.Qa,this.Qa+=1);switch(typeof h){case "string":this.Fb(h,l);return;case "number":case "boolean":this.Eb(h+0,l);return;case "object":if(null===h){this.Db(l);return}if(null!=h.length){this.Ab(h,l);return}}throw"Wrong API use : tried to bind a value of an unknown type ("+
|
||||
h+").";};a.prototype.Cb=function(h){var l=this;Object.keys(h).forEach(function(p){var q=mc(l.Ra,p);0!==q&&l.tb(h[p],q)});return!0};a.prototype.Bb=function(h){for(var l=0;l<h.length;l+=1)this.tb(h[l],l+1);return!0};a.prototype.reset=function(){return 0===yc(this.Ra)&&0===xc(this.Ra)};a.prototype.freemem=function(){for(var h;void 0!==(h=this.lb.pop());)oa(h)};a.prototype.free=function(){var h=0===zc(this.Ra);delete this.db.fb[this.Ra];this.Ra=0;return h};b.prototype.next=function(){if(null===this.eb)return{done:!0};
|
||||
null!==this.$a&&(this.$a.free(),this.$a=null);if(!this.db.db)throw this.nb(),Error("Database closed");var h=pa(),l=y(4);qa(d);qa(l);try{this.db.handleError(aa(this.db.db,this.jb,-1,d,l));this.jb=x(l,"i32");var p=x(d,"i32");if(0===p)return this.nb(),{done:!0};this.$a=new a(p,this.db);this.db.fb[p]=this.$a;return{value:this.$a,done:!1}}catch(q){throw this.pb=A(this.jb),this.nb(),q;}finally{ra(h)}};b.prototype.nb=function(){oa(this.eb);this.eb=null};b.prototype.getRemainingSQL=function(){return null!==
|
||||
this.pb?this.pb:A(this.jb)};"function"===typeof Symbol&&"symbol"===typeof Symbol.iterator&&(b.prototype[Symbol.iterator]=function(){return this});c.prototype.run=function(h,l){if(!this.db)throw"Database closed";if(l){h=this.prepare(h,l);try{h.step()}finally{h.free()}}else this.handleError(t(this.db,h,0,0,d));return this};c.prototype.exec=function(h,l){if(!this.db)throw"Database closed";var p=pa(),q=null;try{var B=ba(h)+1,ha=y(B);k(h,z,ha,B);var D=ha;var ia=y(4);for(h=[];0!==x(D,"i8");){qa(d);qa(ia);
|
||||
this.handleError(aa(this.db,D,-1,d,ia));var ja=x(d,"i32");D=x(ia,"i32");if(0!==ja){B=null;q=new a(ja,this);for(null!=l&&q.bind(l);q.step();)null===B&&(B={columns:q.getColumnNames(),values:[]},h.push(B)),B.values.push(q.get());q.free()}}return h}catch(E){throw q&&q.free(),E;}finally{ra(p)}};c.prototype.each=function(h,l,p,q){"function"===typeof l&&(q=p,p=l,l=void 0);h=this.prepare(h,l);try{for(;h.step();)p(h.getAsObject())}finally{h.free()}if("function"===typeof q)return q()};c.prototype.prepare=function(h,
|
||||
l){qa(d);this.handleError(u(this.db,h,-1,d,0));h=x(d,"i32");if(0===h)throw"Nothing to prepare";var p=new a(h,this);null!=l&&p.bind(l);return this.fb[h]=p};c.prototype.iterateStatements=function(h){return new b(h,this)};c.prototype["export"]=function(){Object.values(this.fb).forEach(function(l){l.free()});Object.values(this.Xa).forEach(sa);this.Xa={};this.handleError(m(this.db));var h=ta(this.filename);this.handleError(g(this.filename,d));this.db=x(d,"i32");return h};c.prototype.close=function(){null!==
|
||||
this.db&&(Object.values(this.fb).forEach(function(h){h.free()}),Object.values(this.Xa).forEach(sa),this.Xa={},this.handleError(m(this.db)),ua("/"+this.filename),this.db=null)};c.prototype.handleError=function(h){if(0===h)return null;h=oc(this.db);throw Error(h);};c.prototype.getRowsModified=function(){return w(this.db)};c.prototype.create_function=function(h,l){Object.prototype.hasOwnProperty.call(this.Xa,h)&&(sa(this.Xa[h]),delete this.Xa[h]);var p=va(function(q,B,ha){for(var D,ia=[],ja=0;ja<B;ja+=
|
||||
1){var E=x(ha+4*ja,"i32"),T=Bc(E);if(1===T||2===T)E=Fc(E);else if(3===T)E=Dc(E);else if(4===T){T=E;E=Cc(T);T=Ec(T);for(var wb=new Uint8Array(E),Ba=0;Ba<E;Ba+=1)wb[Ba]=z[T+Ba];E=wb}else E=null;ia.push(E)}try{D=l.apply(null,ia)}catch(Mc){tb(q,Mc,-1);return}switch(typeof D){case "boolean":Jc(q,D?1:0);break;case "number":Gc(q,D);break;case "string":Hc(q,D,-1,-1);break;case "object":null===D?sb(q):null!=D.length?(B=na(D),Ic(q,B,D.length,-1),oa(B)):tb(q,"Wrong API use : tried to return a value of an unknown type ("+
|
||||
D+").",-1);break;default:sb(q)}});this.Xa[h]=p;this.handleError(Ac(this.db,h,l.length,1,0,p,0,0,0));return this};e.Database=c};var wa={},F;for(F in e)e.hasOwnProperty(F)&&(wa[F]=e[F]);var xa="./this.program",ya=!1,za=!1,Aa=!1,Ca=!1;ya="object"===typeof window;za="function"===typeof importScripts;Aa="object"===typeof process&&"object"===typeof process.versions&&"string"===typeof process.versions.node;Ca=!ya&&!Aa&&!za;var G="",Da,Ea,Fa,Ga,Ha;
|
||||
if(Aa)G=za?require("path").dirname(G)+"/":__dirname+"/",Da=function(a,b){Ga||(Ga=require("fs"));Ha||(Ha=require("path"));a=Ha.normalize(a);return Ga.readFileSync(a,b?null:"utf8")},Fa=function(a){a=Da(a,!0);a.buffer||(a=new Uint8Array(a));assert(a.buffer);return a},1<process.argv.length&&(xa=process.argv[1].replace(/\\/g,"/")),process.argv.slice(2),"undefined"!==typeof module&&(module.exports=e),process.on("uncaughtException",function(a){throw a;}),process.on("unhandledRejection",I),e.inspect=function(){return"[Emscripten Module object]"};
|
||||
else if(Ca)"undefined"!=typeof read&&(Da=function(a){return read(a)}),Fa=function(a){if("function"===typeof readbuffer)return new Uint8Array(readbuffer(a));a=read(a,"binary");assert("object"===typeof a);return a},"undefined"!==typeof print&&("undefined"===typeof console&&(console={}),console.log=print,console.warn=console.error="undefined"!==typeof printErr?printErr:print);else if(ya||za)za?G=self.location.href:"undefined"!==typeof document&&document.currentScript&&(G=document.currentScript.src),
|
||||
G=0!==G.indexOf("blob:")?G.substr(0,G.lastIndexOf("/")+1):"",Da=function(a){var b=new XMLHttpRequest;b.open("GET",a,!1);b.send(null);return b.responseText},za&&(Fa=function(a){var b=new XMLHttpRequest;b.open("GET",a,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)}),Ea=function(a,b,c){var d=new XMLHttpRequest;d.open("GET",a,!0);d.responseType="arraybuffer";d.onload=function(){200==d.status||0==d.status&&d.response?b(d.response):c()};d.onerror=c;d.send(null)};
|
||||
var Ia=e.print||console.log.bind(console),J=e.printErr||console.warn.bind(console);for(F in wa)wa.hasOwnProperty(F)&&(e[F]=wa[F]);wa=null;e.thisProgram&&(xa=e.thisProgram);var Ja=[],Ka;function sa(a){Ka.delete(K.get(a));Ja.push(a)}
|
||||
function va(a){if(!Ka){Ka=new WeakMap;for(var b=0;b<K.length;b++){var c=K.get(b);c&&Ka.set(c,b)}}if(Ka.has(a))a=Ka.get(a);else{if(Ja.length)b=Ja.pop();else{try{K.grow(1)}catch(g){if(!(g instanceof RangeError))throw g;throw"Unable to grow wasm table. Set ALLOW_TABLE_GROWTH.";}b=K.length-1}try{K.set(b,a)}catch(g){if(!(g instanceof TypeError))throw g;if("function"===typeof WebAssembly.Function){var d={i:"i32",j:"i64",f:"f32",d:"f64"},f={parameters:[],results:[]};for(c=1;4>c;++c)f.parameters.push(d["viii"[c]]);
|
||||
c=new WebAssembly.Function(f,a)}else{d=[1,0,1,96];f={i:127,j:126,f:125,d:124};d.push(3);for(c=0;3>c;++c)d.push(f["iii"[c]]);d.push(0);d[1]=d.length-2;c=new Uint8Array([0,97,115,109,1,0,0,0].concat(d,[2,7,1,1,101,1,102,0,0,7,5,1,1,102,0,0]));c=new WebAssembly.Module(c);c=(new WebAssembly.Instance(c,{e:{f:a}})).exports.f}K.set(b,c)}Ka.set(a,b);a=b}return a}var La;e.wasmBinary&&(La=e.wasmBinary);var noExitRuntime=e.noExitRuntime||!0;"object"!==typeof WebAssembly&&I("no native wasm support detected");
|
||||
function qa(a){var b="i32";"*"===b.charAt(b.length-1)&&(b="i32");switch(b){case "i1":z[a>>0]=0;break;case "i8":z[a>>0]=0;break;case "i16":Ma[a>>1]=0;break;case "i32":L[a>>2]=0;break;case "i64":M=[0,(N=0,1<=+Math.abs(N)?0<N?(Math.min(+Math.floor(N/4294967296),4294967295)|0)>>>0:~~+Math.ceil((N-+(~~N>>>0))/4294967296)>>>0:0)];L[a>>2]=M[0];L[a+4>>2]=M[1];break;case "float":Na[a>>2]=0;break;case "double":Oa[a>>3]=0;break;default:I("invalid type for setValue: "+b)}}
|
||||
function x(a,b){b=b||"i8";"*"===b.charAt(b.length-1)&&(b="i32");switch(b){case "i1":return z[a>>0];case "i8":return z[a>>0];case "i16":return Ma[a>>1];case "i32":return L[a>>2];case "i64":return L[a>>2];case "float":return Na[a>>2];case "double":return Oa[a>>3];default:I("invalid type for getValue: "+b)}return null}var Pa,Qa=!1;function assert(a,b){a||I("Assertion failed: "+b)}function Ra(a){var b=e["_"+a];assert(b,"Cannot call unknown function "+a+", make sure it is exported");return b}
|
||||
function Sa(a,b,c,d){var f={string:function(u){var C=0;if(null!==u&&void 0!==u&&0!==u){var H=(u.length<<2)+1;C=y(H);k(u,n,C,H)}return C},array:function(u){var C=y(u.length);z.set(u,C);return C}},g=Ra(a),m=[];a=0;if(d)for(var t=0;t<d.length;t++){var w=f[c[t]];w?(0===a&&(a=pa()),m[t]=w(d[t])):m[t]=d[t]}c=g.apply(null,m);c=function(u){return"string"===b?A(u):"boolean"===b?!!u:u}(c);0!==a&&ra(a);return c}var Ta=0,Ua=1;
|
||||
function na(a){var b=Ta==Ua?y(a.length):ca(a.length);a.subarray||a.slice?n.set(a,b):n.set(new Uint8Array(a),b);return b}var Va="undefined"!==typeof TextDecoder?new TextDecoder("utf8"):void 0;
|
||||
function Wa(a,b,c){var d=b+c;for(c=b;a[c]&&!(c>=d);)++c;if(16<c-b&&a.subarray&&Va)return Va.decode(a.subarray(b,c));for(d="";b<c;){var f=a[b++];if(f&128){var g=a[b++]&63;if(192==(f&224))d+=String.fromCharCode((f&31)<<6|g);else{var m=a[b++]&63;f=224==(f&240)?(f&15)<<12|g<<6|m:(f&7)<<18|g<<12|m<<6|a[b++]&63;65536>f?d+=String.fromCharCode(f):(f-=65536,d+=String.fromCharCode(55296|f>>10,56320|f&1023))}}else d+=String.fromCharCode(f)}return d}function A(a,b){return a?Wa(n,a,b):""}
|
||||
function k(a,b,c,d){if(!(0<d))return 0;var f=c;d=c+d-1;for(var g=0;g<a.length;++g){var m=a.charCodeAt(g);if(55296<=m&&57343>=m){var t=a.charCodeAt(++g);m=65536+((m&1023)<<10)|t&1023}if(127>=m){if(c>=d)break;b[c++]=m}else{if(2047>=m){if(c+1>=d)break;b[c++]=192|m>>6}else{if(65535>=m){if(c+2>=d)break;b[c++]=224|m>>12}else{if(c+3>=d)break;b[c++]=240|m>>18;b[c++]=128|m>>12&63}b[c++]=128|m>>6&63}b[c++]=128|m&63}}b[c]=0;return c-f}
|
||||
function ba(a){for(var b=0,c=0;c<a.length;++c){var d=a.charCodeAt(c);55296<=d&&57343>=d&&(d=65536+((d&1023)<<10)|a.charCodeAt(++c)&1023);127>=d?++b:b=2047>=d?b+2:65535>=d?b+3:b+4}return b}function Xa(a){var b=ba(a)+1,c=ca(b);c&&k(a,z,c,b);return c}var Ya,z,n,Ma,L,Na,Oa;
|
||||
function Za(){var a=Pa.buffer;Ya=a;e.HEAP8=z=new Int8Array(a);e.HEAP16=Ma=new Int16Array(a);e.HEAP32=L=new Int32Array(a);e.HEAPU8=n=new Uint8Array(a);e.HEAPU16=new Uint16Array(a);e.HEAPU32=new Uint32Array(a);e.HEAPF32=Na=new Float32Array(a);e.HEAPF64=Oa=new Float64Array(a)}var K,$a=[],ab=[],bb=[];function cb(){var a=e.preRun.shift();$a.unshift(a)}var db=0,eb=null,fb=null;e.preloadedImages={};e.preloadedAudios={};
|
||||
function I(a){if(e.onAbort)e.onAbort(a);J(a);Qa=!0;throw new WebAssembly.RuntimeError("abort("+a+"). Build with -s ASSERTIONS=1 for more info.");}function gb(){return O.startsWith("data:application/octet-stream;base64,")}var O;O="sql-wasm.wasm";if(!gb()){var hb=O;O=e.locateFile?e.locateFile(hb,G):G+hb}function ib(){var a=O;try{if(a==O&&La)return new Uint8Array(La);if(Fa)return Fa(a);throw"both async and sync fetching of the wasm failed";}catch(b){I(b)}}
|
||||
function jb(){if(!La&&(ya||za)){if("function"===typeof fetch&&!O.startsWith("file://"))return fetch(O,{credentials:"same-origin"}).then(function(a){if(!a.ok)throw"failed to load wasm binary file at '"+O+"'";return a.arrayBuffer()}).catch(function(){return ib()});if(Ea)return new Promise(function(a,b){Ea(O,function(c){a(new Uint8Array(c))},b)})}return Promise.resolve().then(function(){return ib()})}var N,M;
|
||||
function kb(a){for(;0<a.length;){var b=a.shift();if("function"==typeof b)b(e);else{var c=b.Rb;"number"===typeof c?void 0===b.mb?K.get(c)():K.get(c)(b.mb):c(void 0===b.mb?null:b.mb)}}}function lb(a){return a.replace(/\b_Z[\w\d_]+/g,function(b){return b===b?b:b+" ["+b+"]"})}
|
||||
function mb(){function a(m){return(m=m.toTimeString().match(/\(([A-Za-z ]+)\)$/))?m[1]:"GMT"}if(!nb){nb=!0;var b=(new Date).getFullYear(),c=new Date(b,0,1),d=new Date(b,6,1);b=c.getTimezoneOffset();var f=d.getTimezoneOffset(),g=Math.max(b,f);L[ob()>>2]=60*g;L[pb()>>2]=Number(b!=f);c=a(c);d=a(d);c=Xa(c);d=Xa(d);f<b?(L[qb()>>2]=c,L[qb()+4>>2]=d):(L[qb()>>2]=d,L[qb()+4>>2]=c)}}var nb;
|
||||
function ub(a,b){for(var c=0,d=a.length-1;0<=d;d--){var f=a[d];"."===f?a.splice(d,1):".."===f?(a.splice(d,1),c++):c&&(a.splice(d,1),c--)}if(b)for(;c;c--)a.unshift("..");return a}function r(a){var b="/"===a.charAt(0),c="/"===a.substr(-1);(a=ub(a.split("/").filter(function(d){return!!d}),!b).join("/"))||b||(a=".");a&&c&&(a+="/");return(b?"/":"")+a}
|
||||
function vb(a){var b=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/.exec(a).slice(1);a=b[0];b=b[1];if(!a&&!b)return".";b&&(b=b.substr(0,b.length-1));return a+b}function xb(a){if("/"===a)return"/";a=r(a);a=a.replace(/\/$/,"");var b=a.lastIndexOf("/");return-1===b?a:a.substr(b+1)}
|
||||
function yb(){if("object"===typeof crypto&&"function"===typeof crypto.getRandomValues){var a=new Uint8Array(1);return function(){crypto.getRandomValues(a);return a[0]}}if(Aa)try{var b=require("crypto");return function(){return b.randomBytes(1)[0]}}catch(c){}return function(){I("randomDevice")}}
|
||||
function zb(){for(var a="",b=!1,c=arguments.length-1;-1<=c&&!b;c--){b=0<=c?arguments[c]:"/";if("string"!==typeof b)throw new TypeError("Arguments to path.resolve must be strings");if(!b)return"";a=b+"/"+a;b="/"===b.charAt(0)}a=ub(a.split("/").filter(function(d){return!!d}),!b).join("/");return(b?"/":"")+a||"."}var Ab=[];function Bb(a,b){Ab[a]={input:[],output:[],cb:b};Cb(a,Db)}
|
||||
var Db={open:function(a){var b=Ab[a.node.rdev];if(!b)throw new P(43);a.tty=b;a.seekable=!1},close:function(a){a.tty.cb.flush(a.tty)},flush:function(a){a.tty.cb.flush(a.tty)},read:function(a,b,c,d){if(!a.tty||!a.tty.cb.xb)throw new P(60);for(var f=0,g=0;g<d;g++){try{var m=a.tty.cb.xb(a.tty)}catch(t){throw new P(29);}if(void 0===m&&0===f)throw new P(6);if(null===m||void 0===m)break;f++;b[c+g]=m}f&&(a.node.timestamp=Date.now());return f},write:function(a,b,c,d){if(!a.tty||!a.tty.cb.qb)throw new P(60);
|
||||
try{for(var f=0;f<d;f++)a.tty.cb.qb(a.tty,b[c+f])}catch(g){throw new P(29);}d&&(a.node.timestamp=Date.now());return f}},Eb={xb:function(a){if(!a.input.length){var b=null;if(Aa){var c=Buffer.zb?Buffer.zb(256):new Buffer(256),d=0;try{d=Ga.readSync(process.stdin.fd,c,0,256,null)}catch(f){if(f.toString().includes("EOF"))d=0;else throw f;}0<d?b=c.slice(0,d).toString("utf-8"):b=null}else"undefined"!=typeof window&&"function"==typeof window.prompt?(b=window.prompt("Input: "),null!==b&&(b+="\n")):"function"==
|
||||
typeof readline&&(b=readline(),null!==b&&(b+="\n"));if(!b)return null;a.input=ma(b,!0)}return a.input.shift()},qb:function(a,b){null===b||10===b?(Ia(Wa(a.output,0)),a.output=[]):0!=b&&a.output.push(b)},flush:function(a){a.output&&0<a.output.length&&(Ia(Wa(a.output,0)),a.output=[])}},Fb={qb:function(a,b){null===b||10===b?(J(Wa(a.output,0)),a.output=[]):0!=b&&a.output.push(b)},flush:function(a){a.output&&0<a.output.length&&(J(Wa(a.output,0)),a.output=[])}},Q={Va:null,Wa:function(){return Q.createNode(null,
|
||||
"/",16895,0)},createNode:function(a,b,c,d){if(24576===(c&61440)||4096===(c&61440))throw new P(63);Q.Va||(Q.Va={dir:{node:{Ua:Q.Ma.Ua,Ta:Q.Ma.Ta,lookup:Q.Ma.lookup,gb:Q.Ma.gb,rename:Q.Ma.rename,unlink:Q.Ma.unlink,rmdir:Q.Ma.rmdir,readdir:Q.Ma.readdir,symlink:Q.Ma.symlink},stream:{Za:Q.Na.Za}},file:{node:{Ua:Q.Ma.Ua,Ta:Q.Ma.Ta},stream:{Za:Q.Na.Za,read:Q.Na.read,write:Q.Na.write,sb:Q.Na.sb,hb:Q.Na.hb,ib:Q.Na.ib}},link:{node:{Ua:Q.Ma.Ua,Ta:Q.Ma.Ta,readlink:Q.Ma.readlink},stream:{}},ub:{node:{Ua:Q.Ma.Ua,
|
||||
Ta:Q.Ma.Ta},stream:Gb}});c=Hb(a,b,c,d);R(c.mode)?(c.Ma=Q.Va.dir.node,c.Na=Q.Va.dir.stream,c.Oa={}):32768===(c.mode&61440)?(c.Ma=Q.Va.file.node,c.Na=Q.Va.file.stream,c.Sa=0,c.Oa=null):40960===(c.mode&61440)?(c.Ma=Q.Va.link.node,c.Na=Q.Va.link.stream):8192===(c.mode&61440)&&(c.Ma=Q.Va.ub.node,c.Na=Q.Va.ub.stream);c.timestamp=Date.now();a&&(a.Oa[b]=c,a.timestamp=c.timestamp);return c},Sb:function(a){return a.Oa?a.Oa.subarray?a.Oa.subarray(0,a.Sa):new Uint8Array(a.Oa):new Uint8Array(0)},vb:function(a,
|
||||
b){var c=a.Oa?a.Oa.length:0;c>=b||(b=Math.max(b,c*(1048576>c?2:1.125)>>>0),0!=c&&(b=Math.max(b,256)),c=a.Oa,a.Oa=new Uint8Array(b),0<a.Sa&&a.Oa.set(c.subarray(0,a.Sa),0))},Ob:function(a,b){if(a.Sa!=b)if(0==b)a.Oa=null,a.Sa=0;else{var c=a.Oa;a.Oa=new Uint8Array(b);c&&a.Oa.set(c.subarray(0,Math.min(b,a.Sa)));a.Sa=b}},Ma:{Ua:function(a){var b={};b.dev=8192===(a.mode&61440)?a.id:1;b.ino=a.id;b.mode=a.mode;b.nlink=1;b.uid=0;b.gid=0;b.rdev=a.rdev;R(a.mode)?b.size=4096:32768===(a.mode&61440)?b.size=a.Sa:
|
||||
40960===(a.mode&61440)?b.size=a.link.length:b.size=0;b.atime=new Date(a.timestamp);b.mtime=new Date(a.timestamp);b.ctime=new Date(a.timestamp);b.Gb=4096;b.blocks=Math.ceil(b.size/b.Gb);return b},Ta:function(a,b){void 0!==b.mode&&(a.mode=b.mode);void 0!==b.timestamp&&(a.timestamp=b.timestamp);void 0!==b.size&&Q.Ob(a,b.size)},lookup:function(){throw Ib[44];},gb:function(a,b,c,d){return Q.createNode(a,b,c,d)},rename:function(a,b,c){if(R(a.mode)){try{var d=Jb(b,c)}catch(g){}if(d)for(var f in d.Oa)throw new P(55);
|
||||
}delete a.parent.Oa[a.name];a.parent.timestamp=Date.now();a.name=c;b.Oa[c]=a;b.timestamp=a.parent.timestamp;a.parent=b},unlink:function(a,b){delete a.Oa[b];a.timestamp=Date.now()},rmdir:function(a,b){var c=Jb(a,b),d;for(d in c.Oa)throw new P(55);delete a.Oa[b];a.timestamp=Date.now()},readdir:function(a){var b=[".",".."],c;for(c in a.Oa)a.Oa.hasOwnProperty(c)&&b.push(c);return b},symlink:function(a,b,c){a=Q.createNode(a,b,41471,0);a.link=c;return a},readlink:function(a){if(40960!==(a.mode&61440))throw new P(28);
|
||||
return a.link}},Na:{read:function(a,b,c,d,f){var g=a.node.Oa;if(f>=a.node.Sa)return 0;a=Math.min(a.node.Sa-f,d);if(8<a&&g.subarray)b.set(g.subarray(f,f+a),c);else for(d=0;d<a;d++)b[c+d]=g[f+d];return a},write:function(a,b,c,d,f,g){b.buffer===z.buffer&&(g=!1);if(!d)return 0;a=a.node;a.timestamp=Date.now();if(b.subarray&&(!a.Oa||a.Oa.subarray)){if(g)return a.Oa=b.subarray(c,c+d),a.Sa=d;if(0===a.Sa&&0===f)return a.Oa=b.slice(c,c+d),a.Sa=d;if(f+d<=a.Sa)return a.Oa.set(b.subarray(c,c+d),f),d}Q.vb(a,f+
|
||||
d);if(a.Oa.subarray&&b.subarray)a.Oa.set(b.subarray(c,c+d),f);else for(g=0;g<d;g++)a.Oa[f+g]=b[c+g];a.Sa=Math.max(a.Sa,f+d);return d},Za:function(a,b,c){1===c?b+=a.position:2===c&&32768===(a.node.mode&61440)&&(b+=a.node.Sa);if(0>b)throw new P(28);return b},sb:function(a,b,c){Q.vb(a.node,b+c);a.node.Sa=Math.max(a.node.Sa,b+c)},hb:function(a,b,c,d,f,g){if(0!==b)throw new P(28);if(32768!==(a.node.mode&61440))throw new P(43);a=a.node.Oa;if(g&2||a.buffer!==Ya){if(0<d||d+c<a.length)a.subarray?a=a.subarray(d,
|
||||
d+c):a=Array.prototype.slice.call(a,d,d+c);d=!0;g=65536*Math.ceil(c/65536);for(b=ca(g);c<g;)z[b+c++]=0;c=b;if(!c)throw new P(48);z.set(a,c)}else d=!1,c=a.byteOffset;return{Nb:c,kb:d}},ib:function(a,b,c,d,f){if(32768!==(a.node.mode&61440))throw new P(43);if(f&2)return 0;Q.Na.write(a,b,0,d,c,!1);return 0}}},Kb=null,Lb={},S=[],Mb=1,U=null,Nb=!0,V={},P=null,Ib={};
|
||||
function W(a,b){a=zb("/",a);b=b||{};if(!a)return{path:"",node:null};var c={wb:!0,rb:0},d;for(d in c)void 0===b[d]&&(b[d]=c[d]);if(8<b.rb)throw new P(32);a=ub(a.split("/").filter(function(m){return!!m}),!1);var f=Kb;c="/";for(d=0;d<a.length;d++){var g=d===a.length-1;if(g&&b.parent)break;f=Jb(f,a[d]);c=r(c+"/"+a[d]);f.ab&&(!g||g&&b.wb)&&(f=f.ab.root);if(!g||b.Ya)for(g=0;40960===(f.mode&61440);)if(f=Ob(c),c=zb(vb(c),f),f=W(c,{rb:b.rb}).node,40<g++)throw new P(32);}return{path:c,node:f}}
|
||||
function Pb(a){for(var b;;){if(a===a.parent)return a=a.Wa.yb,b?"/"!==a[a.length-1]?a+"/"+b:a+b:a;b=b?a.name+"/"+b:a.name;a=a.parent}}function Qb(a,b){for(var c=0,d=0;d<b.length;d++)c=(c<<5)-c+b.charCodeAt(d)|0;return(a+c>>>0)%U.length}function Rb(a){var b=Qb(a.parent.id,a.name);if(U[b]===a)U[b]=a.bb;else for(b=U[b];b;){if(b.bb===a){b.bb=a.bb;break}b=b.bb}}
|
||||
function Jb(a,b){var c;if(c=(c=Sb(a,"x"))?c:a.Ma.lookup?0:2)throw new P(c,a);for(c=U[Qb(a.id,b)];c;c=c.bb){var d=c.name;if(c.parent.id===a.id&&d===b)return c}return a.Ma.lookup(a,b)}function Hb(a,b,c,d){a=new Tb(a,b,c,d);b=Qb(a.parent.id,a.name);a.bb=U[b];return U[b]=a}function R(a){return 16384===(a&61440)}var Ub={r:0,"r+":2,w:577,"w+":578,a:1089,"a+":1090};function Vb(a){var b=["r","w","rw"][a&3];a&512&&(b+="w");return b}
|
||||
function Sb(a,b){if(Nb)return 0;if(!b.includes("r")||a.mode&292){if(b.includes("w")&&!(a.mode&146)||b.includes("x")&&!(a.mode&73))return 2}else return 2;return 0}function Wb(a,b){try{return Jb(a,b),20}catch(c){}return Sb(a,"wx")}function Xb(a,b,c){try{var d=Jb(a,b)}catch(f){return f.Pa}if(a=Sb(a,"wx"))return a;if(c){if(!R(d.mode))return 54;if(d===d.parent||"/"===Pb(d))return 10}else if(R(d.mode))return 31;return 0}function Yb(a){var b=4096;for(a=a||0;a<=b;a++)if(!S[a])return a;throw new P(33);}
|
||||
function Zb(a,b){$b||($b=function(){},$b.prototype={});var c=new $b,d;for(d in a)c[d]=a[d];a=c;b=Yb(b);a.fd=b;return S[b]=a}var Gb={open:function(a){a.Na=Lb[a.node.rdev].Na;a.Na.open&&a.Na.open(a)},Za:function(){throw new P(70);}};function Cb(a,b){Lb[a]={Na:b}}
|
||||
function ac(a,b){var c="/"===b,d=!b;if(c&&Kb)throw new P(10);if(!c&&!d){var f=W(b,{wb:!1});b=f.path;f=f.node;if(f.ab)throw new P(10);if(!R(f.mode))throw new P(54);}b={type:a,Tb:{},yb:b,Lb:[]};a=a.Wa(b);a.Wa=b;b.root=a;c?Kb=a:f&&(f.ab=b,f.Wa&&f.Wa.Lb.push(b))}function ea(a,b,c){var d=W(a,{parent:!0}).node;a=xb(a);if(!a||"."===a||".."===a)throw new P(28);var f=Wb(d,a);if(f)throw new P(f);if(!d.Ma.gb)throw new P(63);return d.Ma.gb(d,a,b,c)}
|
||||
function X(a,b){return ea(a,(void 0!==b?b:511)&1023|16384,0)}function bc(a,b,c){"undefined"===typeof c&&(c=b,b=438);ea(a,b|8192,c)}function cc(a,b){if(!zb(a))throw new P(44);var c=W(b,{parent:!0}).node;if(!c)throw new P(44);b=xb(b);var d=Wb(c,b);if(d)throw new P(d);if(!c.Ma.symlink)throw new P(63);c.Ma.symlink(c,b,a)}
|
||||
function ua(a){var b=W(a,{parent:!0}).node,c=xb(a),d=Jb(b,c),f=Xb(b,c,!1);if(f)throw new P(f);if(!b.Ma.unlink)throw new P(63);if(d.ab)throw new P(10);try{V.willDeletePath&&V.willDeletePath(a)}catch(g){J("FS.trackingDelegate['willDeletePath']('"+a+"') threw an exception: "+g.message)}b.Ma.unlink(b,c);Rb(d);try{if(V.onDeletePath)V.onDeletePath(a)}catch(g){J("FS.trackingDelegate['onDeletePath']('"+a+"') threw an exception: "+g.message)}}
|
||||
function Ob(a){a=W(a).node;if(!a)throw new P(44);if(!a.Ma.readlink)throw new P(28);return zb(Pb(a.parent),a.Ma.readlink(a))}function dc(a,b){a=W(a,{Ya:!b}).node;if(!a)throw new P(44);if(!a.Ma.Ua)throw new P(63);return a.Ma.Ua(a)}function ec(a){return dc(a,!0)}function fa(a,b){a="string"===typeof a?W(a,{Ya:!0}).node:a;if(!a.Ma.Ta)throw new P(63);a.Ma.Ta(a,{mode:b&4095|a.mode&-4096,timestamp:Date.now()})}
|
||||
function fc(a){a="string"===typeof a?W(a,{Ya:!0}).node:a;if(!a.Ma.Ta)throw new P(63);a.Ma.Ta(a,{timestamp:Date.now()})}function hc(a,b){if(0>b)throw new P(28);a="string"===typeof a?W(a,{Ya:!0}).node:a;if(!a.Ma.Ta)throw new P(63);if(R(a.mode))throw new P(31);if(32768!==(a.mode&61440))throw new P(28);var c=Sb(a,"w");if(c)throw new P(c);a.Ma.Ta(a,{size:b,timestamp:Date.now()})}
|
||||
function v(a,b,c,d){if(""===a)throw new P(44);if("string"===typeof b){var f=Ub[b];if("undefined"===typeof f)throw Error("Unknown file open mode: "+b);b=f}c=b&64?("undefined"===typeof c?438:c)&4095|32768:0;if("object"===typeof a)var g=a;else{a=r(a);try{g=W(a,{Ya:!(b&131072)}).node}catch(m){}}f=!1;if(b&64)if(g){if(b&128)throw new P(20);}else g=ea(a,c,0),f=!0;if(!g)throw new P(44);8192===(g.mode&61440)&&(b&=-513);if(b&65536&&!R(g.mode))throw new P(54);if(!f&&(c=g?40960===(g.mode&61440)?32:R(g.mode)&&
|
||||
("r"!==Vb(b)||b&512)?31:Sb(g,Vb(b)):44))throw new P(c);b&512&&hc(g,0);b&=-131713;d=Zb({node:g,path:Pb(g),flags:b,seekable:!0,position:0,Na:g.Na,Qb:[],error:!1},d);d.Na.open&&d.Na.open(d);!e.logReadFiles||b&1||(Kc||(Kc={}),a in Kc||(Kc[a]=1,J("FS.trackingDelegate error on read file: "+a)));try{V.onOpenFile&&(g=0,1!==(b&2097155)&&(g|=1),0!==(b&2097155)&&(g|=2),V.onOpenFile(a,g))}catch(m){J("FS.trackingDelegate['onOpenFile']('"+a+"', flags) threw an exception: "+m.message)}return d}
|
||||
function la(a){if(null===a.fd)throw new P(8);a.ob&&(a.ob=null);try{a.Na.close&&a.Na.close(a)}catch(b){throw b;}finally{S[a.fd]=null}a.fd=null}function Lc(a,b,c){if(null===a.fd)throw new P(8);if(!a.seekable||!a.Na.Za)throw new P(70);if(0!=c&&1!=c&&2!=c)throw new P(28);a.position=a.Na.Za(a,b,c);a.Qb=[]}
|
||||
function Nc(a,b,c,d,f){if(0>d||0>f)throw new P(28);if(null===a.fd)throw new P(8);if(1===(a.flags&2097155))throw new P(8);if(R(a.node.mode))throw new P(31);if(!a.Na.read)throw new P(28);var g="undefined"!==typeof f;if(!g)f=a.position;else if(!a.seekable)throw new P(70);b=a.Na.read(a,b,c,d,f);g||(a.position+=b);return b}
|
||||
function ka(a,b,c,d,f,g){if(0>d||0>f)throw new P(28);if(null===a.fd)throw new P(8);if(0===(a.flags&2097155))throw new P(8);if(R(a.node.mode))throw new P(31);if(!a.Na.write)throw new P(28);a.seekable&&a.flags&1024&&Lc(a,0,2);var m="undefined"!==typeof f;if(!m)f=a.position;else if(!a.seekable)throw new P(70);b=a.Na.write(a,b,c,d,f,g);m||(a.position+=b);try{if(a.path&&V.onWriteToFile)V.onWriteToFile(a.path)}catch(t){J("FS.trackingDelegate['onWriteToFile']('"+a.path+"') threw an exception: "+t.message)}return b}
|
||||
function ta(a){var b={encoding:"binary"};b=b||{};b.flags=b.flags||0;b.encoding=b.encoding||"binary";if("utf8"!==b.encoding&&"binary"!==b.encoding)throw Error('Invalid encoding type "'+b.encoding+'"');var c,d=v(a,b.flags);a=dc(a).size;var f=new Uint8Array(a);Nc(d,f,0,a,0);"utf8"===b.encoding?c=Wa(f,0):"binary"===b.encoding&&(c=f);la(d);return c}
|
||||
function Oc(){P||(P=function(a,b){this.node=b;this.Pb=function(c){this.Pa=c};this.Pb(a);this.message="FS error"},P.prototype=Error(),P.prototype.constructor=P,[44].forEach(function(a){Ib[a]=new P(a);Ib[a].stack="<generic error, no stack>"}))}var Pc;function da(a,b){var c=0;a&&(c|=365);b&&(c|=146);return c}
|
||||
function Qc(a,b,c){a=r("/dev/"+a);var d=da(!!b,!!c);Rc||(Rc=64);var f=Rc++<<8|0;Cb(f,{open:function(g){g.seekable=!1},close:function(){c&&c.buffer&&c.buffer.length&&c(10)},read:function(g,m,t,w){for(var u=0,C=0;C<w;C++){try{var H=b()}catch(aa){throw new P(29);}if(void 0===H&&0===u)throw new P(6);if(null===H||void 0===H)break;u++;m[t+C]=H}u&&(g.node.timestamp=Date.now());return u},write:function(g,m,t,w){for(var u=0;u<w;u++)try{c(m[t+u])}catch(C){throw new P(29);}w&&(g.node.timestamp=Date.now());return u}});
|
||||
bc(a,d,f)}var Rc,Y={},$b,Kc,Sc={};
|
||||
function Tc(a,b,c){try{var d=a(b)}catch(f){if(f&&f.node&&r(b)!==r(Pb(f.node)))return-54;throw f;}L[c>>2]=d.dev;L[c+4>>2]=0;L[c+8>>2]=d.ino;L[c+12>>2]=d.mode;L[c+16>>2]=d.nlink;L[c+20>>2]=d.uid;L[c+24>>2]=d.gid;L[c+28>>2]=d.rdev;L[c+32>>2]=0;M=[d.size>>>0,(N=d.size,1<=+Math.abs(N)?0<N?(Math.min(+Math.floor(N/4294967296),4294967295)|0)>>>0:~~+Math.ceil((N-+(~~N>>>0))/4294967296)>>>0:0)];L[c+40>>2]=M[0];L[c+44>>2]=M[1];L[c+48>>2]=4096;L[c+52>>2]=d.blocks;L[c+56>>2]=d.atime.getTime()/1E3|0;L[c+60>>2]=
|
||||
0;L[c+64>>2]=d.mtime.getTime()/1E3|0;L[c+68>>2]=0;L[c+72>>2]=d.ctime.getTime()/1E3|0;L[c+76>>2]=0;M=[d.ino>>>0,(N=d.ino,1<=+Math.abs(N)?0<N?(Math.min(+Math.floor(N/4294967296),4294967295)|0)>>>0:~~+Math.ceil((N-+(~~N>>>0))/4294967296)>>>0:0)];L[c+80>>2]=M[0];L[c+84>>2]=M[1];return 0}var Uc=void 0;function Vc(){Uc+=4;return L[Uc-4>>2]}function Z(a){a=S[a];if(!a)throw new P(8);return a}var Wc;
|
||||
Aa?Wc=function(){var a=process.hrtime();return 1E3*a[0]+a[1]/1E6}:"undefined"!==typeof dateNow?Wc=dateNow:Wc=function(){return performance.now()};var Xc={};function Yc(){if(!Zc){var a={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:("object"===typeof navigator&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8",_:xa||"./this.program"},b;for(b in Xc)void 0===Xc[b]?delete a[b]:a[b]=Xc[b];var c=[];for(b in a)c.push(b+"="+a[b]);Zc=c}return Zc}var Zc;
|
||||
function Tb(a,b,c,d){a||(a=this);this.parent=a;this.Wa=a.Wa;this.ab=null;this.id=Mb++;this.name=b;this.mode=c;this.Ma={};this.Na={};this.rdev=d}Object.defineProperties(Tb.prototype,{read:{get:function(){return 365===(this.mode&365)},set:function(a){a?this.mode|=365:this.mode&=-366}},write:{get:function(){return 146===(this.mode&146)},set:function(a){a?this.mode|=146:this.mode&=-147}}});Oc();U=Array(4096);ac(Q,"/");X("/tmp");X("/home");X("/home/web_user");
|
||||
(function(){X("/dev");Cb(259,{read:function(){return 0},write:function(b,c,d,f){return f}});bc("/dev/null",259);Bb(1280,Eb);Bb(1536,Fb);bc("/dev/tty",1280);bc("/dev/tty1",1536);var a=yb();Qc("random",a);Qc("urandom",a);X("/dev/shm");X("/dev/shm/tmp")})();
|
||||
(function(){X("/proc");var a=X("/proc/self");X("/proc/self/fd");ac({Wa:function(){var b=Hb(a,"fd",16895,73);b.Ma={lookup:function(c,d){var f=S[+d];if(!f)throw new P(8);c={parent:null,Wa:{yb:"fake"},Ma:{readlink:function(){return f.path}}};return c.parent=c}};return b}},"/proc/self/fd")})();function ma(a,b){var c=Array(ba(a)+1);a=k(a,c,0,c.length);b&&(c.length=a);return c}
|
||||
var cd={a:function(a,b,c,d){I("Assertion failed: "+A(a)+", at: "+[b?A(b):"unknown filename",c,d?A(d):"unknown function"])},s:function(a,b){mb();a=new Date(1E3*L[a>>2]);L[b>>2]=a.getSeconds();L[b+4>>2]=a.getMinutes();L[b+8>>2]=a.getHours();L[b+12>>2]=a.getDate();L[b+16>>2]=a.getMonth();L[b+20>>2]=a.getFullYear()-1900;L[b+24>>2]=a.getDay();var c=new Date(a.getFullYear(),0,1);L[b+28>>2]=(a.getTime()-c.getTime())/864E5|0;L[b+36>>2]=-(60*a.getTimezoneOffset());var d=(new Date(a.getFullYear(),6,1)).getTimezoneOffset();
|
||||
c=c.getTimezoneOffset();a=(d!=c&&a.getTimezoneOffset()==Math.min(c,d))|0;L[b+32>>2]=a;a=L[qb()+(a?4:0)>>2];L[b+40>>2]=a;return b},F:function(a,b){try{a=A(a);if(b&-8)var c=-28;else{var d;(d=W(a,{Ya:!0}).node)?(a="",b&4&&(a+="r"),b&2&&(a+="w"),b&1&&(a+="x"),c=a&&Sb(d,a)?-2:0):c=-44}return c}catch(f){return"undefined"!==typeof Y&&f instanceof P||I(f),-f.Pa}},h:function(a,b){try{return a=A(a),fa(a,b),0}catch(c){return"undefined"!==typeof Y&&c instanceof P||I(c),-c.Pa}},y:function(a){try{return a=A(a),
|
||||
fc(a),0}catch(b){return"undefined"!==typeof Y&&b instanceof P||I(b),-b.Pa}},i:function(a,b){try{var c=S[a];if(!c)throw new P(8);fa(c.node,b);return 0}catch(d){return"undefined"!==typeof Y&&d instanceof P||I(d),-d.Pa}},z:function(a){try{var b=S[a];if(!b)throw new P(8);fc(b.node);return 0}catch(c){return"undefined"!==typeof Y&&c instanceof P||I(c),-c.Pa}},b:function(a,b,c){Uc=c;try{var d=Z(a);switch(b){case 0:var f=Vc();return 0>f?-28:v(d.path,d.flags,0,f).fd;case 1:case 2:return 0;case 3:return d.flags;
|
||||
case 4:return f=Vc(),d.flags|=f,0;case 12:return f=Vc(),Ma[f+0>>1]=2,0;case 13:case 14:return 0;case 16:case 8:return-28;case 9:return L[$c()>>2]=28,-1;default:return-28}}catch(g){return"undefined"!==typeof Y&&g instanceof P||I(g),-g.Pa}},j:function(a,b){try{var c=Z(a);return Tc(dc,c.path,b)}catch(d){return"undefined"!==typeof Y&&d instanceof P||I(d),-d.Pa}},E:function(a,b,c){try{var d=S[a];if(!d)throw new P(8);if(0===(d.flags&2097155))throw new P(28);hc(d.node,c);return 0}catch(f){return"undefined"!==
|
||||
typeof Y&&f instanceof P||I(f),-f.Pa}},C:function(a,b){try{if(0===b)return-28;if(b<ba("/")+1)return-68;k("/",n,a,b);return a}catch(c){return"undefined"!==typeof Y&&c instanceof P||I(c),-c.Pa}},x:function(){return 0},d:function(){return 42},k:function(a,b){try{return a=A(a),Tc(ec,a,b)}catch(c){return"undefined"!==typeof Y&&c instanceof P||I(c),-c.Pa}},J:function(a,b){try{return a=A(a),a=r(a),"/"===a[a.length-1]&&(a=a.substr(0,a.length-1)),X(a,b),0}catch(c){return"undefined"!==typeof Y&&c instanceof
|
||||
P||I(c),-c.Pa}},G:function(a,b,c,d,f,g){try{a:{g<<=12;var m=!1;if(0!==(d&16)&&0!==a%65536)var t=-28;else{if(0!==(d&32)){var w=ad(65536,b);if(!w){t=-48;break a}bd(w,0,b);m=!0}else{var u=S[f];if(!u){t=-8;break a}var C=g;if(0!==(c&2)&&0===(d&2)&&2!==(u.flags&2097155))throw new P(2);if(1===(u.flags&2097155))throw new P(2);if(!u.Na.hb)throw new P(43);var H=u.Na.hb(u,a,b,C,c,d);w=H.Nb;m=H.kb}Sc[w]={Kb:w,Jb:b,kb:m,fd:f,Mb:c,flags:d,offset:g};t=w}}return t}catch(aa){return"undefined"!==typeof Y&&aa instanceof
|
||||
P||I(aa),-aa.Pa}},H:function(a,b){try{if(-1===(a|0)||0===b)var c=-28;else{var d=Sc[a];if(d&&b===d.Jb){var f=S[d.fd];if(f&&d.Mb&2){var g=d.flags,m=d.offset,t=n.slice(a,a+b);f&&f.Na.ib&&f.Na.ib(f,t,m,b,g)}Sc[a]=null;d.kb&&oa(d.Kb)}c=0}return c}catch(w){return"undefined"!==typeof Y&&w instanceof P||I(w),-w.Pa}},I:function(a,b,c){Uc=c;try{var d=A(a),f=c?Vc():0;return v(d,b,f).fd}catch(g){return"undefined"!==typeof Y&&g instanceof P||I(g),-g.Pa}},A:function(a,b,c){try{a=A(a);if(0>=c)var d=-28;else{var f=
|
||||
Ob(a),g=Math.min(c,ba(f)),m=z[b+g];k(f,n,b,c+1);z[b+g]=m;d=g}return d}catch(t){return"undefined"!==typeof Y&&t instanceof P||I(t),-t.Pa}},u:function(a){try{a=A(a);var b=W(a,{parent:!0}).node,c=xb(a),d=Jb(b,c),f=Xb(b,c,!0);if(f)throw new P(f);if(!b.Ma.rmdir)throw new P(63);if(d.ab)throw new P(10);try{V.willDeletePath&&V.willDeletePath(a)}catch(g){J("FS.trackingDelegate['willDeletePath']('"+a+"') threw an exception: "+g.message)}b.Ma.rmdir(b,c);Rb(d);try{if(V.onDeletePath)V.onDeletePath(a)}catch(g){J("FS.trackingDelegate['onDeletePath']('"+
|
||||
a+"') threw an exception: "+g.message)}return 0}catch(g){return"undefined"!==typeof Y&&g instanceof P||I(g),-g.Pa}},e:function(a,b){try{return a=A(a),Tc(dc,a,b)}catch(c){return"undefined"!==typeof Y&&c instanceof P||I(c),-c.Pa}},w:function(a){try{return a=A(a),ua(a),0}catch(b){return"undefined"!==typeof Y&&b instanceof P||I(b),-b.Pa}},l:function(){return 2147483648},n:function(a,b,c){n.copyWithin(a,b,b+c)},c:function(a){var b=n.length;a>>>=0;if(2147483648<a)return!1;for(var c=1;4>=c;c*=2){var d=b*
|
||||
(1+.2/c);d=Math.min(d,a+100663296);d=Math.max(a,d);0<d%65536&&(d+=65536-d%65536);a:{try{Pa.grow(Math.min(2147483648,d)-Ya.byteLength+65535>>>16);Za();var f=1;break a}catch(g){}f=void 0}if(f)return!0}return!1},r:function(a){for(var b=Wc();Wc()-b<a;);},p:function(a,b){try{var c=0;Yc().forEach(function(d,f){var g=b+c;f=L[a+4*f>>2]=g;for(g=0;g<d.length;++g)z[f++>>0]=d.charCodeAt(g);z[f>>0]=0;c+=d.length+1});return 0}catch(d){return"undefined"!==typeof Y&&d instanceof P||I(d),d.Pa}},q:function(a,b){try{var c=
|
||||
Yc();L[a>>2]=c.length;var d=0;c.forEach(function(f){d+=f.length+1});L[b>>2]=d;return 0}catch(f){return"undefined"!==typeof Y&&f instanceof P||I(f),f.Pa}},f:function(a){try{var b=Z(a);la(b);return 0}catch(c){return"undefined"!==typeof Y&&c instanceof P||I(c),c.Pa}},o:function(a,b){try{var c=Z(a);z[b>>0]=c.tty?2:R(c.mode)?3:40960===(c.mode&61440)?7:4;return 0}catch(d){return"undefined"!==typeof Y&&d instanceof P||I(d),d.Pa}},t:function(a,b,c,d){try{a:{for(var f=Z(a),g=a=0;g<c;g++){var m=L[b+(8*g+4)>>
|
||||
2],t=Nc(f,z,L[b+8*g>>2],m,void 0);if(0>t){var w=-1;break a}a+=t;if(t<m)break}w=a}L[d>>2]=w;return 0}catch(u){return"undefined"!==typeof Y&&u instanceof P||I(u),u.Pa}},m:function(a,b,c,d,f){try{var g=Z(a);a=4294967296*c+(b>>>0);if(-9007199254740992>=a||9007199254740992<=a)return-61;Lc(g,a,d);M=[g.position>>>0,(N=g.position,1<=+Math.abs(N)?0<N?(Math.min(+Math.floor(N/4294967296),4294967295)|0)>>>0:~~+Math.ceil((N-+(~~N>>>0))/4294967296)>>>0:0)];L[f>>2]=M[0];L[f+4>>2]=M[1];g.ob&&0===a&&0===d&&(g.ob=
|
||||
null);return 0}catch(m){return"undefined"!==typeof Y&&m instanceof P||I(m),m.Pa}},D:function(a){try{var b=Z(a);return b.Na&&b.Na.fsync?-b.Na.fsync(b):0}catch(c){return"undefined"!==typeof Y&&c instanceof P||I(c),c.Pa}},v:function(a,b,c,d){try{a:{for(var f=Z(a),g=a=0;g<c;g++){var m=ka(f,z,L[b+8*g>>2],L[b+(8*g+4)>>2],void 0);if(0>m){var t=-1;break a}a+=m}t=a}L[d>>2]=t;return 0}catch(w){return"undefined"!==typeof Y&&w instanceof P||I(w),w.Pa}},g:function(a){var b=Date.now();L[a>>2]=b/1E3|0;L[a+4>>2]=
|
||||
b%1E3*1E3|0;return 0},K:function(a){var b=Date.now()/1E3|0;a&&(L[a>>2]=b);return b},B:function(a,b){if(b){var c=b+8;b=1E3*L[c>>2];b+=L[c+4>>2]/1E3}else b=Date.now();a=A(a);try{var d=W(a,{Ya:!0}).node;d.Ma.Ta(d,{timestamp:Math.max(b,b)});var f=0}catch(g){if(!(g instanceof P)){b:{f=Error();if(!f.stack){try{throw Error();}catch(m){f=m}if(!f.stack){f="(no stack trace available)";break b}}f=f.stack.toString()}e.extraStackTrace&&(f+="\n"+e.extraStackTrace());f=lb(f);throw g+" : "+f;}f=g.Pa;L[$c()>>2]=f;
|
||||
f=-1}return f}};
|
||||
(function(){function a(f){e.asm=f.exports;Pa=e.asm.L;Za();K=e.asm.Da;ab.unshift(e.asm.M);db--;e.monitorRunDependencies&&e.monitorRunDependencies(db);0==db&&(null!==eb&&(clearInterval(eb),eb=null),fb&&(f=fb,fb=null,f()))}function b(f){a(f.instance)}function c(f){return jb().then(function(g){return WebAssembly.instantiate(g,d)}).then(f,function(g){J("failed to asynchronously prepare wasm: "+g);I(g)})}var d={a:cd};db++;e.monitorRunDependencies&&e.monitorRunDependencies(db);if(e.instantiateWasm)try{return e.instantiateWasm(d,a)}catch(f){return J("Module.instantiateWasm callback failed with error: "+
|
||||
f),!1}(function(){return La||"function"!==typeof WebAssembly.instantiateStreaming||gb()||O.startsWith("file://")||"function"!==typeof fetch?c(b):fetch(O,{credentials:"same-origin"}).then(function(f){return WebAssembly.instantiateStreaming(f,d).then(b,function(g){J("wasm streaming compile failed: "+g);J("falling back to ArrayBuffer instantiation");return c(b)})})})();return{}})();e.___wasm_call_ctors=function(){return(e.___wasm_call_ctors=e.asm.M).apply(null,arguments)};
|
||||
var bd=e._memset=function(){return(bd=e._memset=e.asm.N).apply(null,arguments)};e._sqlite3_free=function(){return(e._sqlite3_free=e.asm.O).apply(null,arguments)};var $c=e.___errno_location=function(){return($c=e.___errno_location=e.asm.P).apply(null,arguments)};e._sqlite3_step=function(){return(e._sqlite3_step=e.asm.Q).apply(null,arguments)};e._sqlite3_finalize=function(){return(e._sqlite3_finalize=e.asm.R).apply(null,arguments)};
|
||||
e._sqlite3_prepare_v2=function(){return(e._sqlite3_prepare_v2=e.asm.S).apply(null,arguments)};e._sqlite3_reset=function(){return(e._sqlite3_reset=e.asm.T).apply(null,arguments)};e._sqlite3_clear_bindings=function(){return(e._sqlite3_clear_bindings=e.asm.U).apply(null,arguments)};e._sqlite3_value_blob=function(){return(e._sqlite3_value_blob=e.asm.V).apply(null,arguments)};e._sqlite3_value_text=function(){return(e._sqlite3_value_text=e.asm.W).apply(null,arguments)};
|
||||
e._sqlite3_value_bytes=function(){return(e._sqlite3_value_bytes=e.asm.X).apply(null,arguments)};e._sqlite3_value_double=function(){return(e._sqlite3_value_double=e.asm.Y).apply(null,arguments)};e._sqlite3_value_int=function(){return(e._sqlite3_value_int=e.asm.Z).apply(null,arguments)};e._sqlite3_value_type=function(){return(e._sqlite3_value_type=e.asm._).apply(null,arguments)};e._sqlite3_result_blob=function(){return(e._sqlite3_result_blob=e.asm.$).apply(null,arguments)};
|
||||
e._sqlite3_result_double=function(){return(e._sqlite3_result_double=e.asm.aa).apply(null,arguments)};e._sqlite3_result_error=function(){return(e._sqlite3_result_error=e.asm.ba).apply(null,arguments)};e._sqlite3_result_int=function(){return(e._sqlite3_result_int=e.asm.ca).apply(null,arguments)};e._sqlite3_result_int64=function(){return(e._sqlite3_result_int64=e.asm.da).apply(null,arguments)};e._sqlite3_result_null=function(){return(e._sqlite3_result_null=e.asm.ea).apply(null,arguments)};
|
||||
e._sqlite3_result_text=function(){return(e._sqlite3_result_text=e.asm.fa).apply(null,arguments)};e._sqlite3_column_count=function(){return(e._sqlite3_column_count=e.asm.ga).apply(null,arguments)};e._sqlite3_data_count=function(){return(e._sqlite3_data_count=e.asm.ha).apply(null,arguments)};e._sqlite3_column_blob=function(){return(e._sqlite3_column_blob=e.asm.ia).apply(null,arguments)};e._sqlite3_column_bytes=function(){return(e._sqlite3_column_bytes=e.asm.ja).apply(null,arguments)};
|
||||
e._sqlite3_column_double=function(){return(e._sqlite3_column_double=e.asm.ka).apply(null,arguments)};e._sqlite3_column_text=function(){return(e._sqlite3_column_text=e.asm.la).apply(null,arguments)};e._sqlite3_column_type=function(){return(e._sqlite3_column_type=e.asm.ma).apply(null,arguments)};e._sqlite3_column_name=function(){return(e._sqlite3_column_name=e.asm.na).apply(null,arguments)};e._sqlite3_bind_blob=function(){return(e._sqlite3_bind_blob=e.asm.oa).apply(null,arguments)};
|
||||
e._sqlite3_bind_double=function(){return(e._sqlite3_bind_double=e.asm.pa).apply(null,arguments)};e._sqlite3_bind_int=function(){return(e._sqlite3_bind_int=e.asm.qa).apply(null,arguments)};e._sqlite3_bind_text=function(){return(e._sqlite3_bind_text=e.asm.ra).apply(null,arguments)};e._sqlite3_bind_parameter_index=function(){return(e._sqlite3_bind_parameter_index=e.asm.sa).apply(null,arguments)};e._sqlite3_sql=function(){return(e._sqlite3_sql=e.asm.ta).apply(null,arguments)};
|
||||
e._sqlite3_normalized_sql=function(){return(e._sqlite3_normalized_sql=e.asm.ua).apply(null,arguments)};e._sqlite3_errmsg=function(){return(e._sqlite3_errmsg=e.asm.va).apply(null,arguments)};e._sqlite3_exec=function(){return(e._sqlite3_exec=e.asm.wa).apply(null,arguments)};e._sqlite3_changes=function(){return(e._sqlite3_changes=e.asm.xa).apply(null,arguments)};e._sqlite3_close_v2=function(){return(e._sqlite3_close_v2=e.asm.ya).apply(null,arguments)};
|
||||
e._sqlite3_create_function_v2=function(){return(e._sqlite3_create_function_v2=e.asm.za).apply(null,arguments)};e._sqlite3_open=function(){return(e._sqlite3_open=e.asm.Aa).apply(null,arguments)};var ca=e._malloc=function(){return(ca=e._malloc=e.asm.Ba).apply(null,arguments)},oa=e._free=function(){return(oa=e._free=e.asm.Ca).apply(null,arguments)};e._RegisterExtensionFunctions=function(){return(e._RegisterExtensionFunctions=e.asm.Ea).apply(null,arguments)};
|
||||
var qb=e.__get_tzname=function(){return(qb=e.__get_tzname=e.asm.Fa).apply(null,arguments)},pb=e.__get_daylight=function(){return(pb=e.__get_daylight=e.asm.Ga).apply(null,arguments)},ob=e.__get_timezone=function(){return(ob=e.__get_timezone=e.asm.Ha).apply(null,arguments)},pa=e.stackSave=function(){return(pa=e.stackSave=e.asm.Ia).apply(null,arguments)},ra=e.stackRestore=function(){return(ra=e.stackRestore=e.asm.Ja).apply(null,arguments)},y=e.stackAlloc=function(){return(y=e.stackAlloc=e.asm.Ka).apply(null,
|
||||
arguments)},ad=e._memalign=function(){return(ad=e._memalign=e.asm.La).apply(null,arguments)};e.cwrap=function(a,b,c,d){c=c||[];var f=c.every(function(g){return"number"===g});return"string"!==b&&f&&!d?Ra(a):function(){return Sa(a,b,c,arguments)}};e.UTF8ToString=A;e.stackSave=pa;e.stackRestore=ra;e.stackAlloc=y;var dd;fb=function ed(){dd||fd();dd||(fb=ed)};
|
||||
function fd(){function a(){if(!dd&&(dd=!0,e.calledRun=!0,!Qa)){e.noFSInit||Pc||(Pc=!0,Oc(),e.stdin=e.stdin,e.stdout=e.stdout,e.stderr=e.stderr,e.stdin?Qc("stdin",e.stdin):cc("/dev/tty","/dev/stdin"),e.stdout?Qc("stdout",null,e.stdout):cc("/dev/tty","/dev/stdout"),e.stderr?Qc("stderr",null,e.stderr):cc("/dev/tty1","/dev/stderr"),v("/dev/stdin",0),v("/dev/stdout",1),v("/dev/stderr",1));Nb=!1;kb(ab);if(e.onRuntimeInitialized)e.onRuntimeInitialized();if(e.postRun)for("function"==typeof e.postRun&&(e.postRun=
|
||||
[e.postRun]);e.postRun.length;){var b=e.postRun.shift();bb.unshift(b)}kb(bb)}}if(!(0<db)){if(e.preRun)for("function"==typeof e.preRun&&(e.preRun=[e.preRun]);e.preRun.length;)cb();kb($a);0<db||(e.setStatus?(e.setStatus("Running..."),setTimeout(function(){setTimeout(function(){e.setStatus("")},1);a()},1)):a())}}e.run=fd;if(e.preInit)for("function"==typeof e.preInit&&(e.preInit=[e.preInit]);0<e.preInit.length;)e.preInit.pop()();fd();
|
||||
|
||||
|
||||
// The shell-pre.js and emcc-generated code goes above
|
||||
return Module;
|
||||
}); // The end of the promise being returned
|
||||
|
||||
return initSqlJsPromise;
|
||||
} // The end of our initSqlJs function
|
||||
|
||||
// This bit below is copied almost exactly from what you get when you use the MODULARIZE=1 flag with emcc
|
||||
// However, we don't want to use the emcc modularization. See shell-pre.js
|
||||
if (typeof exports === 'object' && typeof module === 'object'){
|
||||
module.exports = initSqlJs;
|
||||
// This will allow the module to be used in ES6 or CommonJS
|
||||
module.exports.default = initSqlJs;
|
||||
}
|
||||
else if (typeof define === 'function' && define['amd']) {
|
||||
define([], function() { return initSqlJs; });
|
||||
}
|
||||
else if (typeof exports === 'object'){
|
||||
exports["Module"] = initSqlJs;
|
||||
}
|
||||
BIN
lib/sql-js/dist/sql-wasm.wasm
vendored
Normal file
9
lib/sql-js/make.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
docker build -t sqliteviz/sqljs .
|
||||
|
||||
rm -r dist || true
|
||||
|
||||
CONTAINER=$(docker create sqliteviz/sqljs)
|
||||
docker cp $CONTAINER:/tmp/build/dist .
|
||||
docker rm $CONTAINER
|
||||
4
lib/sql-js/package.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "sql.js",
|
||||
"main": "./dist/sql-wasm.js"
|
||||
}
|
||||
860
package-lock.json
generated
11
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sqliteviz",
|
||||
"version": "1.0.0",
|
||||
"version": "0.14.1",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -12,15 +12,14 @@
|
||||
"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.57.1",
|
||||
"papaparse": "^5.3.1",
|
||||
"plotly.js": "^1.58.4",
|
||||
"promise-worker": "^2.0.1",
|
||||
"react": "^16.13.1",
|
||||
"react-chart-editor": "^0.42.0",
|
||||
"react-chart-editor": "^0.45.0",
|
||||
"react-dom": "^16.13.1",
|
||||
"sql.js": "^1.3.0",
|
||||
"sql.js": "file:./lib/sql-js",
|
||||
"sqlite-parser": "^1.0.1",
|
||||
"vue": "^2.6.11",
|
||||
"vue-codemirror": "^4.0.6",
|
||||
|
||||
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 11 KiB |
BIN
public/Logo48x48.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 798 B After Width: | Height: | Size: 774 B |
@@ -8,7 +8,7 @@
|
||||
<link rel="manifest" href="<%= BASE_URL %>manifest.webmanifest">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
<style>
|
||||
#loading-wrapper {
|
||||
#sqliteviz-loading-wrapper {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -17,7 +17,7 @@
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#loading-text {
|
||||
#sqliteviz-loading-text {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@@ -28,7 +28,7 @@
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.svg-container {
|
||||
#sqliteviz-loading-wrapper svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
@@ -36,7 +36,7 @@
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.loader-svg {
|
||||
#sqliteviz-loading-wrapper circle {
|
||||
position: absolute;
|
||||
left: 0; right: 0; top: 0; bottom: 0;
|
||||
fill: none;
|
||||
@@ -45,16 +45,16 @@
|
||||
stroke: #119DFF;
|
||||
}
|
||||
|
||||
.loader-svg.bg {
|
||||
#sqliteviz-loading-wrapper circle.bg {
|
||||
stroke: #C8D4E3;
|
||||
}
|
||||
|
||||
.loader-svg.front {
|
||||
#sqliteviz-loading-wrapper circle.front {
|
||||
stroke-dasharray: 402px;
|
||||
animation: loading 2s linear 0s infinite;
|
||||
animation: sqliteviz-loading 2s linear 0s infinite;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
@keyframes sqliteviz-loading {
|
||||
0% {
|
||||
stroke-dasharray: 100px 402px;
|
||||
stroke-dashoffset: 0;
|
||||
@@ -75,17 +75,17 @@
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app">
|
||||
<div id="loading-wrapper">
|
||||
<div id="loading-text">LOADING</div>
|
||||
<svg class="svg-container" height="170" width="170" viewBox="0 0 170 170">
|
||||
<div id="sqliteviz-loading-wrapper">
|
||||
<div id="sqliteviz-loading-text">LOADING</div>
|
||||
<svg height="170" width="170" viewBox="0 0 170 170">
|
||||
<circle
|
||||
class="loader-svg bg"
|
||||
class="bg"
|
||||
cx="85"
|
||||
cy="85"
|
||||
r="80"
|
||||
/>
|
||||
<circle
|
||||
class="loader-svg front"
|
||||
class="front"
|
||||
cx="85"
|
||||
cy="85"
|
||||
r="80"
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
"description": "Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases or CSV.",
|
||||
"display": "fullscreen",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "Logo48x48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "Logo192x192.png",
|
||||
"sizes": "192x192",
|
||||
|
||||
@@ -60,4 +60,7 @@ button,
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
.CodeMirror-hints {
|
||||
z-index: 999 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 42 KiB |
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ascii from '@/ascii'
|
||||
import ascii from './ascii'
|
||||
import DropDownChevron from '@/components/svg/dropDownChevron'
|
||||
import ClearIcon from '@/components/svg/clear'
|
||||
|
||||
@@ -10,14 +10,23 @@ export default {
|
||||
getResult (source) {
|
||||
const result = {}
|
||||
if (source.meta.fields) {
|
||||
result.columns = source.meta.fields
|
||||
result.columns = source.meta.fields.map(col => col.trim())
|
||||
result.values = source.data.map(row => {
|
||||
const resultRow = []
|
||||
result.columns.forEach(col => { resultRow.push(row[col]) })
|
||||
source.meta.fields.forEach(col => {
|
||||
let value = row[col]
|
||||
if (value instanceof Date) {
|
||||
value = value.toISOString()
|
||||
}
|
||||
resultRow.push(value)
|
||||
})
|
||||
|
||||
return resultRow
|
||||
})
|
||||
} else {
|
||||
result.values = source.data
|
||||
result.values = source.data.map(row => row.map(value =>
|
||||
value instanceof Date ? value.toISOString() : value
|
||||
))
|
||||
result.columns = []
|
||||
for (let i = 1; i <= source.data[0].length; i++) {
|
||||
result.columns.push(`col${i}`)
|
||||
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>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="db-uploader-container" :style="{ width }">
|
||||
<change-db-icon v-if="type === 'small'" @click.native="browse"/>
|
||||
<div v-if="['regular', 'illustrated'].includes(type)" class="drop-area-container">
|
||||
<div v-if="type === 'illustrated'" class="drop-area-container">
|
||||
<div
|
||||
class="drop-area"
|
||||
@dragover.prevent="state = 'dragover'"
|
||||
@@ -26,7 +26,8 @@
|
||||
ref="fileImg"
|
||||
:class="{
|
||||
'swing': state === 'dragover',
|
||||
'fly': state === 'drop'
|
||||
'fly': state === 'dropping',
|
||||
'hidden': state === 'dropped'
|
||||
}"
|
||||
:src="require('@/assets/images/file.png')"
|
||||
/>
|
||||
@@ -41,112 +42,22 @@
|
||||
<div id="error" class="error"></div>
|
||||
|
||||
<!--Parse csv dialog -->
|
||||
<modal name="parse" classes="dialog" 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>
|
||||
<csv-import
|
||||
ref="addCsv"
|
||||
:file="file"
|
||||
:db="newDb"
|
||||
dialog-name="importFromCsv"
|
||||
@cancel="cancelCsvImport"
|
||||
@finish="finish"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import fu from '@/file.utils'
|
||||
import csv from '@/csv'
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
import TextField from '@/components/TextField'
|
||||
import DelimiterSelector from '@/components/DelimiterSelector'
|
||||
import CheckBox from '@/components/CheckBox'
|
||||
import SqlTable from '@/components/SqlTable'
|
||||
import Logs from '@/components/Logs'
|
||||
import fIo from '@/lib/utils/fileIo'
|
||||
import ChangeDbIcon from '@/components/svg/changeDb'
|
||||
import time from '@/time'
|
||||
import database from '@/database'
|
||||
|
||||
const csvMimeTypes = [
|
||||
'text/csv',
|
||||
'text/x-csv',
|
||||
'application/x-csv',
|
||||
'application/csv',
|
||||
'text/x-comma-separated-values',
|
||||
'text/comma-separated-values'
|
||||
]
|
||||
import database from '@/lib/database'
|
||||
import CsvImport from '@/components/CsvImport'
|
||||
|
||||
export default {
|
||||
name: 'DbUploader',
|
||||
@@ -154,9 +65,9 @@ export default {
|
||||
type: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'regular',
|
||||
default: 'small',
|
||||
validator: (value) => {
|
||||
return ['regular', 'illustrated', 'small'].includes(value)
|
||||
return ['illustrated', 'small'].includes(value)
|
||||
}
|
||||
},
|
||||
width: {
|
||||
@@ -167,27 +78,13 @@ export default {
|
||||
},
|
||||
components: {
|
||||
ChangeDbIcon,
|
||||
TextField,
|
||||
DelimiterSelector,
|
||||
CloseIcon,
|
||||
CheckBox,
|
||||
SqlTable,
|
||||
Logs
|
||||
CsvImport
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
state: '',
|
||||
animationPromise: Promise.resolve(),
|
||||
file: null,
|
||||
schema: null,
|
||||
delimiter: '',
|
||||
quoteChar: '"',
|
||||
escapeChar: '"',
|
||||
header: true,
|
||||
previewData: null,
|
||||
importCsvMessages: [],
|
||||
disableDialog: false,
|
||||
importCsvCompleted: false,
|
||||
newDb: null
|
||||
}
|
||||
},
|
||||
@@ -196,206 +93,50 @@ export default {
|
||||
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
|
||||
}
|
||||
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 tabId = await this.$store.dispatch('addTab', { query: 'select * from csv_import' })
|
||||
this.$store.commit('setCurrentTabId', tabId)
|
||||
}
|
||||
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.createDb(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
|
||||
.then(this.finish)
|
||||
},
|
||||
|
||||
async checkFile (file) {
|
||||
this.state = 'drop'
|
||||
if (csvMimeTypes.includes(file.type)) {
|
||||
this.file = file
|
||||
this.header = true
|
||||
this.quoteChar = '"'
|
||||
this.escapeChar = '"'
|
||||
this.delimiter = ''
|
||||
return Promise.all([this.previewCSV(), this.animationPromise])
|
||||
.then(() => {
|
||||
this.$modal.show('parse')
|
||||
})
|
||||
} else {
|
||||
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 () {
|
||||
fu.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
|
||||
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
|
||||
.then(this.checkFile)
|
||||
},
|
||||
|
||||
@@ -431,6 +172,7 @@ export default {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#img-container {
|
||||
@@ -502,42 +244,16 @@ export default {
|
||||
#file-img.fly {
|
||||
animation: fly ease-in-out 1s 1 normal;
|
||||
transform-origin: center center;
|
||||
top: 183px;
|
||||
left: 225px;
|
||||
transition: top 1s ease-in-out, left 1s ease-in-out;
|
||||
}
|
||||
@keyframes fly {
|
||||
100% { transform: rotate(360deg) scale(0.5); }
|
||||
}
|
||||
/* Parse CSV dialog */
|
||||
.chars {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.char-input {
|
||||
margin-right: 44px;
|
||||
}
|
||||
.preview-table {
|
||||
margin-top: 32px;
|
||||
100% {
|
||||
transform: rotate(360deg) scale(0.5);
|
||||
top: 183px;
|
||||
left: 225px;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
#file-img.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
<template>
|
||||
<svg :class="['svg-container', animationClass ]" height="20" width="20" viewBox="0 0 20 20">
|
||||
<svg :class="animationClass" :height="size" :width="size" :viewBox="`0 0 ${size} ${size}`">
|
||||
<circle
|
||||
class="loader-svg bg"
|
||||
cx="10"
|
||||
cy="10"
|
||||
r="8"
|
||||
:style="{ strokeWidth }"
|
||||
:cx="size / 2"
|
||||
:cy="size / 2"
|
||||
:r="radius"
|
||||
/>
|
||||
<circle
|
||||
class="loader-svg front"
|
||||
:style="{ strokeDasharray: circleProgress }"
|
||||
cx="10"
|
||||
cy="10"
|
||||
r="8"
|
||||
:style="{ strokeDasharray: circleProgress, strokeDashoffset: offset, strokeWidth }"
|
||||
:cx="size / 2"
|
||||
:cy="size / 2"
|
||||
:r="radius"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -19,15 +20,35 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'LoadingIndicator',
|
||||
props: ['progress'],
|
||||
props: {
|
||||
progress: {
|
||||
type: Number,
|
||||
required: false
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 20
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
circleProgress () {
|
||||
const dash = (50.24 * this.progress) / 100
|
||||
const space = 50.24 - dash
|
||||
const circle = this.radius * 3.14 * 2
|
||||
const dash = this.progress ? (circle * this.progress) / 100 : circle * 1 / 3
|
||||
const space = circle - dash
|
||||
return `${dash}px, ${space}px`
|
||||
},
|
||||
animationClass () {
|
||||
return this.progress === undefined ? 'loading' : 'progress'
|
||||
},
|
||||
radius () {
|
||||
return this.size / 2 - this.strokeWidth
|
||||
},
|
||||
offset () {
|
||||
return this.radius * 3.14 / 2
|
||||
},
|
||||
strokeWidth () {
|
||||
return this.size / 10
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +59,6 @@ export default {
|
||||
position: absolute;
|
||||
left: 0; right: 0; top: 0; bottom: 0;
|
||||
fill: none;
|
||||
stroke-width: 2px;
|
||||
stroke-linecap: round;
|
||||
stroke: var(--color-accent);
|
||||
}
|
||||
@@ -48,27 +68,30 @@ export default {
|
||||
}
|
||||
|
||||
.loading .loader-svg.front {
|
||||
stroke-dasharray: 40.24px;
|
||||
will-change: transform;
|
||||
animation: fill-animation-loading 1s cubic-bezier(1,1,1,1) 0s infinite;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
/*
|
||||
We can't change anything in loading animation except transform, opacity and filter. Because in
|
||||
our case the Main Thread can be busy and animation will be frozen (e. g. getting a result set
|
||||
from the web-worker after query execution).
|
||||
But transform, opacity and filter trigger changes only in the Composite Layer stage in rendering
|
||||
waterfall. Hence they can be processed only with Compositor Thread while the Main Thread
|
||||
processes something else.
|
||||
https://www.viget.com/articles/animation-performance-101-browser-under-the-hood/
|
||||
*/
|
||||
@keyframes fill-animation-loading {
|
||||
0% {
|
||||
stroke-dasharray: 10px 40.24px;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
50% {
|
||||
stroke-dasharray: 25.12px;
|
||||
stroke-dashoffset: 25.12px;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 10px 40.24px;
|
||||
stroke-dashoffset: 50.24px;
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.progress .loader-svg.front {
|
||||
stroke-dashoffset: 12.56;
|
||||
transition: stroke-dasharray 0.2s;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,14 +63,16 @@ export default {
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--color-border-light);
|
||||
box-sizing: border-box;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
.msg {
|
||||
padding: 16px 7px;
|
||||
padding: 12px 7px;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.msg:last-child {
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import splitter from '@/splitter'
|
||||
import splitter from './splitter'
|
||||
|
||||
export default {
|
||||
name: 'Splitpanes',
|
||||
@@ -41,6 +41,7 @@
|
||||
<div class="table-footer-count">
|
||||
{{ dataSet.values.length}} {{dataSet.values.length === 1 ? 'row' : 'rows'}} retrieved
|
||||
<span v-if="preview">for preview</span>
|
||||
<span v-if="time">in {{ time }}</span>
|
||||
</div>
|
||||
<pager v-show="pageCount > 1" :page-count="pageCount" v-model="currentPage" />
|
||||
</div>
|
||||
@@ -48,12 +49,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Pager from '@/components/Pager'
|
||||
import Pager from './Pager'
|
||||
|
||||
export default {
|
||||
name: 'SqlTable',
|
||||
components: { Pager },
|
||||
props: ['dataSet', 'height', 'preview'],
|
||||
props: ['dataSet', 'time', 'height', 'preview'],
|
||||
data () {
|
||||
return {
|
||||
header: null,
|
||||
@@ -87,4 +87,7 @@ input.error {
|
||||
margin-top: 2px;
|
||||
position: absolute;
|
||||
}
|
||||
.text-field-error:first-letter {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
|
||||
61
src/components/svg/addTable.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<span>
|
||||
<svg
|
||||
class="icon"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
@click.stop="$emit('click')"
|
||||
@mouseover="showTooltip"
|
||||
@mouseout="hideTooltip"
|
||||
>
|
||||
<g clip-path="url(#clip0)">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="
|
||||
M13.6573 1.5H2.59985C1.77485 1.5 1.09985 2.175 1.09985 3V13.6649C1.09985 14.4899
|
||||
1.77485 15.1649 2.59985
|
||||
15.1649H9.84V13.6649H8.87866V9.08244H13.6573V9.83777H15.1573V3C15.1573
|
||||
2.17 14.4873 1.5 13.6573 1.5ZM13.6573
|
||||
7.58244V3H8.87866V7.58244H13.6573ZM7.37866 3H2.59985V7.58244H7.37866V3ZM2.59985
|
||||
9.08244V13.6649H7.37866V9.08244H2.59985ZM13.1702
|
||||
10.8434H15.6702V13.1717H18.0001V15.6717H15.6702V18H13.1702V15.6717H10.8401V13.1717H13.1702V10.8434Z
|
||||
"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="18" height="18" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span class="icon-tooltip" :style="tooltipStyle">
|
||||
Add new table from CSV
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'AddTableIcon',
|
||||
mixins: [tooltipMixin],
|
||||
props: ['tooltip']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
display: block;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon:hover path {
|
||||
fill: var(--color-accent);
|
||||
}
|
||||
</style>
|
||||
@@ -16,13 +16,13 @@
|
||||
/>
|
||||
</svg>
|
||||
<span class="icon-tooltip" :style="tooltipStyle">
|
||||
Change database
|
||||
Load another database or CSV
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'changeDbIcon',
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'ExportIcon',
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'HintIcon',
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
export default {
|
||||
* generateChunks (arr, size) {
|
||||
const count = Math.ceil(arr.length / size)
|
||||
|
||||
for (let i = 0; i <= count - 1; i++) {
|
||||
const start = size * i
|
||||
const end = start + size
|
||||
yield arr.slice(start, end)
|
||||
}
|
||||
},
|
||||
|
||||
getInsertStmt (columns) {
|
||||
const colList = `"${columns.join('", "')}"`
|
||||
const params = columns.map(() => '?').join(', ')
|
||||
return `INSERT INTO csv_import (${colList}) VALUES (${params});`
|
||||
},
|
||||
|
||||
getCreateStatement (columns, values) {
|
||||
let result = 'CREATE table csv_import('
|
||||
columns.forEach((col, index) => {
|
||||
// Get the first row of values to determine types
|
||||
const value = values[0][index]
|
||||
let type = ''
|
||||
switch (typeof value) {
|
||||
case 'number': {
|
||||
type = 'REAL'
|
||||
break
|
||||
}
|
||||
case 'boolean': {
|
||||
type = 'INTEGER'
|
||||
break
|
||||
}
|
||||
case 'string': {
|
||||
type = 'TEXT'
|
||||
break
|
||||
}
|
||||
default: type = 'TEXT'
|
||||
}
|
||||
result += `"${col}" ${type}, `
|
||||
})
|
||||
result = result.replace(/,\s$/, ');')
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import initSqlJs from 'sql.js/dist/sql-wasm.js'
|
||||
import dbUtils from '@/db.utils'
|
||||
import dbUtils from './_statements'
|
||||
|
||||
let SQL = null
|
||||
const sqlModuleReady = initSqlJs().then(sqlModule => { SQL = sqlModule })
|
||||
@@ -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 })
|
||||
90
src/lib/database/_statements.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import sqliteParser from 'sqlite-parser'
|
||||
|
||||
export default {
|
||||
* generateChunks (arr, size) {
|
||||
const count = Math.ceil(arr.length / size)
|
||||
|
||||
for (let i = 0; i <= count - 1; i++) {
|
||||
const start = size * i
|
||||
const end = start + size
|
||||
yield arr.slice(start, end)
|
||||
}
|
||||
},
|
||||
|
||||
getInsertStmt (tabName, columns) {
|
||||
const colList = `"${columns.join('", "')}"`
|
||||
const params = columns.map(() => '?').join(', ')
|
||||
return `INSERT INTO "${tabName}" (${colList}) VALUES (${params});`
|
||||
},
|
||||
|
||||
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]
|
||||
let type = ''
|
||||
switch (typeof value) {
|
||||
case 'number': {
|
||||
type = 'REAL'
|
||||
break
|
||||
}
|
||||
case 'boolean': {
|
||||
type = 'INTEGER'
|
||||
break
|
||||
}
|
||||
case 'string': {
|
||||
type = 'TEXT'
|
||||
break
|
||||
}
|
||||
default: type = 'TEXT'
|
||||
}
|
||||
result += `"${col}" ${type}, `
|
||||
})
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import registerPromiseWorker from 'promise-worker/register'
|
||||
import Sql from '@/sql'
|
||||
import Sql from './_sql'
|
||||
|
||||
const sqlReady = Sql.build()
|
||||
|
||||
@@ -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,8 +1,8 @@
|
||||
import sqliteParser from 'sqlite-parser'
|
||||
import fu from '@/file.utils'
|
||||
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/
|
||||
import Worker from '@/db.worker.js'
|
||||
import Worker from './_worker.js'
|
||||
|
||||
// Use promise-worker in order to turn worker into the promise based one:
|
||||
// https://github.com/nolanlawson/promise-worker
|
||||
@@ -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,33 +52,35 @@ class Database {
|
||||
delete this.importProgresses[id]
|
||||
}
|
||||
|
||||
async createDb (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) {
|
||||
const fileContent = await fu.readAsArrayBuffer(file)
|
||||
const fileContent = file ? await fu.readAsArrayBuffer(file) : null
|
||||
const res = await this.pw.postMessage({ action: 'open', buffer: fileContent })
|
||||
|
||||
if (res.error) {
|
||||
throw new Error(res.error)
|
||||
}
|
||||
|
||||
return this.getSchema(file.name.replace(/\.[^.]+$/, ''))
|
||||
this.dbName = file ? fu.getFileName(file) : 'database'
|
||||
this.refreshSchema()
|
||||
}
|
||||
|
||||
async getSchema (name) {
|
||||
async refreshSchema () {
|
||||
const getSchemaSql = `
|
||||
SELECT name, sql
|
||||
FROM sqlite_master
|
||||
@@ -85,21 +89,21 @@ class Database {
|
||||
const result = await this.execute(getSchemaSql)
|
||||
// Parse DDL statements to get column names and types
|
||||
const parsedSchema = []
|
||||
result.values.forEach(item => {
|
||||
parsedSchema.push({
|
||||
name: item[0],
|
||||
columns: getColumns(item[1])
|
||||
if (result && result.values) {
|
||||
result.values.forEach(item => {
|
||||
parsedSchema.push({
|
||||
name: item[0],
|
||||
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) {
|
||||
@@ -117,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=.+"/, '')
|
||||
.replace(/(?<=tokenize=.+)"remove_diacritics=.+"/, '')
|
||||
.replace(/(?<=tokenize=.+)"separators=.+"/, '')
|
||||
.replace(/tokenize=.+(?=(,|\)))/, 'tokenize=unicode61')
|
||||
|
||||
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 _
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { nanoid } from 'nanoid'
|
||||
import fu from '@/file.utils'
|
||||
import fu from '@/lib/utils/fileIo'
|
||||
|
||||
export default {
|
||||
getStoredQueries () {
|
||||
@@ -1,4 +1,15 @@
|
||||
export default {
|
||||
isDatabase (file) {
|
||||
const dbTypes = ['application/vnd.sqlite3', 'application/x-sqlite3']
|
||||
return file.type
|
||||
? dbTypes.includes(file.type)
|
||||
: /\.(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')
|
||||
15
src/lib/utils/time.js
Normal file
@@ -0,0 +1,15 @@
|
||||
export default {
|
||||
getPeriod (start, end) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import App from '@/App.vue'
|
||||
import router from '@/router'
|
||||
import store from '@/store'
|
||||
import { VuePlugin } from 'vuera'
|
||||
import VModal from 'vue-js-modal'
|
||||
|
||||
@@ -13,7 +13,7 @@ import '@/assets/styles/tooltips.css'
|
||||
import '@/assets/styles/messages.css'
|
||||
|
||||
if (!['localhost', '127.0.0.1'].includes(location.hostname)) {
|
||||
import('../registerServiceWorker') // eslint-disable-line no-unused-expressions
|
||||
import('./registerServiceWorker') // eslint-disable-line no-unused-expressions
|
||||
}
|
||||
|
||||
Vue.use(VuePlugin)
|
||||
|
||||
@@ -13,7 +13,7 @@ function invokeServiceWorkerUpdateFlow (registration) {
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', async () => {
|
||||
const registration = await navigator.serviceWorker.register('/service-worker.js')
|
||||
const registration = await navigator.serviceWorker.register('service-worker.js')
|
||||
// ensure the case when the updatefound event was missed is also handled
|
||||
// by re-invoking the prompt when there's a waiting Service Worker
|
||||
if (registration.waiting) {
|
||||
50
src/router.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import Editor from '@/views/Main/Editor'
|
||||
import MyQueries from '@/views/Main/MyQueries'
|
||||
import Welcome from '@/views/Welcome'
|
||||
import Main from '@/views/Main'
|
||||
import store from '@/store'
|
||||
import database from '@/lib/database'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Welcome',
|
||||
component: Welcome
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'Main',
|
||||
component: Main,
|
||||
children: [
|
||||
{
|
||||
path: '/editor',
|
||||
name: 'Editor',
|
||||
component: Editor
|
||||
},
|
||||
{
|
||||
path: '/my-queries',
|
||||
name: 'MyQueries',
|
||||
component: MyQueries
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
routes
|
||||
})
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
if (!store.state.db) {
|
||||
const newDb = database.getNewDatabase()
|
||||
await newDb.loadDb()
|
||||
store.commit('setDb', newDb)
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -1,39 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import Editor from '@/views/Editor'
|
||||
import MyQueries from '@/views/MyQueries'
|
||||
import Home from '@/views/Home'
|
||||
import MainView from '@/views/MainView'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Welcome',
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'MainView',
|
||||
component: MainView,
|
||||
children: [
|
||||
{
|
||||
path: '/editor',
|
||||
name: 'Editor',
|
||||
component: Editor
|
||||
},
|
||||
{
|
||||
path: '/my-queries',
|
||||
name: 'MyQueries',
|
||||
component: MyQueries
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
30
src/store/actions.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
export default {
|
||||
async addTab ({ state }, data) {
|
||||
const tab = data ? JSON.parse(JSON.stringify(data)) : {}
|
||||
// If no data then create a new blank one...
|
||||
// No data.id means to create new tab, but not blank,
|
||||
// e.g. with 'select * from csv_import' query after csv import
|
||||
if (!data || !data.id) {
|
||||
tab.id = nanoid()
|
||||
tab.name = null
|
||||
tab.tempName = state.untitledLastIndex
|
||||
? `Untitled ${state.untitledLastIndex}`
|
||||
: 'Untitled'
|
||||
tab.isUnsaved = true
|
||||
} else {
|
||||
tab.isUnsaved = false
|
||||
}
|
||||
|
||||
// add new tab only if was not already opened
|
||||
if (!state.tabs.some(openedTab => openedTab.id === tab.id)) {
|
||||
state.tabs.push(tab)
|
||||
if (!tab.name) {
|
||||
state.untitledLastIndex += 1
|
||||
}
|
||||
}
|
||||
|
||||
return tab.id
|
||||
}
|
||||
}
|
||||
@@ -1,112 +1,11 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import { nanoid } from 'nanoid'
|
||||
import state from '@/store/state'
|
||||
import mutations from '@/store/mutations'
|
||||
import actions from '@/store/actions'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export const state = {
|
||||
schema: null,
|
||||
dbFile: null,
|
||||
dbName: null,
|
||||
tabs: [],
|
||||
currentTab: null,
|
||||
currentTabId: null,
|
||||
untitledLastIndex: 0,
|
||||
predefinedQueries: [],
|
||||
db: null
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
setDb (state, db) {
|
||||
if (state.db) {
|
||||
state.db.shutDown()
|
||||
}
|
||||
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]
|
||||
const oldId = tab.id
|
||||
|
||||
if (id && state.currentTabId === oldId) {
|
||||
state.currentTabId = id
|
||||
}
|
||||
|
||||
if (id) { tab.id = id }
|
||||
if (name) { tab.name = name }
|
||||
if (query) { tab.query = query }
|
||||
if (chart) { tab.chart = chart }
|
||||
if (isUnsaved !== undefined) { tab.isUnsaved = isUnsaved }
|
||||
if (!isUnsaved) {
|
||||
// Saved query is not predefined
|
||||
delete tab.isPredefined
|
||||
}
|
||||
|
||||
Vue.set(state.tabs, index, tab)
|
||||
},
|
||||
deleteTab (state, index) {
|
||||
// If closing tab is the current opened
|
||||
if (state.tabs[index].id === state.currentTabId) {
|
||||
if (index < state.tabs.length - 1) {
|
||||
state.currentTabId = state.tabs[index + 1].id
|
||||
} else if (index > 0) {
|
||||
state.currentTabId = state.tabs[index - 1].id
|
||||
} else {
|
||||
state.currentTabId = null
|
||||
state.currentTab = null
|
||||
state.untitledLastIndex = 0
|
||||
}
|
||||
}
|
||||
state.tabs.splice(index, 1)
|
||||
},
|
||||
setCurrentTabId (state, id) {
|
||||
state.currentTabId = id
|
||||
},
|
||||
setCurrentTab (state, tab) {
|
||||
state.currentTab = tab
|
||||
},
|
||||
updatePredefinedQueries (state, queries) {
|
||||
if (Array.isArray(queries)) {
|
||||
state.predefinedQueries = queries
|
||||
} else {
|
||||
state.predefinedQueries = [queries]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
async addTab ({ state }, data) {
|
||||
const tab = data ? JSON.parse(JSON.stringify(data)) : {}
|
||||
// If no data then create a new blank one...
|
||||
// No data.id means to create new tab, but not blank,
|
||||
// e.g. with 'select * from csv_import' query after csv import
|
||||
if (!data || !data.id) {
|
||||
tab.id = nanoid()
|
||||
tab.name = null
|
||||
tab.tempName = state.untitledLastIndex
|
||||
? `Untitled ${state.untitledLastIndex}`
|
||||
: 'Untitled'
|
||||
tab.isUnsaved = true
|
||||
} else {
|
||||
tab.isUnsaved = false
|
||||
}
|
||||
|
||||
// add new tab only if was not already opened
|
||||
if (!state.tabs.some(openedTab => openedTab.id === tab.id)) {
|
||||
state.tabs.push(tab)
|
||||
if (!tab.name) {
|
||||
state.untitledLastIndex += 1
|
||||
}
|
||||
}
|
||||
|
||||
return tab.id
|
||||
}
|
||||
}
|
||||
|
||||
export default new Vuex.Store({
|
||||
state,
|
||||
mutations,
|
||||
|
||||
59
src/store/mutations.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
export default {
|
||||
setDb (state, db) {
|
||||
if (state.db) {
|
||||
state.db.shutDown()
|
||||
}
|
||||
state.db = db
|
||||
},
|
||||
|
||||
updateTab (state, { index, name, id, query, chart, isUnsaved }) {
|
||||
const tab = state.tabs[index]
|
||||
const oldId = tab.id
|
||||
|
||||
if (id && state.currentTabId === oldId) {
|
||||
state.currentTabId = id
|
||||
}
|
||||
|
||||
if (id) { tab.id = id }
|
||||
if (name) { tab.name = name }
|
||||
if (query) { tab.query = query }
|
||||
if (chart) { tab.chart = chart }
|
||||
if (isUnsaved !== undefined) { tab.isUnsaved = isUnsaved }
|
||||
if (!isUnsaved) {
|
||||
// Saved query is not predefined
|
||||
delete tab.isPredefined
|
||||
}
|
||||
|
||||
Vue.set(state.tabs, index, tab)
|
||||
},
|
||||
deleteTab (state, index) {
|
||||
// If closing tab is the current opened
|
||||
if (state.tabs[index].id === state.currentTabId) {
|
||||
if (index < state.tabs.length - 1) {
|
||||
state.currentTabId = state.tabs[index + 1].id
|
||||
} else if (index > 0) {
|
||||
state.currentTabId = state.tabs[index - 1].id
|
||||
} else {
|
||||
state.currentTabId = null
|
||||
state.currentTab = null
|
||||
state.untitledLastIndex = 0
|
||||
}
|
||||
}
|
||||
state.tabs.splice(index, 1)
|
||||
},
|
||||
setCurrentTabId (state, id) {
|
||||
state.currentTabId = id
|
||||
},
|
||||
setCurrentTab (state, tab) {
|
||||
state.currentTab = tab
|
||||
},
|
||||
updatePredefinedQueries (state, queries) {
|
||||
if (Array.isArray(queries)) {
|
||||
state.predefinedQueries = queries
|
||||
} else {
|
||||
state.predefinedQueries = [queries]
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/store/state.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
tabs: [],
|
||||
currentTab: null,
|
||||
currentTabId: null,
|
||||
untitledLastIndex: 0,
|
||||
predefinedQueries: [],
|
||||
db: null
|
||||
}
|
||||
36
src/time.js
@@ -1,36 +0,0 @@
|
||||
export default {
|
||||
getPeriod (start, end) {
|
||||
let diff = end.getTime() - start.getTime()
|
||||
let result = ''
|
||||
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||
diff -= days * (1000 * 60 * 60 * 24)
|
||||
if (days) {
|
||||
result += days + ' d '
|
||||
}
|
||||
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60))
|
||||
diff -= hours * (1000 * 60 * 60)
|
||||
if (hours) {
|
||||
result += hours + ' h '
|
||||
}
|
||||
|
||||
const mins = Math.floor(diff / (1000 * 60))
|
||||
diff -= mins * (1000 * 60)
|
||||
if (mins) {
|
||||
result += mins + ' m '
|
||||
}
|
||||
|
||||
const seconds = Math.floor(diff / (1000))
|
||||
diff -= seconds * (1000)
|
||||
if (seconds) {
|
||||
result += seconds + ' s '
|
||||
}
|
||||
|
||||
if (diff) {
|
||||
result += diff + ' ms '
|
||||
}
|
||||
|
||||
return result.replace(/\s$/, '')
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<splitpanes
|
||||
class="schema-tabs-splitter"
|
||||
:before="{ size: 20, max: 30 }"
|
||||
:after="{ size: 80, max: 100 }"
|
||||
>
|
||||
<template #left-pane>
|
||||
<schema v-if="$store.state.schema"/>
|
||||
<div v-else id="empty-schema-container">
|
||||
<div class="warning">
|
||||
Database is not loaded. Queries can’t be run without database.
|
||||
</div>
|
||||
<db-uploader id="db-uploader" width="100%"/>
|
||||
</div>
|
||||
</template>
|
||||
<template #right-pane>
|
||||
<tabs />
|
||||
</template>
|
||||
</splitpanes>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Splitpanes from '@/components/Splitpanes'
|
||||
import Schema from '@/components/Schema'
|
||||
import Tabs from '@/components/Tabs'
|
||||
import DbUploader from '@/components/DbUploader'
|
||||
|
||||
export default {
|
||||
name: 'Editor',
|
||||
components: {
|
||||
Schema,
|
||||
Splitpanes,
|
||||
Tabs,
|
||||
DbUploader
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.schema-tabs-splitter {
|
||||
height: 100%;
|
||||
background-color: var(--color-white);
|
||||
}
|
||||
#empty-schema-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 200px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#db-uploader {
|
||||
flex-grow: 1;
|
||||
padding: 24px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.warning {
|
||||
padding: 12px 24px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
>>>.drop-area {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
>>>.drop-area .text {
|
||||
max-width: 200px;
|
||||
}
|
||||
</style>
|
||||
90
src/views/Main/AppDiagnosticInfo.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div id="app-info-container">
|
||||
<img
|
||||
id="app-info-icon"
|
||||
:src="require('@/assets/images/info.svg')"
|
||||
@click="$modal.show('app-info')"
|
||||
/>
|
||||
<modal name="app-info" classes="dialog" height="auto" width="400px">
|
||||
<div class="dialog-header">
|
||||
App info
|
||||
<close-icon @click="$modal.hide('app-info')"/>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<div v-for="(item, index) in info" :key="index" class="info-item">
|
||||
{{item.name}}
|
||||
<div class="divider"/>
|
||||
<div class="options">
|
||||
<div v-for="(opt, index) in item.info" :key="index">
|
||||
{{opt}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
|
||||
export default {
|
||||
name: 'AppDiagnosticInfo',
|
||||
components: { CloseIcon },
|
||||
data () {
|
||||
return {
|
||||
info: [
|
||||
{
|
||||
name: 'sqliteviz version',
|
||||
info: [require('../../../package.json').version]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
async created () {
|
||||
const state = this.$store.state
|
||||
let result = await state.db.execute('select sqlite_version()')
|
||||
this.info.push({
|
||||
name: 'SQLite version',
|
||||
info: result.values[0]
|
||||
})
|
||||
|
||||
result = await state.db.execute('PRAGMA compile_options')
|
||||
this.info.push({
|
||||
name: 'SQLite compile options',
|
||||
info: result.values.map(row => row[0])
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#app-info-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
#app-info-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-left: 32px;
|
||||
}
|
||||
.divider {
|
||||
height: 1px;
|
||||
background-color: var(--color-border);
|
||||
margin: 4px 0;
|
||||
}
|
||||
.options {
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
margin-left: 8px;
|
||||
overflow: auto;
|
||||
max-height: 170px;
|
||||
}
|
||||
.info-item {
|
||||
margin-bottom: 32px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.info-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -5,11 +5,12 @@
|
||||
</div>
|
||||
<div id="db">
|
||||
<div @click="schemaVisible = !schemaVisible" class="db-name">
|
||||
<tree-chevron :expanded="schemaVisible"/>
|
||||
<tree-chevron v-show="schema.length > 0" :expanded="schemaVisible"/>
|
||||
{{ dbName }}
|
||||
</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 TableDescription from '@/components/TableDescription'
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,7 +109,7 @@ export default {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
box-sizing: border-box;
|
||||
background-image: linear-gradient(white 73%, transparent);;
|
||||
background-image: linear-gradient(white 73%, rgba(255, 255, 255, 0));
|
||||
z-index: 2;
|
||||
}
|
||||
.schema, .db-name {
|
||||
@@ -28,7 +28,7 @@ import plotly from 'plotly.js/dist/plotly'
|
||||
import 'react-chart-editor/lib/react-chart-editor.min.css'
|
||||
|
||||
import PlotlyEditor from 'react-chart-editor'
|
||||
import chart from '@/chart'
|
||||
import chartHelper from './chartHelper'
|
||||
import dereference from 'react-chart-editor/lib/lib/dereference'
|
||||
|
||||
export default {
|
||||
@@ -49,10 +49,10 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
dataSources () {
|
||||
return chart.getDataSourcesFromSqlResult(this.sqlResult)
|
||||
return chartHelper.getDataSourcesFromSqlResult(this.sqlResult)
|
||||
},
|
||||
dataSourceOptions () {
|
||||
return chart.getOptionsFromDataSources(this.dataSources)
|
||||
return chartHelper.getOptionsFromDataSources(this.dataSources)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -71,7 +71,7 @@ export default {
|
||||
this.$emit('update')
|
||||
},
|
||||
getChartStateForSave () {
|
||||
return chart.getChartStateForSave(this.state, this.dataSources)
|
||||
return chartHelper.getChartStateForSave(this.state, this.dataSources)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import CM from 'codemirror'
|
||||
import 'codemirror/addon/hint/show-hint.js'
|
||||
import 'codemirror/addon/hint/sql-hint.js'
|
||||
import store from '@/store'
|
||||
import { debounce } from 'debounce'
|
||||
|
||||
export function getHints (cm, options) {
|
||||
const token = cm.getTokenAt(cm.getCursor()).string.toUpperCase()
|
||||
@@ -18,28 +17,34 @@ 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.db.schema
|
||||
return schema && schema.length === 1 ? schema[0].name : null
|
||||
},
|
||||
completeSingle: false,
|
||||
completeOnSingleClick: true,
|
||||
alignWithWord: false
|
||||
}
|
||||
|
||||
export default {
|
||||
show: debounce(function (editor) {
|
||||
// Don't show autocomplete after a space or semicolon or in string literals
|
||||
const token = editor.getTokenAt(editor.getCursor())
|
||||
const ch = token.string.slice(-1)
|
||||
const tokenType = token.type
|
||||
if (tokenType === 'string' || !ch || ch === ' ' || ch === ';') {
|
||||
return
|
||||
}
|
||||
|
||||
CM.showHint(editor, getHints, hintOptions)
|
||||
}, 400)
|
||||
export function showHintOnDemand (editor) {
|
||||
CM.showHint(editor, getHints, hintOptions)
|
||||
}
|
||||
|
||||
export default function showHint (editor) {
|
||||
// Don't show autocomplete after a space or semicolon or in string literals
|
||||
const token = editor.getTokenAt(editor.getCursor())
|
||||
const ch = token.string.slice(-1)
|
||||
const tokenType = token.type
|
||||
if (tokenType === 'string' || !ch || ch === ' ' || ch === ';') {
|
||||
return
|
||||
}
|
||||
|
||||
CM.showHint(editor, getHints, hintOptions)
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<div class="codemirror-container">
|
||||
<codemirror v-model="query" :options="cmOptions" @changes="onChange" />
|
||||
<codemirror ref="cm" v-model="query" :options="cmOptions" @changes="onChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import hint from '@/hint'
|
||||
import showHint, { showHintOnDemand } from './hint'
|
||||
import time from '@/lib/utils/time'
|
||||
import { codemirror } from 'vue-codemirror'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import 'codemirror/mode/sql/sql.js'
|
||||
@@ -27,8 +28,8 @@ export default {
|
||||
theme: 'neo',
|
||||
lineNumbers: true,
|
||||
line: true,
|
||||
autofocus: true,
|
||||
autoRefresh: true
|
||||
autoRefresh: true,
|
||||
extraKeys: { 'Ctrl-Space': showHintOnDemand }
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -38,7 +39,10 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChange: hint.show
|
||||
onChange: time.debounce(showHint, 400),
|
||||
focus () {
|
||||
this.$refs.cm.codemirror.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -8,7 +8,7 @@
|
||||
>
|
||||
<template #left-pane>
|
||||
<div class="query-editor">
|
||||
<sql-editor v-model="query" />
|
||||
<sql-editor ref="sqlEditor" v-model="query" />
|
||||
</div>
|
||||
</template>
|
||||
<template #right-pane>
|
||||
@@ -21,7 +21,8 @@
|
||||
>
|
||||
Run your query and get results here
|
||||
</div>
|
||||
<div v-show="isGettingResults" class="table-preview result-in-progress">
|
||||
<div v-if="isGettingResults" class="table-preview result-in-progress">
|
||||
<loading-indicator :size="30"/>
|
||||
Fetching results...
|
||||
</div>
|
||||
<div
|
||||
@@ -30,10 +31,8 @@
|
||||
>
|
||||
No rows retrieved according to your query
|
||||
</div>
|
||||
<div v-show="error" class="table-preview error">
|
||||
{{ error }}
|
||||
</div>
|
||||
<sql-table v-if="result" :data-set="result" :height="tableViewHeight" />
|
||||
<logs v-if="error" :messages="[error]"/>
|
||||
<sql-table v-if="result" :data-set="result" :time="time" :height="tableViewHeight" />
|
||||
</div>
|
||||
<chart
|
||||
:visible="view === 'chart'"
|
||||
@@ -50,10 +49,13 @@
|
||||
|
||||
<script>
|
||||
import SqlTable from '@/components/SqlTable'
|
||||
import SqlEditor from '@/components/SqlEditor'
|
||||
import Splitpanes from '@/components/Splitpanes'
|
||||
import ViewSwitcher from '@/components/ViewSwitcher'
|
||||
import Chart from '@/components/Chart'
|
||||
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||
import SqlEditor from './SqlEditor'
|
||||
import ViewSwitcher from './ViewSwitcher'
|
||||
import Chart from './Chart'
|
||||
import Logs from '@/components/Logs'
|
||||
import time from '@/lib/utils/time'
|
||||
|
||||
export default {
|
||||
name: 'Tab',
|
||||
@@ -63,7 +65,9 @@ export default {
|
||||
SqlTable,
|
||||
Splitpanes,
|
||||
ViewSwitcher,
|
||||
Chart
|
||||
Chart,
|
||||
LoadingIndicator,
|
||||
Logs
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -73,7 +77,8 @@ export default {
|
||||
tableViewHeight: 0,
|
||||
isGettingResults: false,
|
||||
error: null,
|
||||
resizeObserver: null
|
||||
resizeObserver: null,
|
||||
time: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -81,9 +86,6 @@ export default {
|
||||
return this.id === this.$store.state.currentTabId
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.commit('setCurrentTab', this)
|
||||
},
|
||||
mounted () {
|
||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
||||
this.resizeObserver.observe(this.$refs.bottomPane)
|
||||
@@ -93,9 +95,14 @@ export default {
|
||||
this.resizeObserver.unobserve(this.$refs.bottomPane)
|
||||
},
|
||||
watch: {
|
||||
isActive () {
|
||||
if (this.isActive) {
|
||||
this.$store.commit('setCurrentTab', this)
|
||||
isActive: {
|
||||
immediate: true,
|
||||
async handler () {
|
||||
if (this.isActive) {
|
||||
this.$store.commit('setCurrentTab', this)
|
||||
await this.$nextTick()
|
||||
this.$refs.sqlEditor.focus()
|
||||
}
|
||||
}
|
||||
},
|
||||
query () {
|
||||
@@ -110,12 +117,16 @@ export default {
|
||||
this.error = null
|
||||
const state = this.$store.state
|
||||
try {
|
||||
const start = new Date()
|
||||
this.result = await state.db.execute(this.query + ';')
|
||||
const schema = await state.db.getSchema(state.dbName)
|
||||
this.$store.commit('saveSchema', schema)
|
||||
this.time = time.getPeriod(start, new Date())
|
||||
} catch (err) {
|
||||
this.error = err
|
||||
this.error = {
|
||||
type: 'error',
|
||||
message: err
|
||||
}
|
||||
}
|
||||
state.db.refreshSchema()
|
||||
this.isGettingResults = false
|
||||
},
|
||||
handleResize () {
|
||||
@@ -131,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,11 +194,32 @@ export default {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.table-preview.error {
|
||||
color: var(--color-text-error);
|
||||
.result-in-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
will-change: opacity;
|
||||
/*
|
||||
We need to show loader in 1 sec after starting query execution. We can't do that with
|
||||
setTimeout because the main thread can be busy by getting a result set from the web worker.
|
||||
But we can use CSS animation for opacity. Opacity triggers changes only in the Composite Layer
|
||||
stage in rendering waterfall. Hence it can be processed only with Compositor Thread while
|
||||
the Main Thread processes a result set.
|
||||
https://www.viget.com/articles/animation-performance-101-browser-under-the-hood/
|
||||
*/
|
||||
animation: show-loader 1s linear 0s 1;
|
||||
}
|
||||
|
||||
.table-preview.error::first-letter {
|
||||
text-transform: capitalize;
|
||||
@keyframes show-loader {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
99% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -62,7 +62,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Tab from '@/components/Tab'
|
||||
import Tab from './Tab'
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
|
||||
export default {
|
||||
62
src/views/Main/Editor/index.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div>
|
||||
<splitpanes
|
||||
class="schema-tabs-splitter"
|
||||
:before="{ size: 20, max: 30 }"
|
||||
:after="{ size: 80, max: 100 }"
|
||||
>
|
||||
<template #left-pane>
|
||||
<schema/>
|
||||
</template>
|
||||
<template #right-pane>
|
||||
<tabs />
|
||||
</template>
|
||||
</splitpanes>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Splitpanes from '@/components/Splitpanes'
|
||||
import Schema from './Schema'
|
||||
import Tabs from './Tabs'
|
||||
|
||||
export default {
|
||||
name: 'Editor',
|
||||
components: {
|
||||
Schema,
|
||||
Splitpanes,
|
||||
Tabs
|
||||
},
|
||||
async beforeCreate () {
|
||||
const schema = this.$store.state.db.schema
|
||||
if (!schema || schema.length === 0) {
|
||||
const stmt = [
|
||||
'/*',
|
||||
' * Your database is empty. In order to start building charts',
|
||||
' * you should create a table and insert data into it.',
|
||||
' */',
|
||||
'CREATE TABLE house',
|
||||
'(',
|
||||
' name TEXT,',
|
||||
' points INTEGER',
|
||||
');',
|
||||
'INSERT INTO house VALUES',
|
||||
"('Gryffindor', 100),",
|
||||
"('Hufflepuff', 90),",
|
||||
"('Ravenclaw', 95),",
|
||||
"('Slytherin', 80);"
|
||||
].join('\n')
|
||||
|
||||
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
||||
this.$store.commit('setCurrentTabId', tabId)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.schema-tabs-splitter {
|
||||
height: 100%;
|
||||
background-color: var(--color-white);
|
||||
}
|
||||
</style>
|
||||
@@ -3,8 +3,9 @@
|
||||
<div>
|
||||
<router-link to="/editor">Editor</router-link>
|
||||
<router-link to="/my-queries">My queries</router-link>
|
||||
<a href="https://github.com/lana-k/sqliteviz/wiki" target="_blank">Help</a>
|
||||
</div>
|
||||
<div>
|
||||
<div id="nav-buttons">
|
||||
<button
|
||||
id="run-btn"
|
||||
v-if="currentQuery && $route.path === '/editor'"
|
||||
@@ -30,6 +31,7 @@
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
<app-diagnostic-info />
|
||||
</div>
|
||||
|
||||
<!--Save Query dialog -->
|
||||
@@ -62,13 +64,15 @@
|
||||
<script>
|
||||
import TextField from '@/components/TextField'
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
import storedQueries from '@/storedQueries'
|
||||
import storedQueries from '@/lib/storedQueries'
|
||||
import AppDiagnosticInfo from './AppDiagnosticInfo'
|
||||
|
||||
export default {
|
||||
name: 'MainMenu',
|
||||
components: {
|
||||
TextField,
|
||||
CloseIcon
|
||||
CloseIcon,
|
||||
AppDiagnosticInfo
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -96,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 () {
|
||||
@@ -212,7 +216,7 @@ nav {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
padding: 0 52px;
|
||||
padding: 0 16px 0 52px;
|
||||
z-index: 999;
|
||||
}
|
||||
a {
|
||||
@@ -237,4 +241,8 @@ button {
|
||||
#save-note img {
|
||||
margin: -3px 6px 0 0;
|
||||
}
|
||||
|
||||
#nav-buttons {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
@@ -141,16 +141,16 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RenameIcon from '@/components/svg/rename'
|
||||
import CopyIcon from '@/components/svg/copy'
|
||||
import RenameIcon from './svg/rename'
|
||||
import CopyIcon from './svg/copy'
|
||||
import ExportIcon from '@/components/svg/export'
|
||||
import DeleteIcon from '@/components/svg/delete'
|
||||
import DeleteIcon from './svg/delete'
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
import TextField from '@/components/TextField'
|
||||
import CheckBox from '@/components/CheckBox'
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
import storedQueries from '@/storedQueries'
|
||||
import fu from '@/file.utils'
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
import storedQueries from '@/lib/storedQueries'
|
||||
import fu from '@/lib/utils/fileIo'
|
||||
|
||||
export default {
|
||||
name: 'MyQueries',
|
||||
@@ -448,6 +448,7 @@ export default {
|
||||
}
|
||||
|
||||
.rounded-bg {
|
||||
padding-top: 40px;
|
||||
margin: 0 auto;
|
||||
max-width: 1500px;
|
||||
width: 100%;
|
||||
@@ -23,7 +23,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'CopyIcon',
|
||||
@@ -23,7 +23,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'DeleteIcon',
|
||||
@@ -23,7 +23,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'RenameIcon',
|
||||
@@ -8,11 +8,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MainMenu from '@/components/MainMenu'
|
||||
import MainMenu from './MainMenu'
|
||||
import '@/assets/styles/scrollbars.css'
|
||||
|
||||
export default {
|
||||
name: 'MainView',
|
||||
name: 'Main',
|
||||
components: { MainMenu }
|
||||
}
|
||||
</script>
|
||||
@@ -4,8 +4,8 @@
|
||||
<div id="note">
|
||||
Sqliteviz is fully client-side. Your database never leaves your computer.
|
||||
</div>
|
||||
<button id ="skip" class="secondary" @click="$router.push('/editor')">
|
||||
Skip database loading
|
||||
<button id="skip" class="secondary" @click="$router.push('/editor')">
|
||||
Create empty database
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -14,7 +14,7 @@
|
||||
import DbUploader from '@/components/DbUploader'
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
name: 'Welcome',
|
||||
components: { DbUploader }
|
||||
}
|
||||
</script>
|
||||
706
tests/components/CsvImport/CsvImport.spec.js
Normal file
@@ -0,0 +1,706 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import Vuex from 'vuex'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import CsvImport from '@/components/CsvImport'
|
||||
import csv from '@/components/CsvImport/csv'
|
||||
|
||||
describe('CsvImport.vue', () => {
|
||||
let state = {}
|
||||
let actions = {}
|
||||
let mutations = {}
|
||||
let store = {}
|
||||
let clock
|
||||
let wrapper
|
||||
const newTabId = 1
|
||||
const file = { name: 'my data.csv' }
|
||||
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers()
|
||||
|
||||
// mock store state and mutations
|
||||
state = {}
|
||||
mutations = {
|
||||
setDb: sinon.stub(),
|
||||
setCurrentTabId: sinon.stub()
|
||||
}
|
||||
actions = {
|
||||
addTab: sinon.stub().resolves(newTabId)
|
||||
}
|
||||
store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
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(CsvImport, {
|
||||
store,
|
||||
propsData: {
|
||||
file,
|
||||
dialogName: 'addCsv',
|
||||
db
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('previews', async () => {
|
||||
sinon.stub(csv, 'parse').resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo'],
|
||||
[2, 'bar']
|
||||
]
|
||||
},
|
||||
messages: [{
|
||||
code: 'UndetectableDelimiter',
|
||||
message: 'Comma was used as a standart delimiter',
|
||||
row: 0,
|
||||
type: 'info',
|
||||
hint: undefined
|
||||
}]
|
||||
})
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
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('"')
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.checked).to.equal(true)
|
||||
const rows = wrapper.findAll('tbody tr')
|
||||
expect(rows).to.have.lengthOf(2)
|
||||
expect(rows.at(0).findAll('td').at(0).text()).to.equal('1')
|
||||
expect(rows.at(0).findAll('td').at(1).text()).to.equal('foo')
|
||||
expect(rows.at(1).findAll('td').at(0).text()).to.equal('2')
|
||||
expect(rows.at(1).findAll('td').at(1).text()).to.equal('bar')
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.include('Information about row 0. Comma was used as a standart delimiter.')
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.include('Preview parsing is completed in')
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||
})
|
||||
|
||||
it('reparses when parameters changes', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.open()
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: ',',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[2, 'bar']
|
||||
]
|
||||
},
|
||||
hasErrors: false
|
||||
})
|
||||
await wrapper.find('.delimiter-selector-container input').setValue(',')
|
||||
expect(parse.callCount).to.equal(2)
|
||||
await csv.parse.returnValues[1]
|
||||
|
||||
let rows = wrapper.findAll('tbody tr')
|
||||
expect(rows).to.have.lengthOf(1)
|
||||
expect(rows.at(0).findAll('td').at(0).text()).to.equal('2')
|
||||
expect(rows.at(0).findAll('td').at(1).text()).to.equal('bar')
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.include('Preview parsing is completed in')
|
||||
|
||||
parse.onCall(2).resolves({
|
||||
delimiter: ',',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[3, 'baz']
|
||||
]
|
||||
},
|
||||
hasErrors: true,
|
||||
messages: [{
|
||||
code: 'MissingQuotes',
|
||||
message: 'Quote is missed',
|
||||
row: 0,
|
||||
type: 'error',
|
||||
hint: 'Edit your CSV so that the field has a closing quote char.'
|
||||
}]
|
||||
})
|
||||
|
||||
await wrapper.find('#quote-char input').setValue("'")
|
||||
expect(parse.callCount).to.equal(3)
|
||||
await csv.parse.returnValues[2]
|
||||
rows = wrapper.findAll('tbody tr')
|
||||
expect(rows).to.have.lengthOf(1)
|
||||
expect(rows.at(0).findAll('td').at(0).text()).to.equal('3')
|
||||
expect(rows.at(0).findAll('td').at(1).text()).to.equal('baz')
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.contain('Error in row 0. Quote is missed. Edit your CSV so that the field has a closing quote char.')
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.not.contain('Preview parsing is completed in')
|
||||
|
||||
parse.onCall(3).resolves({
|
||||
delimiter: ',',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[4, 'qux']
|
||||
]
|
||||
},
|
||||
hasErrors: false
|
||||
})
|
||||
await wrapper.find('#escape-char input').setValue("'")
|
||||
expect(parse.callCount).to.equal(4)
|
||||
await csv.parse.returnValues[3]
|
||||
rows = wrapper.findAll('tbody tr')
|
||||
expect(rows).to.have.lengthOf(1)
|
||||
expect(rows.at(0).findAll('td').at(0).text()).to.equal('4')
|
||||
expect(rows.at(0).findAll('td').at(1).text()).to.equal('qux')
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.contain('Preview parsing is completed in')
|
||||
|
||||
parse.onCall(4).resolves({
|
||||
delimiter: ',',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[5, 'corge']
|
||||
]
|
||||
},
|
||||
hasErrors: false
|
||||
})
|
||||
await wrapper.findComponent({ name: 'check-box' }).trigger('click')
|
||||
expect(parse.callCount).to.equal(5)
|
||||
await csv.parse.returnValues[4]
|
||||
rows = wrapper.findAll('tbody tr')
|
||||
expect(rows).to.have.lengthOf(1)
|
||||
expect(rows.at(0).findAll('td').at(0).text()).to.equal('5')
|
||||
expect(rows.at(0).findAll('td').at(1).text()).to.equal('corge')
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.include('Preview parsing is completed in')
|
||||
})
|
||||
|
||||
it('has proper state before parsing is complete', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
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())
|
||||
.to.equal('Parsing CSV...')
|
||||
|
||||
// After 1 second - loading indicator is shown
|
||||
await clock.tick(1000)
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(true)
|
||||
|
||||
// All the dialog controls are disabled
|
||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
||||
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 resolveParsing()
|
||||
await parse.returnValues[1]
|
||||
|
||||
// Loading indicator is not shown when parsing is compete
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(false)
|
||||
})
|
||||
|
||||
it('parsing is completed successfully', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo'],
|
||||
[2, 'bar']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
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()
|
||||
|
||||
// Parsing success in the logs
|
||||
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(1).text())
|
||||
.to.include('2 rows are parsed successfully in')
|
||||
|
||||
// All the dialog controls are disabled
|
||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
||||
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 () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo'],
|
||||
[2, 'bar']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: [{
|
||||
code: 'UndetectableDelimiter',
|
||||
message: 'Comma was used as a standart delimiter',
|
||||
type: 'info',
|
||||
hint: undefined
|
||||
}]
|
||||
})
|
||||
|
||||
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()
|
||||
|
||||
// Parsing success in the logs
|
||||
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
|
||||
expect(logs).to.have.lengthOf(4)
|
||||
expect(logs.at(1).text()).to.include('2 rows are parsed in')
|
||||
expect(logs.at(2).text()).to.equals('Comma was used as a standart delimiter.')
|
||||
|
||||
// All the dialog controls are disabled
|
||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
||||
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 () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo'],
|
||||
[2, 'bar']
|
||||
]
|
||||
},
|
||||
hasErrors: true,
|
||||
messages: [{
|
||||
code: 'Error',
|
||||
message: 'Something is wrong',
|
||||
type: 'error',
|
||||
hint: undefined
|
||||
}]
|
||||
})
|
||||
|
||||
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()
|
||||
|
||||
// Parsing success in the logs
|
||||
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
|
||||
expect(logs).to.have.lengthOf(3)
|
||||
expect(logs.at(1).text()).to.include('Parsing ended with errors.')
|
||||
expect(logs.at(2).text()).to.equals('Something is wrong.')
|
||||
|
||||
// All the dialog controls are enabled
|
||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||
})
|
||||
|
||||
it('has proper state before import is completed', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo'],
|
||||
[2, 'bar']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
let resolveImport = sinon.stub()
|
||||
wrapper.vm.db.addTableFromCsv = sinon.stub()
|
||||
.resolves(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()
|
||||
|
||||
// Parsing success in the logs
|
||||
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(2).text())
|
||||
.to.equal('Importing CSV into a SQLite database...')
|
||||
|
||||
// After 1 second - loading indicator is shown
|
||||
await clock.tick(1000)
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(true)
|
||||
|
||||
// All the dialog controls are disabled
|
||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
||||
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(wrapper.vm.db.addTableFromCsv.getCall(0).args[0]).to.equal('foo') // table name
|
||||
|
||||
// After resolving - loading indicator is not shown
|
||||
await resolveImport()
|
||||
await wrapper.vm.db.addTableFromCsv.returnValues[0]
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(false)
|
||||
})
|
||||
|
||||
it('import success', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
// we need to separate calles because messages will mutate
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo'],
|
||||
[2, 'bar']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
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()
|
||||
|
||||
// Import success in the logs
|
||||
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
|
||||
expect(logs).to.have.lengthOf(3)
|
||||
expect(logs.at(2).text()).to.contain('Importing CSV into a SQLite database is completed in')
|
||||
|
||||
// All the dialog controls are enabled
|
||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(true)
|
||||
})
|
||||
|
||||
it('import fails', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
// we need to separate calles because messages will mutate
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo'],
|
||||
[2, 'bar']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
wrapper.vm.db.addTableFromCsv = sinon.stub().rejects(new Error('fail'))
|
||||
|
||||
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()
|
||||
|
||||
// Import success in the logs
|
||||
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
|
||||
expect(logs).to.have.lengthOf(4)
|
||||
expect(logs.at(2).text()).to.contain('Importing CSV into a SQLite database...')
|
||||
expect(logs.at(3).text()).to.equal('Error: fail.')
|
||||
|
||||
// All the dialog controls are enabled
|
||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
})
|
||||
|
||||
it('import finish', async () => {
|
||||
sinon.stub(csv, 'parse').resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
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)
|
||||
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
|
||||
expect(wrapper.emitted('finish')).to.have.lengthOf(1)
|
||||
})
|
||||
|
||||
it('import cancel', async () => {
|
||||
sinon.stub(csv, 'parse').resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
await wrapper.vm.previewCsv()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-cancel').trigger('click')
|
||||
|
||||
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('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')
|
||||
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/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 '@/csv'
|
||||
import csv from '@/components/CsvImport/csv'
|
||||
import Papa from 'papaparse'
|
||||
|
||||
describe('csv.js', () => {
|
||||
@@ -11,18 +11,18 @@ describe('csv.js', () => {
|
||||
it('getResult with fields', () => {
|
||||
const source = {
|
||||
data: [
|
||||
{ id: 1, name: 'foo' },
|
||||
{ id: 2, name: 'bar' }
|
||||
{ id: 1, 'name ': 'foo', date: new Date('2021-06-30T14:10:24.717Z') },
|
||||
{ id: 2, 'name ': 'bar', date: new Date('2021-07-30T14:10:15.717Z') }
|
||||
],
|
||||
meta: {
|
||||
fields: ['id', 'name']
|
||||
fields: ['id', 'name ', 'date']
|
||||
}
|
||||
}
|
||||
expect(csv.getResult(source)).to.eql({
|
||||
columns: ['id', 'name'],
|
||||
columns: ['id', 'name', 'date'],
|
||||
values: [
|
||||
[1, 'foo'],
|
||||
[2, 'bar']
|
||||
[1, 'foo', '2021-06-30T14:10:24.717Z'],
|
||||
[2, 'bar', '2021-07-30T14:10:15.717Z']
|
||||
]
|
||||
})
|
||||
})
|
||||
@@ -30,16 +30,16 @@ describe('csv.js', () => {
|
||||
it('getResult without fields', () => {
|
||||
const source = {
|
||||
data: [
|
||||
[1, 'foo'],
|
||||
[2, 'bar']
|
||||
[1, 'foo', new Date('2021-06-30T14:10:24.717Z')],
|
||||
[2, 'bar', new Date('2021-07-30T14:10:15.717Z')]
|
||||
],
|
||||
meta: {}
|
||||
}
|
||||
expect(csv.getResult(source)).to.eql({
|
||||
columns: ['col1', 'col2'],
|
||||
columns: ['col1', 'col2', 'col3'],
|
||||
values: [
|
||||
[1, 'foo'],
|
||||
[2, 'bar']
|
||||
[1, 'foo', '2021-06-30T14:10:24.717Z'],
|
||||
[2, 'bar', '2021-07-30T14:10:15.717Z']
|
||||
]
|
||||
})
|
||||
})
|
||||
@@ -2,39 +2,41 @@ import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import Vuex from 'vuex'
|
||||
import { shallowMount, mount } from '@vue/test-utils'
|
||||
import DbUploader from '@/components/DbUploader.vue'
|
||||
import fu from '@/file.utils'
|
||||
import database from '@/database'
|
||||
import csv from '@/csv'
|
||||
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 = {
|
||||
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 = {}
|
||||
const file = { name: 'test.db' }
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock db loading
|
||||
const schema = {}
|
||||
const db = {
|
||||
loadDb: sinon.stub().resolves(schema)
|
||||
loadDb: sinon.stub().resolves()
|
||||
}
|
||||
sinon.stub(database, 'getNewDatabase').returns(db)
|
||||
|
||||
@@ -44,22 +46,27 @@ describe('DbUploader.vue', () => {
|
||||
|
||||
// mount the component
|
||||
const wrapper = shallowMount(DbUploader, {
|
||||
attachTo: place,
|
||||
store,
|
||||
mocks: { $router, $route }
|
||||
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]
|
||||
expect(mutations.saveSchema.calledOnceWith(state, schema)).to.equal(true)
|
||||
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 schema = {}
|
||||
const db = {
|
||||
loadDb: sinon.stub().resolves(schema)
|
||||
loadDb: sinon.stub().resolves()
|
||||
}
|
||||
sinon.stub(database, 'getNewDatabase').returns(db)
|
||||
|
||||
@@ -69,12 +76,16 @@ describe('DbUploader.vue', () => {
|
||||
|
||||
// mount the component
|
||||
const wrapper = shallowMount(DbUploader, {
|
||||
attachTo: place,
|
||||
store,
|
||||
mocks: { $router, $route }
|
||||
mocks: { $router, $route },
|
||||
propsData: {
|
||||
type: 'illustrated'
|
||||
}
|
||||
})
|
||||
|
||||
// mock a file dropped by a user
|
||||
const file = {}
|
||||
const file = { name: 'test.db' }
|
||||
const dropData = { dataTransfer: new DataTransfer() }
|
||||
Object.defineProperty(dropData.dataTransfer, 'files', {
|
||||
value: [file],
|
||||
@@ -84,19 +95,20 @@ describe('DbUploader.vue', () => {
|
||||
await wrapper.find('.drop-area').trigger('drop', dropData)
|
||||
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
|
||||
await db.loadDb.returnValues[0]
|
||||
expect(mutations.saveSchema.calledOnceWith(state, schema)).to.equal(true)
|
||||
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 = {}
|
||||
const file = { name: 'test.db' }
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock db loading
|
||||
const schema = {}
|
||||
const db = {
|
||||
loadDb: sinon.stub().resolves(schema)
|
||||
loadDb: sinon.stub().resolves()
|
||||
}
|
||||
sinon.stub(database, 'getNewDatabase').returns(db)
|
||||
|
||||
@@ -106,736 +118,82 @@ describe('DbUploader.vue', () => {
|
||||
|
||||
// mount the component
|
||||
const wrapper = shallowMount(DbUploader, {
|
||||
attachTo: place,
|
||||
store,
|
||||
mocks: { $router, $route }
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DbUploader.vue import CSV', () => {
|
||||
let state = {}
|
||||
let mutations = {}
|
||||
let actions = {}
|
||||
const newTabId = 1
|
||||
let store = {}
|
||||
|
||||
// mock router
|
||||
const $router = { }
|
||||
const $route = { path: '/' }
|
||||
|
||||
let clock
|
||||
let wrapper
|
||||
|
||||
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()
|
||||
}
|
||||
actions = {
|
||||
addTab: sinon.stub().resolves(newTabId)
|
||||
}
|
||||
store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
$router.push = sinon.stub()
|
||||
|
||||
// mount the component
|
||||
wrapper = mount(DbUploader, {
|
||||
store,
|
||||
mocks: { $router, $route }
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
wrapper.destroy()
|
||||
})
|
||||
|
||||
it('shows parse dialog if gets csv file', async () => {
|
||||
sinon.stub(csv, 'parse').resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo'],
|
||||
[2, 'bar']
|
||||
]
|
||||
},
|
||||
messages: [{
|
||||
code: 'UndetectableDelimiter',
|
||||
message: 'Comma was used as a standart delimiter',
|
||||
row: 0,
|
||||
type: 'info',
|
||||
hint: undefined
|
||||
}]
|
||||
})
|
||||
// mock getting a file from user
|
||||
const file = { name: 'test.csv' }
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('[data-modal="parse"]').exists()).to.equal(true)
|
||||
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('"')
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.checked).to.equal(true)
|
||||
const rows = wrapper.findAll('tbody tr')
|
||||
expect(rows).to.have.lengthOf(2)
|
||||
expect(rows.at(0).findAll('td').at(0).text()).to.equal('1')
|
||||
expect(rows.at(0).findAll('td').at(1).text()).to.equal('foo')
|
||||
expect(rows.at(1).findAll('td').at(0).text()).to.equal('2')
|
||||
expect(rows.at(1).findAll('td').at(1).text()).to.equal('bar')
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.include('Information about row 0. Comma was used as a standart delimiter.')
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.include('Preview parsing is completed in')
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||
})
|
||||
// mock router
|
||||
const $router = { push: sinon.stub() }
|
||||
const $route = { path: '/editor' }
|
||||
|
||||
it('reparses when parameters changes', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
// 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 csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: ',',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[2, 'bar']
|
||||
]
|
||||
},
|
||||
hasErrors: false
|
||||
})
|
||||
await wrapper.find('.delimiter-selector-container input').setValue(',')
|
||||
expect(parse.callCount).to.equal(2)
|
||||
await csv.parse.returnValues[1]
|
||||
|
||||
let rows = wrapper.findAll('tbody tr')
|
||||
expect(rows).to.have.lengthOf(1)
|
||||
expect(rows.at(0).findAll('td').at(0).text()).to.equal('2')
|
||||
expect(rows.at(0).findAll('td').at(1).text()).to.equal('bar')
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.include('Preview parsing is completed in')
|
||||
|
||||
parse.onCall(2).resolves({
|
||||
delimiter: ',',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[3, 'baz']
|
||||
]
|
||||
},
|
||||
hasErrors: true,
|
||||
messages: [{
|
||||
code: 'MissingQuotes',
|
||||
message: 'Quote is missed',
|
||||
row: 0,
|
||||
type: 'error',
|
||||
hint: 'Edit your CSV so that the field has a closing quote char.'
|
||||
}]
|
||||
})
|
||||
|
||||
await wrapper.find('#quote-char input').setValue("'")
|
||||
expect(parse.callCount).to.equal(3)
|
||||
await csv.parse.returnValues[2]
|
||||
rows = wrapper.findAll('tbody tr')
|
||||
expect(rows).to.have.lengthOf(1)
|
||||
expect(rows.at(0).findAll('td').at(0).text()).to.equal('3')
|
||||
expect(rows.at(0).findAll('td').at(1).text()).to.equal('baz')
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.contain('Error in row 0. Quote is missed. Edit your CSV so that the field has a closing quote char.')
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.not.contain('Preview parsing is completed in')
|
||||
|
||||
parse.onCall(3).resolves({
|
||||
delimiter: ',',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[4, 'qux']
|
||||
]
|
||||
},
|
||||
hasErrors: false
|
||||
})
|
||||
await wrapper.find('#escape-char input').setValue("'")
|
||||
expect(parse.callCount).to.equal(4)
|
||||
await csv.parse.returnValues[3]
|
||||
rows = wrapper.findAll('tbody tr')
|
||||
expect(rows).to.have.lengthOf(1)
|
||||
expect(rows.at(0).findAll('td').at(0).text()).to.equal('4')
|
||||
expect(rows.at(0).findAll('td').at(1).text()).to.equal('qux')
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.contain('Preview parsing is completed in')
|
||||
|
||||
parse.onCall(4).resolves({
|
||||
delimiter: ',',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[5, 'corge']
|
||||
]
|
||||
},
|
||||
hasErrors: false
|
||||
})
|
||||
await wrapper.findComponent({ name: 'check-box' }).trigger('click')
|
||||
expect(parse.callCount).to.equal(5)
|
||||
await csv.parse.returnValues[4]
|
||||
rows = wrapper.findAll('tbody tr')
|
||||
expect(rows).to.have.lengthOf(1)
|
||||
expect(rows.at(0).findAll('td').at(0).text()).to.equal('5')
|
||||
expect(rows.at(0).findAll('td').at(1).text()).to.equal('corge')
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.include('Preview parsing is completed in')
|
||||
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('has proper state before parsing is complete', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
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'
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
let resolveParsing
|
||||
parse.onCall(1).returns(new Promise(resolve => {
|
||||
resolveParsing = resolve
|
||||
}))
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
|
||||
// "Parsing CSV..." in the logs
|
||||
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(1).text())
|
||||
.to.equal('Parsing CSV...')
|
||||
|
||||
// After 1 second - loading indicator is shown
|
||||
await clock.tick(1000)
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(true)
|
||||
|
||||
// All the dialog controls are disabled
|
||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
||||
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 resolveParsing()
|
||||
await parse.returnValues[1]
|
||||
|
||||
// Loading indicator is not shown when parsing is compete
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(false)
|
||||
})
|
||||
|
||||
it('parsing is completed successfully', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo'],
|
||||
[2, 'bar']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
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 csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// Parsing success in the logs
|
||||
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(1).text())
|
||||
.to.include('2 rows are parsed successfully in')
|
||||
|
||||
// All the dialog controls are disabled
|
||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
||||
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)
|
||||
})
|
||||
|
||||
it('parsing is completed with notes', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo'],
|
||||
[2, 'bar']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: [{
|
||||
code: 'UndetectableDelimiter',
|
||||
message: 'Comma was used as a standart delimiter',
|
||||
type: 'info',
|
||||
hint: undefined
|
||||
}]
|
||||
})
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// Parsing success in the logs
|
||||
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
|
||||
expect(logs).to.have.lengthOf(4)
|
||||
expect(logs.at(1).text()).to.include('2 rows are parsed in')
|
||||
expect(logs.at(2).text()).to.equals('Comma was used as a standart delimiter.')
|
||||
|
||||
// All the dialog controls are disabled
|
||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
||||
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)
|
||||
})
|
||||
|
||||
it('parsing is completed with errors', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo'],
|
||||
[2, 'bar']
|
||||
]
|
||||
},
|
||||
hasErrors: true,
|
||||
messages: [{
|
||||
code: 'Error',
|
||||
message: 'Something is wrong',
|
||||
type: 'error',
|
||||
hint: undefined
|
||||
}]
|
||||
})
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// Parsing success in the logs
|
||||
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
|
||||
expect(logs).to.have.lengthOf(3)
|
||||
expect(logs.at(1).text()).to.include('Parsing ended with errors.')
|
||||
expect(logs.at(2).text()).to.equals('Something is wrong.')
|
||||
|
||||
// All the dialog controls are enabled
|
||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||
})
|
||||
|
||||
it('has proper state before import is completed', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo'],
|
||||
[2, 'bar']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
let resolveImport = sinon.stub()
|
||||
const newDb = {
|
||||
createDb: sinon.stub().resolves(new Promise(resolve => { resolveImport = resolve })),
|
||||
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()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// Parsing success in the logs
|
||||
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(2).text())
|
||||
.to.equal('Importing CSV into a SQLite database...')
|
||||
|
||||
// After 1 second - loading indicator is shown
|
||||
await clock.tick(1000)
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(true)
|
||||
|
||||
// All the dialog controls are disabled
|
||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
||||
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.createDb.getCall(0).args[0]).to.equal('foo') // file name
|
||||
|
||||
// After resolving - loading indicator is not shown
|
||||
await resolveImport()
|
||||
await newDb.createDb.returnValues[0]
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(false)
|
||||
})
|
||||
|
||||
it('import success', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo'],
|
||||
[2, 'bar']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
const schema = {}
|
||||
const newDb = {
|
||||
createDb: 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()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// Import success in the logs
|
||||
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
|
||||
expect(logs).to.have.lengthOf(3)
|
||||
expect(logs.at(2).text()).to.contain('Importing CSV into a SQLite database is completed in')
|
||||
|
||||
// All the dialog controls are enabled
|
||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(true)
|
||||
})
|
||||
|
||||
it('import fails', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo'],
|
||||
[2, 'bar']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
const newDb = {
|
||||
createDb: sinon.stub().rejects(new Error('fail')),
|
||||
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()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// Import success in the logs
|
||||
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
|
||||
expect(logs).to.have.lengthOf(4)
|
||||
expect(logs.at(2).text()).to.contain('Importing CSV into a SQLite database...')
|
||||
expect(logs.at(3).text()).to.equal('Error: fail.')
|
||||
|
||||
// All the dialog controls are enabled
|
||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
})
|
||||
|
||||
it('import final', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo'],
|
||||
[2, 'bar']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
const schema = {}
|
||||
const newDb = {
|
||||
createDb: 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()
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
it('import cancel', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo'],
|
||||
[2, 'bar']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
const schema = {}
|
||||
const newDb = {
|
||||
createDb: 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.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)
|
||||
await CsvImport.$emit('cancel')
|
||||
expect(wrapper.vm.newDb).to.equal(null)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from 'chai'
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import LoadingIndicator from '@/components/LoadingIndicator.vue'
|
||||
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||
|
||||
describe('LoadingIndicator.vue', () => {
|
||||
it('Calculates animation class', async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from 'chai'
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import Logs from '@/components/Logs.vue'
|
||||
import Logs from '@/components/Logs'
|
||||
|
||||
let place
|
||||
describe('Logs.vue', () => {
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { mount, createLocalVue } from '@vue/test-utils'
|
||||
import Vuex from 'vuex'
|
||||
import Schema from '@/components/Schema.vue'
|
||||
import TableDescription from '@/components/TableDescription.vue'
|
||||
|
||||
const localVue = createLocalVue()
|
||||
localVue.use(Vuex)
|
||||
|
||||
describe('Schema.vue', () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('Renders DB name on initial', () => {
|
||||
// mock store state
|
||||
const state = {
|
||||
dbName: 'fooDB'
|
||||
}
|
||||
const store = new Vuex.Store({ state })
|
||||
|
||||
// mout the component
|
||||
const wrapper = mount(Schema, { store, localVue })
|
||||
|
||||
// check DB name and schema visibility
|
||||
expect(wrapper.find('.db-name').text()).to.equal('fooDB')
|
||||
expect(wrapper.find('.schema').isVisible()).to.equal(true)
|
||||
})
|
||||
|
||||
it('Schema visibility is toggled when click on DB name', async () => {
|
||||
// mock store state
|
||||
const state = {
|
||||
dbName: 'fooDB'
|
||||
}
|
||||
const store = new Vuex.Store({ state })
|
||||
|
||||
// mout the component
|
||||
const wrapper = mount(Schema, { store, localVue })
|
||||
|
||||
// click and check visibility
|
||||
await wrapper.find('.db-name').trigger('click')
|
||||
expect(wrapper.find('.schema').isVisible()).to.equal(false)
|
||||
await wrapper.find('.db-name').trigger('click')
|
||||
expect(wrapper.find('.schema').isVisible()).to.equal(true)
|
||||
})
|
||||
|
||||
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' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
const store = new Vuex.Store({ state })
|
||||
|
||||
// mount the component
|
||||
const wrapper = mount(Schema, { store, localVue })
|
||||
|
||||
// apply filters and check the list of tables
|
||||
await wrapper.find('#schema-filter input').setValue('foo')
|
||||
let tables = wrapper.findAllComponents(TableDescription)
|
||||
expect(tables).to.have.lengthOf(2)
|
||||
expect(tables.at(0).vm.name).to.equal('foo')
|
||||
expect(tables.at(1).vm.name).to.equal('foobar')
|
||||
|
||||
await wrapper.find('#schema-filter input').setValue('bar')
|
||||
tables = wrapper.findAllComponents(TableDescription)
|
||||
expect(tables).to.have.lengthOf(2)
|
||||
expect(tables.at(0).vm.name).to.equal('bar')
|
||||
expect(tables.at(1).vm.name).to.equal('foobar')
|
||||
|
||||
await wrapper.find('#schema-filter input').setValue('')
|
||||
tables = wrapper.findAllComponents(TableDescription)
|
||||
expect(tables).to.have.lengthOf(3)
|
||||
expect(tables.at(0).vm.name).to.equal('foo')
|
||||
expect(tables.at(1).vm.name).to.equal('bar')
|
||||
expect(tables.at(2).vm.name).to.equal('foobar')
|
||||
})
|
||||
|
||||
it('exports db', async () => {
|
||||
const state = {
|
||||
dbName: 'fooDB',
|
||||
db: {
|
||||
export: sinon.stub().resolves()
|
||||
}
|
||||
}
|
||||
const store = new Vuex.Store({ state })
|
||||
const wrapper = mount(Schema, { store, localVue })
|
||||
|
||||
await wrapper.findComponent({ name: 'export-icon' }).trigger('click')
|
||||
expect(state.db.export.calledOnceWith('fooDB'))
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from 'chai'
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import Splitpanes from '@/components/Splitpanes.vue'
|
||||
import Splitpanes from '@/components/Splitpanes'
|
||||
|
||||
describe('Splitpanes.vue', () => {
|
||||
it('renders correctly - vertical', () => {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import splitter from '@/splitter'
|
||||
import splitter from '@/components/Splitpanes/splitter'
|
||||
|
||||
describe('splitter.js', () => {
|
||||
afterEach(() => {
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Pager from '@/components/Pager.vue'
|
||||
import Pager from '@/components/SqlTable/Pager'
|
||||
|
||||
describe('Pager.vue', () => {
|
||||
afterEach(() => {
|
||||
@@ -1,34 +0,0 @@
|
||||
import { expect } from 'chai'
|
||||
import dbUtils from '@/db.utils'
|
||||
|
||||
describe('db.utils.js', () => {
|
||||
it('generateChunks', () => {
|
||||
const arr = ['1', '2', '3', '4', '5']
|
||||
const size = 2
|
||||
const chunks = dbUtils.generateChunks(arr, size)
|
||||
const output = []
|
||||
for (const chunk of chunks) {
|
||||
output.push(chunk)
|
||||
}
|
||||
expect(output[0]).to.eql(['1', '2'])
|
||||
expect(output[1]).to.eql(['3', '4'])
|
||||
expect(output[2]).to.eql(['5'])
|
||||
})
|
||||
|
||||
it('getInsertStmt', () => {
|
||||
const columns = ['id', 'name']
|
||||
expect(dbUtils.getInsertStmt(columns))
|
||||
.to.equal('INSERT INTO csv_import ("id", "name") VALUES (?, ?);')
|
||||
})
|
||||
|
||||
it('getCreateStatement', () => {
|
||||
const columns = ['id', 'name', 'isAdmin', 'startDate']
|
||||
const values = [
|
||||
[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);'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -2,14 +2,14 @@ import chai from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import initSqlJs from 'sql.js'
|
||||
import Sql from '@/sql'
|
||||
import Sql from '@/lib/database/_sql'
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
chai.should()
|
||||
|
||||
const getSQL = initSqlJs()
|
||||
|
||||
describe('sql.js', () => {
|
||||
describe('_sql.js', () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
62
tests/lib/database/_statements.spec.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import { expect } from 'chai'
|
||||
import stmts from '@/lib/database/_statements'
|
||||
|
||||
describe('_statements.js', () => {
|
||||
it('generateChunks', () => {
|
||||
const arr = ['1', '2', '3', '4', '5']
|
||||
const size = 2
|
||||
const chunks = stmts.generateChunks(arr, size)
|
||||
const output = []
|
||||
for (const chunk of chunks) {
|
||||
output.push(chunk)
|
||||
}
|
||||
expect(output[0]).to.eql(['1', '2'])
|
||||
expect(output[1]).to.eql(['3', '4'])
|
||||
expect(output[2]).to.eql(['5'])
|
||||
})
|
||||
|
||||
it('getInsertStmt', () => {
|
||||
const columns = ['id', 'name']
|
||||
expect(stmts.getInsertStmt('foo', columns))
|
||||
.to.equal('INSERT INTO "foo" ("id", "name") VALUES (?, ?);')
|
||||
})
|
||||
|
||||
it('getCreateStatement', () => {
|
||||
const columns = ['id', 'name', 'isAdmin', 'startDate']
|
||||
const values = [
|
||||
[1, 'foo', true, new Date()],
|
||||
[2, 'bar', false, new Date()]
|
||||
]
|
||||
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' }
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -2,8 +2,8 @@ import chai from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import initSqlJs from 'sql.js'
|
||||
import database from '@/database'
|
||||
import fu from '@/file.utils'
|
||||
import database from '@/lib/database'
|
||||
import fu from '@/lib/utils/fileIo'
|
||||
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
@@ -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.createDb('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('createDb 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.createDb('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_")
|
||||
})
|
||||
})
|
||||
238
tests/lib/database/sqliteExtensions.spec.js
Normal file
@@ -0,0 +1,238 @@
|
||||
import chai from 'chai'
|
||||
import database from '@/lib/database'
|
||||
|
||||
const expect = chai.expect
|
||||
|
||||
describe('SQLite extensions', function () {
|
||||
let db
|
||||
|
||||
beforeEach(() => {
|
||||
db = database.getNewDatabase()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
db.shutDown()
|
||||
})
|
||||
|
||||
it('supports contrib trigonometric functions', async function () {
|
||||
const actual = await db.execute(`
|
||||
SELECT
|
||||
abs(3.1415926 - pi()) < 0.000001,
|
||||
abs(1 - cos(2 * pi())) < 0.000001,
|
||||
abs(0 - sin(pi())) < 0.000001,
|
||||
abs(0 - tan(0)) < 0.000001,
|
||||
abs(0 - cot(pi() / 2)) < 0.000001,
|
||||
abs(1 - acos(cos(1))) < 0.000001,
|
||||
abs(1 - asin(sin(1))) < 0.000001,
|
||||
abs(1 - atan(tan(1))) < 0.000001,
|
||||
abs(1 - cosh(0)) < 0.000001,
|
||||
abs(0 - sinh(0)) < 0.000001,
|
||||
abs(tanh(1) + tanh(-1)) < 0.000001,
|
||||
abs(coth(1) + coth(-1)) < 0.000001,
|
||||
abs(1 - acosh(cosh(1))) < 0.000001,
|
||||
abs(1 - asinh(sinh(1))) < 0.000001,
|
||||
abs(1 - atanh(tanh(1))) < 0.000001,
|
||||
abs(180 - degrees(pi())) < 0.000001,
|
||||
abs(pi() - radians(180)) < 0.000001,
|
||||
abs(pi() / 2 - atan2(1, 0)) < 0.000001
|
||||
`)
|
||||
expect(actual.values).to.eql([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])
|
||||
})
|
||||
|
||||
it('supports contrib math functions', async function () {
|
||||
const actual = await db.execute(`
|
||||
SELECT
|
||||
exp(0),
|
||||
log(exp(1)),
|
||||
log10(10000),
|
||||
power(2, 3),
|
||||
sign(-10) + sign(20),
|
||||
sqrt(square(16)),
|
||||
ceil(-1.95) + ceil(1.95),
|
||||
floor(-1.95) + floor(1.95)
|
||||
`)
|
||||
expect(actual.values).to.eql([[1, 1, 4, 8, 0, 16, 1, -1]])
|
||||
})
|
||||
|
||||
it('supports contrib string functions', async function () {
|
||||
const actual = await db.execute(`
|
||||
SELECT
|
||||
replicate('ab', 4),
|
||||
charindex('ab', 'foobarabbarfoo'),
|
||||
charindex('ab', 'foobarabbarfoo', 8),
|
||||
leftstr('foobar', 2),
|
||||
rightstr('foobar', 2),
|
||||
reverse('foobar'),
|
||||
proper('fooBar'),
|
||||
padl('foo', 5),
|
||||
padr('foo', 5),
|
||||
padc('foo', 5),
|
||||
strfilter('abcba', 'bc')
|
||||
`)
|
||||
expect(actual.values).to.eql([
|
||||
['abababab', 7, 0, 'fo', 'ar', 'raboof', 'Foobar', ' foo', 'foo ', ' foo ', 'bcb']
|
||||
])
|
||||
})
|
||||
|
||||
it('supports contrib aggregate functions', async function () {
|
||||
const actual = await db.execute(`
|
||||
WITH RECURSIVE series(x) AS (
|
||||
SELECT 1
|
||||
UNION ALL
|
||||
SELECT x + 1
|
||||
FROM series
|
||||
WHERE x + 1 <= 12
|
||||
)
|
||||
SELECT
|
||||
abs( 3.77406806 - stdev(x)) < 0.000001,
|
||||
abs(14.24358974 - variance(x)) < 0.000001,
|
||||
mode(x),
|
||||
median(x),
|
||||
lower_quartile(x),
|
||||
upper_quartile(x)
|
||||
FROM (
|
||||
SELECT x
|
||||
FROM series
|
||||
UNION ALL
|
||||
VALUES (1)
|
||||
)
|
||||
`)
|
||||
expect(actual.values).to.eql([[1, 1, 1, 6, 3, 9]])
|
||||
})
|
||||
|
||||
it('supports generate_series', async function () {
|
||||
const actual = await db.execute(`
|
||||
SELECT value
|
||||
FROM generate_series(5, 20, 5)
|
||||
`)
|
||||
expect(actual.values).to.eql([[5], [10], [15], [20]])
|
||||
})
|
||||
|
||||
it('supports transitive_closure', async function () {
|
||||
const actual = await db.execute(`
|
||||
CREATE TABLE node(
|
||||
node_id INTEGER NOT NULL PRIMARY KEY,
|
||||
parent_id INTEGER,
|
||||
name VARCHAR(127),
|
||||
FOREIGN KEY (parent_id) REFERENCES node(node_id)
|
||||
);
|
||||
CREATE INDEX node_parent_id_idx ON node(parent_id);
|
||||
|
||||
CREATE VIRTUAL TABLE node_closure USING transitive_closure(
|
||||
tablename = "node",
|
||||
idcolumn = "node_id",
|
||||
parentcolumn = "parent_id"
|
||||
);
|
||||
|
||||
INSERT INTO node VALUES
|
||||
(1, NULL, 'tests'),
|
||||
(2, 1, 'lib'),
|
||||
(3, 2, 'database'),
|
||||
(4, 2, 'utils'),
|
||||
(5, 2, 'storedQueries.spec.js'),
|
||||
(6, 3, '_sql.spec.js'),
|
||||
(7, 3, '_statements.spec.js'),
|
||||
(8, 3, 'database.spec.js'),
|
||||
(9, 3, 'sqliteExtensions.spec.js'),
|
||||
(10, 4, 'fileIo.spec.js'),
|
||||
(11, 4, 'time.spec.js');
|
||||
|
||||
SELECT name
|
||||
FROM node
|
||||
WHERE node_id IN (
|
||||
SELECT nc.id
|
||||
FROM node_closure AS nc
|
||||
WHERE nc.root = 2 AND nc.depth = 2
|
||||
);
|
||||
`)
|
||||
expect(actual.values).to.eql([
|
||||
['_sql.spec.js'],
|
||||
['_statements.spec.js'],
|
||||
['database.spec.js'],
|
||||
['sqliteExtensions.spec.js'],
|
||||
['fileIo.spec.js'],
|
||||
['time.spec.js']
|
||||
])
|
||||
})
|
||||
|
||||
it('supports UUID functions', async function () {
|
||||
const actual = await db.execute(`
|
||||
SELECT
|
||||
length(uuid()),
|
||||
uuid_str(uuid_blob('26a8349c8a7f4cbeb519bf792c3d7ac6'))
|
||||
`)
|
||||
expect(actual.values).to.eql([[36, '26a8349c-8a7f-4cbe-b519-bf792c3d7ac6']])
|
||||
})
|
||||
|
||||
it('supports regexp', async function () {
|
||||
const actual = await db.execute(`
|
||||
SELECT
|
||||
regexp('=\\s?\\d+', 'const foo = 123; const bar = "bar"'),
|
||||
regexpi('=\\s?\\d+', 'const foo = 123; const bar = "bar"'),
|
||||
'const foo = 123; const bar = "bar"' REGEXP '=\\s?\\d+'
|
||||
`)
|
||||
expect(actual.values).to.eql([[1, 1, 1]])
|
||||
})
|
||||
|
||||
it('supports pivot virtual table', async function () {
|
||||
const actual = await db.execute(`
|
||||
CREATE TABLE point(x REAL, y REAL, z REAL);
|
||||
INSERT INTO point VALUES
|
||||
(5,3,3.2), (5,6,4.3), (5,9,5.4),
|
||||
(10,3,4), (10,6,3.8), (10,9,3.6),
|
||||
(15,3,4.8), (15,6,4), (15,9,3.5);
|
||||
|
||||
CREATE VIRTUAL TABLE pivot USING pivot_vtab(
|
||||
(SELECT y FROM point GROUP BY y),
|
||||
(SELECT x, x FROM point GROUP BY x),
|
||||
(SELECT z FROM point WHERE y = :y AND x = :x)
|
||||
);
|
||||
|
||||
CREATE TEMPORARY TABLE surface AS
|
||||
SELECT xt.x, p.*
|
||||
FROM (
|
||||
SELECT row_number() OVER () rownum, *
|
||||
FROM pivot
|
||||
) p
|
||||
JOIN (
|
||||
SELECT row_number() OVER () rownum, x
|
||||
FROM point
|
||||
GROUP BY x
|
||||
) xt USING(rownum);
|
||||
ALTER TABLE surface DROP COLUMN rownum;
|
||||
SELECT * FROM surface;
|
||||
`)
|
||||
expect(actual.columns).to.eql(['x', 'y', '5.0', '10.0', '15.0'])
|
||||
expect(actual.values).to.eql([
|
||||
[5, 3, 3.2, 4, 4.8],
|
||||
[10, 6, 4.3, 3.8, 4],
|
||||
[15, 9, 5.4, 3.6, 3.5]
|
||||
])
|
||||
})
|
||||
|
||||
it('supports FTS5', async function () {
|
||||
const actual = await db.execute(`
|
||||
CREATE VIRTUAL TABLE email USING fts5(sender, title, body, tokenize = 'porter ascii');
|
||||
|
||||
INSERT INTO email VALUES
|
||||
(
|
||||
'foo@localhost',
|
||||
'fts3/4',
|
||||
'FTS3 and FTS4 are SQLite virtual table modules that allows users to perform '
|
||||
|| 'full-text searches on a set of documents.'
|
||||
),
|
||||
(
|
||||
'bar@localhost',
|
||||
'fts4',
|
||||
'FTS5 is an SQLite virtual table module that provides full-text search '
|
||||
|| 'functionality to database applications.'
|
||||
);
|
||||
|
||||
SELECT sender
|
||||
FROM email
|
||||
WHERE body MATCH '"full-text" NOT document'
|
||||
ORDER BY rank;
|
||||
`)
|
||||
expect(actual.values).to.eql([['bar@localhost']])
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import storedQueries from '@/storedQueries.js'
|
||||
import fu from '@/file.utils'
|
||||
import storedQueries from '@/lib/storedQueries'
|
||||
import fu from '@/lib/utils/fileIo'
|
||||
|
||||
describe('storedQueries.js', () => {
|
||||
beforeEach(() => {
|
||||
@@ -1,8 +1,8 @@
|
||||
import { expect } from 'chai'
|
||||
import fu from '@/file.utils'
|
||||
import fIo from '@/lib/utils/fileIo'
|
||||
import sinon from 'sinon'
|
||||
|
||||
describe('file.utils.js', () => {
|
||||
describe('fileIo.js', () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
@@ -15,7 +15,7 @@ describe('file.utils.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('file.utils.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('file.utils.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('file.utils.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,6 +103,34 @@ describe('file.utils.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(fIo.isDatabase(file)).to.equal(true)
|
||||
|
||||
file = { type: 'application/x-sqlite3' }
|
||||
expect(fIo.isDatabase(file)).to.equal(true)
|
||||
|
||||
file = { type: '', name: 'test.db' }
|
||||
expect(fIo.isDatabase(file)).to.equal(true)
|
||||
|
||||
file = { type: '', name: 'test.sqlite' }
|
||||
expect(fIo.isDatabase(file)).to.equal(true)
|
||||
|
||||
file = { type: '', name: 'test.sqlite3' }
|
||||
expect(fIo.isDatabase(file)).to.equal(true)
|
||||
|
||||
file = { type: '', name: 'test.csv' }
|
||||
expect(fIo.isDatabase(file)).to.equal(false)
|
||||
|
||||
file = { type: 'text', name: 'test.db' }
|
||||
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')
|
||||
})
|
||||
})
|
||||