@rozenite/sqlite-plugin 1.7.0-rc.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/CHANGELOG.md +8 -0
- package/LICENSE +20 -0
- package/README.md +102 -0
- package/dist/devtools/assets/panel-B3paLkwG.js +82 -0
- package/dist/devtools/assets/panel-CIU0JBOs.css +1 -0
- package/dist/devtools/panel.html +31 -0
- package/dist/react-native/chunks/bridge-values.cjs +5 -0
- package/dist/react-native/chunks/bridge-values.js +258 -0
- package/dist/react-native/chunks/index.require.cjs +1 -0
- package/dist/react-native/chunks/index.require.js +118 -0
- package/dist/react-native/chunks/useRozeniteSqlitePlugin.require.cjs +1 -0
- package/dist/react-native/chunks/useRozeniteSqlitePlugin.require.js +189 -0
- package/dist/react-native/index.cjs +1 -0
- package/dist/react-native/index.d.ts +178 -0
- package/dist/react-native/index.js +16 -0
- package/dist/rozenite.json +1 -0
- package/package.json +83 -0
- package/postcss.config.js +6 -0
- package/react-native.ts +55 -0
- package/rozenite.config.ts +8 -0
- package/src/react-native/adapters/__tests__/expo-sqlite.test.ts +94 -0
- package/src/react-native/adapters/expo-sqlite.ts +230 -0
- package/src/react-native/adapters/generic.ts +88 -0
- package/src/react-native/adapters/index.ts +9 -0
- package/src/react-native/sqlite-view.ts +24 -0
- package/src/react-native/useRozeniteSqlitePlugin.ts +262 -0
- package/src/shared/__tests__/bridge-values.test.ts +34 -0
- package/src/shared/__tests__/sql.test.ts +55 -0
- package/src/shared/bridge-values.ts +170 -0
- package/src/shared/protocol.ts +41 -0
- package/src/shared/sql.ts +420 -0
- package/src/shared/types.ts +81 -0
- package/src/ui/__tests__/sql-editor-utils.test.ts +135 -0
- package/src/ui/__tests__/sqlite-row-edit-value.test.ts +22 -0
- package/src/ui/__tests__/sqlite-row-mutations.test.ts +310 -0
- package/src/ui/__tests__/sqlite-table-column-order.test.ts +83 -0
- package/src/ui/__tests__/value-utils.test.tsx +12 -0
- package/src/ui/cell-detail-drawer.tsx +65 -0
- package/src/ui/globals.css +1415 -0
- package/src/ui/panel.tsx +2815 -0
- package/src/ui/query-result-table.tsx +199 -0
- package/src/ui/sql-editor-utils.ts +352 -0
- package/src/ui/sql-editor.tsx +509 -0
- package/src/ui/sqlite-data-table.tsx +296 -0
- package/src/ui/sqlite-introspection.ts +189 -0
- package/src/ui/sqlite-modal-controls.tsx +32 -0
- package/src/ui/sqlite-row-delete-modal.tsx +130 -0
- package/src/ui/sqlite-row-edit-modal.tsx +487 -0
- package/src/ui/sqlite-row-edit-value.ts +53 -0
- package/src/ui/sqlite-row-mutations.ts +246 -0
- package/src/ui/sqlite-table-column-order.ts +154 -0
- package/src/ui/use-sqlite-requests.ts +205 -0
- package/src/ui/utils.ts +107 -0
- package/src/ui/value-utils.tsx +162 -0
- package/tsconfig.json +36 -0
- package/vite.config.ts +20 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import {
|
|
2
|
+
flexRender,
|
|
3
|
+
getCoreRowModel,
|
|
4
|
+
useReactTable,
|
|
5
|
+
type ColumnDef,
|
|
6
|
+
type ColumnSizingState,
|
|
7
|
+
type Header,
|
|
8
|
+
type OnChangeFn,
|
|
9
|
+
type RowData,
|
|
10
|
+
} from '@tanstack/react-table';
|
|
11
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
12
|
+
import {
|
|
13
|
+
useMemo,
|
|
14
|
+
useRef,
|
|
15
|
+
useState,
|
|
16
|
+
type KeyboardEvent as ReactKeyboardEvent,
|
|
17
|
+
type ReactNode,
|
|
18
|
+
} from 'react';
|
|
19
|
+
import { formatNumber } from './utils';
|
|
20
|
+
import { SQLITE_ROW_NUMBER_COLUMN_ID } from './sqlite-table-column-order';
|
|
21
|
+
|
|
22
|
+
const joinClassNames = (
|
|
23
|
+
...classNames: Array<string | false | null | undefined>
|
|
24
|
+
) => classNames.filter(Boolean).join(' ');
|
|
25
|
+
|
|
26
|
+
const LoadingState = ({ columns }: { columns: number }) => (
|
|
27
|
+
<div className="sqlite-results-loading" aria-live="polite">
|
|
28
|
+
{Array.from({ length: 6 }, (_, rowIndex) => (
|
|
29
|
+
<div
|
|
30
|
+
key={`loading-${rowIndex}`}
|
|
31
|
+
className="sqlite-results-loading-row"
|
|
32
|
+
style={{
|
|
33
|
+
gridTemplateColumns: `repeat(${Math.max(columns, 3)}, minmax(12rem, 1fr))`,
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
{Array.from({ length: Math.max(columns, 3) }, (_, columnIndex) => (
|
|
37
|
+
<span
|
|
38
|
+
key={`${rowIndex}-${columnIndex}`}
|
|
39
|
+
className="sqlite-results-loading-bar"
|
|
40
|
+
/>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
type SqliteDataTableProps<TData extends RowData> = {
|
|
48
|
+
tableId: string;
|
|
49
|
+
data: TData[];
|
|
50
|
+
columns: ColumnDef<TData, unknown>[];
|
|
51
|
+
columnOrder: string[];
|
|
52
|
+
onColumnOrderChange: OnChangeFn<string[]>;
|
|
53
|
+
emptyTitle: string;
|
|
54
|
+
emptyDescription: string;
|
|
55
|
+
loading?: boolean;
|
|
56
|
+
loadingColumnCount?: number;
|
|
57
|
+
shellClassName?: string;
|
|
58
|
+
scrollContainerClassName?: string;
|
|
59
|
+
tableClassName?: string;
|
|
60
|
+
showRowNumbers?: boolean;
|
|
61
|
+
rowNumberOffset?: number;
|
|
62
|
+
onRowClick?: (row: TData, rowIndex: number) => void;
|
|
63
|
+
getRowAriaLabel?: (row: TData, rowIndex: number) => string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type SortableColumnHeaderProps<TData extends RowData> = {
|
|
67
|
+
header: Header<TData, unknown>;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const SortableColumnHeader = <TData extends RowData>({
|
|
71
|
+
header,
|
|
72
|
+
}: SortableColumnHeaderProps<TData>) => {
|
|
73
|
+
return (
|
|
74
|
+
<th
|
|
75
|
+
scope="col"
|
|
76
|
+
className={joinClassNames(
|
|
77
|
+
header.column.id === SQLITE_ROW_NUMBER_COLUMN_ID &&
|
|
78
|
+
'sqlite-results-number-col',
|
|
79
|
+
header.column.getIsResizing() && 'sqlite-table-column-resizing',
|
|
80
|
+
)}
|
|
81
|
+
style={{
|
|
82
|
+
width: header.getSize(),
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
<div className="sqlite-table-header-content">
|
|
86
|
+
<div className="min-w-0">
|
|
87
|
+
{header.isPlaceholder
|
|
88
|
+
? null
|
|
89
|
+
: flexRender(header.column.columnDef.header, header.getContext())}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
{header.column.getCanResize() ? (
|
|
93
|
+
<div
|
|
94
|
+
aria-hidden="true"
|
|
95
|
+
className={joinClassNames(
|
|
96
|
+
'sqlite-column-resizer',
|
|
97
|
+
header.column.getIsResizing() && 'is-active',
|
|
98
|
+
)}
|
|
99
|
+
onDoubleClick={() => header.column.resetSize()}
|
|
100
|
+
onMouseDown={header.getResizeHandler()}
|
|
101
|
+
onTouchStart={header.getResizeHandler()}
|
|
102
|
+
/>
|
|
103
|
+
) : null}
|
|
104
|
+
</th>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const SqliteDataTable = <TData extends RowData>({
|
|
109
|
+
tableId,
|
|
110
|
+
data,
|
|
111
|
+
columns,
|
|
112
|
+
columnOrder,
|
|
113
|
+
onColumnOrderChange,
|
|
114
|
+
emptyTitle,
|
|
115
|
+
emptyDescription,
|
|
116
|
+
loading = false,
|
|
117
|
+
loadingColumnCount,
|
|
118
|
+
shellClassName,
|
|
119
|
+
scrollContainerClassName,
|
|
120
|
+
tableClassName,
|
|
121
|
+
showRowNumbers = false,
|
|
122
|
+
rowNumberOffset = 0,
|
|
123
|
+
onRowClick,
|
|
124
|
+
getRowAriaLabel,
|
|
125
|
+
}: SqliteDataTableProps<TData>) => {
|
|
126
|
+
const [columnSizing, setColumnSizing] = useState<ColumnSizingState>({});
|
|
127
|
+
const scrollElementRef = useRef<HTMLDivElement | null>(null);
|
|
128
|
+
|
|
129
|
+
const rowNumberColumn = useMemo<ColumnDef<TData, unknown>>(
|
|
130
|
+
() => ({
|
|
131
|
+
id: SQLITE_ROW_NUMBER_COLUMN_ID,
|
|
132
|
+
header: '#',
|
|
133
|
+
enableResizing: false,
|
|
134
|
+
size: 72,
|
|
135
|
+
minSize: 72,
|
|
136
|
+
maxSize: 72,
|
|
137
|
+
cell: ({ row }) => (
|
|
138
|
+
<span className="sqlite-results-row-number sqlite-tabular">
|
|
139
|
+
{formatNumber(rowNumberOffset + row.index + 1)}
|
|
140
|
+
</span>
|
|
141
|
+
),
|
|
142
|
+
}),
|
|
143
|
+
[rowNumberOffset],
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const tableColumns = useMemo(
|
|
147
|
+
() => (showRowNumbers ? [rowNumberColumn, ...columns] : columns),
|
|
148
|
+
[columns, rowNumberColumn, showRowNumbers],
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const table = useReactTable({
|
|
152
|
+
data,
|
|
153
|
+
columns: tableColumns,
|
|
154
|
+
defaultColumn: {
|
|
155
|
+
minSize: 120,
|
|
156
|
+
size: 220,
|
|
157
|
+
},
|
|
158
|
+
state: {
|
|
159
|
+
columnOrder,
|
|
160
|
+
columnSizing,
|
|
161
|
+
},
|
|
162
|
+
onColumnOrderChange,
|
|
163
|
+
onColumnSizingChange: setColumnSizing,
|
|
164
|
+
columnResizeMode: 'onChange',
|
|
165
|
+
getCoreRowModel: getCoreRowModel(),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const loadingColumns =
|
|
169
|
+
loadingColumnCount ?? columns.length + (showRowNumbers ? 1 : 0);
|
|
170
|
+
const tableRows = table.getRowModel().rows;
|
|
171
|
+
const rowVirtualizer = useVirtualizer({
|
|
172
|
+
count: tableRows.length,
|
|
173
|
+
getScrollElement: () => scrollElementRef.current,
|
|
174
|
+
estimateSize: () => 36,
|
|
175
|
+
overscan: 10,
|
|
176
|
+
getItemKey: (index) => tableRows[index]?.id ?? index,
|
|
177
|
+
measureElement:
|
|
178
|
+
typeof window !== 'undefined' &&
|
|
179
|
+
!window.navigator.userAgent.includes('Firefox')
|
|
180
|
+
? (element) => element?.getBoundingClientRect().height ?? 0
|
|
181
|
+
: undefined,
|
|
182
|
+
});
|
|
183
|
+
const virtualRows = rowVirtualizer.getVirtualItems();
|
|
184
|
+
|
|
185
|
+
const handleRowKeyDown = (
|
|
186
|
+
event: ReactKeyboardEvent<HTMLTableRowElement>,
|
|
187
|
+
row: TData,
|
|
188
|
+
rowIndex: number,
|
|
189
|
+
) => {
|
|
190
|
+
if (!onRowClick || (event.key !== 'Enter' && event.key !== ' ')) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
event.preventDefault();
|
|
195
|
+
onRowClick(row, rowIndex);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const renderTable = (): ReactNode => (
|
|
199
|
+
<table
|
|
200
|
+
className={joinClassNames('sqlite-results-table', tableClassName)}
|
|
201
|
+
style={{ minWidth: table.getTotalSize() }}
|
|
202
|
+
>
|
|
203
|
+
<thead>
|
|
204
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
205
|
+
<tr key={headerGroup.id} className="sqlite-table-row-shell">
|
|
206
|
+
{headerGroup.headers.map((header) => (
|
|
207
|
+
<SortableColumnHeader key={header.id} header={header} />
|
|
208
|
+
))}
|
|
209
|
+
</tr>
|
|
210
|
+
))}
|
|
211
|
+
</thead>
|
|
212
|
+
<tbody
|
|
213
|
+
style={{
|
|
214
|
+
height: rowVirtualizer.getTotalSize(),
|
|
215
|
+
}}
|
|
216
|
+
>
|
|
217
|
+
{virtualRows.map((virtualRow) => {
|
|
218
|
+
const row = tableRows[virtualRow.index];
|
|
219
|
+
|
|
220
|
+
if (!row) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<tr
|
|
226
|
+
key={row.id}
|
|
227
|
+
ref={(node) => {
|
|
228
|
+
if (node) {
|
|
229
|
+
rowVirtualizer.measureElement(node);
|
|
230
|
+
}
|
|
231
|
+
}}
|
|
232
|
+
data-index={virtualRow.index}
|
|
233
|
+
className={joinClassNames(onRowClick && 'sqlite-results-row')}
|
|
234
|
+
role={onRowClick ? 'button' : undefined}
|
|
235
|
+
tabIndex={onRowClick ? 0 : undefined}
|
|
236
|
+
aria-label={
|
|
237
|
+
onRowClick
|
|
238
|
+
? (getRowAriaLabel?.(row.original, row.index) ??
|
|
239
|
+
`Inspect row ${row.index + 1}`)
|
|
240
|
+
: undefined
|
|
241
|
+
}
|
|
242
|
+
onClick={() => onRowClick?.(row.original, row.index)}
|
|
243
|
+
onKeyDown={(event) =>
|
|
244
|
+
handleRowKeyDown(event, row.original, row.index)
|
|
245
|
+
}
|
|
246
|
+
style={{
|
|
247
|
+
transform: `translateY(${virtualRow.start}px)`,
|
|
248
|
+
}}
|
|
249
|
+
>
|
|
250
|
+
{row.getVisibleCells().map((cell) => (
|
|
251
|
+
<td
|
|
252
|
+
key={cell.id}
|
|
253
|
+
className={joinClassNames(
|
|
254
|
+
cell.column.id === SQLITE_ROW_NUMBER_COLUMN_ID &&
|
|
255
|
+
'sqlite-results-row-number',
|
|
256
|
+
)}
|
|
257
|
+
style={{ width: cell.column.getSize() }}
|
|
258
|
+
>
|
|
259
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
260
|
+
</td>
|
|
261
|
+
))}
|
|
262
|
+
</tr>
|
|
263
|
+
);
|
|
264
|
+
})}
|
|
265
|
+
</tbody>
|
|
266
|
+
</table>
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<div
|
|
271
|
+
className={joinClassNames('sqlite-results-shell', shellClassName)}
|
|
272
|
+
data-table-id={tableId}
|
|
273
|
+
>
|
|
274
|
+
<div
|
|
275
|
+
ref={scrollElementRef}
|
|
276
|
+
className={joinClassNames(
|
|
277
|
+
'sqlite-results-scroll',
|
|
278
|
+
scrollContainerClassName,
|
|
279
|
+
)}
|
|
280
|
+
>
|
|
281
|
+
{loading ? (
|
|
282
|
+
<LoadingState columns={loadingColumns} />
|
|
283
|
+
) : data.length === 0 || columns.length === 0 ? (
|
|
284
|
+
<div className="sqlite-results-empty">
|
|
285
|
+
<div className="max-w-sm space-y-2 text-center">
|
|
286
|
+
<p className="text-base font-medium text-white">{emptyTitle}</p>
|
|
287
|
+
<p className="text-sm text-slate-400">{emptyDescription}</p>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
) : (
|
|
291
|
+
renderTable()
|
|
292
|
+
)}
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
);
|
|
296
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { escapeSqlString, quoteSqlIdentifier } from '../shared/sql';
|
|
2
|
+
import type { SqliteQueryResult } from '../shared/types';
|
|
3
|
+
|
|
4
|
+
export type SqliteEntityType = 'table' | 'view';
|
|
5
|
+
|
|
6
|
+
export type SqliteSchema = {
|
|
7
|
+
seq: number;
|
|
8
|
+
name: string;
|
|
9
|
+
file: string | null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type SqliteEntity = {
|
|
13
|
+
schemaName: string;
|
|
14
|
+
name: string;
|
|
15
|
+
type: SqliteEntityType;
|
|
16
|
+
sql: string | null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type SqliteColumnInfo = {
|
|
20
|
+
cid: number;
|
|
21
|
+
name: string;
|
|
22
|
+
type: string;
|
|
23
|
+
notNull: boolean;
|
|
24
|
+
defaultValue: string | null;
|
|
25
|
+
primaryKeyOrder: number;
|
|
26
|
+
hidden: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type SqliteForeignKeyInfo = {
|
|
30
|
+
id: number;
|
|
31
|
+
seq: number;
|
|
32
|
+
table: string;
|
|
33
|
+
from: string;
|
|
34
|
+
to: string | null;
|
|
35
|
+
onUpdate: string;
|
|
36
|
+
onDelete: string;
|
|
37
|
+
match: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type SqliteIndexInfo = {
|
|
41
|
+
seq: number;
|
|
42
|
+
name: string;
|
|
43
|
+
unique: boolean;
|
|
44
|
+
origin: string;
|
|
45
|
+
partial: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type SqliteIndexColumnInfo = {
|
|
49
|
+
seqno: number;
|
|
50
|
+
cid: number;
|
|
51
|
+
name: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const buildQualifiedEntityName = (schemaName: string, entityName: string) =>
|
|
55
|
+
`${quoteSqlIdentifier(schemaName)}.${quoteSqlIdentifier(entityName)}`;
|
|
56
|
+
|
|
57
|
+
const buildPragmaPrefix = (schemaName: string) =>
|
|
58
|
+
`${quoteSqlIdentifier(schemaName)}.`;
|
|
59
|
+
|
|
60
|
+
const asString = (value: unknown) =>
|
|
61
|
+
typeof value === 'string' ? value : String(value ?? '');
|
|
62
|
+
const asNullableString = (value: unknown) =>
|
|
63
|
+
value == null ? null : String(value);
|
|
64
|
+
const asNumber = (value: unknown) => {
|
|
65
|
+
const parsed = Number(value ?? 0);
|
|
66
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const LIST_SCHEMAS_SQL = 'PRAGMA database_list';
|
|
70
|
+
|
|
71
|
+
export const buildListEntitiesSql = (schemaName: string) => `
|
|
72
|
+
SELECT
|
|
73
|
+
name,
|
|
74
|
+
type,
|
|
75
|
+
sql
|
|
76
|
+
FROM ${quoteSqlIdentifier(schemaName)}.sqlite_schema
|
|
77
|
+
WHERE type IN ('table', 'view')
|
|
78
|
+
AND name NOT LIKE 'sqlite_%'
|
|
79
|
+
ORDER BY CASE type WHEN 'table' THEN 0 ELSE 1 END, name COLLATE NOCASE
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
export const buildBrowseEntitySql = (
|
|
83
|
+
schemaName: string,
|
|
84
|
+
entityName: string,
|
|
85
|
+
limit: number,
|
|
86
|
+
offset: number,
|
|
87
|
+
rowIdIdentifier?: string | null,
|
|
88
|
+
) =>
|
|
89
|
+
`SELECT ${
|
|
90
|
+
rowIdIdentifier ? `${rowIdIdentifier} AS "__sqlite-hidden-rowid__", ` : ''
|
|
91
|
+
}* FROM ${buildQualifiedEntityName(schemaName, entityName)} LIMIT ${Math.max(1, Math.floor(limit))} OFFSET ${Math.max(0, Math.floor(offset))}`;
|
|
92
|
+
|
|
93
|
+
export const buildEntityCountSql = (schemaName: string, entityName: string) =>
|
|
94
|
+
`SELECT COUNT(*) AS count FROM ${buildQualifiedEntityName(schemaName, entityName)}`;
|
|
95
|
+
|
|
96
|
+
export const buildCreateSqlLookup = (
|
|
97
|
+
schemaName: string,
|
|
98
|
+
entityName: string,
|
|
99
|
+
) => `
|
|
100
|
+
SELECT sql
|
|
101
|
+
FROM ${quoteSqlIdentifier(schemaName)}.sqlite_schema
|
|
102
|
+
WHERE type IN ('table', 'view')
|
|
103
|
+
AND name = ${escapeSqlString(entityName)}
|
|
104
|
+
LIMIT 1
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
export const buildTableXInfoSql = (schemaName: string, entityName: string) =>
|
|
108
|
+
`PRAGMA ${buildPragmaPrefix(schemaName)}table_xinfo(${escapeSqlString(entityName)})`;
|
|
109
|
+
|
|
110
|
+
export const buildForeignKeySql = (schemaName: string, entityName: string) =>
|
|
111
|
+
`PRAGMA ${buildPragmaPrefix(schemaName)}foreign_key_list(${escapeSqlString(entityName)})`;
|
|
112
|
+
|
|
113
|
+
export const buildIndexListSql = (schemaName: string, entityName: string) =>
|
|
114
|
+
`PRAGMA ${buildPragmaPrefix(schemaName)}index_list(${escapeSqlString(entityName)})`;
|
|
115
|
+
|
|
116
|
+
export const buildIndexInfoSql = (schemaName: string, indexName: string) =>
|
|
117
|
+
`PRAGMA ${buildPragmaPrefix(schemaName)}index_info(${escapeSqlString(indexName)})`;
|
|
118
|
+
|
|
119
|
+
export const parseSchemas = (result: SqliteQueryResult): SqliteSchema[] =>
|
|
120
|
+
result.rows
|
|
121
|
+
.map((row) => ({
|
|
122
|
+
seq: asNumber(row.seq),
|
|
123
|
+
name: asString(row.name),
|
|
124
|
+
file: asNullableString(row.file),
|
|
125
|
+
}))
|
|
126
|
+
.filter((schema) => !!schema.name);
|
|
127
|
+
|
|
128
|
+
export const parseEntities = (
|
|
129
|
+
result: SqliteQueryResult,
|
|
130
|
+
schemaName: string,
|
|
131
|
+
): SqliteEntity[] =>
|
|
132
|
+
result.rows
|
|
133
|
+
.map((row) => ({
|
|
134
|
+
schemaName,
|
|
135
|
+
name: asString(row.name),
|
|
136
|
+
type: (asString(row.type) === 'view'
|
|
137
|
+
? 'view'
|
|
138
|
+
: 'table') as SqliteEntityType,
|
|
139
|
+
sql: asNullableString(row.sql),
|
|
140
|
+
}))
|
|
141
|
+
.filter((entity) => !!entity.name);
|
|
142
|
+
|
|
143
|
+
export const parseColumns = (result: SqliteQueryResult): SqliteColumnInfo[] =>
|
|
144
|
+
result.rows.map((row) => ({
|
|
145
|
+
cid: asNumber(row.cid),
|
|
146
|
+
name: asString(row.name),
|
|
147
|
+
type: asString(row.type),
|
|
148
|
+
notNull: asNumber(row.notnull) === 1,
|
|
149
|
+
defaultValue: asNullableString(row.dflt_value),
|
|
150
|
+
primaryKeyOrder: asNumber(row.pk),
|
|
151
|
+
hidden: asNumber(row.hidden),
|
|
152
|
+
}));
|
|
153
|
+
|
|
154
|
+
export const parseForeignKeys = (
|
|
155
|
+
result: SqliteQueryResult,
|
|
156
|
+
): SqliteForeignKeyInfo[] =>
|
|
157
|
+
result.rows.map((row) => ({
|
|
158
|
+
id: asNumber(row.id),
|
|
159
|
+
seq: asNumber(row.seq),
|
|
160
|
+
table: asString(row.table),
|
|
161
|
+
from: asString(row.from),
|
|
162
|
+
to: asNullableString(row.to),
|
|
163
|
+
onUpdate: asString(row.on_update),
|
|
164
|
+
onDelete: asString(row.on_delete),
|
|
165
|
+
match: asString(row.match),
|
|
166
|
+
}));
|
|
167
|
+
|
|
168
|
+
export const parseIndexes = (result: SqliteQueryResult): SqliteIndexInfo[] =>
|
|
169
|
+
result.rows.map((row) => ({
|
|
170
|
+
seq: asNumber(row.seq),
|
|
171
|
+
name: asString(row.name),
|
|
172
|
+
unique: asNumber(row.unique) === 1,
|
|
173
|
+
origin: asString(row.origin),
|
|
174
|
+
partial: asNumber(row.partial) === 1,
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
export const parseIndexColumns = (
|
|
178
|
+
result: SqliteQueryResult,
|
|
179
|
+
): SqliteIndexColumnInfo[] =>
|
|
180
|
+
result.rows
|
|
181
|
+
.map((row) => ({
|
|
182
|
+
seqno: asNumber(row.seqno),
|
|
183
|
+
cid: asNumber(row.cid),
|
|
184
|
+
name: asString(row.name),
|
|
185
|
+
}))
|
|
186
|
+
.filter((column) => !!column.name);
|
|
187
|
+
|
|
188
|
+
export const parseCount = (result: SqliteQueryResult) =>
|
|
189
|
+
asNumber(result.rows[0]?.count);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { X } from 'lucide-react';
|
|
2
|
+
|
|
3
|
+
const toneButtonClassName =
|
|
4
|
+
'sqlite-button inline-flex items-center justify-center gap-2 rounded-xl px-3 py-2 text-sm font-medium';
|
|
5
|
+
|
|
6
|
+
export const sqliteSecondaryButtonClassName = `${toneButtonClassName} sqlite-button-secondary`;
|
|
7
|
+
export const sqlitePrimaryButtonClassName = `${toneButtonClassName} sqlite-button-primary`;
|
|
8
|
+
export const sqliteModalIconButtonClassName = `${sqliteSecondaryButtonClassName} h-10 w-10 shrink-0 px-0 py-0`;
|
|
9
|
+
|
|
10
|
+
type SqliteModalCloseButtonProps = {
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
ariaLabel?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const SqliteModalCloseButton = ({
|
|
17
|
+
onClose,
|
|
18
|
+
disabled = false,
|
|
19
|
+
ariaLabel = 'Close modal',
|
|
20
|
+
}: SqliteModalCloseButtonProps) => {
|
|
21
|
+
return (
|
|
22
|
+
<button
|
|
23
|
+
type="button"
|
|
24
|
+
className={sqliteModalIconButtonClassName}
|
|
25
|
+
aria-label={ariaLabel}
|
|
26
|
+
onClick={onClose}
|
|
27
|
+
disabled={disabled}
|
|
28
|
+
>
|
|
29
|
+
<X aria-hidden="true" className="h-4 w-4" />
|
|
30
|
+
</button>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Modal, useOverlayState } from '@heroui/react';
|
|
2
|
+
import { AlertTriangle, Trash2 } from 'lucide-react';
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
SqliteModalCloseButton,
|
|
6
|
+
sqliteSecondaryButtonClassName,
|
|
7
|
+
} from './sqlite-modal-controls';
|
|
8
|
+
|
|
9
|
+
type SqliteRowDeleteModalProps = {
|
|
10
|
+
isOpen: boolean;
|
|
11
|
+
rowNumber: number;
|
|
12
|
+
entityName: string;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
onDelete: () => Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const toneButtonClassName =
|
|
18
|
+
'sqlite-button inline-flex items-center justify-center gap-2 rounded-xl px-3 py-2 text-sm font-medium';
|
|
19
|
+
const dangerButtonClassName = `${toneButtonClassName} border border-rose-400/30 bg-rose-500/16 text-rose-50 hover:bg-rose-500/24`;
|
|
20
|
+
|
|
21
|
+
export const SqliteRowDeleteModal = ({
|
|
22
|
+
isOpen,
|
|
23
|
+
rowNumber,
|
|
24
|
+
entityName,
|
|
25
|
+
onClose,
|
|
26
|
+
onDelete,
|
|
27
|
+
}: SqliteRowDeleteModalProps) => {
|
|
28
|
+
const overlay = useOverlayState({
|
|
29
|
+
isOpen,
|
|
30
|
+
onOpenChange: (open: boolean) => {
|
|
31
|
+
if (!open) {
|
|
32
|
+
onClose();
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
const [deleting, setDeleting] = useState(false);
|
|
37
|
+
const [error, setError] = useState<string | null>(null);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (isOpen) {
|
|
41
|
+
setDeleting(false);
|
|
42
|
+
setError(null);
|
|
43
|
+
}
|
|
44
|
+
}, [isOpen]);
|
|
45
|
+
|
|
46
|
+
const handleDelete = async () => {
|
|
47
|
+
try {
|
|
48
|
+
setDeleting(true);
|
|
49
|
+
setError(null);
|
|
50
|
+
await onDelete();
|
|
51
|
+
onClose();
|
|
52
|
+
} catch (nextError) {
|
|
53
|
+
setError(
|
|
54
|
+
nextError instanceof Error ? nextError.message : String(nextError),
|
|
55
|
+
);
|
|
56
|
+
} finally {
|
|
57
|
+
setDeleting(false);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Modal state={overlay}>
|
|
63
|
+
<Modal.Backdrop
|
|
64
|
+
variant="blur"
|
|
65
|
+
isDismissable={!deleting}
|
|
66
|
+
className="bg-[rgba(5,10,16,0.24)] backdrop-blur-[2px]"
|
|
67
|
+
>
|
|
68
|
+
<Modal.Container placement="center" size="md" scroll="inside">
|
|
69
|
+
<Modal.Dialog
|
|
70
|
+
aria-label={`Delete row ${rowNumber} from ${entityName}`}
|
|
71
|
+
className="w-full max-w-xl overflow-hidden border border-white/10 bg-[#0a121b] p-0 text-white shadow-[0_30px_90px_rgba(0,0,0,0.42)]"
|
|
72
|
+
>
|
|
73
|
+
<div className="flex items-center justify-between gap-4 border-b border-white/8 px-5 py-5">
|
|
74
|
+
<div className="flex items-center gap-3">
|
|
75
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-rose-500/12 text-rose-200">
|
|
76
|
+
<AlertTriangle aria-hidden="true" className="h-5 w-5" />
|
|
77
|
+
</div>
|
|
78
|
+
<div>
|
|
79
|
+
<h2 className="text-lg font-semibold text-white">
|
|
80
|
+
Delete Row {rowNumber}
|
|
81
|
+
</h2>
|
|
82
|
+
<p className="mt-1 text-sm text-slate-400">{entityName}</p>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
<SqliteModalCloseButton onClose={onClose} disabled={deleting} />
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<Modal.Body className="space-y-0 p-0">
|
|
89
|
+
<div className="space-y-5 px-5 py-5">
|
|
90
|
+
<p className="text-sm leading-6 text-slate-300">
|
|
91
|
+
This will permanently delete the selected row and immediately
|
|
92
|
+
refetch the current page.
|
|
93
|
+
</p>
|
|
94
|
+
|
|
95
|
+
{error ? (
|
|
96
|
+
<div className="sqlite-inline-error" aria-live="polite">
|
|
97
|
+
<div>
|
|
98
|
+
<p className="font-medium text-rose-100">Delete Failed</p>
|
|
99
|
+
<p className="mt-1 text-sm text-rose-100/90">{error}</p>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
) : null}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div className="flex items-center justify-end gap-3 border-t border-white/8 px-5 py-5">
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
className={sqliteSecondaryButtonClassName}
|
|
109
|
+
onClick={onClose}
|
|
110
|
+
disabled={deleting}
|
|
111
|
+
>
|
|
112
|
+
Cancel
|
|
113
|
+
</button>
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
className={dangerButtonClassName}
|
|
117
|
+
onClick={() => void handleDelete()}
|
|
118
|
+
disabled={deleting}
|
|
119
|
+
>
|
|
120
|
+
<Trash2 aria-hidden="true" className="h-4 w-4" />
|
|
121
|
+
{deleting ? 'Deleting…' : 'Delete Row'}
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
</Modal.Body>
|
|
125
|
+
</Modal.Dialog>
|
|
126
|
+
</Modal.Container>
|
|
127
|
+
</Modal.Backdrop>
|
|
128
|
+
</Modal>
|
|
129
|
+
);
|
|
130
|
+
};
|