@rozenite/sqlite-plugin 2.2.0 → 2.3.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 (54) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/devtools/assets/{panel-B2O8mY15.js → panel-CLyprPq_.js} +41 -41
  3. package/dist/devtools/panel.html +1 -1
  4. package/dist/react-native/cjs/package.json +3 -0
  5. package/dist/react-native/cjs/react-native.js +37 -0
  6. package/dist/react-native/cjs/src/react-native/adapters/expo-sqlite.js +121 -0
  7. package/dist/react-native/cjs/src/react-native/adapters/generic.js +33 -0
  8. package/dist/react-native/cjs/src/react-native/adapters/index.js +7 -0
  9. package/dist/react-native/cjs/src/react-native/sqlite-view.js +11 -0
  10. package/dist/react-native/cjs/src/react-native/useRozeniteSqlitePlugin.js +178 -0
  11. package/dist/react-native/cjs/src/react-native/useSqliteAgentTools.js +115 -0
  12. package/dist/react-native/cjs/src/shared/bridge-values.js +126 -0
  13. package/dist/react-native/cjs/src/shared/protocol.js +4 -0
  14. package/dist/react-native/cjs/src/shared/sql.js +339 -0
  15. package/dist/react-native/cjs/src/shared/types.js +2 -0
  16. package/dist/react-native/package.json +3 -0
  17. package/dist/react-native/react-native.d.ts +15 -0
  18. package/dist/react-native/react-native.js +43 -0
  19. package/dist/react-native/src/react-native/adapters/expo-sqlite.d.ts +28 -0
  20. package/dist/react-native/src/react-native/adapters/expo-sqlite.js +117 -0
  21. package/dist/react-native/src/react-native/adapters/generic.d.ts +19 -0
  22. package/dist/react-native/src/react-native/adapters/generic.js +29 -0
  23. package/dist/react-native/src/react-native/adapters/index.d.ts +2 -0
  24. package/dist/react-native/src/react-native/adapters/index.js +2 -0
  25. package/dist/react-native/src/react-native/sqlite-view.d.ts +5 -0
  26. package/dist/react-native/src/react-native/sqlite-view.js +7 -0
  27. package/dist/react-native/src/react-native/useRozeniteSqlitePlugin.d.ts +6 -0
  28. package/dist/react-native/src/react-native/useRozeniteSqlitePlugin.js +174 -0
  29. package/dist/react-native/src/react-native/useSqliteAgentTools.d.ts +2 -0
  30. package/dist/react-native/src/react-native/useSqliteAgentTools.js +111 -0
  31. package/dist/react-native/src/shared/bridge-values.d.ts +3 -0
  32. package/dist/react-native/src/shared/bridge-values.js +120 -0
  33. package/dist/react-native/src/shared/protocol.d.ts +38 -0
  34. package/dist/react-native/src/shared/protocol.js +1 -0
  35. package/dist/react-native/src/shared/sql.d.ts +14 -0
  36. package/dist/react-native/src/shared/sql.js +328 -0
  37. package/dist/react-native/src/shared/types.d.ts +56 -0
  38. package/dist/react-native/src/shared/types.js +1 -0
  39. package/dist/rozenite.json +1 -1
  40. package/package.json +12 -11
  41. package/react-native.ts +8 -1
  42. package/rozenite.config.ts +1 -0
  43. package/src/__tests__/release-bundle.test.ts +32 -0
  44. package/dist/react-native/chunks/bridge-values.require.cjs +0 -2
  45. package/dist/react-native/chunks/bridge-values.require.js +0 -61
  46. package/dist/react-native/chunks/index.require.cjs +0 -1
  47. package/dist/react-native/chunks/index.require.js +0 -107
  48. package/dist/react-native/chunks/sql.require.cjs +0 -4
  49. package/dist/react-native/chunks/sql.require.js +0 -219
  50. package/dist/react-native/chunks/useRozeniteSqlitePlugin.require.cjs +0 -1
  51. package/dist/react-native/chunks/useRozeniteSqlitePlugin.require.js +0 -283
  52. package/dist/react-native/index.cjs +0 -1
  53. package/dist/react-native/index.d.ts +0 -208
  54. package/dist/react-native/index.js +0 -22
@@ -0,0 +1,174 @@
1
+ import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge';
2
+ import { useEffect, useMemo, useRef } from 'react';
3
+ import { formatSqliteError } from '../shared/bridge-values';
4
+ import { PLUGIN_ID } from '../shared/protocol';
5
+ import { normalizeSingleStatementSql, splitSqlStatements } from '../shared/sql';
6
+ import { createSqliteDatabaseViews } from './sqlite-view';
7
+ import { useSqliteAgentTools } from './useSqliteAgentTools';
8
+ const safeError = (error) => formatSqliteError(error);
9
+ const isExecuteStatementsError = (error) => error instanceof Error && ('completedResults' in error || 'failedStatementIndex' in error);
10
+ export const useRozeniteSqlitePlugin = ({ adapters }) => {
11
+ const views = useMemo(() => createSqliteDatabaseViews(adapters), [adapters]);
12
+ useSqliteAgentTools(views);
13
+ const client = useRozeniteDevToolsClient({
14
+ pluginId: PLUGIN_ID,
15
+ });
16
+ const subscriptionsRef = useRef([]);
17
+ const databaseQueuesRef = useRef(new Map());
18
+ useEffect(() => {
19
+ if (!client) {
20
+ return;
21
+ }
22
+ const enqueueDatabaseTask = async (databaseId, task) => {
23
+ const queue = databaseQueuesRef.current.get(databaseId) ?? Promise.resolve();
24
+ const next = queue.catch(() => undefined).then(task);
25
+ databaseQueuesRef.current.set(databaseId, next.then(() => undefined, () => undefined));
26
+ return next;
27
+ };
28
+ const resolveDatabase = (databaseId) => {
29
+ const database = views.find((view) => view.id === databaseId);
30
+ if (!database) {
31
+ throw new Error(`Unknown database "${databaseId}".`);
32
+ }
33
+ return database;
34
+ };
35
+ const executeStatements = async (databaseId, statements) => {
36
+ const database = resolveDatabase(databaseId);
37
+ const normalizedStatements = statements.map(({ sql, params }) => ({
38
+ sql: normalizeSingleStatementSql(sql),
39
+ params,
40
+ }));
41
+ const results = await database.executeStatements(normalizedStatements);
42
+ if (results.length !== normalizedStatements.length) {
43
+ throw new Error(`Expected ${normalizedStatements.length} statement result(s), received ${results.length}.`);
44
+ }
45
+ return {
46
+ inputs: normalizedStatements,
47
+ results,
48
+ };
49
+ };
50
+ const executeSingleQuery = async (databaseId, sql, params) => {
51
+ const execution = await executeStatements(databaseId, [
52
+ {
53
+ sql,
54
+ params,
55
+ },
56
+ ]);
57
+ const result = execution.results[0];
58
+ if (!result) {
59
+ throw new Error('The query completed without a result payload.');
60
+ }
61
+ return result;
62
+ };
63
+ client.send('sqlite:ready', { timestamp: Date.now() });
64
+ subscriptionsRef.current.push(client.onMessage('sqlite:list-databases', ({ requestId }) => {
65
+ client.send('sqlite:list-databases:result', {
66
+ requestId,
67
+ databases: views.map(({ id, name, adapterId, adapterName }) => ({
68
+ id,
69
+ name,
70
+ adapterId,
71
+ adapterName,
72
+ })),
73
+ });
74
+ }));
75
+ subscriptionsRef.current.push(client.onMessage('sqlite:query', async ({ requestId, databaseId, sql, params }) => {
76
+ try {
77
+ const result = await enqueueDatabaseTask(databaseId, () => executeSingleQuery(databaseId, sql, params));
78
+ client.send('sqlite:query:result', {
79
+ requestId,
80
+ databaseId,
81
+ result,
82
+ });
83
+ }
84
+ catch (error) {
85
+ client.send('sqlite:query:result', {
86
+ requestId,
87
+ databaseId,
88
+ error: safeError(error),
89
+ });
90
+ }
91
+ }));
92
+ subscriptionsRef.current.push(client.onMessage('sqlite:execute-script', async ({ requestId, databaseId, sql }) => {
93
+ try {
94
+ const result = await enqueueDatabaseTask(databaseId, async () => {
95
+ const statementSegments = splitSqlStatements(sql);
96
+ if (statementSegments.length === 0) {
97
+ throw new Error('Query cannot be empty.');
98
+ }
99
+ const statementInputs = statementSegments.map((statement) => ({
100
+ sql: statement.text,
101
+ }));
102
+ try {
103
+ const execution = await executeStatements(databaseId, statementInputs);
104
+ return {
105
+ statements: statementSegments.map((statement, index) => ({
106
+ index,
107
+ start: statement.start,
108
+ end: statement.end,
109
+ input: execution.inputs[index],
110
+ execution: {
111
+ input: execution.inputs[index],
112
+ result: execution.results[index],
113
+ },
114
+ })),
115
+ totalStatementCount: statementSegments.length,
116
+ failedStatementIndex: null,
117
+ };
118
+ }
119
+ catch (error) {
120
+ if (!isExecuteStatementsError(error)) {
121
+ throw error;
122
+ }
123
+ const failedStatementIndex = Math.max(0, Math.min(typeof error.failedStatementIndex === 'number'
124
+ ? error.failedStatementIndex
125
+ : (error.completedResults?.length ?? 0), statementSegments.length - 1));
126
+ const completedResults = (error.completedResults ?? []).slice(0, failedStatementIndex);
127
+ const completedStatements = completedResults.map((queryResult, index) => ({
128
+ index,
129
+ start: statementSegments[index].start,
130
+ end: statementSegments[index].end,
131
+ input: statementInputs[index],
132
+ execution: {
133
+ input: statementInputs[index],
134
+ result: queryResult,
135
+ },
136
+ }));
137
+ return {
138
+ statements: [
139
+ ...completedStatements,
140
+ {
141
+ index: failedStatementIndex,
142
+ start: statementSegments[failedStatementIndex].start,
143
+ end: statementSegments[failedStatementIndex].end,
144
+ input: statementInputs[failedStatementIndex],
145
+ error: safeError(error),
146
+ },
147
+ ],
148
+ totalStatementCount: statementSegments.length,
149
+ failedStatementIndex,
150
+ };
151
+ }
152
+ });
153
+ client.send('sqlite:execute-script:result', {
154
+ requestId,
155
+ databaseId,
156
+ result,
157
+ });
158
+ }
159
+ catch (error) {
160
+ client.send('sqlite:execute-script:result', {
161
+ requestId,
162
+ databaseId,
163
+ error: safeError(error),
164
+ });
165
+ }
166
+ }));
167
+ return () => {
168
+ subscriptionsRef.current.forEach((subscription) => subscription.remove());
169
+ subscriptionsRef.current = [];
170
+ databaseQueuesRef.current.clear();
171
+ };
172
+ }, [client, views]);
173
+ return client;
174
+ };
@@ -0,0 +1,2 @@
1
+ import type { SqliteDatabaseView } from './sqlite-view';
2
+ export declare const useSqliteAgentTools: (views: SqliteDatabaseView[]) => void;
@@ -0,0 +1,111 @@
1
+ import { useCallback } from 'react';
2
+ import { useRozenitePluginAgentTool } from '@rozenite/agent-bridge';
3
+ import { formatSqliteError } from '../shared/bridge-values';
4
+ import { normalizeSingleStatementSql, splitSqlStatements } from '../shared/sql';
5
+ const pluginId = '@rozenite/sqlite-plugin';
6
+ const listDatabasesTool = {
7
+ name: 'list-databases',
8
+ description: 'List all registered SQLite databases.',
9
+ inputSchema: { type: 'object', properties: {} },
10
+ };
11
+ const executeSqlTool = {
12
+ name: 'execute-sql',
13
+ description: 'Execute one or more SQL statements against a database. Supports SELECT, INSERT, UPDATE, DELETE, PRAGMA, DDL, and multi-statement scripts. Returns per-statement results including rows, columns, and metadata. Statements are executed in order and stop on first error.',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ databaseId: {
18
+ type: 'string',
19
+ description: 'Database ID from list-databases.',
20
+ },
21
+ sql: {
22
+ type: 'string',
23
+ description: 'SQL to execute. May contain multiple semicolon-separated statements.',
24
+ },
25
+ },
26
+ required: ['databaseId', 'sql'],
27
+ },
28
+ };
29
+ const isExecuteStatementsError = (error) => error instanceof Error && ('completedResults' in error || 'failedStatementIndex' in error);
30
+ export const useSqliteAgentTools = (views) => {
31
+ const resolveDatabase = useCallback((databaseId) => {
32
+ const database = views.find((view) => view.id === databaseId);
33
+ if (!database) {
34
+ const available = views.map((v) => v.id).join(', ');
35
+ throw new Error(`Unknown databaseId "${databaseId}". Available: ${available || '(none)'}`);
36
+ }
37
+ return database;
38
+ }, [views]);
39
+ useRozenitePluginAgentTool({
40
+ pluginId,
41
+ tool: listDatabasesTool,
42
+ handler: () => ({
43
+ databases: views.map(({ id, name, adapterId, adapterName }) => ({
44
+ id,
45
+ name,
46
+ adapterId,
47
+ adapterName,
48
+ })),
49
+ }),
50
+ });
51
+ useRozenitePluginAgentTool({
52
+ pluginId,
53
+ tool: executeSqlTool,
54
+ handler: async ({ databaseId, sql }) => {
55
+ const database = resolveDatabase(databaseId);
56
+ const statementSegments = splitSqlStatements(sql);
57
+ if (statementSegments.length === 0) {
58
+ throw new Error('SQL cannot be empty.');
59
+ }
60
+ const statementInputs = statementSegments.map((segment) => ({
61
+ sql: normalizeSingleStatementSql(segment.text),
62
+ }));
63
+ try {
64
+ const results = await database.executeStatements(statementInputs);
65
+ return {
66
+ databaseId,
67
+ totalStatementCount: statementSegments.length,
68
+ failedStatementIndex: null,
69
+ statements: statementSegments.map((_, index) => {
70
+ const result = results[index];
71
+ return {
72
+ index,
73
+ sql: statementInputs[index].sql,
74
+ rows: result.rows,
75
+ columns: result.columns,
76
+ metadata: result.metadata,
77
+ };
78
+ }),
79
+ };
80
+ }
81
+ catch (error) {
82
+ if (!isExecuteStatementsError(error)) {
83
+ throw new Error(formatSqliteError(error));
84
+ }
85
+ const failedStatementIndex = Math.max(0, Math.min(typeof error.failedStatementIndex === 'number'
86
+ ? error.failedStatementIndex
87
+ : (error.completedResults?.length ?? 0), statementSegments.length - 1));
88
+ const completedResults = (error.completedResults ?? []).slice(0, failedStatementIndex);
89
+ return {
90
+ databaseId,
91
+ totalStatementCount: statementSegments.length,
92
+ failedStatementIndex,
93
+ statements: [
94
+ ...completedResults.map((result, index) => ({
95
+ index,
96
+ sql: statementInputs[index].sql,
97
+ rows: result.rows,
98
+ columns: result.columns,
99
+ metadata: result.metadata,
100
+ })),
101
+ {
102
+ index: failedStatementIndex,
103
+ sql: statementInputs[failedStatementIndex].sql,
104
+ error: formatSqliteError(error),
105
+ },
106
+ ],
107
+ };
108
+ }
109
+ },
110
+ });
111
+ };
@@ -0,0 +1,3 @@
1
+ export declare const encodeSqliteBridgeValue: (value: unknown) => unknown;
2
+ export declare const decodeSqliteBridgeValue: (value: unknown) => unknown;
3
+ export declare const formatSqliteError: (error: unknown) => string;
@@ -0,0 +1,120 @@
1
+ const SQLITE_BRIDGE_BINARY_TYPE = '__rozeniteSqliteBinary';
2
+ const isRecord = (value) => !!value && typeof value === 'object';
3
+ const isEncodedBinaryValue = (value) => isRecord(value) &&
4
+ value[SQLITE_BRIDGE_BINARY_TYPE] === true &&
5
+ Array.isArray(value.data) &&
6
+ value.data.every((item) => typeof item === 'number');
7
+ export const encodeSqliteBridgeValue = (value) => {
8
+ if (value == null ||
9
+ typeof value === 'string' ||
10
+ typeof value === 'number' ||
11
+ typeof value === 'boolean') {
12
+ return value;
13
+ }
14
+ if (value instanceof Uint8Array) {
15
+ return {
16
+ [SQLITE_BRIDGE_BINARY_TYPE]: true,
17
+ data: Array.from(value),
18
+ };
19
+ }
20
+ if (value instanceof ArrayBuffer) {
21
+ return {
22
+ [SQLITE_BRIDGE_BINARY_TYPE]: true,
23
+ data: Array.from(new Uint8Array(value)),
24
+ };
25
+ }
26
+ if (Array.isArray(value)) {
27
+ return value.map(encodeSqliteBridgeValue);
28
+ }
29
+ if (isRecord(value)) {
30
+ return Object.fromEntries(Object.entries(value).map(([key, nestedValue]) => [
31
+ key,
32
+ encodeSqliteBridgeValue(nestedValue),
33
+ ]));
34
+ }
35
+ return String(value);
36
+ };
37
+ export const decodeSqliteBridgeValue = (value) => {
38
+ if (value == null ||
39
+ typeof value === 'string' ||
40
+ typeof value === 'number' ||
41
+ typeof value === 'boolean') {
42
+ return value;
43
+ }
44
+ if (isEncodedBinaryValue(value)) {
45
+ return new Uint8Array(value.data);
46
+ }
47
+ if (Array.isArray(value)) {
48
+ return value.map(decodeSqliteBridgeValue);
49
+ }
50
+ if (isRecord(value)) {
51
+ return Object.fromEntries(Object.entries(value).map(([key, nestedValue]) => [
52
+ key,
53
+ decodeSqliteBridgeValue(nestedValue),
54
+ ]));
55
+ }
56
+ return value;
57
+ };
58
+ const getStringField = (value, key) => {
59
+ if (!isRecord(value)) {
60
+ return null;
61
+ }
62
+ const field = value[key];
63
+ if (typeof field !== 'string') {
64
+ return null;
65
+ }
66
+ const trimmed = field.trim();
67
+ return trimmed ? trimmed : null;
68
+ };
69
+ const stringifyFallback = (value) => {
70
+ try {
71
+ return JSON.stringify(value);
72
+ }
73
+ catch {
74
+ return String(value);
75
+ }
76
+ };
77
+ const describeError = (error) => {
78
+ if (error instanceof Error) {
79
+ const code = getStringField(error, 'code');
80
+ const reason = getStringField(error, 'reason');
81
+ const message = error.message.trim();
82
+ const detailParts = [message || null, reason].filter((part, index, parts) => !!part && parts.indexOf(part) === index);
83
+ const detail = detailParts.join(' | ') || error.name;
84
+ return code ? `[${code}] ${detail}` : detail;
85
+ }
86
+ if (isRecord(error)) {
87
+ const code = getStringField(error, 'code');
88
+ const message = getStringField(error, 'message');
89
+ const reason = getStringField(error, 'reason');
90
+ const detail = message ?? reason ?? stringifyFallback(error);
91
+ return code ? `[${code}] ${detail}` : detail;
92
+ }
93
+ if (typeof error === 'string') {
94
+ return error.trim() || null;
95
+ }
96
+ if (error == null) {
97
+ return null;
98
+ }
99
+ return stringifyFallback(error);
100
+ };
101
+ const getCause = (error) => {
102
+ if (!isRecord(error)) {
103
+ return undefined;
104
+ }
105
+ return error.cause;
106
+ };
107
+ export const formatSqliteError = (error) => {
108
+ const visited = new Set();
109
+ const parts = [];
110
+ let current = error;
111
+ while (current !== undefined && current !== null && !visited.has(current)) {
112
+ visited.add(current);
113
+ const description = describeError(current);
114
+ if (description && !parts.includes(description)) {
115
+ parts.push(description);
116
+ }
117
+ current = getCause(current);
118
+ }
119
+ return parts.join('\nCaused by: ') || 'Unknown SQLite error.';
120
+ };
@@ -0,0 +1,38 @@
1
+ import type { SqliteDatabaseInfo, SqliteQueryParams, SqliteQueryResult, SqliteScriptResult } from './types';
2
+ export declare const PLUGIN_ID = "@rozenite/sqlite-plugin";
3
+ export type SqliteEventMap = {
4
+ 'sqlite:ready': {
5
+ timestamp: number;
6
+ };
7
+ 'sqlite:list-databases': {
8
+ requestId: string;
9
+ };
10
+ 'sqlite:list-databases:result': {
11
+ requestId: string;
12
+ databases: SqliteDatabaseInfo[];
13
+ error?: string;
14
+ };
15
+ 'sqlite:query': {
16
+ requestId: string;
17
+ databaseId: string;
18
+ sql: string;
19
+ params?: SqliteQueryParams;
20
+ };
21
+ 'sqlite:query:result': {
22
+ requestId: string;
23
+ databaseId: string;
24
+ result?: SqliteQueryResult;
25
+ error?: string;
26
+ };
27
+ 'sqlite:execute-script': {
28
+ requestId: string;
29
+ databaseId: string;
30
+ sql: string;
31
+ };
32
+ 'sqlite:execute-script:result': {
33
+ requestId: string;
34
+ databaseId: string;
35
+ result?: SqliteScriptResult;
36
+ error?: string;
37
+ };
38
+ };
@@ -0,0 +1 @@
1
+ export const PLUGIN_ID = '@rozenite/sqlite-plugin';
@@ -0,0 +1,14 @@
1
+ import type { SqliteStatementType } from './types';
2
+ export declare const countSqlStatements: (sql: string) => number;
3
+ export type SqlStatementSegment = {
4
+ text: string;
5
+ start: number;
6
+ end: number;
7
+ };
8
+ export declare const splitSqlStatements: (sql: string) => SqlStatementSegment[];
9
+ export declare const getStatementAtCursor: (sql: string, cursor: number) => SqlStatementSegment | null;
10
+ export declare const normalizeSingleStatementSql: (sql: string) => string;
11
+ export declare const classifySqlStatement: (sql: string) => SqliteStatementType;
12
+ export declare const statementReturnsRows: (statementType: SqliteStatementType) => statementType is "select" | "pragma" | "explain" | "with";
13
+ export declare const quoteSqlIdentifier: (identifier: string) => string;
14
+ export declare const escapeSqlString: (value: string) => string;