@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.
- package/CHANGELOG.md +8 -0
- package/LICENSE +20 -0
- package/README.md +102 -0
- package/dist/devtools/assets/panel-B3paLkwG.js +82 -0
- package/dist/devtools/assets/panel-CIU0JBOs.css +1 -0
- package/dist/devtools/panel.html +31 -0
- package/dist/react-native/chunks/bridge-values.cjs +5 -0
- package/dist/react-native/chunks/bridge-values.js +258 -0
- package/dist/react-native/chunks/index.require.cjs +1 -0
- package/dist/react-native/chunks/index.require.js +118 -0
- package/dist/react-native/chunks/useRozeniteSqlitePlugin.require.cjs +1 -0
- package/dist/react-native/chunks/useRozeniteSqlitePlugin.require.js +189 -0
- package/dist/react-native/index.cjs +1 -0
- package/dist/react-native/index.d.ts +178 -0
- package/dist/react-native/index.js +16 -0
- package/dist/rozenite.json +1 -0
- package/package.json +83 -0
- package/postcss.config.js +6 -0
- package/react-native.ts +55 -0
- package/rozenite.config.ts +8 -0
- package/src/react-native/adapters/__tests__/expo-sqlite.test.ts +94 -0
- package/src/react-native/adapters/expo-sqlite.ts +230 -0
- package/src/react-native/adapters/generic.ts +88 -0
- package/src/react-native/adapters/index.ts +9 -0
- package/src/react-native/sqlite-view.ts +24 -0
- package/src/react-native/useRozeniteSqlitePlugin.ts +262 -0
- package/src/shared/__tests__/bridge-values.test.ts +34 -0
- package/src/shared/__tests__/sql.test.ts +55 -0
- package/src/shared/bridge-values.ts +170 -0
- package/src/shared/protocol.ts +41 -0
- package/src/shared/sql.ts +420 -0
- package/src/shared/types.ts +81 -0
- package/src/ui/__tests__/sql-editor-utils.test.ts +135 -0
- package/src/ui/__tests__/sqlite-row-edit-value.test.ts +22 -0
- package/src/ui/__tests__/sqlite-row-mutations.test.ts +310 -0
- package/src/ui/__tests__/sqlite-table-column-order.test.ts +83 -0
- package/src/ui/__tests__/value-utils.test.tsx +12 -0
- package/src/ui/cell-detail-drawer.tsx +65 -0
- package/src/ui/globals.css +1415 -0
- package/src/ui/panel.tsx +2815 -0
- package/src/ui/query-result-table.tsx +199 -0
- package/src/ui/sql-editor-utils.ts +352 -0
- package/src/ui/sql-editor.tsx +509 -0
- package/src/ui/sqlite-data-table.tsx +296 -0
- package/src/ui/sqlite-introspection.ts +189 -0
- package/src/ui/sqlite-modal-controls.tsx +32 -0
- package/src/ui/sqlite-row-delete-modal.tsx +130 -0
- package/src/ui/sqlite-row-edit-modal.tsx +487 -0
- package/src/ui/sqlite-row-edit-value.ts +53 -0
- package/src/ui/sqlite-row-mutations.ts +246 -0
- package/src/ui/sqlite-table-column-order.ts +154 -0
- package/src/ui/use-sqlite-requests.ts +205 -0
- package/src/ui/utils.ts +107 -0
- package/src/ui/value-utils.tsx +162 -0
- package/tsconfig.json +36 -0
- package/vite.config.ts +20 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { RozeniteDevToolsClient } from '../../../plugin-bridge/src/index.ts';
|
|
2
|
+
|
|
3
|
+
declare type CreateExpoSqliteAdapter = createExpoSqliteAdapter_2;
|
|
4
|
+
|
|
5
|
+
export declare let createExpoSqliteAdapter: CreateExpoSqliteAdapter;
|
|
6
|
+
|
|
7
|
+
declare const createExpoSqliteAdapter_2: (options: CreateExpoSqliteAdapterOptions) => SqliteAdapter;
|
|
8
|
+
|
|
9
|
+
export declare type CreateExpoSqliteAdapterOptions = SingleDatabaseOptions_2 | MultiDatabaseOptions_2;
|
|
10
|
+
|
|
11
|
+
declare type CreateSqliteAdapter = createSqliteAdapter_2;
|
|
12
|
+
|
|
13
|
+
export declare let createSqliteAdapter: CreateSqliteAdapter;
|
|
14
|
+
|
|
15
|
+
declare const createSqliteAdapter_2: (options: CreateSqliteAdapterOptions) => SqliteAdapter;
|
|
16
|
+
|
|
17
|
+
export declare type CreateSqliteAdapterOptions = SingleDatabaseOptions | MultiDatabaseOptions;
|
|
18
|
+
|
|
19
|
+
export declare type ExpoSqliteLike = {
|
|
20
|
+
getAllAsync: (...args: any[]) => Promise<Record<string, unknown>[]>;
|
|
21
|
+
runAsync: (...args: any[]) => Promise<{
|
|
22
|
+
changes: number;
|
|
23
|
+
lastInsertRowId: number;
|
|
24
|
+
}>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
declare type MultiDatabaseOptions = {
|
|
28
|
+
databases: Record<string, SqliteExecuteStatementsRunner | SqliteDatabaseConfig>;
|
|
29
|
+
adapterId?: string;
|
|
30
|
+
adapterName?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
declare type MultiDatabaseOptions_2 = {
|
|
34
|
+
databases: Record<string, ExpoSqliteLike | {
|
|
35
|
+
database: ExpoSqliteLike;
|
|
36
|
+
name?: string;
|
|
37
|
+
}>;
|
|
38
|
+
adapterId?: string;
|
|
39
|
+
adapterName?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
declare type RozeniteSqlitePluginOptions = {
|
|
43
|
+
adapters: SqliteAdapter[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
declare type SingleDatabaseOptions = {
|
|
47
|
+
database: SqliteExecuteStatementsRunner | SqliteDatabaseConfig;
|
|
48
|
+
adapterId?: string;
|
|
49
|
+
adapterName?: string;
|
|
50
|
+
databaseName?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
declare type SingleDatabaseOptions_2 = {
|
|
54
|
+
database: ExpoSqliteLike | {
|
|
55
|
+
database: ExpoSqliteLike;
|
|
56
|
+
name?: string;
|
|
57
|
+
};
|
|
58
|
+
adapterId?: string;
|
|
59
|
+
adapterName?: string;
|
|
60
|
+
databaseName?: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export declare type SqliteAdapter = {
|
|
64
|
+
id: string;
|
|
65
|
+
name: string;
|
|
66
|
+
databases: SqliteDatabaseNode[];
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
declare type SqliteDatabaseConfig = {
|
|
70
|
+
name?: string;
|
|
71
|
+
executeStatements: SqliteExecuteStatementsRunner;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export declare type SqliteDatabaseInfo = {
|
|
75
|
+
id: string;
|
|
76
|
+
name: string;
|
|
77
|
+
adapterId: string;
|
|
78
|
+
adapterName: string;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export declare type SqliteDatabaseNode = {
|
|
82
|
+
id: string;
|
|
83
|
+
name: string;
|
|
84
|
+
executeStatements: SqliteExecuteStatementsRunner;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
declare type SqliteEventMap = {
|
|
88
|
+
'sqlite:ready': {
|
|
89
|
+
timestamp: number;
|
|
90
|
+
};
|
|
91
|
+
'sqlite:list-databases': {
|
|
92
|
+
requestId: string;
|
|
93
|
+
};
|
|
94
|
+
'sqlite:list-databases:result': {
|
|
95
|
+
requestId: string;
|
|
96
|
+
databases: SqliteDatabaseInfo[];
|
|
97
|
+
error?: string;
|
|
98
|
+
};
|
|
99
|
+
'sqlite:query': {
|
|
100
|
+
requestId: string;
|
|
101
|
+
databaseId: string;
|
|
102
|
+
sql: string;
|
|
103
|
+
params?: SqliteQueryParams;
|
|
104
|
+
};
|
|
105
|
+
'sqlite:query:result': {
|
|
106
|
+
requestId: string;
|
|
107
|
+
databaseId: string;
|
|
108
|
+
result?: SqliteQueryResult;
|
|
109
|
+
error?: string;
|
|
110
|
+
};
|
|
111
|
+
'sqlite:execute-script': {
|
|
112
|
+
requestId: string;
|
|
113
|
+
databaseId: string;
|
|
114
|
+
sql: string;
|
|
115
|
+
};
|
|
116
|
+
'sqlite:execute-script:result': {
|
|
117
|
+
requestId: string;
|
|
118
|
+
databaseId: string;
|
|
119
|
+
result?: SqliteScriptResult;
|
|
120
|
+
error?: string;
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export declare type SqliteExecuteStatementsError = Error & {
|
|
125
|
+
completedResults?: SqliteQueryResult[];
|
|
126
|
+
failedStatementIndex?: number;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export declare type SqliteExecuteStatementsRunner = (statements: SqliteStatementInput[]) => Promise<SqliteQueryResult[]>;
|
|
130
|
+
|
|
131
|
+
export declare type SqliteQueryMetadata = {
|
|
132
|
+
statementType: SqliteStatementType;
|
|
133
|
+
rowCount: number;
|
|
134
|
+
changes: number | null;
|
|
135
|
+
lastInsertRowId: number | null;
|
|
136
|
+
durationMs: number;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export declare type SqliteQueryParams = unknown[] | Record<string, unknown>;
|
|
140
|
+
|
|
141
|
+
export declare type SqliteQueryResult = {
|
|
142
|
+
rows: Record<string, unknown>[];
|
|
143
|
+
columns: string[];
|
|
144
|
+
metadata: SqliteQueryMetadata;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export declare type SqliteScriptResult = {
|
|
148
|
+
statements: SqliteScriptStatementResult[];
|
|
149
|
+
totalStatementCount: number;
|
|
150
|
+
failedStatementIndex: number | null;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export declare type SqliteScriptStatementResult = {
|
|
154
|
+
index: number;
|
|
155
|
+
start: number;
|
|
156
|
+
end: number;
|
|
157
|
+
input: SqliteStatementInput;
|
|
158
|
+
execution?: SqliteStatementExecutionResult;
|
|
159
|
+
error?: string;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export declare type SqliteStatementExecutionResult = {
|
|
163
|
+
input: SqliteStatementInput;
|
|
164
|
+
result: SqliteQueryResult;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export declare type SqliteStatementInput = {
|
|
168
|
+
sql: string;
|
|
169
|
+
params?: SqliteQueryParams;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export declare type SqliteStatementType = 'select' | 'insert' | 'update' | 'delete' | 'pragma' | 'create' | 'alter' | 'drop' | 'explain' | 'with' | 'other';
|
|
173
|
+
|
|
174
|
+
export declare let useRozeniteSqlitePlugin: useRozeniteSqlitePlugin_2;
|
|
175
|
+
|
|
176
|
+
declare const useRozeniteSqlitePlugin_2: ({ adapters, }: RozeniteSqlitePluginOptions) => RozeniteDevToolsClient<SqliteEventMap> | null;
|
|
177
|
+
|
|
178
|
+
export { }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
let t, a, i;
|
|
2
|
+
const r = typeof window < "u" && window.navigator.product !== "ReactNative", d = process.env.NODE_ENV !== "production", n = typeof window > "u";
|
|
3
|
+
d && !r && !n ? (t = require("./chunks/index.require.js").createSqliteAdapter, a = require("./chunks/index.require.js").createExpoSqliteAdapter, i = require("./chunks/useRozeniteSqlitePlugin.require.js").useRozeniteSqlitePlugin) : (t = (e) => ({
|
|
4
|
+
id: e.adapterId ?? "sqlite",
|
|
5
|
+
name: e.adapterName ?? "SQLite",
|
|
6
|
+
databases: []
|
|
7
|
+
}), a = (e) => ({
|
|
8
|
+
id: e.adapterId ?? "expo-sqlite",
|
|
9
|
+
name: e.adapterName ?? "Expo SQLite",
|
|
10
|
+
databases: []
|
|
11
|
+
}), i = () => null);
|
|
12
|
+
export {
|
|
13
|
+
a as createExpoSqliteAdapter,
|
|
14
|
+
t as createSqliteAdapter,
|
|
15
|
+
i as useRozeniteSqlitePlugin
|
|
16
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"@rozenite/sqlite-plugin","version":"1.7.0-rc.0","description":"SQLite inspector for Rozenite.","panels":[{"name":"SQLite","source":"/devtools/panel.html"}]}
|
package/package.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rozenite/sqlite-plugin",
|
|
3
|
+
"version": "1.7.0-rc.0",
|
|
4
|
+
"description": "SQLite inspector for Rozenite.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/react-native/index.cjs",
|
|
7
|
+
"module": "./dist/react-native/index.js",
|
|
8
|
+
"types": "./dist/react-native/index.d.ts",
|
|
9
|
+
"homepage": "https://github.com/callstackincubator/rozenite#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/callstackincubator/rozenite/issues"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/callstackincubator/rozenite.git"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@rozenite/plugin-bridge": "1.6.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@dnd-kit/core": "^6.3.1",
|
|
22
|
+
"@dnd-kit/sortable": "^8.0.0",
|
|
23
|
+
"@codemirror/autocomplete": "^6.20.1",
|
|
24
|
+
"@codemirror/commands": "^6.10.3",
|
|
25
|
+
"@codemirror/lang-sql": "^6.10.0",
|
|
26
|
+
"@codemirror/language": "^6.12.3",
|
|
27
|
+
"@codemirror/search": "^6.6.0",
|
|
28
|
+
"@codemirror/state": "^6.6.0",
|
|
29
|
+
"@codemirror/view": "^6.40.0",
|
|
30
|
+
"@heroui/react": "^3.0.1",
|
|
31
|
+
"@heroui/styles": "^3.0.1",
|
|
32
|
+
"@lezer/highlight": "^1.2.3",
|
|
33
|
+
"@tailwindcss/postcss": "^4.2.2",
|
|
34
|
+
"@tanstack/react-table": "^8.21.3",
|
|
35
|
+
"@tanstack/react-virtual": "^3.0.0",
|
|
36
|
+
"@types/react": "~19.2.2",
|
|
37
|
+
"autoprefixer": "^10.4.21",
|
|
38
|
+
"expo-sqlite": "^55.0.11",
|
|
39
|
+
"lucide-react": "^0.263.1",
|
|
40
|
+
"postcss": "^8.5.6",
|
|
41
|
+
"react": "19.2.0",
|
|
42
|
+
"react-dom": "19.2.0",
|
|
43
|
+
"react-json-tree": "^0.20.0",
|
|
44
|
+
"react-native": "0.83.1",
|
|
45
|
+
"react-native-web": "^0.21.2",
|
|
46
|
+
"sql-formatter": "^15.7.2",
|
|
47
|
+
"tailwindcss": "^4.2.2",
|
|
48
|
+
"typescript": "~5.9.3",
|
|
49
|
+
"vite": "^7.3.1",
|
|
50
|
+
"vitest": "^3.2.4",
|
|
51
|
+
"@rozenite/vite-plugin": "1.6.0",
|
|
52
|
+
"rozenite": "1.6.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"expo-sqlite": "*",
|
|
56
|
+
"react": "*",
|
|
57
|
+
"react-native": "*"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"expo-sqlite": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"license": "MIT",
|
|
65
|
+
"exports": {
|
|
66
|
+
".": {
|
|
67
|
+
"types": "./dist/react-native/index.d.ts",
|
|
68
|
+
"import": "./dist/react-native/index.js",
|
|
69
|
+
"require": "./dist/react-native/index.cjs"
|
|
70
|
+
},
|
|
71
|
+
"./package.json": "./package.json"
|
|
72
|
+
},
|
|
73
|
+
"publishConfig": {
|
|
74
|
+
"access": "public"
|
|
75
|
+
},
|
|
76
|
+
"scripts": {
|
|
77
|
+
"build": "rozenite build",
|
|
78
|
+
"dev": "rozenite dev",
|
|
79
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
80
|
+
"lint": "eslint .",
|
|
81
|
+
"test": "vitest --run"
|
|
82
|
+
}
|
|
83
|
+
}
|
package/react-native.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
SqliteAdapter,
|
|
3
|
+
SqliteDatabaseInfo,
|
|
4
|
+
SqliteDatabaseNode,
|
|
5
|
+
SqliteExecuteStatementsError,
|
|
6
|
+
SqliteExecuteStatementsRunner,
|
|
7
|
+
SqliteQueryMetadata,
|
|
8
|
+
SqliteQueryParams,
|
|
9
|
+
SqliteQueryResult,
|
|
10
|
+
SqliteScriptResult,
|
|
11
|
+
SqliteScriptStatementResult,
|
|
12
|
+
SqliteStatementExecutionResult,
|
|
13
|
+
SqliteStatementInput,
|
|
14
|
+
SqliteStatementType,
|
|
15
|
+
} from './src/shared/types';
|
|
16
|
+
export type { CreateSqliteAdapterOptions } from './src/react-native/adapters/generic';
|
|
17
|
+
export type {
|
|
18
|
+
CreateExpoSqliteAdapterOptions,
|
|
19
|
+
ExpoSqliteLike,
|
|
20
|
+
} from './src/react-native/adapters/expo-sqlite';
|
|
21
|
+
|
|
22
|
+
type CreateSqliteAdapter =
|
|
23
|
+
typeof import('./src/react-native/adapters').createSqliteAdapter;
|
|
24
|
+
type CreateExpoSqliteAdapter =
|
|
25
|
+
typeof import('./src/react-native/adapters').createExpoSqliteAdapter;
|
|
26
|
+
|
|
27
|
+
export let createSqliteAdapter: CreateSqliteAdapter;
|
|
28
|
+
export let createExpoSqliteAdapter: CreateExpoSqliteAdapter;
|
|
29
|
+
export let useRozeniteSqlitePlugin: typeof import('./src/react-native/useRozeniteSqlitePlugin').useRozeniteSqlitePlugin;
|
|
30
|
+
|
|
31
|
+
const isWeb =
|
|
32
|
+
typeof window !== 'undefined' && window.navigator.product !== 'ReactNative';
|
|
33
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
34
|
+
const isServer = typeof window === 'undefined';
|
|
35
|
+
|
|
36
|
+
if (isDev && !isWeb && !isServer) {
|
|
37
|
+
createSqliteAdapter =
|
|
38
|
+
require('./src/react-native/adapters').createSqliteAdapter;
|
|
39
|
+
createExpoSqliteAdapter =
|
|
40
|
+
require('./src/react-native/adapters').createExpoSqliteAdapter;
|
|
41
|
+
useRozeniteSqlitePlugin =
|
|
42
|
+
require('./src/react-native/useRozeniteSqlitePlugin').useRozeniteSqlitePlugin;
|
|
43
|
+
} else {
|
|
44
|
+
createSqliteAdapter = (options) => ({
|
|
45
|
+
id: options.adapterId ?? 'sqlite',
|
|
46
|
+
name: options.adapterName ?? 'SQLite',
|
|
47
|
+
databases: [],
|
|
48
|
+
});
|
|
49
|
+
createExpoSqliteAdapter = (options) => ({
|
|
50
|
+
id: options.adapterId ?? 'expo-sqlite',
|
|
51
|
+
name: options.adapterName ?? 'Expo SQLite',
|
|
52
|
+
databases: [],
|
|
53
|
+
});
|
|
54
|
+
useRozeniteSqlitePlugin = () => null;
|
|
55
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createExpoSqliteAdapter } from '../expo-sqlite';
|
|
3
|
+
|
|
4
|
+
describe('createExpoSqliteAdapter', () => {
|
|
5
|
+
it('executes statement arrays sequentially and preserves result order', async () => {
|
|
6
|
+
const database = {
|
|
7
|
+
getAllAsync: vi.fn(async () => [{ id: 1, name: 'Ada' }]),
|
|
8
|
+
runAsync: vi.fn(async () => ({ changes: 1, lastInsertRowId: 7 })),
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const adapter = createExpoSqliteAdapter({
|
|
12
|
+
database,
|
|
13
|
+
databaseName: 'main.db',
|
|
14
|
+
});
|
|
15
|
+
const [mainDatabase] = adapter.databases;
|
|
16
|
+
|
|
17
|
+
const results = await mainDatabase.executeStatements([
|
|
18
|
+
{ sql: 'SELECT id, name FROM users;' },
|
|
19
|
+
{ sql: "INSERT INTO users(name) VALUES('Grace');" },
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
expect(database.getAllAsync).toHaveBeenCalledWith(
|
|
23
|
+
'SELECT id, name FROM users',
|
|
24
|
+
);
|
|
25
|
+
expect(database.runAsync).toHaveBeenCalledWith(
|
|
26
|
+
"INSERT INTO users(name) VALUES('Grace')",
|
|
27
|
+
);
|
|
28
|
+
expect(results).toHaveLength(2);
|
|
29
|
+
expect(results[0]?.metadata.statementType).toBe('select');
|
|
30
|
+
expect(results[1]?.metadata.statementType).toBe('insert');
|
|
31
|
+
expect(results[1]?.metadata.lastInsertRowId).toBe(7);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('allows explicit transaction statements to run as normal statements', async () => {
|
|
35
|
+
const database = {
|
|
36
|
+
getAllAsync: vi.fn(),
|
|
37
|
+
runAsync: vi.fn(async () => ({ changes: 0, lastInsertRowId: 0 })),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const adapter = createExpoSqliteAdapter({
|
|
41
|
+
database,
|
|
42
|
+
databaseName: 'main.db',
|
|
43
|
+
});
|
|
44
|
+
const [mainDatabase] = adapter.databases;
|
|
45
|
+
|
|
46
|
+
const results = await mainDatabase.executeStatements([
|
|
47
|
+
{ sql: 'BEGIN;' },
|
|
48
|
+
{ sql: 'ROLLBACK;' },
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
expect(database.runAsync).toHaveBeenNthCalledWith(1, 'BEGIN');
|
|
52
|
+
expect(database.runAsync).toHaveBeenNthCalledWith(2, 'ROLLBACK');
|
|
53
|
+
expect(results.map((result) => result.metadata.statementType)).toEqual([
|
|
54
|
+
'other',
|
|
55
|
+
'other',
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('throws batch execution metadata when a later statement fails', async () => {
|
|
60
|
+
const database = {
|
|
61
|
+
getAllAsync: vi.fn(async () => [{ id: 1 }]),
|
|
62
|
+
runAsync: vi.fn(async (sql: string) => {
|
|
63
|
+
if (sql === "INSERT INTO logs(message) VALUES('boom')") {
|
|
64
|
+
throw new Error('constraint failed');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { changes: 1, lastInsertRowId: 1 };
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const adapter = createExpoSqliteAdapter({
|
|
72
|
+
database,
|
|
73
|
+
databaseName: 'main.db',
|
|
74
|
+
});
|
|
75
|
+
const [mainDatabase] = adapter.databases;
|
|
76
|
+
|
|
77
|
+
await expect(
|
|
78
|
+
mainDatabase.executeStatements([
|
|
79
|
+
{ sql: 'SELECT id FROM users;' },
|
|
80
|
+
{ sql: "INSERT INTO logs(message) VALUES('boom');" },
|
|
81
|
+
]),
|
|
82
|
+
).rejects.toMatchObject({
|
|
83
|
+
message: 'constraint failed',
|
|
84
|
+
failedStatementIndex: 1,
|
|
85
|
+
completedResults: [
|
|
86
|
+
{
|
|
87
|
+
metadata: {
|
|
88
|
+
statementType: 'select',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import {
|
|
2
|
+
classifySqlStatement,
|
|
3
|
+
normalizeSingleStatementSql,
|
|
4
|
+
statementReturnsRows,
|
|
5
|
+
} from '../../shared/sql';
|
|
6
|
+
import {
|
|
7
|
+
decodeSqliteBridgeValue,
|
|
8
|
+
formatSqliteError,
|
|
9
|
+
} from '../../shared/bridge-values';
|
|
10
|
+
import type {
|
|
11
|
+
SqliteAdapter,
|
|
12
|
+
SqliteExecuteStatementsError,
|
|
13
|
+
SqliteExecuteStatementsRunner,
|
|
14
|
+
SqliteStatementInput,
|
|
15
|
+
SqliteQueryResult,
|
|
16
|
+
} from '../../shared/types';
|
|
17
|
+
import {
|
|
18
|
+
createSqliteAdapter,
|
|
19
|
+
type CreateSqliteAdapterOptions,
|
|
20
|
+
} from './generic';
|
|
21
|
+
|
|
22
|
+
export type ExpoSqliteLike = {
|
|
23
|
+
getAllAsync: (...args: any[]) => Promise<Record<string, unknown>[]>;
|
|
24
|
+
runAsync: (...args: any[]) => Promise<{
|
|
25
|
+
changes: number;
|
|
26
|
+
lastInsertRowId: number;
|
|
27
|
+
}>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type SingleDatabaseOptions = {
|
|
31
|
+
database: ExpoSqliteLike | { database: ExpoSqliteLike; name?: string };
|
|
32
|
+
adapterId?: string;
|
|
33
|
+
adapterName?: string;
|
|
34
|
+
databaseName?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type MultiDatabaseOptions = {
|
|
38
|
+
databases: Record<
|
|
39
|
+
string,
|
|
40
|
+
ExpoSqliteLike | { database: ExpoSqliteLike; name?: string }
|
|
41
|
+
>;
|
|
42
|
+
adapterId?: string;
|
|
43
|
+
adapterName?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type CreateExpoSqliteAdapterOptions =
|
|
47
|
+
| SingleDatabaseOptions
|
|
48
|
+
| MultiDatabaseOptions;
|
|
49
|
+
|
|
50
|
+
const now = () =>
|
|
51
|
+
typeof performance !== 'undefined' && typeof performance.now === 'function'
|
|
52
|
+
? performance.now()
|
|
53
|
+
: Date.now();
|
|
54
|
+
|
|
55
|
+
const safeError = (error: unknown) => formatSqliteError(error);
|
|
56
|
+
|
|
57
|
+
const createExecuteStatementsError = (
|
|
58
|
+
message: string,
|
|
59
|
+
options: {
|
|
60
|
+
completedResults?: SqliteQueryResult[];
|
|
61
|
+
failedStatementIndex?: number;
|
|
62
|
+
cause?: unknown;
|
|
63
|
+
} = {},
|
|
64
|
+
): SqliteExecuteStatementsError => Object.assign(new Error(message), options);
|
|
65
|
+
|
|
66
|
+
const toBridgeSafeValue = (value: unknown): unknown => {
|
|
67
|
+
if (
|
|
68
|
+
value == null ||
|
|
69
|
+
typeof value === 'string' ||
|
|
70
|
+
typeof value === 'number' ||
|
|
71
|
+
typeof value === 'boolean'
|
|
72
|
+
) {
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (value instanceof Uint8Array) {
|
|
77
|
+
return Array.from(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (value instanceof ArrayBuffer) {
|
|
81
|
+
return Array.from(new Uint8Array(value));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
return value.map(toBridgeSafeValue);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (typeof value === 'object') {
|
|
89
|
+
return Object.fromEntries(
|
|
90
|
+
Object.entries(value).map(([key, nestedValue]) => [
|
|
91
|
+
key,
|
|
92
|
+
toBridgeSafeValue(nestedValue),
|
|
93
|
+
]),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return String(value);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const normalizeRows = (rows: Record<string, unknown>[]) =>
|
|
101
|
+
rows.map((row) =>
|
|
102
|
+
Object.fromEntries(
|
|
103
|
+
Object.entries(row).map(([key, value]) => [
|
|
104
|
+
key,
|
|
105
|
+
toBridgeSafeValue(value),
|
|
106
|
+
]),
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const executeSingleStatement = async (
|
|
111
|
+
database: ExpoSqliteLike,
|
|
112
|
+
{ sql, params }: SqliteStatementInput,
|
|
113
|
+
): Promise<SqliteQueryResult> => {
|
|
114
|
+
const normalizedSql = normalizeSingleStatementSql(sql);
|
|
115
|
+
const statementType = classifySqlStatement(normalizedSql);
|
|
116
|
+
const startedAt = now();
|
|
117
|
+
const decodedParams =
|
|
118
|
+
params === undefined ? undefined : decodeSqliteBridgeValue(params);
|
|
119
|
+
|
|
120
|
+
if (statementReturnsRows(statementType)) {
|
|
121
|
+
const rows = normalizeRows(
|
|
122
|
+
decodedParams === undefined
|
|
123
|
+
? await database.getAllAsync(normalizedSql)
|
|
124
|
+
: await database.getAllAsync(normalizedSql, decodedParams),
|
|
125
|
+
);
|
|
126
|
+
const durationMs = now() - startedAt;
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
rows,
|
|
130
|
+
columns: Object.keys(rows[0] ?? {}),
|
|
131
|
+
metadata: {
|
|
132
|
+
statementType,
|
|
133
|
+
rowCount: rows.length,
|
|
134
|
+
changes: null,
|
|
135
|
+
lastInsertRowId: null,
|
|
136
|
+
durationMs,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const result =
|
|
142
|
+
decodedParams === undefined
|
|
143
|
+
? await database.runAsync(normalizedSql)
|
|
144
|
+
: await database.runAsync(normalizedSql, decodedParams);
|
|
145
|
+
const durationMs = now() - startedAt;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
rows: [],
|
|
149
|
+
columns: [],
|
|
150
|
+
metadata: {
|
|
151
|
+
statementType,
|
|
152
|
+
rowCount: 0,
|
|
153
|
+
changes: typeof result.changes === 'number' ? result.changes : null,
|
|
154
|
+
lastInsertRowId:
|
|
155
|
+
typeof result.lastInsertRowId === 'number'
|
|
156
|
+
? result.lastInsertRowId
|
|
157
|
+
: null,
|
|
158
|
+
durationMs,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const createExpoExecuteStatementsRunner = (
|
|
164
|
+
database: ExpoSqliteLike,
|
|
165
|
+
): SqliteExecuteStatementsRunner => {
|
|
166
|
+
return async (statements) => {
|
|
167
|
+
const results: SqliteQueryResult[] = [];
|
|
168
|
+
|
|
169
|
+
for (let index = 0; index < statements.length; index += 1) {
|
|
170
|
+
try {
|
|
171
|
+
results.push(await executeSingleStatement(database, statements[index]));
|
|
172
|
+
} catch (error) {
|
|
173
|
+
throw createExecuteStatementsError(safeError(error), {
|
|
174
|
+
completedResults: results,
|
|
175
|
+
failedStatementIndex: index,
|
|
176
|
+
cause: error,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return results;
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const resolveDatabaseConfig = (
|
|
186
|
+
config: ExpoSqliteLike | { database: ExpoSqliteLike; name?: string },
|
|
187
|
+
) => ('database' in config ? config : { database: config });
|
|
188
|
+
|
|
189
|
+
export const createExpoSqliteAdapter = (
|
|
190
|
+
options: CreateExpoSqliteAdapterOptions,
|
|
191
|
+
): SqliteAdapter => {
|
|
192
|
+
const genericOptions: CreateSqliteAdapterOptions =
|
|
193
|
+
'databases' in options
|
|
194
|
+
? {
|
|
195
|
+
adapterId: options.adapterId ?? 'expo-sqlite',
|
|
196
|
+
adapterName: options.adapterName ?? 'Expo SQLite',
|
|
197
|
+
databases: Object.fromEntries(
|
|
198
|
+
Object.entries(options.databases).map(([key, config]) => {
|
|
199
|
+
const resolved = resolveDatabaseConfig(config);
|
|
200
|
+
|
|
201
|
+
return [
|
|
202
|
+
key,
|
|
203
|
+
{
|
|
204
|
+
name: resolved.name ?? key,
|
|
205
|
+
executeStatements: createExpoExecuteStatementsRunner(
|
|
206
|
+
resolved.database,
|
|
207
|
+
),
|
|
208
|
+
},
|
|
209
|
+
];
|
|
210
|
+
}),
|
|
211
|
+
),
|
|
212
|
+
}
|
|
213
|
+
: {
|
|
214
|
+
adapterId: options.adapterId ?? 'expo-sqlite',
|
|
215
|
+
adapterName: options.adapterName ?? 'Expo SQLite',
|
|
216
|
+
databaseName: options.databaseName,
|
|
217
|
+
database: (() => {
|
|
218
|
+
const resolved = resolveDatabaseConfig(options.database);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
name: resolved.name ?? options.databaseName,
|
|
222
|
+
executeStatements: createExpoExecuteStatementsRunner(
|
|
223
|
+
resolved.database,
|
|
224
|
+
),
|
|
225
|
+
};
|
|
226
|
+
})(),
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
return createSqliteAdapter(genericOptions);
|
|
230
|
+
};
|