rozenite-sqlite 0.0.11 → 0.0.12
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 +6 -0
- package/README.md +1 -1
- package/dist/assets/panel-BKWy6irM.js +58 -0
- package/dist/panel.html +1 -1
- package/dist/react-native.cjs +1 -1
- package/dist/react-native.d.ts +1 -1
- package/dist/react-native.js +59 -22
- package/dist/rozenite.json +1 -1
- package/package.json +2 -2
- package/rozenite.config.ts +1 -1
- package/src/components/DataTable.tsx +34 -37
- package/src/components/Dropdown.tsx +1 -4
- package/src/components/Placeholder.tsx +81 -0
- package/src/components/RowDetailPanel.tsx +81 -30
- package/src/constants.ts +4 -0
- package/src/hooks/useBridgeSync.ts +87 -16
- package/src/hooks/useExplorerState.ts +63 -7
- package/src/hooks/useRozeniteSQLite.ts +60 -1
- package/src/panel.tsx +6 -2
- package/dist/assets/panel-3I5ccfMq.js +0 -58
|
@@ -14,16 +14,22 @@ function fetchData(client: NonNullable<BridgeClient>, dbName: string, tableName:
|
|
|
14
14
|
client.send(EVENTS.SQL_EXECUTE, { dbName, query: `SELECT * FROM "${tableName}"` });
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export interface MutationResult {
|
|
18
|
+
success: boolean;
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
export function useBridgeSync(
|
|
18
23
|
dispatch: React.Dispatch<Action>,
|
|
19
24
|
selectedDB: string | null,
|
|
20
25
|
selectedTable: string | null,
|
|
21
26
|
) {
|
|
22
27
|
const client = useRozeniteDevToolsClient({ pluginId: PLUGIN_ID });
|
|
23
|
-
const pendingRef = useRef<'tables' | 'data' | '
|
|
28
|
+
const pendingRef = useRef<'tables' | 'data' | 'query' | null>(null);
|
|
24
29
|
const selectedDBRef = useRef(selectedDB);
|
|
25
30
|
const selectedTableRef = useRef(selectedTable);
|
|
26
31
|
const customQueryResolverRef = useRef<((r: { rows: RowData[]; columns: string[]; error?: string }) => void) | null>(null);
|
|
32
|
+
const mutationResolverRef = useRef<((r: MutationResult) => void) | null>(null);
|
|
27
33
|
useEffect(() => { selectedDBRef.current = selectedDB; }, [selectedDB]);
|
|
28
34
|
useEffect(() => { selectedTableRef.current = selectedTable; }, [selectedTable]);
|
|
29
35
|
|
|
@@ -72,20 +78,20 @@ export function useBridgeSync(
|
|
|
72
78
|
} else if (mode === 'data') {
|
|
73
79
|
const rows: RowData[] = Array.isArray(payload) ? (payload as RowData[]) : [];
|
|
74
80
|
dispatch({ type: 'SET_DATA', rows, columns: rows.length > 0 ? Object.keys(rows[0]) : [] });
|
|
75
|
-
} else if (mode === 'clear') {
|
|
76
|
-
const db = selectedDBRef.current;
|
|
77
|
-
const table = selectedTableRef.current;
|
|
78
|
-
if (db && table) {
|
|
79
|
-
pendingRef.current = 'data';
|
|
80
|
-
dispatch({ type: 'LOAD_DATA_START' });
|
|
81
|
-
fetchData(client, db, table);
|
|
82
|
-
}
|
|
83
81
|
}
|
|
84
82
|
});
|
|
85
83
|
|
|
84
|
+
const sub3 = client.onMessage(EVENTS.MUTATION_RESULT, (payload: unknown) => {
|
|
85
|
+
const resolver = mutationResolverRef.current;
|
|
86
|
+
mutationResolverRef.current = null;
|
|
87
|
+
const result = payload as MutationResult;
|
|
88
|
+
resolver?.(result);
|
|
89
|
+
});
|
|
90
|
+
|
|
86
91
|
return () => {
|
|
87
92
|
sub1.remove();
|
|
88
93
|
sub2.remove();
|
|
94
|
+
sub3.remove();
|
|
89
95
|
};
|
|
90
96
|
}, [client, dispatch]);
|
|
91
97
|
|
|
@@ -120,12 +126,28 @@ export function useBridgeSync(
|
|
|
120
126
|
}
|
|
121
127
|
}, [client, selectedDB, selectedTable, dispatch]);
|
|
122
128
|
|
|
123
|
-
const clearTable = useCallback(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
+
const clearTable = useCallback(
|
|
130
|
+
(): Promise<MutationResult> =>
|
|
131
|
+
new Promise((resolve) => {
|
|
132
|
+
const db = selectedDBRef.current;
|
|
133
|
+
const table = selectedTableRef.current;
|
|
134
|
+
if (!client || !db || !table) {
|
|
135
|
+
resolve({ success: false, error: 'No database or table selected' });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
mutationResolverRef.current = (result) => {
|
|
139
|
+
resolve(result);
|
|
140
|
+
// Refresh data after successful clear
|
|
141
|
+
if (result.success && client) {
|
|
142
|
+
pendingRef.current = 'data';
|
|
143
|
+
dispatch({ type: 'LOAD_DATA_START' });
|
|
144
|
+
fetchData(client, db, table);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
client.send(EVENTS.CLEAR_TABLE, { dbName: db, table });
|
|
148
|
+
}),
|
|
149
|
+
[client, dispatch],
|
|
150
|
+
);
|
|
129
151
|
|
|
130
152
|
const runCustomQuery = useCallback(
|
|
131
153
|
(sql: string): Promise<{ rows: RowData[]; columns: string[]; error?: string }> =>
|
|
@@ -142,5 +164,54 @@ export function useBridgeSync(
|
|
|
142
164
|
[client],
|
|
143
165
|
);
|
|
144
166
|
|
|
145
|
-
|
|
167
|
+
const saveRowToDB = useCallback(
|
|
168
|
+
(row: RowData, columns: string[]): Promise<MutationResult> =>
|
|
169
|
+
new Promise((resolve) => {
|
|
170
|
+
const db = selectedDBRef.current;
|
|
171
|
+
const table = selectedTableRef.current;
|
|
172
|
+
if (!client || !db || !table) {
|
|
173
|
+
resolve({ success: false, error: 'No database or table selected' });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// Detect primary key: use 'id' if it exists, otherwise use first column
|
|
177
|
+
const primaryKey = columns.includes('id') ? 'id' : columns[0];
|
|
178
|
+
if (!primaryKey) {
|
|
179
|
+
resolve({ success: false, error: 'Cannot determine primary key' });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
mutationResolverRef.current = resolve;
|
|
183
|
+
client.send(EVENTS.SAVE_ROW, { dbName: db, table, row, primaryKey });
|
|
184
|
+
}),
|
|
185
|
+
[client],
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const deleteRowFromDB = useCallback(
|
|
189
|
+
(row: RowData, columns: string[]): Promise<MutationResult> =>
|
|
190
|
+
new Promise((resolve) => {
|
|
191
|
+
const db = selectedDBRef.current;
|
|
192
|
+
const table = selectedTableRef.current;
|
|
193
|
+
if (!client || !db || !table) {
|
|
194
|
+
resolve({ success: false, error: 'No database or table selected' });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// Detect primary key: use 'id' if it exists, otherwise use first column
|
|
198
|
+
const primaryKey = columns.includes('id') ? 'id' : columns[0];
|
|
199
|
+
if (!primaryKey) {
|
|
200
|
+
resolve({ success: false, error: 'Cannot determine primary key' });
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const primaryKeyValue = row[primaryKey];
|
|
204
|
+
mutationResolverRef.current = resolve;
|
|
205
|
+
client.send(EVENTS.DELETE_ROW, { dbName: db, table, primaryKey, primaryKeyValue });
|
|
206
|
+
}),
|
|
207
|
+
[client],
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const reconnect = useCallback(() => {
|
|
211
|
+
if (!client) return;
|
|
212
|
+
dispatch({ type: 'RESET' });
|
|
213
|
+
client.send(EVENTS.GET_DB_LIST, true);
|
|
214
|
+
}, [client, dispatch]);
|
|
215
|
+
|
|
216
|
+
return { refresh, clearTable, runCustomQuery, saveRowToDB, deleteRowFromDB, reconnect };
|
|
146
217
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useReducer, useCallback } from 'react';
|
|
1
|
+
import { useReducer, useCallback, useState } from 'react';
|
|
2
2
|
import type { RowData } from '../theme';
|
|
3
|
-
import { useBridgeSync } from './useBridgeSync';
|
|
3
|
+
import { useBridgeSync, type MutationResult } from './useBridgeSync';
|
|
4
4
|
|
|
5
5
|
export type ExplorerStatus = 'connecting' | 'idle' | 'loadingTables' | 'loadingData' | 'error';
|
|
6
6
|
|
|
@@ -144,17 +144,73 @@ function reducer(state: ExplorerState, action: Action): ExplorerState {
|
|
|
144
144
|
|
|
145
145
|
export function useExplorerState() {
|
|
146
146
|
const [state, dispatch] = useReducer(reducer, initial);
|
|
147
|
-
const { selectedDB, selectedTable } = state;
|
|
147
|
+
const { selectedDB, selectedTable, columns, selectedRowIndex, rows } = state;
|
|
148
|
+
const [mutating, setMutating] = useState(false);
|
|
149
|
+
const [mutationError, setMutationError] = useState<string | null>(null);
|
|
148
150
|
|
|
149
|
-
const { refresh, clearTable, runCustomQuery } = useBridgeSync(dispatch, selectedDB, selectedTable);
|
|
151
|
+
const { refresh, clearTable, runCustomQuery, saveRowToDB, deleteRowFromDB, reconnect } = useBridgeSync(dispatch, selectedDB, selectedTable);
|
|
150
152
|
|
|
151
153
|
const selectDB = useCallback((db: string) => dispatch({ type: 'SELECT_DB', db }), []);
|
|
152
154
|
const selectTable = useCallback((table: string) => dispatch({ type: 'SELECT_TABLE', table }), []);
|
|
153
155
|
const selectRow = useCallback((index: number) => dispatch({ type: 'SELECT_ROW', index }), []);
|
|
154
156
|
const closeRow = useCallback(() => dispatch({ type: 'CLOSE_ROW' }), []);
|
|
155
|
-
const saveRow = useCallback((updated: RowData) => dispatch({ type: 'SAVE_ROW', updated }), []);
|
|
156
|
-
const deleteRow = useCallback(() => dispatch({ type: 'DELETE_ROW' }), []);
|
|
157
157
|
|
|
158
|
-
|
|
158
|
+
const saveRow = useCallback(
|
|
159
|
+
async (updated: RowData): Promise<MutationResult> => {
|
|
160
|
+
setMutating(true);
|
|
161
|
+
setMutationError(null);
|
|
162
|
+
const result = await saveRowToDB(updated, columns);
|
|
163
|
+
setMutating(false);
|
|
164
|
+
if (result.success) {
|
|
165
|
+
dispatch({ type: 'SAVE_ROW', updated });
|
|
166
|
+
} else {
|
|
167
|
+
setMutationError(result.error ?? 'Failed to save row');
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
},
|
|
171
|
+
[saveRowToDB, columns],
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const deleteRow = useCallback(
|
|
175
|
+
async (): Promise<MutationResult> => {
|
|
176
|
+
if (selectedRowIndex === null) {
|
|
177
|
+
return { success: false, error: 'No row selected' };
|
|
178
|
+
}
|
|
179
|
+
const row = rows[selectedRowIndex];
|
|
180
|
+
if (!row) {
|
|
181
|
+
return { success: false, error: 'Row not found' };
|
|
182
|
+
}
|
|
183
|
+
setMutating(true);
|
|
184
|
+
setMutationError(null);
|
|
185
|
+
const result = await deleteRowFromDB(row, columns);
|
|
186
|
+
setMutating(false);
|
|
187
|
+
if (result.success) {
|
|
188
|
+
dispatch({ type: 'DELETE_ROW' });
|
|
189
|
+
} else {
|
|
190
|
+
setMutationError(result.error ?? 'Failed to delete row');
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
},
|
|
194
|
+
[deleteRowFromDB, columns, selectedRowIndex, rows],
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const clearMutationError = useCallback(() => setMutationError(null), []);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
state,
|
|
201
|
+
selectDB,
|
|
202
|
+
selectTable,
|
|
203
|
+
selectRow,
|
|
204
|
+
closeRow,
|
|
205
|
+
saveRow,
|
|
206
|
+
deleteRow,
|
|
207
|
+
refresh,
|
|
208
|
+
clearTable,
|
|
209
|
+
runCustomQuery,
|
|
210
|
+
mutating,
|
|
211
|
+
mutationError,
|
|
212
|
+
clearMutationError,
|
|
213
|
+
reconnect,
|
|
214
|
+
};
|
|
159
215
|
}
|
|
160
216
|
|
|
@@ -14,8 +14,18 @@ export interface RozeniteSQLiteConfig {
|
|
|
14
14
|
sqlExecutor: SQLExecutor;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** Escape a value for safe SQL string interpolation */
|
|
18
|
+
function escapeValue(val: unknown): string {
|
|
19
|
+
if (val === null || val === undefined) return 'NULL';
|
|
20
|
+
if (typeof val === 'number') return String(val);
|
|
21
|
+
if (typeof val === 'boolean') return val ? '1' : '0';
|
|
22
|
+
// Escape single quotes by doubling them
|
|
23
|
+
const str = String(val).replace(/'/g, "''");
|
|
24
|
+
return `'${str}'`;
|
|
25
|
+
}
|
|
26
|
+
|
|
17
27
|
/**
|
|
18
|
-
* Connects your React Native app to the
|
|
28
|
+
* Connects your React Native app to the SQLighter devtools panel.
|
|
19
29
|
*
|
|
20
30
|
* Call this once somewhere near the root of your app (or in the component
|
|
21
31
|
* that holds the database instances). It handles all devtools communication —
|
|
@@ -59,6 +69,55 @@ export function useRozeniteSQLite(config: RozeniteSQLiteConfig): void {
|
|
|
59
69
|
},
|
|
60
70
|
);
|
|
61
71
|
}),
|
|
72
|
+
|
|
73
|
+
client.onMessage(EVENTS.SAVE_ROW, (payload: unknown) => {
|
|
74
|
+
const { dbName, table, row, primaryKey } = payload as {
|
|
75
|
+
dbName: string;
|
|
76
|
+
table: string;
|
|
77
|
+
row: Record<string, unknown>;
|
|
78
|
+
primaryKey: string;
|
|
79
|
+
};
|
|
80
|
+
const pkValue = row[primaryKey];
|
|
81
|
+
const columns = Object.keys(row).filter((k) => k !== primaryKey);
|
|
82
|
+
const setClause = columns.map((col) => `"${col}" = ${escapeValue(row[col])}`).join(', ');
|
|
83
|
+
const query = `UPDATE "${table}" SET ${setClause} WHERE "${primaryKey}" = ${escapeValue(pkValue)}`;
|
|
84
|
+
configRef.current.sqlExecutor(dbName, query).then(
|
|
85
|
+
() => client.send(EVENTS.MUTATION_RESULT, { success: true }),
|
|
86
|
+
(error: unknown) => client.send(EVENTS.MUTATION_RESULT, {
|
|
87
|
+
success: false,
|
|
88
|
+
error: error instanceof Error ? error.message : String(error),
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
}),
|
|
92
|
+
|
|
93
|
+
client.onMessage(EVENTS.DELETE_ROW, (payload: unknown) => {
|
|
94
|
+
const { dbName, table, primaryKey, primaryKeyValue } = payload as {
|
|
95
|
+
dbName: string;
|
|
96
|
+
table: string;
|
|
97
|
+
primaryKey: string;
|
|
98
|
+
primaryKeyValue: unknown;
|
|
99
|
+
};
|
|
100
|
+
const query = `DELETE FROM "${table}" WHERE "${primaryKey}" = ${escapeValue(primaryKeyValue)}`;
|
|
101
|
+
configRef.current.sqlExecutor(dbName, query).then(
|
|
102
|
+
() => client.send(EVENTS.MUTATION_RESULT, { success: true }),
|
|
103
|
+
(error: unknown) => client.send(EVENTS.MUTATION_RESULT, {
|
|
104
|
+
success: false,
|
|
105
|
+
error: error instanceof Error ? error.message : String(error),
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
}),
|
|
109
|
+
|
|
110
|
+
client.onMessage(EVENTS.CLEAR_TABLE, (payload: unknown) => {
|
|
111
|
+
const { dbName, table } = payload as { dbName: string; table: string };
|
|
112
|
+
const query = `DELETE FROM "${table}"`;
|
|
113
|
+
configRef.current.sqlExecutor(dbName, query).then(
|
|
114
|
+
() => client.send(EVENTS.MUTATION_RESULT, { success: true }),
|
|
115
|
+
(error: unknown) => client.send(EVENTS.MUTATION_RESULT, {
|
|
116
|
+
success: false,
|
|
117
|
+
error: error instanceof Error ? error.message : String(error),
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
}),
|
|
62
121
|
];
|
|
63
122
|
|
|
64
123
|
return () => {
|
package/src/panel.tsx
CHANGED
|
@@ -7,8 +7,8 @@ import { SqlPanel } from './components/SqlPanel';
|
|
|
7
7
|
import { DataTable } from './components/DataTable';
|
|
8
8
|
import { RowDetailPanel } from './components/RowDetailPanel';
|
|
9
9
|
|
|
10
|
-
export default function
|
|
11
|
-
const { state, selectDB, selectTable, selectRow, closeRow, saveRow, deleteRow, refresh, clearTable, runCustomQuery } = useExplorerState();
|
|
10
|
+
export default function SQLighterPanel() {
|
|
11
|
+
const { state, selectDB, selectTable, selectRow, closeRow, saveRow, deleteRow, refresh, clearTable, runCustomQuery, mutating, mutationError, clearMutationError, reconnect } = useExplorerState();
|
|
12
12
|
const { databases, selectedDB, tables, selectedTable, rows, columns, selectedRowIndex, status, error } = state;
|
|
13
13
|
|
|
14
14
|
const isLoadingTables = status === 'loadingTables';
|
|
@@ -48,6 +48,7 @@ export default function SQLiteExplorerPanel() {
|
|
|
48
48
|
status={status}
|
|
49
49
|
error={error}
|
|
50
50
|
onClearTable={selectedTable ? clearTable : undefined}
|
|
51
|
+
onReconnect={reconnect}
|
|
51
52
|
/>
|
|
52
53
|
{selectedRow !== null && (
|
|
53
54
|
<RowDetailPanel
|
|
@@ -56,6 +57,9 @@ export default function SQLiteExplorerPanel() {
|
|
|
56
57
|
onClose={closeRow}
|
|
57
58
|
onSave={saveRow}
|
|
58
59
|
onDelete={deleteRow}
|
|
60
|
+
mutating={mutating}
|
|
61
|
+
mutationError={mutationError}
|
|
62
|
+
onClearError={clearMutationError}
|
|
59
63
|
/>
|
|
60
64
|
)}
|
|
61
65
|
</View>
|