@capgo/capacitor-fast-sql 8.0.17 → 8.0.19
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/README.md +49 -4
- package/android/build.gradle +1 -1
- package/android/src/main/java/app/capgo/capacitor/fastsql/CapgoCapacitorFastSqlPlugin.java +11 -1
- package/android/src/main/java/app/capgo/capacitor/fastsql/EncryptedSQLDatabase.java +10 -0
- package/android/src/main/java/app/capgo/capacitor/fastsql/SQLHTTPServer.java +22 -13
- package/ios/Sources/CapgoCapacitorFastSqlPlugin/CapgoCapacitorFastSqlPlugin.swift +1 -1
- package/ios/Sources/CapgoCapacitorFastSqlPlugin/SQLHTTPServer.swift +41 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ This plugin provides direct native SQLite database access with a custom communic
|
|
|
55
55
|
|
|
56
56
|
## iOS Configuration
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
This plugin runs a local HTTP server on `localhost`. iOS App Transport Security (ATS) blocks cleartext HTTP by default, so you **must** allow local networking in your `Info.plist`:
|
|
59
59
|
|
|
60
60
|
```xml
|
|
61
61
|
<key>NSAppTransportSecurity</key>
|
|
@@ -65,9 +65,36 @@ Add to your `Info.plist` if you encounter any issues:
|
|
|
65
65
|
</dict>
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
This only permits cleartext to loopback addresses (`localhost` / `127.0.0.1`) — it does not weaken ATS for external connections.
|
|
69
|
+
|
|
68
70
|
## Android Configuration
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
This plugin runs a local HTTP server on `localhost` to bypass Capacitor's bridge for performance. Android 9+ blocks cleartext (non-HTTPS) traffic by default, so you **must** allow it for `localhost`.
|
|
73
|
+
|
|
74
|
+
**Option A — Scoped to localhost only (recommended):**
|
|
75
|
+
|
|
76
|
+
Create `android/app/src/main/res/xml/network_security_config.xml`:
|
|
77
|
+
|
|
78
|
+
```xml
|
|
79
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
80
|
+
<network-security-config>
|
|
81
|
+
<domain-config cleartextTrafficPermitted="true">
|
|
82
|
+
<domain includeSubdomains="false">localhost</domain>
|
|
83
|
+
<domain includeSubdomains="false">127.0.0.1</domain>
|
|
84
|
+
</domain-config>
|
|
85
|
+
</network-security-config>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Then reference it in your `AndroidManifest.xml`:
|
|
89
|
+
|
|
90
|
+
```xml
|
|
91
|
+
<application
|
|
92
|
+
android:networkSecurityConfig="@xml/network_security_config">
|
|
93
|
+
...
|
|
94
|
+
</application>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Option B — Allow all cleartext (simpler but less secure):**
|
|
71
98
|
|
|
72
99
|
```xml
|
|
73
100
|
<application
|
|
@@ -76,9 +103,27 @@ Add to your `AndroidManifest.xml` if needed:
|
|
|
76
103
|
</application>
|
|
77
104
|
```
|
|
78
105
|
|
|
79
|
-
## Encryption (
|
|
106
|
+
## Encryption (Android)
|
|
107
|
+
|
|
108
|
+
Encryption uses [SQLCipher](https://www.zetetic.net/sqlcipher/) and is opt-in. Add the SQLCipher dependency to your **app-level** `build.gradle`:
|
|
109
|
+
|
|
110
|
+
```gradle
|
|
111
|
+
dependencies {
|
|
112
|
+
implementation 'net.zetetic:sqlcipher-android:4.13.0'
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Then connect with encryption enabled:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
const db = await FastSQL.connect({
|
|
120
|
+
database: 'secure',
|
|
121
|
+
encrypted: true,
|
|
122
|
+
encryptionKey: 'my-secret-key',
|
|
123
|
+
});
|
|
124
|
+
```
|
|
80
125
|
|
|
81
|
-
|
|
126
|
+
If SQLCipher is not installed and `encrypted: true` is passed, the plugin returns a clear error message instead of crashing.
|
|
82
127
|
|
|
83
128
|
## Usage
|
|
84
129
|
|
package/android/build.gradle
CHANGED
|
@@ -54,7 +54,7 @@ dependencies {
|
|
|
54
54
|
implementation 'androidx.sqlite:sqlite:2.6.2'
|
|
55
55
|
implementation 'com.google.code.gson:gson:2.13.2'
|
|
56
56
|
implementation 'org.nanohttpd:nanohttpd:2.3.1'
|
|
57
|
-
|
|
57
|
+
compileOnly 'net.zetetic:sqlcipher-android:4.13.0'
|
|
58
58
|
testImplementation "junit:junit:$junitVersion"
|
|
59
59
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
60
60
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
@@ -19,7 +19,7 @@ import java.util.Map;
|
|
|
19
19
|
@CapacitorPlugin(name = "CapgoCapacitorFastSql")
|
|
20
20
|
public class CapgoCapacitorFastSqlPlugin extends Plugin {
|
|
21
21
|
|
|
22
|
-
private final String pluginVersion = "8.0.
|
|
22
|
+
private final String pluginVersion = "8.0.19";
|
|
23
23
|
|
|
24
24
|
private Map<String, DatabaseConnection> databases = new HashMap<>();
|
|
25
25
|
private SQLHTTPServer server;
|
|
@@ -57,6 +57,16 @@ public class CapgoCapacitorFastSqlPlugin extends Plugin {
|
|
|
57
57
|
// Open database
|
|
58
58
|
DatabaseConnection db;
|
|
59
59
|
if (encrypted) {
|
|
60
|
+
try {
|
|
61
|
+
Class.forName("net.zetetic.database.sqlcipher.SQLiteDatabase");
|
|
62
|
+
} catch (ClassNotFoundException e) {
|
|
63
|
+
call.reject(
|
|
64
|
+
"Encryption is not available. " +
|
|
65
|
+
"Add 'implementation \"net.zetetic:sqlcipher-android:4.13.0\"' " +
|
|
66
|
+
"to your app's build.gradle to enable encryption."
|
|
67
|
+
);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
60
70
|
db = new EncryptedSQLDatabase(dbFile.getAbsolutePath(), encryptionKey);
|
|
61
71
|
} else {
|
|
62
72
|
db = new SQLDatabase(dbFile.getAbsolutePath());
|
|
@@ -23,6 +23,16 @@ public class EncryptedSQLDatabase implements DatabaseConnection {
|
|
|
23
23
|
if (encryptionKey == null || encryptionKey.isEmpty()) {
|
|
24
24
|
throw new Exception("Encryption key is required when encrypted is true");
|
|
25
25
|
}
|
|
26
|
+
try {
|
|
27
|
+
System.loadLibrary("sqlcipher");
|
|
28
|
+
} catch (UnsatisfiedLinkError e) {
|
|
29
|
+
throw new Exception(
|
|
30
|
+
"SQLCipher native library not found. " +
|
|
31
|
+
"Add 'implementation \"net.zetetic:sqlcipher-android:4.13.0\"' " +
|
|
32
|
+
"to your app's build.gradle to enable encryption.",
|
|
33
|
+
e
|
|
34
|
+
);
|
|
35
|
+
}
|
|
26
36
|
byte[] keyBytes = encryptionKey.getBytes(StandardCharsets.UTF_8);
|
|
27
37
|
this.db = SQLiteDatabase.openOrCreateDatabase(path, keyBytes, null, null);
|
|
28
38
|
// Enable foreign keys
|
|
@@ -40,24 +40,33 @@ public class SQLHTTPServer extends NanoHTTPD {
|
|
|
40
40
|
return getListeningPort();
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
private Response addCorsHeaders(Response response) {
|
|
44
|
+
response.addHeader("Access-Control-Allow-Origin", "*");
|
|
45
|
+
response.addHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
46
|
+
response.addHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Database");
|
|
47
|
+
response.addHeader("Access-Control-Max-Age", "86400");
|
|
48
|
+
return response;
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
@Override
|
|
44
52
|
public Response serve(IHTTPSession session) {
|
|
45
|
-
|
|
53
|
+
if (Method.OPTIONS.equals(session.getMethod())) {
|
|
54
|
+
return addCorsHeaders(newFixedLengthResponse(Response.Status.OK, "text/plain", ""));
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
String authHeader = session.getHeaders().get("authorization");
|
|
47
58
|
if (authHeader == null || !authHeader.equals("Bearer " + token)) {
|
|
48
|
-
return newFixedLengthResponse(Response.Status.UNAUTHORIZED, "text/plain", "Unauthorized");
|
|
59
|
+
return addCorsHeaders(newFixedLengthResponse(Response.Status.UNAUTHORIZED, "text/plain", "Unauthorized"));
|
|
49
60
|
}
|
|
50
61
|
|
|
51
|
-
// Get database name
|
|
52
62
|
String database = session.getHeaders().get("x-database");
|
|
53
63
|
if (database == null) {
|
|
54
|
-
return newFixedLengthResponse(Response.Status.BAD_REQUEST, "text/plain", "Database header required");
|
|
64
|
+
return addCorsHeaders(newFixedLengthResponse(Response.Status.BAD_REQUEST, "text/plain", "Database header required"));
|
|
55
65
|
}
|
|
56
66
|
|
|
57
|
-
// Check if database exists
|
|
58
67
|
DatabaseConnection db = databases.get(database);
|
|
59
68
|
if (db == null) {
|
|
60
|
-
return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Database not found");
|
|
69
|
+
return addCorsHeaders(newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Database not found"));
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
String uri = session.getUri();
|
|
@@ -65,20 +74,20 @@ public class SQLHTTPServer extends NanoHTTPD {
|
|
|
65
74
|
|
|
66
75
|
try {
|
|
67
76
|
if (method == Method.POST && uri.equals("/execute")) {
|
|
68
|
-
return handleExecute(session, db);
|
|
77
|
+
return addCorsHeaders(handleExecute(session, db));
|
|
69
78
|
} else if (method == Method.POST && uri.equals("/batch")) {
|
|
70
|
-
return handleBatch(session, db);
|
|
79
|
+
return addCorsHeaders(handleBatch(session, db));
|
|
71
80
|
} else if (method == Method.POST && uri.equals("/transaction/begin")) {
|
|
72
|
-
return handleBeginTransaction(db);
|
|
81
|
+
return addCorsHeaders(handleBeginTransaction(db));
|
|
73
82
|
} else if (method == Method.POST && uri.equals("/transaction/commit")) {
|
|
74
|
-
return handleCommitTransaction(db);
|
|
83
|
+
return addCorsHeaders(handleCommitTransaction(db));
|
|
75
84
|
} else if (method == Method.POST && uri.equals("/transaction/rollback")) {
|
|
76
|
-
return handleRollbackTransaction(db);
|
|
85
|
+
return addCorsHeaders(handleRollbackTransaction(db));
|
|
77
86
|
} else {
|
|
78
|
-
return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Endpoint not found");
|
|
87
|
+
return addCorsHeaders(newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Endpoint not found"));
|
|
79
88
|
}
|
|
80
89
|
} catch (Exception e) {
|
|
81
|
-
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", "Error: " + e.getMessage());
|
|
90
|
+
return addCorsHeaders(newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", "Error: " + e.getMessage()));
|
|
82
91
|
}
|
|
83
92
|
}
|
|
84
93
|
|
|
@@ -10,7 +10,7 @@ import SQLite3
|
|
|
10
10
|
*/
|
|
11
11
|
@objc(CapgoCapacitorFastSqlPlugin)
|
|
12
12
|
public class CapgoCapacitorFastSqlPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
13
|
-
private let pluginVersion: String = "8.0.
|
|
13
|
+
private let pluginVersion: String = "8.0.19"
|
|
14
14
|
public let identifier = "CapgoCapacitorFastSqlPlugin"
|
|
15
15
|
public let jsName = "CapgoCapacitorFastSql"
|
|
16
16
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -45,17 +45,48 @@ class SQLHTTPServer {
|
|
|
45
45
|
isRunning = false
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// MARK: - CORS Headers
|
|
49
|
+
|
|
50
|
+
private let corsHeaders: HTTPHeaders = [
|
|
51
|
+
.accessControlAllowOrigin: "*",
|
|
52
|
+
.accessControlAllowMethods: "POST, OPTIONS",
|
|
53
|
+
.accessControlAllowHeaders: "Content-Type, Authorization, X-Database",
|
|
54
|
+
.accessControlMaxAge: "86400"
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
private func corsPreflightResponse() -> HTTPResponse {
|
|
58
|
+
return HTTPResponse(.ok, headers: corsHeaders)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private func addCorsHeaders(_ response: HTTPResponse) -> HTTPResponse {
|
|
62
|
+
for (key, value) in corsHeaders {
|
|
63
|
+
response.headers[key] = value
|
|
64
|
+
}
|
|
65
|
+
return response
|
|
66
|
+
}
|
|
67
|
+
|
|
48
68
|
// MARK: - Route Setup
|
|
49
69
|
|
|
50
70
|
private func setupRoutes() {
|
|
51
71
|
guard let server = server else { return }
|
|
52
72
|
|
|
73
|
+
// CORS preflight for all endpoints
|
|
74
|
+
let preflightPaths = ["/execute", "/batch", "/transaction/begin", "/transaction/commit", "/transaction/rollback"]
|
|
75
|
+
for path in preflightPaths {
|
|
76
|
+
server.route(.OPTIONS, path) { [weak self] _ in
|
|
77
|
+
guard let self = self else {
|
|
78
|
+
return HTTPResponse(.internalServerError)
|
|
79
|
+
}
|
|
80
|
+
return self.corsPreflightResponse()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
53
84
|
// Execute endpoint
|
|
54
85
|
server.route(.POST, "/execute") { [weak self] request in
|
|
55
86
|
guard let self = self else {
|
|
56
87
|
return HTTPResponse(.internalServerError)
|
|
57
88
|
}
|
|
58
|
-
return self.handleExecuteRequest(request: request)
|
|
89
|
+
return self.addCorsHeaders(self.handleExecuteRequest(request: request))
|
|
59
90
|
}
|
|
60
91
|
|
|
61
92
|
// Batch endpoint
|
|
@@ -63,7 +94,7 @@ class SQLHTTPServer {
|
|
|
63
94
|
guard let self = self else {
|
|
64
95
|
return HTTPResponse(.internalServerError)
|
|
65
96
|
}
|
|
66
|
-
return self.handleBatchRequest(request: request)
|
|
97
|
+
return self.addCorsHeaders(self.handleBatchRequest(request: request))
|
|
67
98
|
}
|
|
68
99
|
|
|
69
100
|
// Transaction endpoints
|
|
@@ -71,21 +102,21 @@ class SQLHTTPServer {
|
|
|
71
102
|
guard let self = self else {
|
|
72
103
|
return HTTPResponse(.internalServerError)
|
|
73
104
|
}
|
|
74
|
-
return self.handleBeginTransactionRequest(request: request)
|
|
105
|
+
return self.addCorsHeaders(self.handleBeginTransactionRequest(request: request))
|
|
75
106
|
}
|
|
76
107
|
|
|
77
108
|
server.route(.POST, "/transaction/commit") { [weak self] request in
|
|
78
109
|
guard let self = self else {
|
|
79
110
|
return HTTPResponse(.internalServerError)
|
|
80
111
|
}
|
|
81
|
-
return self.handleCommitTransactionRequest(request: request)
|
|
112
|
+
return self.addCorsHeaders(self.handleCommitTransactionRequest(request: request))
|
|
82
113
|
}
|
|
83
114
|
|
|
84
115
|
server.route(.POST, "/transaction/rollback") { [weak self] request in
|
|
85
116
|
guard let self = self else {
|
|
86
117
|
return HTTPResponse(.internalServerError)
|
|
87
118
|
}
|
|
88
|
-
return self.handleRollbackTransactionRequest(request: request)
|
|
119
|
+
return self.addCorsHeaders(self.handleRollbackTransactionRequest(request: request))
|
|
89
120
|
}
|
|
90
121
|
}
|
|
91
122
|
|
|
@@ -167,7 +198,7 @@ class SQLHTTPServer {
|
|
|
167
198
|
do {
|
|
168
199
|
let result = try db.execute(statement: statement, params: params)
|
|
169
200
|
let resultData = try JSONSerialization.data(withJSONObject: result)
|
|
170
|
-
return HTTPResponse(.ok, headers: [
|
|
201
|
+
return HTTPResponse(.ok, headers: [.contentType: "application/json"], body: resultData)
|
|
171
202
|
} catch {
|
|
172
203
|
let errorData = "Error: \(error.localizedDescription)".data(using: .utf8)!
|
|
173
204
|
return HTTPResponse(.internalServerError, body: errorData)
|
|
@@ -215,7 +246,7 @@ class SQLHTTPServer {
|
|
|
215
246
|
}
|
|
216
247
|
|
|
217
248
|
let resultData = try JSONSerialization.data(withJSONObject: results)
|
|
218
|
-
return HTTPResponse(.ok, headers: [
|
|
249
|
+
return HTTPResponse(.ok, headers: [.contentType: "application/json"], body: resultData)
|
|
219
250
|
} catch {
|
|
220
251
|
let errorData = "Error: \(error.localizedDescription)".data(using: .utf8)!
|
|
221
252
|
return HTTPResponse(.internalServerError, body: errorData)
|
|
@@ -244,7 +275,7 @@ class SQLHTTPServer {
|
|
|
244
275
|
do {
|
|
245
276
|
try db.beginTransaction()
|
|
246
277
|
let resultData = "{}".data(using: .utf8)!
|
|
247
|
-
return HTTPResponse(.ok, headers: [
|
|
278
|
+
return HTTPResponse(.ok, headers: [.contentType: "application/json"], body: resultData)
|
|
248
279
|
} catch {
|
|
249
280
|
let errorData = "Error: \(error.localizedDescription)".data(using: .utf8)!
|
|
250
281
|
return HTTPResponse(.internalServerError, body: errorData)
|
|
@@ -273,7 +304,7 @@ class SQLHTTPServer {
|
|
|
273
304
|
do {
|
|
274
305
|
try db.commitTransaction()
|
|
275
306
|
let resultData = "{}".data(using: .utf8)!
|
|
276
|
-
return HTTPResponse(.ok, headers: [
|
|
307
|
+
return HTTPResponse(.ok, headers: [.contentType: "application/json"], body: resultData)
|
|
277
308
|
} catch {
|
|
278
309
|
let errorData = "Error: \(error.localizedDescription)".data(using: .utf8)!
|
|
279
310
|
return HTTPResponse(.internalServerError, body: errorData)
|
|
@@ -302,7 +333,7 @@ class SQLHTTPServer {
|
|
|
302
333
|
do {
|
|
303
334
|
try db.rollbackTransaction()
|
|
304
335
|
let resultData = "{}".data(using: .utf8)!
|
|
305
|
-
return HTTPResponse(.ok, headers: [
|
|
336
|
+
return HTTPResponse(.ok, headers: [.contentType: "application/json"], body: resultData)
|
|
306
337
|
} catch {
|
|
307
338
|
let errorData = "Error: \(error.localizedDescription)".data(using: .utf8)!
|
|
308
339
|
return HTTPResponse(.internalServerError, body: errorData)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capgo/capacitor-fast-sql",
|
|
3
|
-
"version": "8.0.
|
|
3
|
+
"version": "8.0.19",
|
|
4
4
|
"description": "High-performance native SQLite plugin with custom protocol for efficient sync operations and IndexedDB replacement",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|