sqlite-wasm-viewer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +37 -0
- package/src/DbViewerWorker.ts +115 -0
- package/src/DbWorker.ts +8 -0
- package/src/ListVirtualizer.ts +125 -0
- package/src/QueryRunner.ts +36 -0
- package/src/dbScanner.ts +33 -0
- package/src/index.ts +157 -0
- package/src/styles.css +65 -0
- package/src/types.ts +34 -0
- package/src/viewerState.ts +49 -0
- package/src/views/EditCellView/EditCellView.ts +74 -0
- package/src/views/EditCellView/styles.css +23 -0
- package/src/views/ExecuteSQLView/ExecuteSQLView.ts +49 -0
- package/src/views/ExecuteSQLView/styles.css +47 -0
- package/src/views/ExplorerView/ExplorerView.ts +94 -0
- package/src/views/ExplorerView/styles.css +30 -0
- package/src/views/SqlLogView/SqlLogView.ts +37 -0
- package/src/views/SqlLogView/styles.css +10 -0
- package/src/views/TableView/TableView.ts +266 -0
- package/src/views/TableView/styles.css +79 -0
- package/tsconfig.json +28 -0
- package/types.d.ts +7 -0
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sqlite-wasm-viewer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A SQLite OPFS viewer that allows to inspect the database and run SQL commands",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "babel ./src --out-dir ./build",
|
|
8
|
+
"tsc": "tsc",
|
|
9
|
+
"start": "nodemon --exec babel-node viewer/src/index.ts",
|
|
10
|
+
"lint": "eslint ."
|
|
11
|
+
},
|
|
12
|
+
"keywords": [],
|
|
13
|
+
"author": "MysticEggs",
|
|
14
|
+
"license": "ISC",
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@babel/cli": "^7.22.9",
|
|
17
|
+
"@babel/core": "^7.22.9",
|
|
18
|
+
"@babel/node": "^7.22.6",
|
|
19
|
+
"@babel/preset-env": "^7.22.9",
|
|
20
|
+
"@babel/preset-typescript": "^7.22.5",
|
|
21
|
+
"@babel/runtime": "^7.22.6",
|
|
22
|
+
"@types/wicg-file-system-access": "^2020.9.6",
|
|
23
|
+
"eslint": "^8.46.0",
|
|
24
|
+
"eslint-config-airbnb-base": "^15.0.0",
|
|
25
|
+
"eslint-config-prettier": "^8.10.0",
|
|
26
|
+
"eslint-plugin-import": "^2.28.0",
|
|
27
|
+
"eslint-plugin-json": "^3.1.0",
|
|
28
|
+
"eslint-plugin-prettier": "^5.0.0",
|
|
29
|
+
"nodemon": "^3.0.1",
|
|
30
|
+
"prettier": "^3.0.1",
|
|
31
|
+
"typescript": "^5.1.6"
|
|
32
|
+
},
|
|
33
|
+
"packageManager": "yarn@3.6.1",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@sqlite.org/sqlite-wasm": "^3.x.x"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// @ts-expect-error Missing types
|
|
2
|
+
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
|
|
3
|
+
import { DbWorkerInput, DbWorkerOutput } from './types';
|
|
4
|
+
|
|
5
|
+
export class DbViewerWorker {
|
|
6
|
+
private initialized = false;
|
|
7
|
+
|
|
8
|
+
sqlite: any;
|
|
9
|
+
|
|
10
|
+
sqliteCApi: any;
|
|
11
|
+
|
|
12
|
+
sqliteDb: any;
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
this.initialized = false;
|
|
16
|
+
this.sqliteDb = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
post(message: MessageEvent<DbWorkerInput>) {
|
|
20
|
+
if (message.data.type !== 'init' && !this.initialized) {
|
|
21
|
+
throw new Error("DbWorker not initialized with 'init' message");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (message.data.type === 'init') {
|
|
25
|
+
if (this.initialized) {
|
|
26
|
+
throw new Error('DbWorker already initialized');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
sqlite3InitModule().then(async (sqlite3: any) => {
|
|
30
|
+
this.sqlite = sqlite3;
|
|
31
|
+
this.sqliteCApi = sqlite3.capi;
|
|
32
|
+
|
|
33
|
+
this.initialized = true;
|
|
34
|
+
this.sendMessage({ type: 'onReady' });
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
switch (message.data.type) {
|
|
39
|
+
case 'readSchema':
|
|
40
|
+
{
|
|
41
|
+
const { path } = message.data;
|
|
42
|
+
|
|
43
|
+
this.sqliteDb = new this.sqlite.oo1.OpfsDb(path, 'c');
|
|
44
|
+
|
|
45
|
+
const sql = `SELECT name, sql FROM sqlite_master WHERE type='table' ORDER BY name`;
|
|
46
|
+
|
|
47
|
+
const result = this.sqliteDb.exec({
|
|
48
|
+
sql,
|
|
49
|
+
returnValue: 'resultRows',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
this.sendMessage({
|
|
53
|
+
type: 'onSchema',
|
|
54
|
+
schema: result,
|
|
55
|
+
dbName: path,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
case 'query':
|
|
60
|
+
{
|
|
61
|
+
const { sql, parameters } = message.data.query;
|
|
62
|
+
|
|
63
|
+
const rawStatement = this.sqliteDb.prepare(sql);
|
|
64
|
+
|
|
65
|
+
const isReader =
|
|
66
|
+
this.sqliteCApi.sqlite3_column_count(rawStatement) > 1;
|
|
67
|
+
const resultRows: any[] = [];
|
|
68
|
+
try {
|
|
69
|
+
if (parameters?.length > 0) {
|
|
70
|
+
rawStatement.bind(parameters);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
while (rawStatement.step()) {
|
|
74
|
+
// Kysely expects the results to be in the object mode
|
|
75
|
+
const row = rawStatement.get({});
|
|
76
|
+
resultRows.push(row);
|
|
77
|
+
}
|
|
78
|
+
} finally {
|
|
79
|
+
rawStatement.finalize();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (isReader) {
|
|
83
|
+
this.sendMessage({
|
|
84
|
+
type: 'onQuery',
|
|
85
|
+
result: { resultRows, tableName: '' },
|
|
86
|
+
label: message.data.label,
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
const changes = this.sqliteCApi.sqlite3_changes(
|
|
90
|
+
this.sqliteDb.pointer
|
|
91
|
+
);
|
|
92
|
+
const lastInsertRowid =
|
|
93
|
+
this.sqliteCApi.sqlite3_last_insert_rowid(
|
|
94
|
+
this.sqliteDb.pointer
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const updates = { changes, lastInsertRowid };
|
|
98
|
+
|
|
99
|
+
this.sendMessage({
|
|
100
|
+
type: 'onQuery',
|
|
101
|
+
result: { resultRows, updates, tableName: '' },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
|
|
107
|
+
default:
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
sendMessage(message: DbWorkerOutput) {
|
|
113
|
+
postMessage(message);
|
|
114
|
+
}
|
|
115
|
+
}
|
package/src/DbWorker.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
interface Config {
|
|
2
|
+
width: number;
|
|
3
|
+
height: number;
|
|
4
|
+
totalRows: number;
|
|
5
|
+
itemHeight: number;
|
|
6
|
+
container: HTMLElement;
|
|
7
|
+
contentRoot: HTMLElement;
|
|
8
|
+
generatorFn: (index: number) => HTMLElement | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class ListVirtualizer {
|
|
12
|
+
private height: number;
|
|
13
|
+
|
|
14
|
+
private itemHeight: number;
|
|
15
|
+
|
|
16
|
+
private totalRows: number;
|
|
17
|
+
|
|
18
|
+
private container: HTMLElement;
|
|
19
|
+
|
|
20
|
+
private contentRoot: HTMLElement;
|
|
21
|
+
|
|
22
|
+
private lastRepaintY: number;
|
|
23
|
+
|
|
24
|
+
private screenItemsLen: number;
|
|
25
|
+
|
|
26
|
+
private scrollTop: number;
|
|
27
|
+
|
|
28
|
+
private generatorFn: (index: number) => HTMLElement | null;
|
|
29
|
+
|
|
30
|
+
constructor(config: Config) {
|
|
31
|
+
this.height = config.height;
|
|
32
|
+
this.itemHeight = config.itemHeight;
|
|
33
|
+
|
|
34
|
+
this.lastRepaintY = 0;
|
|
35
|
+
|
|
36
|
+
this.scrollTop = 0;
|
|
37
|
+
|
|
38
|
+
this.generatorFn = config.generatorFn;
|
|
39
|
+
this.totalRows = config.totalRows;
|
|
40
|
+
|
|
41
|
+
this.container = config.container;
|
|
42
|
+
this.contentRoot = config.contentRoot;
|
|
43
|
+
|
|
44
|
+
this.renderChunk(0);
|
|
45
|
+
|
|
46
|
+
this.container.addEventListener('scroll', this.handleScroll.bind(this));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
handleScroll(e: MouseEvent) {
|
|
50
|
+
const { scrollTop } = e.target as HTMLElement;
|
|
51
|
+
|
|
52
|
+
this.scrollTop = scrollTop;
|
|
53
|
+
const screenItemsLen = Math.ceil(this.height / this.itemHeight);
|
|
54
|
+
const maxBuffer = this.screenItemsLen * this.itemHeight;
|
|
55
|
+
let first = Math.ceil(scrollTop / this.itemHeight - screenItemsLen);
|
|
56
|
+
first = first < 0 ? 0 : first;
|
|
57
|
+
if (
|
|
58
|
+
!this.lastRepaintY ||
|
|
59
|
+
Math.abs(scrollTop - this.lastRepaintY) > maxBuffer
|
|
60
|
+
) {
|
|
61
|
+
this.renderChunk(first);
|
|
62
|
+
this.lastRepaintY = scrollTop;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
renderChunk(fromPos: number) {
|
|
69
|
+
this.screenItemsLen = Math.ceil(this.height / this.itemHeight);
|
|
70
|
+
|
|
71
|
+
// Cache 4 times the number of items that fit in the container viewport
|
|
72
|
+
const cachedItemsLen = this.screenItemsLen * 3;
|
|
73
|
+
|
|
74
|
+
const fragment = document.createDocumentFragment();
|
|
75
|
+
|
|
76
|
+
let itemCount = fromPos + cachedItemsLen;
|
|
77
|
+
if (itemCount > this.totalRows) {
|
|
78
|
+
itemCount = this.totalRows;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (fromPos > 0) {
|
|
82
|
+
const offsetterRowEl = document.createElement('tr');
|
|
83
|
+
const offsetterEl = document.createElement('td');
|
|
84
|
+
offsetterEl.style.height = `${fromPos * this.itemHeight}px`;
|
|
85
|
+
offsetterRowEl.appendChild(offsetterEl);
|
|
86
|
+
|
|
87
|
+
fragment.appendChild(offsetterRowEl);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (let i = fromPos; i < itemCount; i++) {
|
|
91
|
+
const item = this.generatorFn(i);
|
|
92
|
+
|
|
93
|
+
if (!item) {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fragment.appendChild(item);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (itemCount < this.totalRows) {
|
|
101
|
+
const bottomOffsetterRow = document.createElement('tr');
|
|
102
|
+
const offsetterEl = document.createElement('td');
|
|
103
|
+
offsetterEl.style.height = `${
|
|
104
|
+
(this.totalRows - itemCount) * this.itemHeight
|
|
105
|
+
}px`;
|
|
106
|
+
bottomOffsetterRow.appendChild(offsetterEl);
|
|
107
|
+
|
|
108
|
+
fragment.appendChild(bottomOffsetterRow);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.contentRoot.innerHTML = '';
|
|
112
|
+
this.contentRoot.appendChild(fragment);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public setRowCount(rowCount: number) {
|
|
116
|
+
this.totalRows = rowCount;
|
|
117
|
+
|
|
118
|
+
let first = Math.ceil(
|
|
119
|
+
this.scrollTop / this.itemHeight - this.screenItemsLen
|
|
120
|
+
);
|
|
121
|
+
first = first < 0 ? 0 : first;
|
|
122
|
+
|
|
123
|
+
this.renderChunk(first);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Database } from './types';
|
|
2
|
+
|
|
3
|
+
export interface Query {
|
|
4
|
+
sql: string;
|
|
5
|
+
parameters: any[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type Listener = (query: Query) => void;
|
|
9
|
+
|
|
10
|
+
export class QueryRunner {
|
|
11
|
+
private listeners: Listener[];
|
|
12
|
+
|
|
13
|
+
constructor(private db: Database) {
|
|
14
|
+
this.listeners = [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
runQuery(query: Query, label?: string) {
|
|
18
|
+
this.db.post({ type: 'query', query, label });
|
|
19
|
+
|
|
20
|
+
this.listeners.forEach((listener) => {
|
|
21
|
+
listener(query);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
addListener(listener: Listener): () => void {
|
|
26
|
+
if (this.listeners.indexOf(listener) !== -1) {
|
|
27
|
+
throw new Error('Listener is already added');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.listeners.push(listener);
|
|
31
|
+
|
|
32
|
+
return () => {
|
|
33
|
+
this.listeners.splice(this.listeners.indexOf(listener), 1);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/dbScanner.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export async function collectDbFiles(
|
|
2
|
+
isSqliteDatabase: (fileName: string) => boolean
|
|
3
|
+
): Promise<string[]> {
|
|
4
|
+
const root = await navigator.storage.getDirectory();
|
|
5
|
+
|
|
6
|
+
const dbFileHandlers = await getDbFiles(root, isSqliteDatabase);
|
|
7
|
+
|
|
8
|
+
return Promise.all(
|
|
9
|
+
dbFileHandlers.map((dbFile) => {
|
|
10
|
+
return root.resolve(dbFile).then((parts) => parts?.join('/') || '');
|
|
11
|
+
})
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function getDbFiles(
|
|
16
|
+
root: FileSystemDirectoryHandle,
|
|
17
|
+
isSqliteDatabase: (fileName: string) => boolean
|
|
18
|
+
): Promise<FileSystemFileHandle[]> {
|
|
19
|
+
let dbs: FileSystemFileHandle[] = [];
|
|
20
|
+
|
|
21
|
+
for await (const handle of root.values()) {
|
|
22
|
+
const child = handle;
|
|
23
|
+
|
|
24
|
+
if (child.kind === 'directory') {
|
|
25
|
+
const childDbs = await getDbFiles(child, isSqliteDatabase);
|
|
26
|
+
dbs = dbs.concat(dbs, ...childDbs);
|
|
27
|
+
} else if (isSqliteDatabase(child.name)) {
|
|
28
|
+
dbs.push(child);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return dbs;
|
|
33
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import './styles.css';
|
|
2
|
+
|
|
3
|
+
import { Database, DbWorkerOutput } from './types';
|
|
4
|
+
import { TableView } from './views/TableView/TableView';
|
|
5
|
+
import { ExecuteSQLView } from './views/ExecuteSQLView/ExecuteSQLView';
|
|
6
|
+
import { QueryRunner } from './QueryRunner';
|
|
7
|
+
import { initSqlLogView } from './views/SqlLogView/SqlLogView';
|
|
8
|
+
import { DatabaseItem, ExplorerView } from './views/ExplorerView/ExplorerView';
|
|
9
|
+
import { collectDbFiles } from './dbScanner';
|
|
10
|
+
import { EditCellView } from './views/EditCellView/EditCellView';
|
|
11
|
+
import { initState } from './viewerState';
|
|
12
|
+
|
|
13
|
+
let viewer: HTMLDivElement | null = null;
|
|
14
|
+
|
|
15
|
+
let dbListEl: HTMLDivElement | null = null;
|
|
16
|
+
|
|
17
|
+
let tableViewer: TableView | null = null;
|
|
18
|
+
|
|
19
|
+
let middlePanel: HTMLDivElement | null = null;
|
|
20
|
+
|
|
21
|
+
let rightPanel: HTMLDivElement | null = null;
|
|
22
|
+
|
|
23
|
+
let explorerView: ExplorerView | null = null;
|
|
24
|
+
|
|
25
|
+
let queryRunner: QueryRunner | null = null;
|
|
26
|
+
|
|
27
|
+
type Config = {
|
|
28
|
+
isSqliteDatabase: (fileName: string) => boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const defaultSqliteExtension = ['db', 'sqlite'];
|
|
32
|
+
|
|
33
|
+
const config: Config = {
|
|
34
|
+
isSqliteDatabase: (filename: string) => {
|
|
35
|
+
return defaultSqliteExtension.some((ext) =>
|
|
36
|
+
filename.endsWith(`.${ext}`)
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function setConfig(userConfig: Partial<Config>) {
|
|
42
|
+
Object.assign(config, userConfig);
|
|
43
|
+
Object.freeze(config);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function showViewer(): void {
|
|
47
|
+
if (!viewer) {
|
|
48
|
+
viewer = document.createElement('div');
|
|
49
|
+
viewer.id = 'viewer';
|
|
50
|
+
|
|
51
|
+
initState(viewer);
|
|
52
|
+
|
|
53
|
+
const closeBtn = document.createElement('div');
|
|
54
|
+
closeBtn.id = 'close_btn';
|
|
55
|
+
closeBtn.innerText = 'Close';
|
|
56
|
+
closeBtn.onclick = () => {
|
|
57
|
+
hideViewer();
|
|
58
|
+
};
|
|
59
|
+
viewer.appendChild(closeBtn);
|
|
60
|
+
|
|
61
|
+
dbListEl = document.createElement('div');
|
|
62
|
+
dbListEl.id = 'db_list';
|
|
63
|
+
|
|
64
|
+
viewer.appendChild(dbListEl);
|
|
65
|
+
|
|
66
|
+
// Middle Panel
|
|
67
|
+
middlePanel = document.createElement('div');
|
|
68
|
+
middlePanel.id = 'middle_panel';
|
|
69
|
+
|
|
70
|
+
const tableViewEl = document.createElement('div');
|
|
71
|
+
tableViewEl.id = 'table_view';
|
|
72
|
+
middlePanel.appendChild(tableViewEl);
|
|
73
|
+
|
|
74
|
+
viewer.append(middlePanel);
|
|
75
|
+
|
|
76
|
+
// Right Panel
|
|
77
|
+
rightPanel = document.createElement('div');
|
|
78
|
+
rightPanel.id = 'right_panel';
|
|
79
|
+
|
|
80
|
+
const executeSqlView = new ExecuteSQLView(rightPanel);
|
|
81
|
+
|
|
82
|
+
const editCellView = new EditCellView(viewer, rightPanel);
|
|
83
|
+
|
|
84
|
+
viewer.append(rightPanel);
|
|
85
|
+
|
|
86
|
+
const worker = new Worker(new URL('DbWorker.ts', import.meta.url), {
|
|
87
|
+
type: 'module',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
explorerView = new ExplorerView(dbListEl);
|
|
91
|
+
|
|
92
|
+
const collectDbFilesPromise = collectDbFiles(config.isSqliteDatabase);
|
|
93
|
+
|
|
94
|
+
const dbs: { [dbFilepath: string]: DatabaseItem } = {};
|
|
95
|
+
worker.onmessage = (message: MessageEvent<DbWorkerOutput>): void => {
|
|
96
|
+
if (message.data.type === 'onReady') {
|
|
97
|
+
collectDbFilesPromise.then((dbFiles) => {
|
|
98
|
+
dbFiles.forEach((dbFile) => {
|
|
99
|
+
const dbFilepath = dbFile;
|
|
100
|
+
const dbName = dbFile;
|
|
101
|
+
dbs[dbFilepath] = {
|
|
102
|
+
filename: dbName,
|
|
103
|
+
tables: [],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
worker.postMessage({
|
|
107
|
+
type: 'readSchema',
|
|
108
|
+
path: dbFile,
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
} else if (message.data.type === 'onSchema') {
|
|
113
|
+
const tables = message.data.schema.map((tableSchema) => {
|
|
114
|
+
return tableSchema[0];
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
dbs[message.data.dbName].tables = tables;
|
|
118
|
+
|
|
119
|
+
explorerView?.addDatabaseItem(dbs[message.data.dbName]);
|
|
120
|
+
} else if (message.data.type === 'onQuery') {
|
|
121
|
+
if (message.data.label === 'tableView') {
|
|
122
|
+
tableViewer?.setTableResults(
|
|
123
|
+
message.data.result.resultRows || []
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const db: Database = {
|
|
130
|
+
post: (message) => {
|
|
131
|
+
worker.postMessage(message);
|
|
132
|
+
},
|
|
133
|
+
on: (message) => {
|
|
134
|
+
worker.onmessage?.(message);
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
queryRunner = new QueryRunner(db);
|
|
139
|
+
|
|
140
|
+
initSqlLogView(rightPanel, queryRunner);
|
|
141
|
+
|
|
142
|
+
tableViewer = new TableView(viewer, tableViewEl, queryRunner);
|
|
143
|
+
|
|
144
|
+
executeSqlView.setDb(queryRunner);
|
|
145
|
+
editCellView.setDb(queryRunner);
|
|
146
|
+
|
|
147
|
+
worker.postMessage({ type: 'init' });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
document.body.appendChild(viewer);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function hideViewer(): void {
|
|
154
|
+
if (viewer) {
|
|
155
|
+
document.body.removeChild(viewer);
|
|
156
|
+
}
|
|
157
|
+
}
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#viewer {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
right: 0;
|
|
6
|
+
bottom: 0;
|
|
7
|
+
background-color: whitesmoke;
|
|
8
|
+
display: flex;
|
|
9
|
+
padding: 10px;
|
|
10
|
+
padding-top: 20px;
|
|
11
|
+
gap: 8px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#viewer * {
|
|
15
|
+
all: revert;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#viewer .viewHeader {
|
|
19
|
+
max-height: 20px;
|
|
20
|
+
flex-basis: 20px;
|
|
21
|
+
line-height: 1.1rem;
|
|
22
|
+
padding: 8px;
|
|
23
|
+
display: flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
gap: 8px;
|
|
26
|
+
background-color: lightgray;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#viewer #close_btn {
|
|
30
|
+
position: absolute;
|
|
31
|
+
left: 10px;
|
|
32
|
+
top: 0px;
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#viewer #db_list {
|
|
37
|
+
width: 200px;
|
|
38
|
+
padding: 5px;
|
|
39
|
+
background-color: darkgray;
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#viewer #tree_root {
|
|
45
|
+
flex-grow: 1;
|
|
46
|
+
padding: 5px;
|
|
47
|
+
background-color: darkgray;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#viewer #middle_panel {
|
|
51
|
+
background-color: darkgray;
|
|
52
|
+
flex: 1;
|
|
53
|
+
flex-basis: 800px;
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#viewer #right_panel {
|
|
59
|
+
padding: 5px;
|
|
60
|
+
background-color: darkgray;
|
|
61
|
+
flex: 1;
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
gap: 5px;
|
|
65
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type DbWorkerInput =
|
|
2
|
+
| {
|
|
3
|
+
readonly type: 'init';
|
|
4
|
+
}
|
|
5
|
+
| {
|
|
6
|
+
readonly type: 'query';
|
|
7
|
+
readonly query: {
|
|
8
|
+
sql: string;
|
|
9
|
+
parameters: ReadonlyArray<unknown>;
|
|
10
|
+
};
|
|
11
|
+
label?: string;
|
|
12
|
+
}
|
|
13
|
+
| {
|
|
14
|
+
readonly type: 'readSchema';
|
|
15
|
+
readonly path: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type DbWorkerOutput =
|
|
19
|
+
| { readonly type: 'onReady' }
|
|
20
|
+
| { readonly type: 'onSchema'; dbName: string; schema: string[] }
|
|
21
|
+
| {
|
|
22
|
+
readonly type: 'onQuery';
|
|
23
|
+
readonly result: {
|
|
24
|
+
resultRows?: any[];
|
|
25
|
+
tableName: string;
|
|
26
|
+
updates?: { changes: any[]; lastInsertRowid: number };
|
|
27
|
+
};
|
|
28
|
+
label?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type Database = {
|
|
32
|
+
post: (message: DbWorkerInput) => void;
|
|
33
|
+
on: (message: MessageEvent<DbWorkerOutput>) => void;
|
|
34
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface SelectedCell {
|
|
2
|
+
value: string;
|
|
3
|
+
tableName: string;
|
|
4
|
+
columnName: string;
|
|
5
|
+
cellRowId: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class ViewerState {
|
|
9
|
+
private static _instance: ViewerState;
|
|
10
|
+
|
|
11
|
+
static get instance(): ViewerState {
|
|
12
|
+
return ViewerState._instance;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
selectedCell: SelectedCell | undefined;
|
|
16
|
+
|
|
17
|
+
selectedTable: string;
|
|
18
|
+
|
|
19
|
+
hasChanges = false;
|
|
20
|
+
|
|
21
|
+
constructor(private viewerElem: HTMLElement) {
|
|
22
|
+
ViewerState._instance = this;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setSelectedCell(cell: SelectedCell) {
|
|
26
|
+
this.selectedCell = cell;
|
|
27
|
+
|
|
28
|
+
const event = new CustomEvent('cellSelected', { detail: cell });
|
|
29
|
+
this.viewerElem.dispatchEvent(event);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setSelectedTable(tableName: string) {
|
|
33
|
+
this.selectedTable = tableName;
|
|
34
|
+
|
|
35
|
+
const event = new CustomEvent('tableSelected', { detail: tableName });
|
|
36
|
+
this.viewerElem.dispatchEvent(event);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
setHasChanges(hasChanges: boolean) {
|
|
40
|
+
this.hasChanges = hasChanges;
|
|
41
|
+
|
|
42
|
+
const event = new CustomEvent('hasChanges', { detail: hasChanges });
|
|
43
|
+
this.viewerElem.dispatchEvent(event);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function initState(viewerElem: HTMLElement) {
|
|
48
|
+
return new ViewerState(viewerElem);
|
|
49
|
+
}
|