@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,310 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { SqliteEntity } from '../sqlite-introspection';
3
+ import {
4
+ SQLITE_HIDDEN_ROWID_COLUMN_ID,
5
+ buildRowDeleteMutation,
6
+ buildRowUpdateMutation,
7
+ getAvailableRowIdIdentifier,
8
+ getCompatibleValueKinds,
9
+ getEditableColumns,
10
+ getPrimaryKeyColumns,
11
+ getRowMutationDescriptor,
12
+ } from '../sqlite-row-mutations';
13
+
14
+ const buildEntity = (sql: string): SqliteEntity => ({
15
+ schemaName: 'main',
16
+ name: 'projects',
17
+ type: 'table',
18
+ sql,
19
+ });
20
+
21
+ describe('sqlite row mutation helpers', () => {
22
+ it('extracts primary-key columns in SQLite order', () => {
23
+ expect(
24
+ getPrimaryKeyColumns([
25
+ {
26
+ cid: 0,
27
+ name: 'name',
28
+ type: 'TEXT',
29
+ notNull: true,
30
+ defaultValue: null,
31
+ primaryKeyOrder: 2,
32
+ hidden: 0,
33
+ },
34
+ {
35
+ cid: 1,
36
+ name: 'id',
37
+ type: 'INTEGER',
38
+ notNull: true,
39
+ defaultValue: null,
40
+ primaryKeyOrder: 1,
41
+ hidden: 0,
42
+ },
43
+ ]).map((column) => column.name),
44
+ ).toEqual(['id', 'name']);
45
+ });
46
+
47
+ it('keeps only non-hidden, non-primary columns editable', () => {
48
+ expect(
49
+ getEditableColumns([
50
+ {
51
+ cid: 0,
52
+ name: 'id',
53
+ type: 'INTEGER',
54
+ notNull: true,
55
+ defaultValue: null,
56
+ primaryKeyOrder: 1,
57
+ hidden: 0,
58
+ },
59
+ {
60
+ cid: 1,
61
+ name: 'title',
62
+ type: 'TEXT',
63
+ notNull: false,
64
+ defaultValue: null,
65
+ primaryKeyOrder: 0,
66
+ hidden: 0,
67
+ },
68
+ {
69
+ cid: 2,
70
+ name: 'generated_value',
71
+ type: 'TEXT',
72
+ notNull: false,
73
+ defaultValue: null,
74
+ primaryKeyOrder: 0,
75
+ hidden: 2,
76
+ },
77
+ ]).map((column) => column.name),
78
+ ).toEqual(['title']);
79
+ });
80
+
81
+ it('falls back to a usable rowid alias for tables without a primary key', () => {
82
+ const columns = [
83
+ {
84
+ cid: 0,
85
+ name: 'rowid',
86
+ type: 'INTEGER',
87
+ notNull: false,
88
+ defaultValue: null,
89
+ primaryKeyOrder: 0,
90
+ hidden: 0,
91
+ },
92
+ {
93
+ cid: 1,
94
+ name: 'title',
95
+ type: 'TEXT',
96
+ notNull: false,
97
+ defaultValue: null,
98
+ primaryKeyOrder: 0,
99
+ hidden: 0,
100
+ },
101
+ ];
102
+
103
+ expect(getAvailableRowIdIdentifier(columns)).toBe('_rowid_');
104
+ expect(
105
+ getRowMutationDescriptor(
106
+ buildEntity('CREATE TABLE projects(rowid INTEGER, title TEXT)'),
107
+ columns,
108
+ ),
109
+ ).toEqual({
110
+ mode: 'rowid',
111
+ rowIdIdentifier: '_rowid_',
112
+ });
113
+ });
114
+
115
+ it('limits editable kinds to text-compatible inputs for TEXT columns', () => {
116
+ expect(
117
+ getCompatibleValueKinds(
118
+ {
119
+ cid: 0,
120
+ name: 'title',
121
+ type: 'TEXT',
122
+ notNull: false,
123
+ defaultValue: null,
124
+ primaryKeyOrder: 0,
125
+ hidden: 0,
126
+ },
127
+ 'alpha',
128
+ ),
129
+ ).toEqual(['text']);
130
+ });
131
+
132
+ it('limits editable kinds to numeric-compatible inputs for numeric columns', () => {
133
+ expect(
134
+ getCompatibleValueKinds(
135
+ {
136
+ cid: 0,
137
+ name: 'progress',
138
+ type: 'INTEGER',
139
+ notNull: false,
140
+ defaultValue: null,
141
+ primaryKeyOrder: 0,
142
+ hidden: 0,
143
+ },
144
+ 1,
145
+ ),
146
+ ).toEqual(['number']);
147
+ });
148
+
149
+ it('limits editable kinds to boolean-compatible inputs for BOOLEAN columns', () => {
150
+ expect(
151
+ getCompatibleValueKinds(
152
+ {
153
+ cid: 0,
154
+ name: 'enabled',
155
+ type: 'BOOLEAN',
156
+ notNull: false,
157
+ defaultValue: null,
158
+ primaryKeyOrder: 0,
159
+ hidden: 0,
160
+ },
161
+ true,
162
+ ),
163
+ ).toEqual(['boolean']);
164
+ });
165
+
166
+ it('disables row mutations for WITHOUT ROWID tables that lack a primary key', () => {
167
+ expect(
168
+ getRowMutationDescriptor(
169
+ buildEntity(
170
+ 'CREATE TABLE projects(title TEXT, slug TEXT) WITHOUT ROWID',
171
+ ),
172
+ [
173
+ {
174
+ cid: 0,
175
+ name: 'title',
176
+ type: 'TEXT',
177
+ notNull: false,
178
+ defaultValue: null,
179
+ primaryKeyOrder: 0,
180
+ hidden: 0,
181
+ },
182
+ ],
183
+ ),
184
+ ).toBeNull();
185
+ });
186
+
187
+ it('builds an UPDATE statement that matches by primary key', () => {
188
+ expect(
189
+ buildRowUpdateMutation({
190
+ entity: buildEntity(
191
+ 'CREATE TABLE projects(id INTEGER PRIMARY KEY, title TEXT, archived INTEGER)',
192
+ ),
193
+ columns: [
194
+ {
195
+ cid: 0,
196
+ name: 'id',
197
+ type: 'INTEGER',
198
+ notNull: true,
199
+ defaultValue: null,
200
+ primaryKeyOrder: 1,
201
+ hidden: 0,
202
+ },
203
+ {
204
+ cid: 1,
205
+ name: 'title',
206
+ type: 'TEXT',
207
+ notNull: false,
208
+ defaultValue: null,
209
+ primaryKeyOrder: 0,
210
+ hidden: 0,
211
+ },
212
+ {
213
+ cid: 2,
214
+ name: 'archived',
215
+ type: 'INTEGER',
216
+ notNull: false,
217
+ defaultValue: null,
218
+ primaryKeyOrder: 0,
219
+ hidden: 0,
220
+ },
221
+ ],
222
+ row: {
223
+ id: 7,
224
+ title: 'Before',
225
+ archived: 0,
226
+ },
227
+ descriptor: {
228
+ mode: 'primary-key',
229
+ primaryKeyColumns: [
230
+ {
231
+ cid: 0,
232
+ name: 'id',
233
+ type: 'INTEGER',
234
+ notNull: true,
235
+ defaultValue: null,
236
+ primaryKeyOrder: 1,
237
+ hidden: 0,
238
+ },
239
+ ],
240
+ },
241
+ nextValues: {
242
+ title: 'After',
243
+ archived: 1,
244
+ },
245
+ }),
246
+ ).toEqual({
247
+ sql: 'UPDATE "main"."projects" SET "title" = ?, "archived" = ? WHERE "id" = ?',
248
+ params: ['After', 1, 7],
249
+ });
250
+ });
251
+
252
+ it('builds a DELETE statement that matches by composite primary key', () => {
253
+ expect(
254
+ buildRowDeleteMutation({
255
+ entity: buildEntity(
256
+ 'CREATE TABLE projects(tenant_id INTEGER, slug TEXT, PRIMARY KEY (tenant_id, slug))',
257
+ ),
258
+ row: {
259
+ tenant_id: 42,
260
+ slug: 'alpha',
261
+ },
262
+ descriptor: {
263
+ mode: 'primary-key',
264
+ primaryKeyColumns: [
265
+ {
266
+ cid: 0,
267
+ name: 'tenant_id',
268
+ type: 'INTEGER',
269
+ notNull: true,
270
+ defaultValue: null,
271
+ primaryKeyOrder: 1,
272
+ hidden: 0,
273
+ },
274
+ {
275
+ cid: 1,
276
+ name: 'slug',
277
+ type: 'TEXT',
278
+ notNull: true,
279
+ defaultValue: null,
280
+ primaryKeyOrder: 2,
281
+ hidden: 0,
282
+ },
283
+ ],
284
+ },
285
+ }),
286
+ ).toEqual({
287
+ sql: 'DELETE FROM "main"."projects" WHERE "tenant_id" = ? AND "slug" = ?',
288
+ params: [42, 'alpha'],
289
+ });
290
+ });
291
+
292
+ it('builds rowid-backed mutations for tables without primary keys', () => {
293
+ expect(
294
+ buildRowDeleteMutation({
295
+ entity: buildEntity('CREATE TABLE projects(title TEXT)'),
296
+ row: {
297
+ title: 'Draft',
298
+ [SQLITE_HIDDEN_ROWID_COLUMN_ID]: 13,
299
+ },
300
+ descriptor: {
301
+ mode: 'rowid',
302
+ rowIdIdentifier: 'rowid',
303
+ },
304
+ }),
305
+ ).toEqual({
306
+ sql: 'DELETE FROM "main"."projects" WHERE rowid = ?',
307
+ params: [13],
308
+ });
309
+ });
310
+ });
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ SQLITE_ROW_NUMBER_COLUMN_ID,
4
+ buildEntityTableId,
5
+ buildQueryTableId,
6
+ getDefaultTableColumnOrder,
7
+ normalizeTableColumnOrder,
8
+ reorderTableColumnOrder,
9
+ resolveTableColumnOrderUpdate,
10
+ } from '../sqlite-table-column-order';
11
+
12
+ describe('sqlite table column order helpers', () => {
13
+ it('returns the default order when no stored state exists', () => {
14
+ expect(getDefaultTableColumnOrder(['name', 'type', 'nullable'])).toEqual([
15
+ 'name',
16
+ 'type',
17
+ 'nullable',
18
+ ]);
19
+ });
20
+
21
+ it('keeps fixed leading columns in front while pruning stale ids', () => {
22
+ expect(
23
+ normalizeTableColumnOrder({
24
+ columnIds: [SQLITE_ROW_NUMBER_COLUMN_ID, 'name', 'type'],
25
+ fixedLeadingColumnIds: [SQLITE_ROW_NUMBER_COLUMN_ID],
26
+ storedColumnOrder: [
27
+ 'type',
28
+ SQLITE_ROW_NUMBER_COLUMN_ID,
29
+ 'stale',
30
+ 'name',
31
+ 'type',
32
+ ],
33
+ }),
34
+ ).toEqual([SQLITE_ROW_NUMBER_COLUMN_ID, 'type', 'name']);
35
+ });
36
+
37
+ it('appends newly visible columns after the stored order', () => {
38
+ expect(
39
+ normalizeTableColumnOrder({
40
+ columnIds: [SQLITE_ROW_NUMBER_COLUMN_ID, 'name', 'type', 'extra'],
41
+ fixedLeadingColumnIds: [SQLITE_ROW_NUMBER_COLUMN_ID],
42
+ storedColumnOrder: [SQLITE_ROW_NUMBER_COLUMN_ID, 'type', 'name'],
43
+ }),
44
+ ).toEqual([SQLITE_ROW_NUMBER_COLUMN_ID, 'type', 'name', 'extra']);
45
+ });
46
+
47
+ it('reorders only movable columns', () => {
48
+ expect(
49
+ reorderTableColumnOrder({
50
+ columnIds: [SQLITE_ROW_NUMBER_COLUMN_ID, 'name', 'type', 'extra'],
51
+ fixedLeadingColumnIds: [SQLITE_ROW_NUMBER_COLUMN_ID],
52
+ storedColumnOrder: [
53
+ SQLITE_ROW_NUMBER_COLUMN_ID,
54
+ 'name',
55
+ 'type',
56
+ 'extra',
57
+ ],
58
+ activeColumnId: 'extra',
59
+ overColumnId: 'name',
60
+ }),
61
+ ).toEqual([SQLITE_ROW_NUMBER_COLUMN_ID, 'extra', 'name', 'type']);
62
+ });
63
+
64
+ it('normalizes updater-based changes before storing them', () => {
65
+ expect(
66
+ resolveTableColumnOrderUpdate({
67
+ columnIds: [SQLITE_ROW_NUMBER_COLUMN_ID, 'name', 'type', 'extra'],
68
+ fixedLeadingColumnIds: [SQLITE_ROW_NUMBER_COLUMN_ID],
69
+ storedColumnOrder: [SQLITE_ROW_NUMBER_COLUMN_ID, 'name', 'type'],
70
+ nextColumnOrder: (current) => ['type', ...current],
71
+ }),
72
+ ).toEqual([SQLITE_ROW_NUMBER_COLUMN_ID, 'type', 'name', 'extra']);
73
+ });
74
+
75
+ it('builds stable table ids for entity and query surfaces', () => {
76
+ expect(buildEntityTableId('data', 'db-1', 'main', 'users')).toBe(
77
+ 'data:db-1:main:users',
78
+ );
79
+ expect(buildQueryTableId('db-1', ['id', 'name'])).toBe(
80
+ 'query:db-1:["id","name"]',
81
+ );
82
+ });
83
+ });
@@ -0,0 +1,12 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getValuePreview, stringifyValue } from '../value-utils';
3
+
4
+ describe('value utils', () => {
5
+ it('stringifyValue returns a string for undefined values', () => {
6
+ expect(stringifyValue(undefined)).toBe('undefined');
7
+ });
8
+
9
+ it('getValuePreview does not crash for undefined values', () => {
10
+ expect(getValuePreview(undefined)).toBe('undefined');
11
+ });
12
+ });
@@ -0,0 +1,65 @@
1
+ import { Modal, useOverlayState } from '@heroui/react';
2
+ import { Rows } from 'lucide-react';
3
+ import { SqliteModalCloseButton } from './sqlite-modal-controls';
4
+ import { renderStructuredValue } from './value-utils';
5
+
6
+ type CellDetailDrawerProps = {
7
+ value: unknown;
8
+ title: string;
9
+ isOpen: boolean;
10
+ onClose: () => void;
11
+ };
12
+
13
+ export const CellDetailDrawer = ({
14
+ value,
15
+ title,
16
+ isOpen,
17
+ onClose,
18
+ }: CellDetailDrawerProps) => {
19
+ const overlay = useOverlayState({
20
+ isOpen,
21
+ onOpenChange: (open: boolean) => {
22
+ if (!open) {
23
+ onClose();
24
+ }
25
+ },
26
+ });
27
+
28
+ return (
29
+ <Modal state={overlay}>
30
+ <Modal.Backdrop
31
+ variant="blur"
32
+ isDismissable
33
+ className="bg-[rgba(5,10,16,0.24)] backdrop-blur-[2px]"
34
+ >
35
+ <Modal.Container placement="center" size="lg" scroll="inside">
36
+ <Modal.Dialog
37
+ aria-label={title}
38
+ 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)]"
39
+ >
40
+ <div className="flex items-center justify-between gap-4 border-b border-white/8 px-5 py-5">
41
+ <div>
42
+ <div className="flex items-center gap-2">
43
+ <Rows aria-hidden="true" className="h-4 w-4 text-sky-300" />
44
+ <h2 className="text-lg font-semibold text-white">{title}</h2>
45
+ </div>
46
+ <p className="mt-1 text-sm text-slate-400">
47
+ Inspect the selected row in full detail.
48
+ </p>
49
+ </div>
50
+ <SqliteModalCloseButton onClose={onClose} />
51
+ </div>
52
+
53
+ <Modal.Body className="space-y-0 p-0">
54
+ <div className="px-5 py-5">
55
+ <div className="max-h-[70vh] overflow-auto rounded-[22px] border border-white/8 bg-white/[0.02] p-4">
56
+ {renderStructuredValue(value)}
57
+ </div>
58
+ </div>
59
+ </Modal.Body>
60
+ </Modal.Dialog>
61
+ </Modal.Container>
62
+ </Modal.Backdrop>
63
+ </Modal>
64
+ );
65
+ };