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.
@@ -0,0 +1,74 @@
1
+ import { QueryRunner } from '../../QueryRunner';
2
+ import { ViewerState } from '../../viewerState';
3
+
4
+ import './styles.css';
5
+
6
+ interface CurrentCell {
7
+ tableName: string;
8
+ columnName: string;
9
+ cellRowId: string;
10
+ }
11
+
12
+ export class EditCellView {
13
+ queryRunner: QueryRunner | undefined;
14
+
15
+ textArea: HTMLTextAreaElement;
16
+
17
+ currentCell: CurrentCell | undefined;
18
+
19
+ constructor(
20
+ private viewerElem: HTMLElement,
21
+ private rootEl: HTMLDivElement
22
+ ) {
23
+ this.buildDom();
24
+
25
+ viewerElem.addEventListener('cellSelected', (event) => {
26
+ const { detail: cell } = event;
27
+ this.currentCell = cell;
28
+ this.textArea.value = cell.value;
29
+ this.textArea.select();
30
+ });
31
+ }
32
+
33
+ private buildDom() {
34
+ const container = document.createElement('div');
35
+ container.id = 'execute_sql_container';
36
+
37
+ const header = document.createElement('div');
38
+ header.className = 'viewHeader';
39
+ header.innerText = 'Edit Cell';
40
+ container.appendChild(header);
41
+
42
+ this.textArea = document.createElement('textarea');
43
+ this.textArea.id = 'execute_sql_textarea';
44
+ container.appendChild(this.textArea);
45
+
46
+ const executeBtn = document.createElement('button');
47
+ executeBtn.innerText = 'Apply';
48
+ executeBtn.onclick = this.handleApplyEdit.bind(this);
49
+ container.appendChild(executeBtn);
50
+
51
+ this.rootEl.appendChild(container);
52
+ }
53
+
54
+ private handleApplyEdit() {
55
+ if (this.textArea.value) {
56
+ if (!ViewerState.instance.hasChanges) {
57
+ this.queryRunner?.runQuery({
58
+ sql: 'SAVEPOINT "RESTOREPOINT"',
59
+ parameters: [],
60
+ });
61
+ }
62
+ this.queryRunner?.runQuery({
63
+ sql: `UPDATE ${this.currentCell?.tableName} SET "${this.currentCell?.columnName}"=? WHERE "_rowid_"='${this.currentCell?.cellRowId}'`,
64
+ parameters: [this.textArea.value],
65
+ });
66
+
67
+ ViewerState.instance.setHasChanges(true);
68
+ }
69
+ }
70
+
71
+ setDb(queryRunner: QueryRunner) {
72
+ this.queryRunner = queryRunner;
73
+ }
74
+ }
@@ -0,0 +1,23 @@
1
+ #execute_sql_container {
2
+ display: flex;
3
+ flex-direction: column;
4
+ }
5
+
6
+ #execute_sql_editor {
7
+ position: relative;
8
+ background-color: white;
9
+ height: 300px;
10
+ }
11
+
12
+ #execute_sql_textarea {
13
+ resize: none;
14
+ height: 300px;
15
+ background-color: white;
16
+ overflow: auto;
17
+ white-space: nowrap;
18
+ font-size: 10pt;
19
+ font-family: monospace;
20
+ line-height: 1.5;
21
+ tab-size: 2;
22
+ caret-color: black;
23
+ }
@@ -0,0 +1,49 @@
1
+ import './styles.css';
2
+
3
+ import { QueryRunner } from 'src/QueryRunner';
4
+
5
+ export class ExecuteSQLView {
6
+ queryRunner: QueryRunner | undefined;
7
+
8
+ textArea: HTMLTextAreaElement;
9
+
10
+ highlighting: HTMLElement;
11
+
12
+ constructor(private rootEl: HTMLDivElement) {
13
+ this.buildDom();
14
+ }
15
+
16
+ private buildDom() {
17
+ const container = document.createElement('div');
18
+ container.id = 'execute_sql_container';
19
+
20
+ const header = document.createElement('div');
21
+ header.className = 'viewHeader';
22
+ header.innerText = 'Execute SQL';
23
+ container.appendChild(header);
24
+
25
+ this.textArea = document.createElement('textarea');
26
+ this.textArea.id = 'execute_sql_textarea';
27
+ container.appendChild(this.textArea);
28
+
29
+ const executeBtn = document.createElement('button');
30
+ executeBtn.innerText = 'Execute SQL';
31
+ executeBtn.onclick = this.handleExecuteSql.bind(this);
32
+ container.appendChild(executeBtn);
33
+
34
+ this.rootEl.appendChild(container);
35
+ }
36
+
37
+ private handleExecuteSql() {
38
+ if (this.textArea.value) {
39
+ this.queryRunner?.runQuery({
40
+ sql: this.textArea.value,
41
+ parameters: [],
42
+ });
43
+ }
44
+ }
45
+
46
+ setDb(queryRunner: QueryRunner) {
47
+ this.queryRunner = queryRunner;
48
+ }
49
+ }
@@ -0,0 +1,47 @@
1
+ #execute_sql_container {
2
+ display: flex;
3
+ flex-direction: column;
4
+ }
5
+
6
+ #execute_sql_editor {
7
+ position: relative;
8
+ background-color: white;
9
+ height: 300px;
10
+ }
11
+
12
+ #execute_sql_textarea, #execute_sql_highlighting {
13
+ resize: none;
14
+ height: 300px;
15
+ background-color: white;
16
+ overflow: auto;
17
+ white-space: nowrap;
18
+ font-size: 10pt;
19
+ font-family: monospace;
20
+ line-height: 1.5;
21
+ tab-size: 2;
22
+ caret-color: black;
23
+ }
24
+
25
+ #execute_sql_highlighting {
26
+ z-index: 0;
27
+ margin: 0;
28
+ padding: 2px;
29
+ }
30
+
31
+ .highlighting {
32
+ color: blue;
33
+ }
34
+
35
+ .sql-hl-keyword {
36
+ color: purple;
37
+ /* font-weight: 600; */
38
+ }
39
+
40
+ .sql-hl-special {
41
+ color: black;
42
+ }
43
+
44
+ .sql-hl-string {
45
+ color: red;
46
+ /* font-weight: 600; */
47
+ }
@@ -0,0 +1,94 @@
1
+ import { ViewerState } from '../../viewerState';
2
+
3
+ import './styles.css';
4
+
5
+ export interface DatabaseItem {
6
+ filename: string;
7
+ tables: string[];
8
+ }
9
+
10
+ export class ExplorerView {
11
+ private containerEl: HTMLElement;
12
+
13
+ private expandedItems: { [dbFilepath: string]: boolean } = {};
14
+
15
+ private dbs: DatabaseItem[];
16
+
17
+ private selectedItem: HTMLElement | null = null;
18
+
19
+ constructor(rootEl: HTMLDivElement) {
20
+ this.dbs = [];
21
+
22
+ const dbListHeader = document.createElement('div');
23
+ dbListHeader.className = 'viewHeader';
24
+ dbListHeader.innerText = 'Database List';
25
+
26
+ rootEl.appendChild(dbListHeader);
27
+
28
+ this.containerEl = document.createElement('div');
29
+ this.containerEl.id = 'explorer_tree';
30
+ rootEl.appendChild(this.containerEl);
31
+ }
32
+
33
+ public addDatabaseItem(databaseItem: DatabaseItem): void {
34
+ this.dbs.push(databaseItem);
35
+
36
+ this.addDbToDom(databaseItem);
37
+
38
+ if (this.selectedItem === null) {
39
+ const firstTable = document.querySelector(
40
+ '#explorer_tree > .table'
41
+ ) as HTMLElement | undefined;
42
+ if (firstTable) {
43
+ this.selectTable(firstTable);
44
+ }
45
+ }
46
+ }
47
+
48
+ private addDbToDom(databaseItem: DatabaseItem) {
49
+ const dbRoot = document.createDocumentFragment();
50
+
51
+ const dbItem = document.createElement('div');
52
+ dbItem.innerText = databaseItem.filename;
53
+ dbItem.className = 'db';
54
+
55
+ const expandArrow = document.createElement('div');
56
+ expandArrow.className = 'expand';
57
+ expandArrow.innerText = '>';
58
+ expandArrow.style.cursor = 'pointer';
59
+ expandArrow.onclick = () => {
60
+ this.expandedItems[databaseItem.filename] =
61
+ !this.expandedItems[databaseItem.filename];
62
+
63
+ expandArrow.classList.toggle('expanded');
64
+ };
65
+ expandArrow.classList.add('expanded');
66
+ dbItem.appendChild(expandArrow);
67
+
68
+ dbRoot.appendChild(dbItem);
69
+ databaseItem.tables.forEach((table) => {
70
+ const tableItem = document.createElement('div');
71
+ tableItem.innerText = table;
72
+ tableItem.className = 'table';
73
+ tableItem.onclick = () => {
74
+ this.selectTable(tableItem);
75
+ };
76
+
77
+ dbRoot.appendChild(tableItem);
78
+ });
79
+
80
+ this.containerEl.appendChild(dbRoot);
81
+ }
82
+
83
+ private selectTable(tableEl: HTMLElement | null) {
84
+ if (tableEl) {
85
+ const tableName = tableEl.innerText;
86
+
87
+ this.selectedItem?.classList.remove('selected');
88
+ tableEl.classList.add('selected');
89
+ this.selectedItem = tableEl;
90
+
91
+ ViewerState.instance.setSelectedTable(tableName);
92
+ }
93
+ }
94
+ }
@@ -0,0 +1,30 @@
1
+ #explorer_tree {
2
+ padding: 8px;
3
+ padding-left: 20px;
4
+ }
5
+
6
+ #explorer_tree > .db {
7
+ position: relative;
8
+ text-overflow: ellipsis;
9
+ overflow-y: clip;
10
+ }
11
+
12
+ #explorer_tree .expand {
13
+ position: absolute;
14
+ top: 0;
15
+ left: -15px;
16
+ transition: all .3s ease-in;
17
+ }
18
+
19
+ #explorer_tree .expanded {
20
+ transform: rotate(90deg);
21
+ }
22
+
23
+ #explorer_tree > .table {
24
+ margin-left: 20px;
25
+ cursor: pointer;
26
+ }
27
+
28
+ #explorer_tree > .table.selected {
29
+ background-color: rgb(128, 128, 128);
30
+ }
@@ -0,0 +1,37 @@
1
+ import { Query, QueryRunner } from 'src/QueryRunner';
2
+ import './styles.css';
3
+
4
+ class SqlLogView {
5
+ textArea: HTMLTextAreaElement;
6
+
7
+ constructor(rootEl: HTMLDivElement, queryRunner: QueryRunner) {
8
+ queryRunner.addListener(this.handleQueryRun.bind(this));
9
+
10
+ const container = document.createElement('div');
11
+ container.id = 'sql_log_container';
12
+
13
+ const header = document.createElement('div');
14
+ header.className = 'viewHeader';
15
+ header.innerText = 'SQL Log';
16
+
17
+ container.appendChild(header);
18
+
19
+ this.textArea = document.createElement('textarea');
20
+ this.textArea.id = 'query_log_text';
21
+ this.textArea.readOnly = true;
22
+ container.appendChild(this.textArea);
23
+
24
+ rootEl.appendChild(container);
25
+ }
26
+
27
+ handleQueryRun(query: Query) {
28
+ this.textArea.value += `${query.sql}\n`;
29
+ }
30
+ }
31
+
32
+ export function initSqlLogView(
33
+ rootEl: HTMLDivElement,
34
+ queryRunner: QueryRunner
35
+ ) {
36
+ return new SqlLogView(rootEl, queryRunner);
37
+ }
@@ -0,0 +1,10 @@
1
+ #sql_log_container {
2
+ flex-grow: 1;
3
+ display: flex;
4
+ flex-direction: column;
5
+ }
6
+
7
+ #query_log_text {
8
+ resize: none;
9
+ height: 100%;
10
+ }
@@ -0,0 +1,266 @@
1
+ import { ViewerState } from '../../viewerState';
2
+ import { ListVirtualizer } from '../../ListVirtualizer';
3
+ import { QueryRunner } from '../../QueryRunner';
4
+ import './styles.css';
5
+
6
+ export class TableView {
7
+ private container: HTMLDivElement;
8
+
9
+ private viewHeader: HTMLDivElement;
10
+
11
+ private viewHeaderTitle: HTMLSpanElement;
12
+
13
+ private headerRow: HTMLTableRowElement;
14
+
15
+ private bodyRoot: HTMLTableSectionElement;
16
+
17
+ private virtualizer: ListVirtualizer;
18
+
19
+ private rows;
20
+
21
+ private tableName: string;
22
+
23
+ private columnNames: string[] = [];
24
+
25
+ private fitlers: { [column: string]: string } = {};
26
+
27
+ private updateTimer: number | null = null;
28
+
29
+ private selectedCell: HTMLTableCellElement | null = null;
30
+
31
+ constructor(
32
+ private viewerElem: HTMLElement,
33
+ private rootElement: HTMLDivElement,
34
+ private queryRunner: QueryRunner
35
+ ) {
36
+ this.buildDomTemplate();
37
+
38
+ this.viewerElem.addEventListener('tableSelected', (event) => {
39
+ const { detail: tableName } = event;
40
+ this.setTable(tableName);
41
+ });
42
+
43
+ this.virtualizer = new ListVirtualizer({
44
+ width: 500,
45
+ height: 930,
46
+ totalRows: 0,
47
+ itemHeight: 40,
48
+ contentRoot: this.bodyRoot,
49
+ container: this.container,
50
+ generatorFn: (i: number) => {
51
+ const row = this.rows[i];
52
+
53
+ if (!row) {
54
+ return null;
55
+ }
56
+
57
+ const tr = document.createElement('tr');
58
+
59
+ const rowId = row.rowid;
60
+
61
+ Object.keys(row).forEach((columnKey) => {
62
+ if (columnKey === 'rowid') {
63
+ return;
64
+ }
65
+
66
+ const value = row[columnKey];
67
+ const td = document.createElement('td');
68
+ const contentEl = document.createElement('div');
69
+ if (value !== null) {
70
+ contentEl.innerHTML = value;
71
+ } else {
72
+ contentEl.innerHTML = 'NULL';
73
+ contentEl.className = 'nullValue';
74
+ }
75
+
76
+ td.onclick = () => {
77
+ ViewerState.instance.setSelectedCell({
78
+ value,
79
+ cellRowId: rowId,
80
+ columnName: columnKey,
81
+ tableName: ViewerState.instance.selectedTable,
82
+ });
83
+
84
+ if (this.selectedCell) {
85
+ this.selectedCell.classList.remove('selected');
86
+ }
87
+
88
+ td.classList.add('selected');
89
+ this.selectedCell = td;
90
+ };
91
+
92
+ td.appendChild(contentEl);
93
+ tr.appendChild(td);
94
+ });
95
+ this.bodyRoot.appendChild(tr);
96
+
97
+ return tr;
98
+ },
99
+ });
100
+ }
101
+
102
+ setTableResults(rows: any[]) {
103
+ this.rows = rows;
104
+
105
+ this.viewHeaderTitle.innerHTML = this.tableName;
106
+
107
+ this.buildHeader(rows);
108
+
109
+ this.virtualizer.setRowCount(rows.length);
110
+ }
111
+
112
+ buildDomTemplate() {
113
+ this.viewHeader = document.createElement('div');
114
+ this.viewHeader.className = 'viewHeader';
115
+
116
+ this.viewHeaderTitle = document.createElement('span');
117
+ this.viewHeaderTitle.id = 'table_view_header_title';
118
+ this.viewHeader.appendChild(this.viewHeaderTitle);
119
+
120
+ const updateBtn = document.createElement('button');
121
+ updateBtn.innerText = 'Update';
122
+ updateBtn.onclick = () => {
123
+ this.requestRows();
124
+ };
125
+ this.viewHeader.appendChild(updateBtn);
126
+
127
+ const saveBtn = document.createElement('button');
128
+ saveBtn.innerText = 'Save changes';
129
+ saveBtn.onclick = () => {
130
+ this.saveChanges();
131
+ };
132
+ saveBtn.setAttribute('disabled', '');
133
+ this.viewHeader.appendChild(saveBtn);
134
+
135
+ const revertBtn = document.createElement('button');
136
+ revertBtn.innerText = 'Revert changes';
137
+ revertBtn.onclick = () => {
138
+ this.revertChanges();
139
+ };
140
+ revertBtn.setAttribute('disabled', '');
141
+ this.viewHeader.appendChild(revertBtn);
142
+
143
+ this.rootElement.appendChild(this.viewHeader);
144
+
145
+ this.container = document.createElement('div');
146
+ this.container.id = 'table_container';
147
+
148
+ const table = document.createElement('table');
149
+
150
+ const tableHeader = table.createTHead();
151
+ this.headerRow = document.createElement('tr');
152
+ tableHeader.appendChild(this.headerRow);
153
+
154
+ this.bodyRoot = table.createTBody();
155
+ this.container.appendChild(table);
156
+
157
+ this.rootElement.appendChild(this.container);
158
+
159
+ this.viewerElem.addEventListener('hasChanges', (event) => {
160
+ const { detail: hasChanges } = event;
161
+
162
+ if (hasChanges) {
163
+ saveBtn.removeAttribute('disabled');
164
+ revertBtn.removeAttribute('disabled');
165
+ } else {
166
+ saveBtn.setAttribute('disabled', '');
167
+ revertBtn.setAttribute('disabled', '');
168
+ }
169
+ });
170
+ }
171
+
172
+ private buildHeader(rows: any[]) {
173
+ if (this.columnNames.length !== 0) {
174
+ return;
175
+ }
176
+
177
+ const schema =
178
+ rows.length > 0
179
+ ? Object.keys(rows[0]).filter((column) => column !== 'rowid')
180
+ : [];
181
+
182
+ if (schema.length > 0) {
183
+ this.columnNames = schema;
184
+ }
185
+
186
+ this.headerRow.innerHTML = '';
187
+ this.columnNames.forEach((column) => {
188
+ const columnHeader = document.createElement('th');
189
+ columnHeader.className = 'columnHeaderCell';
190
+
191
+ columnHeader.innerHTML = column;
192
+
193
+ const filterFieldCell = document.createElement('th');
194
+ filterFieldCell.className = 'columnFilterCell';
195
+ const filterField = document.createElement('input');
196
+ filterField.oninput = () => {
197
+ this.fitlers[column] = filterField.value;
198
+ this.scheduleUpdate();
199
+ };
200
+ filterField.placeholder = 'Filter';
201
+
202
+ filterFieldCell.appendChild(filterField);
203
+
204
+ this.headerRow.appendChild(columnHeader);
205
+
206
+ columnHeader.appendChild(filterFieldCell);
207
+ });
208
+ }
209
+
210
+ private setTable(name: string) {
211
+ if (this.tableName === name) {
212
+ return;
213
+ }
214
+
215
+ this.tableName = name;
216
+ this.columnNames = [];
217
+ this.fitlers = {};
218
+
219
+ this.requestRows();
220
+ }
221
+
222
+ private requestRows(): void {
223
+ let sql = `SELECT "_rowid_",* FROM ${this.tableName}`;
224
+
225
+ const filterSql: string[] = [];
226
+ Object.entries(this.fitlers).forEach((filterEntry) => {
227
+ const column = filterEntry[0];
228
+ const filter = filterEntry[1];
229
+
230
+ if (filter) {
231
+ filterSql.push(`"${column}" LIKE '%${filter}%'`);
232
+ }
233
+ });
234
+
235
+ if (filterSql.length > 0) {
236
+ sql += ` WHERE ${filterSql.join(' AND ')} ESCAPE '\\'`;
237
+ }
238
+
239
+ this.queryRunner.runQuery({ sql, parameters: [] }, 'tableView');
240
+ }
241
+
242
+ private saveChanges(): void {
243
+ const sql = 'RELEASE "RESTOREPOINT";';
244
+ this.queryRunner.runQuery({ sql, parameters: [] });
245
+ ViewerState.instance.setHasChanges(false);
246
+ }
247
+
248
+ private revertChanges(): void {
249
+ const sql = 'ROLLBACK TO SAVEPOINT "RESTOREPOINT";';
250
+ this.queryRunner.runQuery({ sql, parameters: [] });
251
+
252
+ this.requestRows();
253
+
254
+ ViewerState.instance.setHasChanges(false);
255
+ }
256
+
257
+ private scheduleUpdate() {
258
+ if (this.updateTimer !== null) {
259
+ window.clearTimeout(this.updateTimer);
260
+ }
261
+ this.updateTimer = window.setTimeout(() => {
262
+ this.requestRows();
263
+ this.updateTimer = null;
264
+ }, 300);
265
+ }
266
+ }