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.
@@ -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' | 'clear' | 'query' | null>(null);
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
- if (!client || !selectedDB || !selectedTable) return;
125
- pendingRef.current = 'clear';
126
- dispatch({ type: 'LOAD_DATA_START' });
127
- client.send(EVENTS.SQL_EXECUTE, { dbName: selectedDB, query: `DELETE FROM "${selectedTable}"` });
128
- }, [client, selectedDB, selectedTable, dispatch]);
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
- return { refresh, clearTable, runCustomQuery };
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
- return { state, selectDB, selectTable, selectRow, closeRow, saveRow, deleteRow, refresh, clearTable, runCustomQuery };
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 Rozenite SQLite devtools panel.
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 SQLiteExplorerPanel() {
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>