@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +20 -0
  3. package/README.md +102 -0
  4. package/dist/devtools/assets/panel-B3paLkwG.js +82 -0
  5. package/dist/devtools/assets/panel-CIU0JBOs.css +1 -0
  6. package/dist/devtools/panel.html +31 -0
  7. package/dist/react-native/chunks/bridge-values.cjs +5 -0
  8. package/dist/react-native/chunks/bridge-values.js +258 -0
  9. package/dist/react-native/chunks/index.require.cjs +1 -0
  10. package/dist/react-native/chunks/index.require.js +118 -0
  11. package/dist/react-native/chunks/useRozeniteSqlitePlugin.require.cjs +1 -0
  12. package/dist/react-native/chunks/useRozeniteSqlitePlugin.require.js +189 -0
  13. package/dist/react-native/index.cjs +1 -0
  14. package/dist/react-native/index.d.ts +178 -0
  15. package/dist/react-native/index.js +16 -0
  16. package/dist/rozenite.json +1 -0
  17. package/package.json +83 -0
  18. package/postcss.config.js +6 -0
  19. package/react-native.ts +55 -0
  20. package/rozenite.config.ts +8 -0
  21. package/src/react-native/adapters/__tests__/expo-sqlite.test.ts +94 -0
  22. package/src/react-native/adapters/expo-sqlite.ts +230 -0
  23. package/src/react-native/adapters/generic.ts +88 -0
  24. package/src/react-native/adapters/index.ts +9 -0
  25. package/src/react-native/sqlite-view.ts +24 -0
  26. package/src/react-native/useRozeniteSqlitePlugin.ts +262 -0
  27. package/src/shared/__tests__/bridge-values.test.ts +34 -0
  28. package/src/shared/__tests__/sql.test.ts +55 -0
  29. package/src/shared/bridge-values.ts +170 -0
  30. package/src/shared/protocol.ts +41 -0
  31. package/src/shared/sql.ts +420 -0
  32. package/src/shared/types.ts +81 -0
  33. package/src/ui/__tests__/sql-editor-utils.test.ts +135 -0
  34. package/src/ui/__tests__/sqlite-row-edit-value.test.ts +22 -0
  35. package/src/ui/__tests__/sqlite-row-mutations.test.ts +310 -0
  36. package/src/ui/__tests__/sqlite-table-column-order.test.ts +83 -0
  37. package/src/ui/__tests__/value-utils.test.tsx +12 -0
  38. package/src/ui/cell-detail-drawer.tsx +65 -0
  39. package/src/ui/globals.css +1415 -0
  40. package/src/ui/panel.tsx +2815 -0
  41. package/src/ui/query-result-table.tsx +199 -0
  42. package/src/ui/sql-editor-utils.ts +352 -0
  43. package/src/ui/sql-editor.tsx +509 -0
  44. package/src/ui/sqlite-data-table.tsx +296 -0
  45. package/src/ui/sqlite-introspection.ts +189 -0
  46. package/src/ui/sqlite-modal-controls.tsx +32 -0
  47. package/src/ui/sqlite-row-delete-modal.tsx +130 -0
  48. package/src/ui/sqlite-row-edit-modal.tsx +487 -0
  49. package/src/ui/sqlite-row-edit-value.ts +53 -0
  50. package/src/ui/sqlite-row-mutations.ts +246 -0
  51. package/src/ui/sqlite-table-column-order.ts +154 -0
  52. package/src/ui/use-sqlite-requests.ts +205 -0
  53. package/src/ui/utils.ts +107 -0
  54. package/src/ui/value-utils.tsx +162 -0
  55. package/tsconfig.json +36 -0
  56. package/vite.config.ts +20 -0
@@ -0,0 +1,487 @@
1
+ import { Modal, useOverlayState } from '@heroui/react';
2
+ import { Pencil, Save } from 'lucide-react';
3
+ import { useEffect, useMemo, useState } from 'react';
4
+ import type { SqliteColumnInfo } from './sqlite-introspection';
5
+ import {
6
+ SqliteModalCloseButton,
7
+ sqlitePrimaryButtonClassName,
8
+ sqliteSecondaryButtonClassName,
9
+ } from './sqlite-modal-controls';
10
+ import {
11
+ canColumnBeNull,
12
+ getCompatibleValueKinds,
13
+ getEditableColumns,
14
+ getPrimaryKeyColumns,
15
+ } from './sqlite-row-mutations';
16
+ import {
17
+ parseEditableFieldValue,
18
+ type EditableFieldDraft,
19
+ type EditableValueKind,
20
+ } from './sqlite-row-edit-value';
21
+ import { getValueKind, getValuePreview } from './value-utils';
22
+
23
+ type SqliteRowEditModalProps = {
24
+ isOpen: boolean;
25
+ rowNumber: number;
26
+ entityName: string;
27
+ row: Record<string, unknown> | null;
28
+ columns: SqliteColumnInfo[];
29
+ onClose: () => void;
30
+ onSave: (nextValues: Record<string, unknown>) => Promise<void>;
31
+ };
32
+
33
+ const stringifyDraftValue = (value: unknown, kind: EditableValueKind) => {
34
+ if (value === null) {
35
+ if (kind === 'boolean') {
36
+ return 'false';
37
+ }
38
+
39
+ if (kind === 'blob-ish') {
40
+ return '[]';
41
+ }
42
+
43
+ if (kind === 'json') {
44
+ return '{}';
45
+ }
46
+
47
+ return '';
48
+ }
49
+
50
+ if (value === undefined) {
51
+ if (kind === 'blob-ish') {
52
+ return '[]';
53
+ }
54
+
55
+ if (kind === 'json') {
56
+ return '{}';
57
+ }
58
+
59
+ return kind === 'boolean' ? 'false' : '';
60
+ }
61
+
62
+ if (kind === 'number' && typeof value === 'boolean') {
63
+ return value ? '1' : '0';
64
+ }
65
+
66
+ if (kind === 'boolean' && typeof value === 'number') {
67
+ return value === 0 ? 'false' : 'true';
68
+ }
69
+
70
+ if (kind === 'blob-ish' || kind === 'json') {
71
+ return JSON.stringify(value, null, 2);
72
+ }
73
+
74
+ return String(value);
75
+ };
76
+
77
+ const createFieldDraft = (
78
+ column: SqliteColumnInfo,
79
+ value: unknown,
80
+ ): EditableFieldDraft => {
81
+ const compatibleKinds = getCompatibleValueKinds(column, value);
82
+ const kind = compatibleKinds[0] ?? 'text';
83
+
84
+ return {
85
+ kind,
86
+ rawValue: stringifyDraftValue(value, kind),
87
+ };
88
+ };
89
+
90
+ export const SqliteRowEditModal = ({
91
+ isOpen,
92
+ rowNumber,
93
+ entityName,
94
+ row,
95
+ columns,
96
+ onClose,
97
+ onSave,
98
+ }: SqliteRowEditModalProps) => {
99
+ const overlay = useOverlayState({
100
+ isOpen,
101
+ onOpenChange: (open: boolean) => {
102
+ if (!open) {
103
+ onClose();
104
+ }
105
+ },
106
+ });
107
+ const primaryKeyColumns = useMemo(
108
+ () => getPrimaryKeyColumns(columns),
109
+ [columns],
110
+ );
111
+ const editableColumns = useMemo(() => getEditableColumns(columns), [columns]);
112
+ const [drafts, setDrafts] = useState<Record<string, EditableFieldDraft>>({});
113
+ const [submitting, setSubmitting] = useState(false);
114
+ const [error, setError] = useState<string | null>(null);
115
+
116
+ useEffect(() => {
117
+ if (!isOpen || !row) {
118
+ setDrafts({});
119
+ setSubmitting(false);
120
+ setError(null);
121
+ return;
122
+ }
123
+
124
+ setDrafts(
125
+ Object.fromEntries(
126
+ editableColumns.map((column) => [
127
+ column.name,
128
+ createFieldDraft(column, row[column.name]),
129
+ ]),
130
+ ),
131
+ );
132
+ setSubmitting(false);
133
+ setError(null);
134
+ }, [editableColumns, isOpen, row]);
135
+
136
+ const handleKindChange = (columnName: string, kind: EditableValueKind) => {
137
+ const column = editableColumns.find(
138
+ (candidate) => candidate.name === columnName,
139
+ );
140
+
141
+ setDrafts((current) => ({
142
+ ...current,
143
+ [columnName]: {
144
+ kind,
145
+ rawValue:
146
+ kind === 'boolean'
147
+ ? 'false'
148
+ : kind === 'blob-ish'
149
+ ? '[]'
150
+ : kind === 'json'
151
+ ? '{}'
152
+ : kind === 'null'
153
+ ? ''
154
+ : kind === 'number' &&
155
+ current[columnName]?.rawValue === 'true'
156
+ ? '1'
157
+ : kind === 'number' &&
158
+ current[columnName]?.rawValue === 'false'
159
+ ? '0'
160
+ : (current[columnName]?.rawValue ??
161
+ (column
162
+ ? createFieldDraft(column, null).rawValue
163
+ : '')),
164
+ },
165
+ }));
166
+ };
167
+
168
+ const handleSave = async () => {
169
+ if (!row) {
170
+ return;
171
+ }
172
+
173
+ try {
174
+ setSubmitting(true);
175
+ setError(null);
176
+
177
+ const nextValues = Object.fromEntries(
178
+ editableColumns.map((column) => [
179
+ column.name,
180
+ parseEditableFieldValue(
181
+ drafts[column.name] ?? createFieldDraft(column, row[column.name]),
182
+ ),
183
+ ]),
184
+ );
185
+
186
+ await onSave(nextValues);
187
+ onClose();
188
+ } catch (nextError) {
189
+ setError(
190
+ nextError instanceof Error ? nextError.message : String(nextError),
191
+ );
192
+ } finally {
193
+ setSubmitting(false);
194
+ }
195
+ };
196
+
197
+ return (
198
+ <Modal state={overlay}>
199
+ <Modal.Backdrop
200
+ variant="blur"
201
+ isDismissable={!submitting}
202
+ className="bg-[rgba(5,10,16,0.24)] backdrop-blur-[2px]"
203
+ >
204
+ <Modal.Container placement="center" size="lg" scroll="inside">
205
+ <Modal.Dialog
206
+ aria-label={`Edit row ${rowNumber} in ${entityName}`}
207
+ className="w-full max-w-4xl overflow-hidden border border-white/10 bg-[#0a121b] p-0 text-white shadow-[0_30px_90px_rgba(0,0,0,0.42)]"
208
+ >
209
+ <div className="flex items-center justify-between gap-4 border-b border-white/8 px-5 py-5">
210
+ <div>
211
+ <div className="flex items-center gap-2">
212
+ <Pencil aria-hidden="true" className="h-4 w-4 text-sky-300" />
213
+ <h2 className="text-lg font-semibold text-white">
214
+ Edit Row {rowNumber}
215
+ </h2>
216
+ </div>
217
+ <p className="mt-1 text-sm text-slate-400">{entityName}</p>
218
+ </div>
219
+ <SqliteModalCloseButton onClose={onClose} disabled={submitting} />
220
+ </div>
221
+
222
+ <Modal.Body className="space-y-0 p-0">
223
+ <div className="space-y-5 px-5 py-5">
224
+ {primaryKeyColumns.length > 0 ? (
225
+ <section className="space-y-3">
226
+ <div>
227
+ <h3 className="text-sm font-medium text-slate-200">
228
+ Row Identifier
229
+ </h3>
230
+ <p className="mt-1 text-xs text-slate-400">
231
+ Primary-key fields are shown for reference and cannot be
232
+ edited.
233
+ </p>
234
+ </div>
235
+ <div className="grid gap-3 md:grid-cols-2">
236
+ {primaryKeyColumns.map((column) => (
237
+ <div
238
+ key={column.name}
239
+ className="rounded-2xl border border-white/8 bg-white/[0.03] p-3"
240
+ >
241
+ <div className="flex items-center justify-between gap-3">
242
+ <div>
243
+ <p className="text-sm font-medium text-white">
244
+ {column.name}
245
+ </p>
246
+ <p className="text-xs uppercase tracking-[0.24em] text-slate-500">
247
+ {column.type || 'value'}
248
+ </p>
249
+ </div>
250
+ <span className="sqlite-chip sqlite-chip-static">
251
+ PK
252
+ </span>
253
+ </div>
254
+ <p className="mt-3 break-all font-mono text-sm text-slate-200">
255
+ {getValuePreview(row?.[column.name])}
256
+ </p>
257
+ <p className="mt-1 text-xs text-slate-500">
258
+ {getValueKind(row?.[column.name])}
259
+ </p>
260
+ </div>
261
+ ))}
262
+ </div>
263
+ </section>
264
+ ) : null}
265
+
266
+ <section className="space-y-3">
267
+ <div>
268
+ <h3 className="text-sm font-medium text-slate-200">
269
+ Editable Values
270
+ </h3>
271
+ <p className="mt-1 text-xs text-slate-400">
272
+ Update any non-primary-key column and save to write the
273
+ row back to SQLite.
274
+ </p>
275
+ </div>
276
+
277
+ {editableColumns.length === 0 ? (
278
+ <div className="rounded-2xl border border-white/8 bg-white/[0.03] p-4 text-sm text-slate-300">
279
+ This row does not expose editable, non-primary-key
280
+ columns.
281
+ </div>
282
+ ) : (
283
+ <div className="space-y-4">
284
+ {editableColumns.map((column) => {
285
+ const draft =
286
+ drafts[column.name] ??
287
+ createFieldDraft(column, row?.[column.name]);
288
+ const compatibleKinds = getCompatibleValueKinds(
289
+ column,
290
+ row?.[column.name],
291
+ );
292
+ const allowNull = canColumnBeNull(column);
293
+ const shouldUseTextArea =
294
+ draft.kind === 'blob-ish' ||
295
+ draft.kind === 'json' ||
296
+ (draft.kind === 'text' &&
297
+ draft.rawValue.includes('\n'));
298
+
299
+ return (
300
+ <div
301
+ key={column.name}
302
+ className="rounded-2xl border border-white/8 bg-white/[0.03] p-4"
303
+ >
304
+ <div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
305
+ <div className="min-w-0">
306
+ <label
307
+ htmlFor={`sqlite-row-edit-${column.name}`}
308
+ className="text-sm font-medium text-white"
309
+ >
310
+ {column.name}
311
+ </label>
312
+ <p className="mt-1 text-xs text-slate-500">
313
+ {column.type || 'value'}
314
+ </p>
315
+ </div>
316
+
317
+ {compatibleKinds.length > 1 || allowNull ? (
318
+ <div className="w-full max-w-[12rem]">
319
+ <label
320
+ htmlFor={`sqlite-row-edit-type-${column.name}`}
321
+ className="sr-only"
322
+ >
323
+ Value type
324
+ </label>
325
+ <select
326
+ id={`sqlite-row-edit-type-${column.name}`}
327
+ className="sqlite-select w-full"
328
+ value={draft.kind}
329
+ disabled={submitting}
330
+ onChange={(event) =>
331
+ handleKindChange(
332
+ column.name,
333
+ event.target.value as EditableValueKind,
334
+ )
335
+ }
336
+ >
337
+ {compatibleKinds.map((kind) => (
338
+ <option key={kind} value={kind}>
339
+ {kind === 'blob-ish'
340
+ ? 'Blob'
341
+ : kind === 'json'
342
+ ? 'JSON'
343
+ : kind === 'boolean'
344
+ ? 'Boolean'
345
+ : kind === 'number'
346
+ ? 'Number'
347
+ : 'Text'}
348
+ </option>
349
+ ))}
350
+ {allowNull ? (
351
+ <option value="null">NULL</option>
352
+ ) : null}
353
+ </select>
354
+ </div>
355
+ ) : (
356
+ <span className="sqlite-chip sqlite-chip-static">
357
+ {compatibleKinds[0] === 'blob-ish'
358
+ ? 'Blob'
359
+ : compatibleKinds[0] === 'json'
360
+ ? 'JSON'
361
+ : compatibleKinds[0] === 'boolean'
362
+ ? 'Boolean'
363
+ : compatibleKinds[0] === 'number'
364
+ ? 'Number'
365
+ : 'Text'}
366
+ </span>
367
+ )}
368
+ </div>
369
+
370
+ <div className="mt-3">
371
+ {draft.kind === 'null' ? (
372
+ <div className="rounded-xl border border-dashed border-white/10 bg-black/10 px-3 py-2 text-sm text-slate-400">
373
+ This field will be saved as SQL NULL.
374
+ </div>
375
+ ) : draft.kind === 'boolean' ? (
376
+ <select
377
+ id={`sqlite-row-edit-${column.name}`}
378
+ className="sqlite-select w-full"
379
+ value={draft.rawValue}
380
+ disabled={submitting}
381
+ onChange={(event) =>
382
+ setDrafts((current) => ({
383
+ ...current,
384
+ [column.name]: {
385
+ ...draft,
386
+ rawValue: event.target.value,
387
+ },
388
+ }))
389
+ }
390
+ >
391
+ <option value="true">true</option>
392
+ <option value="false">false</option>
393
+ </select>
394
+ ) : shouldUseTextArea ? (
395
+ <textarea
396
+ id={`sqlite-row-edit-${column.name}`}
397
+ className="sqlite-input min-h-28 w-full resize-y py-3"
398
+ value={draft.rawValue}
399
+ disabled={submitting}
400
+ onChange={(event) =>
401
+ setDrafts((current) => ({
402
+ ...current,
403
+ [column.name]: {
404
+ ...draft,
405
+ rawValue: event.target.value,
406
+ },
407
+ }))
408
+ }
409
+ />
410
+ ) : (
411
+ <input
412
+ id={`sqlite-row-edit-${column.name}`}
413
+ type="text"
414
+ className="sqlite-input w-full"
415
+ value={draft.rawValue}
416
+ disabled={submitting}
417
+ onChange={(event) =>
418
+ setDrafts((current) => ({
419
+ ...current,
420
+ [column.name]: {
421
+ ...draft,
422
+ rawValue: event.target.value,
423
+ },
424
+ }))
425
+ }
426
+ />
427
+ )}
428
+ </div>
429
+
430
+ <p className="mt-2 text-xs text-slate-500">
431
+ Current value:{' '}
432
+ {getValuePreview(row?.[column.name])} (
433
+ {getValueKind(row?.[column.name])}) · Compatible:{' '}
434
+ {compatibleKinds
435
+ .map((kind) =>
436
+ kind === 'blob-ish'
437
+ ? 'blob'
438
+ : kind === 'json'
439
+ ? 'json'
440
+ : kind,
441
+ )
442
+ .join(', ')}
443
+ {allowNull ? ', null' : ''}
444
+ </p>
445
+ </div>
446
+ );
447
+ })}
448
+ </div>
449
+ )}
450
+ </section>
451
+
452
+ {error ? (
453
+ <div className="sqlite-inline-error" aria-live="polite">
454
+ <div>
455
+ <p className="font-medium text-rose-100">Save Failed</p>
456
+ <p className="mt-1 text-sm text-rose-100/90">{error}</p>
457
+ </div>
458
+ </div>
459
+ ) : null}
460
+ </div>
461
+
462
+ <div className="flex items-center justify-end gap-3 border-t border-white/8 px-5 py-5">
463
+ <button
464
+ type="button"
465
+ className={sqliteSecondaryButtonClassName}
466
+ onClick={onClose}
467
+ disabled={submitting}
468
+ >
469
+ Cancel
470
+ </button>
471
+ <button
472
+ type="button"
473
+ className={sqlitePrimaryButtonClassName}
474
+ onClick={() => void handleSave()}
475
+ disabled={submitting || editableColumns.length === 0}
476
+ >
477
+ <Save aria-hidden="true" className="h-4 w-4" />
478
+ {submitting ? 'Saving…' : 'Save Changes'}
479
+ </button>
480
+ </div>
481
+ </Modal.Body>
482
+ </Modal.Dialog>
483
+ </Modal.Container>
484
+ </Modal.Backdrop>
485
+ </Modal>
486
+ );
487
+ };
@@ -0,0 +1,53 @@
1
+ export type EditableValueKind =
2
+ | 'null'
3
+ | 'text'
4
+ | 'number'
5
+ | 'boolean'
6
+ | 'blob-ish'
7
+ | 'json';
8
+
9
+ export type EditableFieldDraft = {
10
+ kind: EditableValueKind;
11
+ rawValue: string;
12
+ };
13
+
14
+ export const parseEditableFieldValue = (draft: EditableFieldDraft): unknown => {
15
+ switch (draft.kind) {
16
+ case 'null':
17
+ return null;
18
+ case 'text':
19
+ return draft.rawValue;
20
+ case 'number': {
21
+ const value = Number(draft.rawValue.trim());
22
+
23
+ if (Number.isNaN(value)) {
24
+ throw new Error('Enter a valid number.');
25
+ }
26
+
27
+ return value;
28
+ }
29
+ case 'boolean': {
30
+ if (draft.rawValue !== 'true' && draft.rawValue !== 'false') {
31
+ throw new Error('Choose either true or false.');
32
+ }
33
+
34
+ return draft.rawValue === 'true';
35
+ }
36
+ case 'blob-ish': {
37
+ const parsed = JSON.parse(draft.rawValue);
38
+
39
+ if (
40
+ !Array.isArray(parsed) ||
41
+ !parsed.every((item) => typeof item === 'number')
42
+ ) {
43
+ throw new Error('Blob values must be JSON arrays of numbers.');
44
+ }
45
+
46
+ return new Uint8Array(parsed);
47
+ }
48
+ case 'json': {
49
+ const parsed = JSON.parse(draft.rawValue);
50
+ return JSON.stringify(parsed);
51
+ }
52
+ }
53
+ };