@capgo/capacitor-fast-sql 8.2.17 → 8.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.
- package/Package.swift +2 -1
- package/README.md +27 -18
- package/android/build.gradle +4 -0
- package/android/consumer-rules.pro +14 -0
- package/android/src/main/java/app/capgo/capacitor/fastsql/BinaryFrame.java +152 -0
- package/android/src/main/java/app/capgo/capacitor/fastsql/CapgoCapacitorFastSqlPlugin.java +5 -1
- package/android/src/main/java/app/capgo/capacitor/fastsql/DatabaseConnection.java +2 -0
- package/android/src/main/java/app/capgo/capacitor/fastsql/EncryptedSQLDatabase.java +13 -2
- package/android/src/main/java/app/capgo/capacitor/fastsql/SQLDatabase.java +13 -2
- package/android/src/main/java/app/capgo/capacitor/fastsql/SQLHTTPServer.java +114 -57
- package/dist/esm/binary-frame.d.ts +6 -0
- package/dist/esm/binary-frame.d.ts.map +1 -0
- package/dist/esm/binary-frame.js +100 -0
- package/dist/esm/definitions.d.ts +28 -0
- package/dist/esm/definitions.d.ts.map +1 -1
- package/dist/esm/fast-sql.d.ts.map +1 -1
- package/dist/esm/fast-sql.js +13 -3
- package/dist/esm/native-sql-connection.d.ts +18 -7
- package/dist/esm/native-sql-connection.d.ts.map +1 -1
- package/dist/esm/native-sql-connection.js +145 -59
- package/dist/esm/web.d.ts +2 -0
- package/dist/esm/web.d.ts.map +1 -1
- package/dist/esm/web.js +24 -2
- package/dist/plugin.cjs.js +282 -64
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +282 -64
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapgoCapacitorFastSqlPlugin/BinaryFrame.swift +95 -0
- package/ios/Sources/CapgoCapacitorFastSqlPlugin/CapgoCapacitorFastSqlPlugin.swift +7 -3
- package/ios/Sources/CapgoCapacitorFastSqlPlugin/SQLDatabase.swift +20 -3
- package/ios/Sources/CapgoCapacitorFastSqlPlugin/SQLHTTPServer.swift +59 -76
- package/ios/Tests/CapgoCapacitorFastSqlPluginTests/BinaryFrameTests.swift +65 -0
- package/ios/Tests/CapgoCapacitorFastSqlPluginTests/Fixtures/binary-v1.bin +0 -0
- package/package.json +5 -2
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CoreFoundation
|
|
3
|
+
|
|
4
|
+
enum BinaryFrameError: Error {
|
|
5
|
+
case invalid
|
|
6
|
+
case limit
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/// Version 1 envelope shared with Android and JavaScript. Data stays binary.
|
|
10
|
+
enum BinaryFrame {
|
|
11
|
+
static let contentType = "application/vnd.capgo.fast-sql.v1+binary"
|
|
12
|
+
static let maxBytes = 64 * 1024 * 1024
|
|
13
|
+
private static let maxMetadata = 1024 * 1024
|
|
14
|
+
private static let headerBytes = 12
|
|
15
|
+
private static let magic: [UInt8] = [70, 83, 81, 49]
|
|
16
|
+
|
|
17
|
+
static func encode(_ value: Any) throws -> Data {
|
|
18
|
+
var attachments = Data()
|
|
19
|
+
var nodes = 0
|
|
20
|
+
var blobs = 0
|
|
21
|
+
func encodeValue(_ value: Any, depth: Int) throws -> Any {
|
|
22
|
+
nodes += 1
|
|
23
|
+
guard nodes <= 100000, depth <= 32 else { throw BinaryFrameError.limit }
|
|
24
|
+
if let bytes = value as? Data {
|
|
25
|
+
blobs += 1
|
|
26
|
+
guard blobs <= 4096, attachments.count + bytes.count <= maxBytes - headerBytes else { throw BinaryFrameError.limit }
|
|
27
|
+
let reference = ["$blob": [attachments.count, bytes.count]]
|
|
28
|
+
attachments.append(bytes)
|
|
29
|
+
return reference
|
|
30
|
+
}
|
|
31
|
+
if let number = value as? NSNumber, !number.doubleValue.isFinite { throw BinaryFrameError.invalid }
|
|
32
|
+
if let array = value as? [Any] { return try array.map { try encodeValue($0, depth: depth + 1) } }
|
|
33
|
+
if let object = value as? [String: Any] {
|
|
34
|
+
return try object.mapValues { try encodeValue($0, depth: depth + 1) }
|
|
35
|
+
}
|
|
36
|
+
return value
|
|
37
|
+
}
|
|
38
|
+
let metadata = try JSONSerialization.data(withJSONObject: encodeValue(value, depth: 0), options: [.fragmentsAllowed])
|
|
39
|
+
guard metadata.count <= maxMetadata, headerBytes + metadata.count + attachments.count <= maxBytes else { throw BinaryFrameError.limit }
|
|
40
|
+
var output = Data(magic)
|
|
41
|
+
for count in [metadata.count, attachments.count] {
|
|
42
|
+
var littleEndian = UInt32(count).littleEndian
|
|
43
|
+
withUnsafeBytes(of: &littleEndian) { output.append(contentsOf: $0) }
|
|
44
|
+
}
|
|
45
|
+
output.append(metadata)
|
|
46
|
+
output.append(attachments)
|
|
47
|
+
return output
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static func decode(_ frame: Data) throws -> Any {
|
|
51
|
+
guard frame.count >= headerBytes, frame.count <= maxBytes, Array(frame.prefix(4)) == magic else { throw BinaryFrameError.invalid }
|
|
52
|
+
let metadataLength = frame.withUnsafeBytes { Int(UInt32(littleEndian: $0.loadUnaligned(fromByteOffset: 4, as: UInt32.self))) }
|
|
53
|
+
let payloadLength = frame.withUnsafeBytes { Int(UInt32(littleEndian: $0.loadUnaligned(fromByteOffset: 8, as: UInt32.self))) }
|
|
54
|
+
let payloadOffset = headerBytes + metadataLength
|
|
55
|
+
guard metadataLength > 0, metadataLength <= maxMetadata, payloadOffset + payloadLength == frame.count else { throw BinaryFrameError.invalid }
|
|
56
|
+
let start = frame.startIndex
|
|
57
|
+
let metadata = frame.subdata(in: (start + headerBytes)..<(start + payloadOffset))
|
|
58
|
+
guard String(data: metadata, encoding: .utf8) != nil else { throw BinaryFrameError.invalid }
|
|
59
|
+
var nesting = 0
|
|
60
|
+
var quoted = false
|
|
61
|
+
var escaped = false
|
|
62
|
+
for byte in metadata {
|
|
63
|
+
if quoted {
|
|
64
|
+
if escaped { escaped = false } else if byte == 92 { escaped = true } else if byte == 34 { quoted = false }
|
|
65
|
+
} else if byte == 34 { quoted = true } else if byte == 91 || byte == 123 {
|
|
66
|
+
nesting += 1
|
|
67
|
+
guard nesting <= 33 else { throw BinaryFrameError.limit }
|
|
68
|
+
} else if byte == 93 || byte == 125 { nesting -= 1 }
|
|
69
|
+
}
|
|
70
|
+
guard let value = try? JSONSerialization.jsonObject(with: metadata, options: [.fragmentsAllowed]) else { throw BinaryFrameError.invalid }
|
|
71
|
+
var nodes = 0
|
|
72
|
+
var blobs = 0
|
|
73
|
+
var referencedBytes = 0
|
|
74
|
+
func decodeValue(_ value: Any, depth: Int) throws -> Any {
|
|
75
|
+
nodes += 1
|
|
76
|
+
guard nodes <= 100000, depth <= 32 else { throw BinaryFrameError.limit }
|
|
77
|
+
if let array = value as? [Any] { return try array.map { try decodeValue($0, depth: depth + 1) } }
|
|
78
|
+
guard let object = value as? [String: Any] else { return value }
|
|
79
|
+
if object.count == 1, let raw = object["$blob"] {
|
|
80
|
+
guard let reference = raw as? [NSNumber], reference.count == 2 else { throw BinaryFrameError.invalid }
|
|
81
|
+
let offset = reference[0].doubleValue
|
|
82
|
+
let count = reference[1].doubleValue
|
|
83
|
+
guard reference.allSatisfy({ CFGetTypeID($0) != CFBooleanGetTypeID() }), offset.isFinite, count.isFinite,
|
|
84
|
+
offset >= 0, count >= 0, offset.rounded() == offset, count.rounded() == count,
|
|
85
|
+
offset + count <= Double(payloadLength) else { throw BinaryFrameError.invalid }
|
|
86
|
+
referencedBytes += Int(count)
|
|
87
|
+
blobs += 1
|
|
88
|
+
guard blobs <= 4096, referencedBytes <= maxBytes else { throw BinaryFrameError.limit }
|
|
89
|
+
return frame.subdata(in: (start + payloadOffset + Int(offset))..<(start + payloadOffset + Int(offset) + Int(count)))
|
|
90
|
+
}
|
|
91
|
+
return try object.mapValues { try decodeValue($0, depth: depth + 1) }
|
|
92
|
+
}
|
|
93
|
+
return try decodeValue(value, depth: 0)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -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.
|
|
13
|
+
private let pluginVersion: String = "8.3.0"
|
|
14
14
|
public let identifier = "CapgoCapacitorFastSqlPlugin"
|
|
15
15
|
public let jsName = "CapgoCapacitorFastSql"
|
|
16
16
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -53,6 +53,7 @@ public class CapgoCapacitorFastSqlPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
53
53
|
call.resolve([
|
|
54
54
|
"port": server.port,
|
|
55
55
|
"token": server.token,
|
|
56
|
+
"binaryProtocolVersion": 1,
|
|
56
57
|
"database": database
|
|
57
58
|
])
|
|
58
59
|
return
|
|
@@ -102,6 +103,7 @@ public class CapgoCapacitorFastSqlPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
102
103
|
call.resolve([
|
|
103
104
|
"port": server.port,
|
|
104
105
|
"token": server.token,
|
|
106
|
+
"binaryProtocolVersion": 1,
|
|
105
107
|
"database": database
|
|
106
108
|
])
|
|
107
109
|
} catch {
|
|
@@ -151,7 +153,8 @@ public class CapgoCapacitorFastSqlPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
151
153
|
|
|
152
154
|
call.resolve([
|
|
153
155
|
"port": server.port,
|
|
154
|
-
"token": server.token
|
|
156
|
+
"token": server.token,
|
|
157
|
+
"binaryProtocolVersion": 1
|
|
155
158
|
])
|
|
156
159
|
}
|
|
157
160
|
|
|
@@ -244,7 +247,8 @@ public class CapgoCapacitorFastSqlPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
244
247
|
|
|
245
248
|
@objc func getPluginVersion(_ call: CAPPluginCall) {
|
|
246
249
|
call.resolve([
|
|
247
|
-
"version": pluginVersion
|
|
250
|
+
"version": pluginVersion,
|
|
251
|
+
"binaryProtocolVersion": 1
|
|
248
252
|
])
|
|
249
253
|
}
|
|
250
254
|
|
|
@@ -14,6 +14,7 @@ private let sqliteTransient = unsafeBitCast(-1, to: sqlite3_destructor_type.self
|
|
|
14
14
|
*/
|
|
15
15
|
class SQLDatabase {
|
|
16
16
|
let isReadOnly: Bool
|
|
17
|
+
private let operationLock = NSRecursiveLock()
|
|
17
18
|
private var db: OpaquePointer?
|
|
18
19
|
private let path: String
|
|
19
20
|
private var inTransaction = false
|
|
@@ -86,13 +87,17 @@ class SQLDatabase {
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
func close() {
|
|
90
|
+
operationLock.lock()
|
|
91
|
+
defer { operationLock.unlock() }
|
|
89
92
|
if let db = db {
|
|
90
93
|
sqlite3_close(db)
|
|
91
94
|
self.db = nil
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
|
|
95
|
-
func execute(statement: String, params: [Any]) throws -> [String: Any] {
|
|
98
|
+
func execute(statement: String, params: [Any], binaryResults: Bool = false) throws -> [String: Any] {
|
|
99
|
+
operationLock.lock()
|
|
100
|
+
defer { operationLock.unlock() }
|
|
96
101
|
guard let db = db else {
|
|
97
102
|
throw SQLError.notOpen
|
|
98
103
|
}
|
|
@@ -139,7 +144,7 @@ class SQLDatabase {
|
|
|
139
144
|
var row: [String: Any] = [:]
|
|
140
145
|
for columnIndex in 0..<columnCount {
|
|
141
146
|
let columnName = columnNames[Int(columnIndex)]
|
|
142
|
-
let value = getColumnValue(stmt: stmt, index: columnIndex)
|
|
147
|
+
let value = getColumnValue(stmt: stmt, index: columnIndex, binaryResults: binaryResults)
|
|
143
148
|
row[columnName] = value
|
|
144
149
|
}
|
|
145
150
|
rows.append(row)
|
|
@@ -163,6 +168,8 @@ class SQLDatabase {
|
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
func beginTransaction() throws {
|
|
171
|
+
operationLock.lock()
|
|
172
|
+
defer { operationLock.unlock() }
|
|
166
173
|
guard !inTransaction else {
|
|
167
174
|
throw SQLError.transactionAlreadyActive
|
|
168
175
|
}
|
|
@@ -171,6 +178,8 @@ class SQLDatabase {
|
|
|
171
178
|
}
|
|
172
179
|
|
|
173
180
|
func commitTransaction() throws {
|
|
181
|
+
operationLock.lock()
|
|
182
|
+
defer { operationLock.unlock() }
|
|
174
183
|
guard inTransaction else {
|
|
175
184
|
throw SQLError.noTransactionActive
|
|
176
185
|
}
|
|
@@ -179,6 +188,8 @@ class SQLDatabase {
|
|
|
179
188
|
}
|
|
180
189
|
|
|
181
190
|
func rollbackTransaction() throws {
|
|
191
|
+
operationLock.lock()
|
|
192
|
+
defer { operationLock.unlock() }
|
|
182
193
|
guard inTransaction else {
|
|
183
194
|
throw SQLError.noTransactionActive
|
|
184
195
|
}
|
|
@@ -197,6 +208,10 @@ class SQLDatabase {
|
|
|
197
208
|
}
|
|
198
209
|
|
|
199
210
|
// Handle different types
|
|
211
|
+
if let data = value as? Data {
|
|
212
|
+
if data.isEmpty { return sqlite3_bind_zeroblob(stmt, index, 0) }
|
|
213
|
+
return data.withUnsafeBytes { sqlite3_bind_blob(stmt, index, $0.baseAddress, Int32(data.count), sqliteTransient) }
|
|
214
|
+
}
|
|
200
215
|
if let str = value as? String {
|
|
201
216
|
return sqlite3_bind_text(stmt, index, str, -1, sqliteTransient)
|
|
202
217
|
} else if let num = value as? NSNumber {
|
|
@@ -227,7 +242,7 @@ class SQLDatabase {
|
|
|
227
242
|
return SQLITE_ERROR
|
|
228
243
|
}
|
|
229
244
|
|
|
230
|
-
private func getColumnValue(stmt: OpaquePointer?, index: Int32) -> Any {
|
|
245
|
+
private func getColumnValue(stmt: OpaquePointer?, index: Int32, binaryResults: Bool) -> Any {
|
|
231
246
|
guard let stmt = stmt else {
|
|
232
247
|
return NSNull()
|
|
233
248
|
}
|
|
@@ -246,11 +261,13 @@ class SQLDatabase {
|
|
|
246
261
|
return NSNull()
|
|
247
262
|
case SQLITE_BLOB:
|
|
248
263
|
if sqlite3_column_bytes(stmt, index) == 0 {
|
|
264
|
+
if binaryResults { return Data() }
|
|
249
265
|
return ["_type": "binary", "_data": ""]
|
|
250
266
|
}
|
|
251
267
|
if let blob = sqlite3_column_blob(stmt, index) {
|
|
252
268
|
let size = sqlite3_column_bytes(stmt, index)
|
|
253
269
|
let data = Data(bytes: blob, count: Int(size))
|
|
270
|
+
if binaryResults { return data }
|
|
254
271
|
return [
|
|
255
272
|
"_type": "binary",
|
|
256
273
|
"_data": data.base64EncodedString()
|
|
@@ -100,7 +100,7 @@ class SQLHTTPServer {
|
|
|
100
100
|
guard let server = server else { return }
|
|
101
101
|
|
|
102
102
|
// CORS preflight for all endpoints
|
|
103
|
-
let preflightPaths = ["/execute", "/batch", "/transaction/begin", "/transaction/commit", "/transaction/rollback"]
|
|
103
|
+
let preflightPaths = ["/execute", "/batch", "/echo", "/transaction/begin", "/transaction/commit", "/transaction/rollback"]
|
|
104
104
|
for path in preflightPaths {
|
|
105
105
|
server.route(.OPTIONS, path) { [weak self] _ in
|
|
106
106
|
guard let self = self else {
|
|
@@ -126,6 +126,11 @@ class SQLHTTPServer {
|
|
|
126
126
|
return self.addCorsHeaders(self.handleBatchRequest(request: request))
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
server.route(.POST, "/echo") { [weak self] request in
|
|
130
|
+
guard let self = self else { return HTTPResponse(.internalServerError) }
|
|
131
|
+
return self.addCorsHeaders(self.handleDataRequest(request: request, kind: "echo"))
|
|
132
|
+
}
|
|
133
|
+
|
|
129
134
|
// Transaction endpoints
|
|
130
135
|
server.route(.POST, "/transaction/begin") { [weak self] request in
|
|
131
136
|
guard let self = self else {
|
|
@@ -196,90 +201,68 @@ class SQLHTTPServer {
|
|
|
196
201
|
// MARK: - Request Handlers
|
|
197
202
|
|
|
198
203
|
private func handleExecuteRequest(request: HTTPRequest) -> HTTPResponse {
|
|
199
|
-
|
|
200
|
-
guard let authHeader = request.headers["Authorization"],
|
|
201
|
-
authHeader == "Bearer \(token)" else {
|
|
202
|
-
return HTTPResponse(.unauthorized)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Get database name
|
|
206
|
-
guard let database = request.headers["X-Database"] else {
|
|
207
|
-
let bodyData = "Database header required".data(using: .utf8)!
|
|
208
|
-
return HTTPResponse(.badRequest, body: bodyData)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Check if database exists
|
|
212
|
-
guard let db = registeredDatabase(forKey: database) else {
|
|
213
|
-
let bodyData = "Database not found".data(using: .utf8)!
|
|
214
|
-
return HTTPResponse(.notFound, body: bodyData)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Parse request body
|
|
218
|
-
let bodyData = request.body
|
|
219
|
-
guard let json = try? JSONSerialization.jsonObject(with: bodyData) as? [String: Any],
|
|
220
|
-
let statement = json["statement"] as? String else {
|
|
221
|
-
let errorData = "Invalid request body".data(using: .utf8)!
|
|
222
|
-
return HTTPResponse(.badRequest, body: errorData)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
let params = (json["params"] as? [Any] ?? [])
|
|
226
|
-
|
|
227
|
-
do {
|
|
228
|
-
let result = try db.execute(statement: statement, params: params)
|
|
229
|
-
let resultData = try JSONSerialization.data(withJSONObject: result)
|
|
230
|
-
return HTTPResponse(.ok, headers: [.contentType: "application/json"], body: resultData)
|
|
231
|
-
} catch {
|
|
232
|
-
let errorData = "Error: \(error.localizedDescription)".data(using: .utf8)!
|
|
233
|
-
return HTTPResponse(.internalServerError, body: errorData)
|
|
234
|
-
}
|
|
204
|
+
return handleDataRequest(request: request, kind: "execute")
|
|
235
205
|
}
|
|
236
206
|
|
|
237
207
|
private func handleBatchRequest(request: HTTPRequest) -> HTTPResponse {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
authHeader == "Bearer \(token)" else {
|
|
241
|
-
return HTTPResponse(.unauthorized)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Get database name
|
|
245
|
-
guard let database = request.headers["X-Database"] else {
|
|
246
|
-
let bodyData = "Database header required".data(using: .utf8)!
|
|
247
|
-
return HTTPResponse(.badRequest, body: bodyData)
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Check if database exists
|
|
251
|
-
guard let db = registeredDatabase(forKey: database) else {
|
|
252
|
-
let bodyData = "Database not found".data(using: .utf8)!
|
|
253
|
-
return HTTPResponse(.notFound, body: bodyData)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Parse request body
|
|
257
|
-
let bodyData = request.body
|
|
258
|
-
guard let json = try? JSONSerialization.jsonObject(with: bodyData) as? [String: Any],
|
|
259
|
-
let operations = json["operations"] as? [[String: Any]] else {
|
|
260
|
-
let errorData = "Invalid request body".data(using: .utf8)!
|
|
261
|
-
return HTTPResponse(.badRequest, body: errorData)
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
var results: [[String: Any]] = []
|
|
208
|
+
return handleDataRequest(request: request, kind: "batch")
|
|
209
|
+
}
|
|
265
210
|
|
|
211
|
+
private func handleDataRequest(request: HTTPRequest, kind: String) -> HTTPResponse {
|
|
212
|
+
guard request.headers["Authorization"] == "Bearer \(token)" else { return HTTPResponse(.unauthorized) }
|
|
213
|
+
guard let database = request.headers["X-Database"] else { return HTTPResponse(.badRequest) }
|
|
214
|
+
guard let db = registeredDatabase(forKey: database) else { return HTTPResponse(.notFound) }
|
|
215
|
+
guard request.body.count <= BinaryFrame.maxBytes else { return HTTPResponse(.payloadTooLarge) }
|
|
216
|
+
let binary = request.headers["Content-Type"] == BinaryFrame.contentType
|
|
266
217
|
do {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
218
|
+
let decoded: Any
|
|
219
|
+
if binary {
|
|
220
|
+
decoded = try BinaryFrame.decode(request.body)
|
|
221
|
+
} else {
|
|
222
|
+
guard let json = try? JSONSerialization.jsonObject(with: request.body) else { throw BinaryFrameError.invalid }
|
|
223
|
+
decoded = json
|
|
224
|
+
}
|
|
225
|
+
guard let json = decoded as? [String: Any] else { throw BinaryFrameError.invalid }
|
|
226
|
+
let result: Any
|
|
227
|
+
if kind == "batch" {
|
|
228
|
+
guard let operations = json["operations"] as? [[String: Any]], operations.count <= 10000 else { throw BinaryFrameError.invalid }
|
|
229
|
+
let parsed = try operations.map { try parseOperation($0) }
|
|
230
|
+
result = try parsed.map { try db.execute(statement: $0.statement, params: $0.params, binaryResults: binary) }
|
|
231
|
+
} else {
|
|
232
|
+
let operation = try parseOperation(json)
|
|
233
|
+
let params = operation.params
|
|
234
|
+
if kind == "echo" {
|
|
235
|
+
guard params.count == 1 else { throw BinaryFrameError.invalid }
|
|
236
|
+
result = ["rows": [["value": params[0]]], "rowsAffected": 0]
|
|
237
|
+
} else {
|
|
238
|
+
result = try db.execute(statement: operation.statement, params: params, binaryResults: binary)
|
|
271
239
|
}
|
|
272
|
-
let params = (operation["params"] as? [Any] ?? [])
|
|
273
|
-
let result = try db.execute(statement: statement, params: params)
|
|
274
|
-
results.append(result)
|
|
275
240
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
241
|
+
let data: Data
|
|
242
|
+
do {
|
|
243
|
+
if !binary && !JSONSerialization.isValidJSONObject(result) { throw BinaryFrameError.invalid }
|
|
244
|
+
data = try binary ? BinaryFrame.encode(result) : JSONSerialization.data(withJSONObject: result)
|
|
245
|
+
} catch {
|
|
246
|
+
return HTTPResponse(.internalServerError, body: Data("Could not encode SQL response; a write may already be committed".utf8))
|
|
247
|
+
}
|
|
248
|
+
return HTTPResponse(.ok, headers: [.contentType: binary ? BinaryFrame.contentType : "application/json"], body: data)
|
|
249
|
+
} catch is BinaryFrameError {
|
|
250
|
+
return HTTPResponse(.badRequest, body: Data("Invalid binary/SQL request".utf8))
|
|
279
251
|
} catch {
|
|
280
|
-
|
|
281
|
-
|
|
252
|
+
return HTTPResponse(.internalServerError, body: Data("Error: \(error.localizedDescription)".utf8))
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private func parseOperation(_ operation: [String: Any]) throws -> (statement: String, params: [Any]) {
|
|
257
|
+
guard let statement = operation["statement"] as? String else { throw BinaryFrameError.invalid }
|
|
258
|
+
if let raw = operation["params"], !(raw is [Any]) { throw BinaryFrameError.invalid }
|
|
259
|
+
for value in operation["params"] as? [Any] ?? [] {
|
|
260
|
+
if value is NSNull || value is String || value is Data { continue }
|
|
261
|
+
if let number = value as? NSNumber, number.doubleValue.isFinite { continue }
|
|
262
|
+
if let blob = value as? [String: Any], blob["_type"] as? String == "binary", blob["_data"] is String { continue }
|
|
263
|
+
throw BinaryFrameError.invalid
|
|
282
264
|
}
|
|
265
|
+
return (statement, operation["params"] as? [Any] ?? [])
|
|
283
266
|
}
|
|
284
267
|
|
|
285
268
|
private func handleBeginTransactionRequest(request: HTTPRequest) -> HTTPResponse {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import CapgoCapacitorFastSqlPlugin
|
|
3
|
+
|
|
4
|
+
final class BinaryFrameTests: XCTestCase {
|
|
5
|
+
func testHTTPRejectsInfiniteResponsesWithoutCrashing() async throws {
|
|
6
|
+
let database = try SQLDatabase(path: ":memory:")
|
|
7
|
+
let server = try SQLHTTPServer(databases: ["test": database])
|
|
8
|
+
try server.start()
|
|
9
|
+
defer { server.stop(); database.close() }
|
|
10
|
+
for binary in [false, true] {
|
|
11
|
+
let url = try XCTUnwrap(URL(string: "http://127.0.0.1:\(server.port)/execute"))
|
|
12
|
+
var request = URLRequest(url: url)
|
|
13
|
+
request.httpMethod = "POST"
|
|
14
|
+
request.timeoutInterval = 10
|
|
15
|
+
request.setValue("Bearer \(server.token)", forHTTPHeaderField: "Authorization")
|
|
16
|
+
request.setValue("test", forHTTPHeaderField: "X-Database")
|
|
17
|
+
request.setValue(binary ? BinaryFrame.contentType : "application/json", forHTTPHeaderField: "Content-Type")
|
|
18
|
+
let payload: [String: Any] = ["statement": "SELECT 1e999 AS value", "params": []]
|
|
19
|
+
request.httpBody = try binary ? BinaryFrame.encode(payload) : JSONSerialization.data(withJSONObject: payload)
|
|
20
|
+
let (_, response) = try await URLSession.shared.data(for: request)
|
|
21
|
+
XCTAssertEqual((response as? HTTPURLResponse)?.statusCode, 500)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func testSharedJavaScriptGolden() throws {
|
|
26
|
+
let url = try XCTUnwrap(Bundle.module.url(forResource: "binary-v1", withExtension: "bin", subdirectory: "Fixtures"))
|
|
27
|
+
let request = try XCTUnwrap(try BinaryFrame.decode(Data(contentsOf: url)) as? [String: Any])
|
|
28
|
+
let params = try XCTUnwrap(request["params"] as? [Any])
|
|
29
|
+
XCTAssertEqual(params[0] as? Data, Data([0, 1, 255]))
|
|
30
|
+
XCTAssertEqual(params[1] as? Data, Data())
|
|
31
|
+
XCTAssertEqual(params[2] as? Double, 1.25)
|
|
32
|
+
XCTAssertEqual(params[3] as? String, "😀")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func testRawDataSQLiteRoundTrip() throws {
|
|
36
|
+
let database = try SQLDatabase(path: ":memory:")
|
|
37
|
+
defer { database.close() }
|
|
38
|
+
let result = try database.execute(statement: "SELECT ? AS bytes, ? AS empty, ? AS missing, ? AS fraction", params: [Data([0, 1, 255]), Data(), NSNull(), 1.25], binaryResults: true)
|
|
39
|
+
let decoded = try XCTUnwrap(try BinaryFrame.decode(BinaryFrame.encode(result)) as? [String: Any])
|
|
40
|
+
let rows = try XCTUnwrap(decoded["rows"] as? [[String: Any]])
|
|
41
|
+
XCTAssertEqual(rows[0]["bytes"] as? Data, Data([0, 1, 255]))
|
|
42
|
+
XCTAssertEqual(rows[0]["empty"] as? Data, Data())
|
|
43
|
+
XCTAssertTrue(rows[0]["missing"] is NSNull)
|
|
44
|
+
XCTAssertEqual(rows[0]["fraction"] as? Double, 1.25)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func testMalformedFrames() throws {
|
|
48
|
+
for number in [Double.infinity, -Double.infinity, Double.nan] {
|
|
49
|
+
XCTAssertThrowsError(try BinaryFrame.encode(["rows": [["value": number]]]))
|
|
50
|
+
}
|
|
51
|
+
let database = try SQLDatabase(path: ":memory:")
|
|
52
|
+
defer { database.close() }
|
|
53
|
+
let infinite = try database.execute(statement: "SELECT 1e999 AS value", params: [], binaryResults: true)
|
|
54
|
+
XCTAssertThrowsError(try BinaryFrame.encode(infinite))
|
|
55
|
+
XCTAssertFalse(JSONSerialization.isValidJSONObject(infinite))
|
|
56
|
+
let valid = try BinaryFrame.encode(["value": Data([1, 2])])
|
|
57
|
+
XCTAssertThrowsError(try BinaryFrame.decode(valid.dropLast()))
|
|
58
|
+
var version = valid
|
|
59
|
+
version[3] = 50
|
|
60
|
+
XCTAssertThrowsError(try BinaryFrame.decode(version))
|
|
61
|
+
for reference in [[-1.0, 1], [0, 999], [0.5, 0]] {
|
|
62
|
+
XCTAssertThrowsError(try BinaryFrame.decode(BinaryFrame.encode(["$blob": reference])))
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capgo/capacitor-fast-sql",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.3.0",
|
|
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",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"android/src/main/",
|
|
11
11
|
"android/build.gradle",
|
|
12
|
+
"android/consumer-rules.pro",
|
|
12
13
|
"dist/",
|
|
13
14
|
"ios/Sources",
|
|
14
15
|
"ios/Tests",
|
|
@@ -55,7 +56,9 @@
|
|
|
55
56
|
"check:cap9-deprecated": "node scripts/check-cap9-deprecated.mjs",
|
|
56
57
|
"example:install": "cd example-app && bun install --frozen-lockfile",
|
|
57
58
|
"example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build",
|
|
58
|
-
"changelog:ai": "node scripts/generate-ai-changelog.mjs"
|
|
59
|
+
"changelog:ai": "node scripts/generate-ai-changelog.mjs",
|
|
60
|
+
"benchmark:check": "bun test tests/benchmark.test.ts && tsc -p benchmarks/tsconfig.json",
|
|
61
|
+
"test:unit": "bun test tests"
|
|
59
62
|
},
|
|
60
63
|
"dependencies": {
|
|
61
64
|
"@sqlite.org/sqlite-wasm": "3.53.0-build1"
|