@rozenite/sqlite-plugin 1.12.0 → 2.0.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 +23 -0
- package/dist/devtools/assets/panel-BarPLQ4-.css +1 -0
- package/dist/devtools/assets/{panel-DjRs5NTl.js → panel-CsA6Ejak.js} +45 -44
- package/dist/devtools/panel.html +2 -2
- package/dist/react-native/index.d.ts +2 -2
- package/dist/rozenite.json +1 -1
- package/package.json +9 -13
- package/src/react-native/useSqliteAgentTools.ts +4 -1
- package/src/ui/__tests__/sqlite-drop-mutations.test.ts +135 -0
- package/src/ui/globals.css +12 -0
- package/src/ui/panel.tsx +214 -0
- package/src/ui/sqlite-drop-modal.tsx +219 -0
- package/src/ui/sqlite-drop-mutations.ts +60 -0
- package/dist/devtools/assets/panel-CIU0JBOs.css +0 -1
- package/postcss.config.js +0 -6
|
@@ -0,0 +1,219 @@
|
|
|
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
|
+
export type SqliteDropModalTarget =
|
|
10
|
+
| {
|
|
11
|
+
kind: 'entity';
|
|
12
|
+
entityType: 'table' | 'view';
|
|
13
|
+
qualifiedName: string;
|
|
14
|
+
confirmationValue: string;
|
|
15
|
+
sql: string;
|
|
16
|
+
}
|
|
17
|
+
| {
|
|
18
|
+
kind: 'database';
|
|
19
|
+
databaseName: string;
|
|
20
|
+
confirmationValue: string;
|
|
21
|
+
tableCount: number;
|
|
22
|
+
viewCount: number;
|
|
23
|
+
sql: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type SqliteDropModalProps = {
|
|
27
|
+
isOpen: boolean;
|
|
28
|
+
target: SqliteDropModalTarget;
|
|
29
|
+
onClose: () => void;
|
|
30
|
+
onConfirm: () => Promise<void>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const toneButtonClassName =
|
|
34
|
+
'sqlite-button inline-flex items-center justify-center gap-2 rounded-xl px-3 py-2 text-sm font-medium';
|
|
35
|
+
const dangerButtonClassName = `${toneButtonClassName} border border-rose-400/30 bg-rose-500/16 text-rose-50 hover:bg-rose-500/24`;
|
|
36
|
+
|
|
37
|
+
const getModalTitle = (target: SqliteDropModalTarget) =>
|
|
38
|
+
target.kind === 'entity'
|
|
39
|
+
? `Drop ${target.entityType === 'view' ? 'View' : 'Table'}`
|
|
40
|
+
: 'Drop All Objects';
|
|
41
|
+
|
|
42
|
+
const getModalSubtitle = (target: SqliteDropModalTarget) =>
|
|
43
|
+
target.kind === 'entity' ? target.qualifiedName : target.databaseName;
|
|
44
|
+
|
|
45
|
+
const getConfirmationLabel = (target: SqliteDropModalTarget) =>
|
|
46
|
+
target.kind === 'entity'
|
|
47
|
+
? `${target.entityType === 'view' ? 'view' : 'table'} name`
|
|
48
|
+
: 'database name';
|
|
49
|
+
|
|
50
|
+
export const SqliteDropModal = ({
|
|
51
|
+
isOpen,
|
|
52
|
+
target,
|
|
53
|
+
onClose,
|
|
54
|
+
onConfirm,
|
|
55
|
+
}: SqliteDropModalProps) => {
|
|
56
|
+
const overlay = useOverlayState({
|
|
57
|
+
isOpen,
|
|
58
|
+
onOpenChange: (open: boolean) => {
|
|
59
|
+
if (!open) {
|
|
60
|
+
onClose();
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
const [dropping, setDropping] = useState(false);
|
|
65
|
+
const [error, setError] = useState<string | null>(null);
|
|
66
|
+
const [confirmationInput, setConfirmationInput] = useState('');
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (isOpen) {
|
|
70
|
+
setDropping(false);
|
|
71
|
+
setError(null);
|
|
72
|
+
setConfirmationInput('');
|
|
73
|
+
}
|
|
74
|
+
}, [isOpen]);
|
|
75
|
+
|
|
76
|
+
const isConfirmed = confirmationInput === target.confirmationValue;
|
|
77
|
+
|
|
78
|
+
const handleConfirm = async () => {
|
|
79
|
+
if (!isConfirmed) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
setDropping(true);
|
|
85
|
+
setError(null);
|
|
86
|
+
await onConfirm();
|
|
87
|
+
onClose();
|
|
88
|
+
} catch (nextError) {
|
|
89
|
+
setError(
|
|
90
|
+
nextError instanceof Error ? nextError.message : String(nextError),
|
|
91
|
+
);
|
|
92
|
+
} finally {
|
|
93
|
+
setDropping(false);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<Modal state={overlay}>
|
|
99
|
+
<Modal.Backdrop
|
|
100
|
+
variant="blur"
|
|
101
|
+
isDismissable={!dropping}
|
|
102
|
+
className="bg-[rgba(5,10,16,0.24)] backdrop-blur-[2px]"
|
|
103
|
+
>
|
|
104
|
+
<Modal.Container placement="center" size="md" scroll="inside">
|
|
105
|
+
<Modal.Dialog
|
|
106
|
+
aria-label={`${getModalTitle(target)} ${getModalSubtitle(target)}`}
|
|
107
|
+
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)]"
|
|
108
|
+
>
|
|
109
|
+
<div className="flex items-center justify-between gap-4 border-b border-white/8 px-5 py-5">
|
|
110
|
+
<div className="flex items-center gap-3">
|
|
111
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-rose-500/12 text-rose-200">
|
|
112
|
+
<AlertTriangle aria-hidden="true" className="h-5 w-5" />
|
|
113
|
+
</div>
|
|
114
|
+
<div>
|
|
115
|
+
<h2 className="text-lg font-semibold text-white">
|
|
116
|
+
{getModalTitle(target)}
|
|
117
|
+
</h2>
|
|
118
|
+
<p className="mt-1 text-sm text-slate-400">
|
|
119
|
+
{getModalSubtitle(target)}
|
|
120
|
+
</p>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<SqliteModalCloseButton onClose={onClose} disabled={dropping} />
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<Modal.Body className="space-y-0 p-0">
|
|
127
|
+
<div className="space-y-5 px-5 py-5">
|
|
128
|
+
{target.kind === 'database' ? (
|
|
129
|
+
<p className="text-sm leading-6 text-slate-300">
|
|
130
|
+
This drops every table and view in{' '}
|
|
131
|
+
<span className="font-medium text-white">
|
|
132
|
+
{target.databaseName}
|
|
133
|
+
</span>{' '}
|
|
134
|
+
— {target.tableCount} table
|
|
135
|
+
{target.tableCount === 1 ? '' : 's'} and {target.viewCount}{' '}
|
|
136
|
+
view{target.viewCount === 1 ? '' : 's'}, along with all
|
|
137
|
+
their rows, indexes and triggers. The database file itself
|
|
138
|
+
is not deleted; it is left empty. This cannot be undone.
|
|
139
|
+
</p>
|
|
140
|
+
) : (
|
|
141
|
+
<p className="text-sm leading-6 text-slate-300">
|
|
142
|
+
This permanently drops{' '}
|
|
143
|
+
<span className="font-medium text-white">
|
|
144
|
+
{target.qualifiedName}
|
|
145
|
+
</span>{' '}
|
|
146
|
+
and everything it owns — rows, indexes and triggers. This
|
|
147
|
+
cannot be undone.
|
|
148
|
+
</p>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
<div className="space-y-2">
|
|
152
|
+
<p className="sqlite-helper-text">SQL to be executed</p>
|
|
153
|
+
<pre className="max-h-40 overflow-auto rounded-lg border border-white/8 bg-black/30 px-3 py-2 text-xs leading-5 text-slate-200">
|
|
154
|
+
{target.sql}
|
|
155
|
+
</pre>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div className="space-y-2">
|
|
159
|
+
<label
|
|
160
|
+
htmlFor="sqlite-drop-confirmation-input"
|
|
161
|
+
className="sqlite-helper-text"
|
|
162
|
+
>
|
|
163
|
+
Type the {getConfirmationLabel(target)}{' '}
|
|
164
|
+
<span className="font-medium text-white">
|
|
165
|
+
{target.confirmationValue}
|
|
166
|
+
</span>{' '}
|
|
167
|
+
to confirm
|
|
168
|
+
</label>
|
|
169
|
+
<input
|
|
170
|
+
id="sqlite-drop-confirmation-input"
|
|
171
|
+
type="text"
|
|
172
|
+
name="dropConfirmation"
|
|
173
|
+
autoComplete="off"
|
|
174
|
+
spellCheck={false}
|
|
175
|
+
className="sqlite-input"
|
|
176
|
+
value={confirmationInput}
|
|
177
|
+
onChange={(event) =>
|
|
178
|
+
setConfirmationInput(event.target.value)
|
|
179
|
+
}
|
|
180
|
+
disabled={dropping}
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
{error ? (
|
|
185
|
+
<div className="sqlite-inline-error" aria-live="polite">
|
|
186
|
+
<div>
|
|
187
|
+
<p className="font-medium text-rose-100">Drop Failed</p>
|
|
188
|
+
<p className="mt-1 text-sm text-rose-100/90">{error}</p>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
) : null}
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div className="flex items-center justify-end gap-3 border-t border-white/8 px-5 py-5">
|
|
195
|
+
<button
|
|
196
|
+
type="button"
|
|
197
|
+
className={sqliteSecondaryButtonClassName}
|
|
198
|
+
onClick={onClose}
|
|
199
|
+
disabled={dropping}
|
|
200
|
+
>
|
|
201
|
+
Cancel
|
|
202
|
+
</button>
|
|
203
|
+
<button
|
|
204
|
+
type="button"
|
|
205
|
+
className={dangerButtonClassName}
|
|
206
|
+
onClick={() => void handleConfirm()}
|
|
207
|
+
disabled={dropping || !isConfirmed}
|
|
208
|
+
>
|
|
209
|
+
<Trash2 aria-hidden="true" className="h-4 w-4" />
|
|
210
|
+
{dropping ? 'Dropping…' : getModalTitle(target)}
|
|
211
|
+
</button>
|
|
212
|
+
</div>
|
|
213
|
+
</Modal.Body>
|
|
214
|
+
</Modal.Dialog>
|
|
215
|
+
</Modal.Container>
|
|
216
|
+
</Modal.Backdrop>
|
|
217
|
+
</Modal>
|
|
218
|
+
);
|
|
219
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { quoteSqlIdentifier } from '../shared/sql';
|
|
2
|
+
import type { SqliteEntity } from './sqlite-introspection';
|
|
3
|
+
|
|
4
|
+
export type SqliteDropTarget = Pick<
|
|
5
|
+
SqliteEntity,
|
|
6
|
+
'schemaName' | 'name' | 'type'
|
|
7
|
+
>;
|
|
8
|
+
|
|
9
|
+
const buildQualifiedEntityName = (schemaName: string, entityName: string) =>
|
|
10
|
+
`${quoteSqlIdentifier(schemaName)}.${quoteSqlIdentifier(entityName)}`;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Builds a single `DROP TABLE` / `DROP VIEW` statement for the given entity.
|
|
14
|
+
* Identifiers are quoted (never parameterized, since SQLite does not support
|
|
15
|
+
* parameter binding for identifiers), so this is the only place that needs to
|
|
16
|
+
* get quoting right.
|
|
17
|
+
*/
|
|
18
|
+
export const buildDropEntitySql = (entity: SqliteDropTarget): string => {
|
|
19
|
+
const keyword = entity.type === 'view' ? 'VIEW' : 'TABLE';
|
|
20
|
+
return `DROP ${keyword} ${buildQualifiedEntityName(entity.schemaName, entity.name)}`;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Orders entities so that views are dropped before tables, preserving the
|
|
25
|
+
* relative order within each group. This keeps a view that references a
|
|
26
|
+
* table from ever being left dangling mid-sweep.
|
|
27
|
+
*/
|
|
28
|
+
export const orderEntitiesForDrop = (
|
|
29
|
+
entities: SqliteDropTarget[],
|
|
30
|
+
): SqliteDropTarget[] => [
|
|
31
|
+
...entities.filter((entity) => entity.type === 'view'),
|
|
32
|
+
...entities.filter((entity) => entity.type === 'table'),
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Builds the full drop-everything script for a database sweep: every view,
|
|
37
|
+
* then every table, each as its own statement. Returns an empty string when
|
|
38
|
+
* there is nothing to drop.
|
|
39
|
+
*/
|
|
40
|
+
export const buildDropAllEntitiesSql = (entities: SqliteDropTarget[]): string =>
|
|
41
|
+
orderEntitiesForDrop(entities)
|
|
42
|
+
.map((entity) => `${buildDropEntitySql(entity)};`)
|
|
43
|
+
.join('\n');
|
|
44
|
+
|
|
45
|
+
export const SQLITE_READ_FOREIGN_KEYS_SQL = 'PRAGMA foreign_keys';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `PRAGMA foreign_keys` is per-connection and not parameterizable; the value
|
|
49
|
+
* is always a literal 0/1 keyword, never user input, so this is safe to
|
|
50
|
+
* inline directly.
|
|
51
|
+
*/
|
|
52
|
+
export const buildSetForeignKeysSql = (enabled: boolean): string =>
|
|
53
|
+
`PRAGMA foreign_keys = ${enabled ? 'ON' : 'OFF'}`;
|
|
54
|
+
|
|
55
|
+
export const isForeignKeysEnabled = (
|
|
56
|
+
rows: Record<string, unknown>[],
|
|
57
|
+
): boolean => {
|
|
58
|
+
const rawValue = rows[0]?.foreign_keys;
|
|
59
|
+
return Number(rawValue) === 1;
|
|
60
|
+
};
|