strata-storage 2.4.3 → 2.6.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/AI-INTEGRATION-GUIDE.md +115 -261
- package/README.md +426 -182
- package/android/AGENTS.md +51 -0
- package/android/CLAUDE.md +89 -0
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/strata/storage/FilesystemStorage.java +287 -0
- package/android/src/main/java/com/strata/storage/SQLiteStorage.java +260 -203
- package/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +357 -69
- package/dist/README.md +426 -182
- package/dist/adapters/capacitor/FilesystemAdapter.d.ts.map +1 -1
- package/dist/adapters/capacitor/FilesystemAdapter.js +2 -1
- package/dist/adapters/capacitor/PreferencesAdapter.d.ts.map +1 -1
- package/dist/adapters/capacitor/PreferencesAdapter.js +2 -1
- package/dist/adapters/capacitor/SecureAdapter.d.ts.map +1 -1
- package/dist/adapters/capacitor/SecureAdapter.js +2 -1
- package/dist/adapters/capacitor/SqliteAdapter.d.ts.map +1 -1
- package/dist/adapters/capacitor/SqliteAdapter.js +2 -1
- package/dist/adapters/web/CacheAdapter.d.ts.map +1 -1
- package/dist/adapters/web/CacheAdapter.js +11 -3
- package/dist/adapters/web/CookieAdapter.d.ts +37 -1
- package/dist/adapters/web/CookieAdapter.d.ts.map +1 -1
- package/dist/adapters/web/CookieAdapter.js +89 -9
- package/dist/adapters/web/IndexedDBAdapter.d.ts.map +1 -1
- package/dist/adapters/web/IndexedDBAdapter.js +10 -2
- package/dist/adapters/web/LocalStorageAdapter.d.ts +31 -0
- package/dist/adapters/web/LocalStorageAdapter.d.ts.map +1 -1
- package/dist/adapters/web/LocalStorageAdapter.js +92 -19
- package/dist/adapters/web/MemoryAdapter.d.ts +24 -0
- package/dist/adapters/web/MemoryAdapter.d.ts.map +1 -1
- package/dist/adapters/web/MemoryAdapter.js +69 -18
- package/dist/adapters/web/SessionStorageAdapter.d.ts +24 -0
- package/dist/adapters/web/SessionStorageAdapter.d.ts.map +1 -1
- package/dist/adapters/web/SessionStorageAdapter.js +71 -9
- package/dist/adapters/web/URLAdapter.d.ts +59 -0
- package/dist/adapters/web/URLAdapter.d.ts.map +1 -0
- package/dist/adapters/web/URLAdapter.js +234 -0
- package/dist/adapters/web/index.d.ts +1 -0
- package/dist/adapters/web/index.d.ts.map +1 -1
- package/dist/adapters/web/index.js +1 -0
- package/dist/android/AGENTS.md +51 -0
- package/dist/android/CLAUDE.md +89 -0
- package/dist/android/build.gradle +1 -1
- package/dist/android/src/main/java/com/strata/storage/FilesystemStorage.java +287 -0
- package/dist/android/src/main/java/com/strata/storage/SQLiteStorage.java +260 -203
- package/dist/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +357 -69
- package/dist/capacitor.d.ts.map +1 -1
- package/dist/capacitor.js +2 -1
- package/dist/core/BaseAdapter.d.ts +8 -0
- package/dist/core/BaseAdapter.d.ts.map +1 -1
- package/dist/core/BaseAdapter.js +34 -14
- package/dist/core/Strata.d.ts +56 -2
- package/dist/core/Strata.d.ts.map +1 -1
- package/dist/core/Strata.js +501 -53
- package/dist/features/encryption.d.ts.map +1 -1
- package/dist/features/encryption.js +3 -2
- package/dist/features/integrity.d.ts +16 -0
- package/dist/features/integrity.d.ts.map +1 -0
- package/dist/features/integrity.js +28 -0
- package/dist/features/observer.d.ts.map +1 -1
- package/dist/features/observer.js +2 -1
- package/dist/features/query.d.ts +7 -1
- package/dist/features/query.d.ts.map +1 -1
- package/dist/features/query.js +9 -2
- package/dist/features/sync.d.ts.map +1 -1
- package/dist/features/sync.js +4 -3
- package/dist/index.d.ts +35 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +55 -30
- package/dist/integrations/angular/index.d.ts +158 -0
- package/dist/integrations/angular/index.d.ts.map +1 -0
- package/dist/integrations/angular/index.js +395 -0
- package/dist/integrations/index.d.ts +15 -0
- package/dist/integrations/index.d.ts.map +1 -0
- package/dist/integrations/index.js +18 -0
- package/dist/integrations/react/index.d.ts +75 -0
- package/dist/integrations/react/index.d.ts.map +1 -0
- package/dist/integrations/react/index.js +191 -0
- package/dist/integrations/vue/index.d.ts +103 -0
- package/dist/integrations/vue/index.d.ts.map +1 -0
- package/dist/integrations/vue/index.js +274 -0
- package/dist/ios/AGENTS.md +48 -0
- package/dist/ios/CLAUDE.md +84 -0
- package/dist/ios/Plugin/FilesystemStorage.swift +218 -0
- package/dist/ios/Plugin/KeychainStorage.swift +139 -50
- package/dist/ios/Plugin/SQLiteStorage.swift +279 -147
- package/dist/ios/Plugin/StrataStoragePlugin.m +23 -0
- package/dist/ios/Plugin/StrataStoragePlugin.swift +272 -65
- package/dist/package.json +21 -5
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +2 -1
- package/dist/types/index.d.ts +58 -9
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +0 -13
- package/dist/utils/errors.d.ts +7 -0
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +15 -3
- package/dist/utils/index.d.ts +63 -5
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +109 -16
- package/dist/utils/logger.d.ts +31 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +63 -0
- package/ios/AGENTS.md +48 -0
- package/ios/CLAUDE.md +84 -0
- package/ios/Plugin/FilesystemStorage.swift +218 -0
- package/ios/Plugin/KeychainStorage.swift +139 -50
- package/ios/Plugin/SQLiteStorage.swift +279 -147
- package/ios/Plugin/StrataStoragePlugin.m +23 -0
- package/ios/Plugin/StrataStoragePlugin.swift +272 -65
- package/package.json +31 -20
- package/scripts/build.js +16 -5
- package/scripts/configure.js +2 -6
- package/scripts/postinstall.js +2 -2
- package/Readme.md +0 -271
|
@@ -2,27 +2,60 @@ import Foundation
|
|
|
2
2
|
import SQLite3
|
|
3
3
|
import os.log
|
|
4
4
|
|
|
5
|
+
/// SQLITE_TRANSIENT tells SQLite to copy bound bytes immediately, which is
|
|
6
|
+
/// required when binding Swift `String`/`Data` whose buffers may be freed
|
|
7
|
+
/// before `sqlite3_step` runs. Passing `nil` (SQLITE_STATIC) here is a
|
|
8
|
+
/// use-after-free hazard for transient Swift buffers.
|
|
9
|
+
private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* SQLite-backed storage for one `(database, table)` pair.
|
|
13
|
+
*
|
|
14
|
+
* Multi-store design:
|
|
15
|
+
* - One `SQLiteStorage` instance owns one open DB handle for one database file.
|
|
16
|
+
* - A single instance can serve many tables in that file; tables are created
|
|
17
|
+
* on first use (see `ensureTable`).
|
|
18
|
+
* - The plugin caches one instance per database filename (see
|
|
19
|
+
* `SQLiteStorageManager`) so the DB is opened once, not per call.
|
|
20
|
+
*
|
|
21
|
+
* Value-shape contract (matches src/plugin/definitions.ts):
|
|
22
|
+
* - `set` receives the FULL `StorageValue` wrapper object
|
|
23
|
+
* `{ value, created, updated, expires?, tags?, metadata?, ... }`. The whole
|
|
24
|
+
* wrapper is JSON-serialized into the `value` column for a perfect
|
|
25
|
+
* round-trip. `created`/`updated`/`expires`/`tags`/`metadata` are ALSO
|
|
26
|
+
* mirrored into dedicated columns so native TTL cleanup + `query` can use
|
|
27
|
+
* them without re-parsing the blob.
|
|
28
|
+
* - `get` parses the `value` column bytes back into the wrapper object and the
|
|
29
|
+
* plugin resolves `{ value: wrapper }`. Corrupt/legacy bytes that do not
|
|
30
|
+
* parse as a JSON object are treated as a miss (never a crash).
|
|
31
|
+
*
|
|
32
|
+
* SQL-safety: only the table identifier is interpolated into SQL, and it is
|
|
33
|
+
* pre-sanitized to `^[A-Za-z0-9_]+$` by the plugin. All user values are bound
|
|
34
|
+
* parameters.
|
|
35
|
+
*/
|
|
5
36
|
@objc public class SQLiteStorage: NSObject {
|
|
6
37
|
private var db: OpaquePointer?
|
|
7
38
|
private let dbName: String
|
|
8
|
-
|
|
39
|
+
/// Tables already verified/created on this handle, to skip redundant DDL.
|
|
40
|
+
private var ensuredTables: Set<String> = []
|
|
9
41
|
private let logger = OSLog(subsystem: "com.strata.storage", category: "SQLiteStorage")
|
|
10
|
-
|
|
11
|
-
|
|
42
|
+
|
|
43
|
+
/// Opens (or creates) the database file `<dbName>` under Documents.
|
|
44
|
+
/// `dbName` MUST already be a sanitized filename (e.g. `storage.db`).
|
|
45
|
+
@objc public init(dbName: String = "strata_storage.db") {
|
|
12
46
|
self.dbName = dbName
|
|
13
47
|
super.init()
|
|
14
48
|
do {
|
|
15
49
|
try openDatabase()
|
|
16
|
-
try createTable()
|
|
17
50
|
} catch {
|
|
18
51
|
os_log("Failed to initialize SQLite storage: %{public}@", log: logger, type: .error, error.localizedDescription)
|
|
19
52
|
}
|
|
20
53
|
}
|
|
21
|
-
|
|
54
|
+
|
|
22
55
|
deinit {
|
|
23
56
|
closeDatabase()
|
|
24
57
|
}
|
|
25
|
-
|
|
58
|
+
|
|
26
59
|
private func openDatabase() throws {
|
|
27
60
|
guard let fileURL = try? FileManager.default
|
|
28
61
|
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
|
|
@@ -37,6 +70,7 @@ import os.log
|
|
|
37
70
|
guard sqlite3_open(fileURL.path, &db) == SQLITE_OK else {
|
|
38
71
|
let errorMessage = String(cString: sqlite3_errmsg(db))
|
|
39
72
|
sqlite3_close(db)
|
|
73
|
+
db = nil
|
|
40
74
|
throw NSError(
|
|
41
75
|
domain: "StrataStorage.SQLiteStorage",
|
|
42
76
|
code: 1002,
|
|
@@ -44,14 +78,24 @@ import os.log
|
|
|
44
78
|
)
|
|
45
79
|
}
|
|
46
80
|
}
|
|
47
|
-
|
|
81
|
+
|
|
48
82
|
private func closeDatabase() {
|
|
49
|
-
|
|
83
|
+
if db != nil {
|
|
84
|
+
sqlite3_close(db)
|
|
85
|
+
db = nil
|
|
86
|
+
}
|
|
50
87
|
}
|
|
51
|
-
|
|
52
|
-
|
|
88
|
+
|
|
89
|
+
/// Creates the table for `table` if it does not exist yet. Cached per
|
|
90
|
+
/// instance so repeated calls are cheap. `table` MUST be pre-sanitized.
|
|
91
|
+
private func ensureTable(_ table: String) throws {
|
|
92
|
+
guard let db = db else {
|
|
93
|
+
throw notInitializedError()
|
|
94
|
+
}
|
|
95
|
+
if ensuredTables.contains(table) { return }
|
|
96
|
+
|
|
53
97
|
let createTableString = """
|
|
54
|
-
CREATE TABLE IF NOT EXISTS \(
|
|
98
|
+
CREATE TABLE IF NOT EXISTS \(table) (
|
|
55
99
|
key TEXT PRIMARY KEY NOT NULL,
|
|
56
100
|
value BLOB NOT NULL,
|
|
57
101
|
created INTEGER NOT NULL,
|
|
@@ -63,6 +107,7 @@ import os.log
|
|
|
63
107
|
"""
|
|
64
108
|
|
|
65
109
|
var createTableStatement: OpaquePointer?
|
|
110
|
+
defer { sqlite3_finalize(createTableStatement) }
|
|
66
111
|
|
|
67
112
|
guard sqlite3_prepare_v2(db, createTableString, -1, &createTableStatement, nil) == SQLITE_OK else {
|
|
68
113
|
let errorMessage = String(cString: sqlite3_errmsg(db))
|
|
@@ -73,10 +118,6 @@ import os.log
|
|
|
73
118
|
)
|
|
74
119
|
}
|
|
75
120
|
|
|
76
|
-
defer {
|
|
77
|
-
sqlite3_finalize(createTableStatement)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
121
|
guard sqlite3_step(createTableStatement) == SQLITE_DONE else {
|
|
81
122
|
let errorMessage = String(cString: sqlite3_errmsg(db))
|
|
82
123
|
throw NSError(
|
|
@@ -85,210 +126,301 @@ import os.log
|
|
|
85
126
|
userInfo: [NSLocalizedDescriptionKey: "Failed to create table: \(errorMessage)"]
|
|
86
127
|
)
|
|
87
128
|
}
|
|
129
|
+
|
|
130
|
+
ensuredTables.insert(table)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private func notInitializedError() -> NSError {
|
|
134
|
+
return NSError(
|
|
135
|
+
domain: "StrataStorage.SQLiteStorage",
|
|
136
|
+
code: 1000,
|
|
137
|
+
userInfo: [NSLocalizedDescriptionKey: "Database not initialized"]
|
|
138
|
+
)
|
|
88
139
|
}
|
|
89
|
-
|
|
90
|
-
@objc public func set(key: String, value: Any, expires: Int64? = nil, tags: [String]? = nil, metadata: [String: Any]? = nil) throws -> Bool {
|
|
91
|
-
guard let db = db else {
|
|
92
|
-
throw NSError(
|
|
93
|
-
domain: "StrataStorage.SQLiteStorage",
|
|
94
|
-
code: 1000,
|
|
95
|
-
userInfo: [NSLocalizedDescriptionKey: "Database not initialized"]
|
|
96
|
-
)
|
|
97
|
-
}
|
|
98
140
|
|
|
99
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Persist a full `StorageValue` wrapper for `key` in `table`.
|
|
143
|
+
*
|
|
144
|
+
* The entire `wrapper` dictionary is JSON-encoded into the `value` column
|
|
145
|
+
* (perfect round-trip). `created`/`updated`/`expires` and the JSON of
|
|
146
|
+
* `tags`/`metadata` are extracted into their own columns for TTL + query.
|
|
147
|
+
* Values are bound parameters; only `table` is interpolated (pre-sanitized).
|
|
148
|
+
*/
|
|
149
|
+
@objc public func set(table: String, key: String, wrapper: [String: Any]) throws -> Bool {
|
|
150
|
+
guard let db = db else { throw notInitializedError() }
|
|
151
|
+
try ensureTable(table)
|
|
100
152
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
153
|
+
// Serialize the whole wrapper for round-trip storage.
|
|
154
|
+
let blob = try JSONSerialization.data(withJSONObject: wrapper, options: [])
|
|
155
|
+
|
|
156
|
+
// Extract mirror columns from the wrapper. created/updated are required
|
|
157
|
+
// by the wrapper contract; fall back to "now" defensively.
|
|
158
|
+
let now = Int64(Date().timeIntervalSince1970 * 1000)
|
|
159
|
+
let created = (wrapper["created"] as? NSNumber)?.int64Value ?? now
|
|
160
|
+
let updated = (wrapper["updated"] as? NSNumber)?.int64Value ?? now
|
|
161
|
+
let expires = (wrapper["expires"] as? NSNumber)?.int64Value
|
|
162
|
+
|
|
163
|
+
let tagsData: Data?
|
|
164
|
+
if let tags = wrapper["tags"] {
|
|
165
|
+
tagsData = try? JSONSerialization.data(withJSONObject: tags, options: [])
|
|
105
166
|
} else {
|
|
106
|
-
|
|
107
|
-
data = try JSONSerialization.data(withJSONObject: value, options: [])
|
|
167
|
+
tagsData = nil
|
|
108
168
|
}
|
|
109
|
-
|
|
110
|
-
let
|
|
111
|
-
let
|
|
112
|
-
|
|
113
|
-
|
|
169
|
+
|
|
170
|
+
let metadataData: Data?
|
|
171
|
+
if let metadata = wrapper["metadata"] {
|
|
172
|
+
metadataData = try? JSONSerialization.data(withJSONObject: metadata, options: [])
|
|
173
|
+
} else {
|
|
174
|
+
metadataData = nil
|
|
175
|
+
}
|
|
176
|
+
|
|
114
177
|
let insertSQL = """
|
|
115
|
-
INSERT OR REPLACE INTO \(
|
|
116
|
-
(key, value, created, updated, expires, tags, metadata)
|
|
178
|
+
INSERT OR REPLACE INTO \(table)
|
|
179
|
+
(key, value, created, updated, expires, tags, metadata)
|
|
117
180
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
118
181
|
"""
|
|
119
|
-
|
|
182
|
+
|
|
120
183
|
var statement: OpaquePointer?
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
184
|
+
defer { sqlite3_finalize(statement) }
|
|
185
|
+
|
|
186
|
+
guard sqlite3_prepare_v2(db, insertSQL, -1, &statement, nil) == SQLITE_OK else {
|
|
187
|
+
return false
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let ok =
|
|
191
|
+
sqlite3_bind_text(statement, 1, key, -1, SQLITE_TRANSIENT) == SQLITE_OK &&
|
|
192
|
+
blob.withUnsafeBytes { sqlite3_bind_blob(statement, 2, $0.baseAddress, Int32(blob.count), SQLITE_TRANSIENT) } == SQLITE_OK &&
|
|
193
|
+
sqlite3_bind_int64(statement, 3, created) == SQLITE_OK &&
|
|
194
|
+
sqlite3_bind_int64(statement, 4, updated) == SQLITE_OK &&
|
|
126
195
|
(expires != nil ? sqlite3_bind_int64(statement, 5, expires!) : sqlite3_bind_null(statement, 5)) == SQLITE_OK &&
|
|
127
|
-
(
|
|
128
|
-
(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return result
|
|
196
|
+
bindOptionalBlob(statement, 6, tagsData) == SQLITE_OK &&
|
|
197
|
+
bindOptionalBlob(statement, 7, metadataData) == SQLITE_OK
|
|
198
|
+
|
|
199
|
+
guard ok else { return false }
|
|
200
|
+
return sqlite3_step(statement) == SQLITE_DONE
|
|
133
201
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
202
|
+
|
|
203
|
+
/// Binds `data` as a blob to `index`, or NULL when `data` is nil.
|
|
204
|
+
private func bindOptionalBlob(_ statement: OpaquePointer?, _ index: Int32, _ data: Data?) -> Int32 {
|
|
205
|
+
guard let data = data else {
|
|
206
|
+
return sqlite3_bind_null(statement, index)
|
|
207
|
+
}
|
|
208
|
+
return data.withUnsafeBytes {
|
|
209
|
+
sqlite3_bind_blob(statement, index, $0.baseAddress, Int32(data.count), SQLITE_TRANSIENT)
|
|
210
|
+
}
|
|
138
211
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Read the wrapper object for `key` from `table`.
|
|
215
|
+
*
|
|
216
|
+
* Returns the decoded `StorageValue` wrapper dictionary, or `nil` for a
|
|
217
|
+
* missing row OR when stored bytes do not parse as a JSON object
|
|
218
|
+
* (legacy/corrupt data is treated as a miss, never a crash).
|
|
219
|
+
*/
|
|
220
|
+
@objc public func get(table: String, key: String) -> [String: Any]? {
|
|
221
|
+
guard let db = db else {
|
|
142
222
|
os_log("Database not initialized", log: logger, type: .error)
|
|
143
223
|
return nil
|
|
144
224
|
}
|
|
225
|
+
// Reading a table that was never created is a miss, not an error.
|
|
226
|
+
try? ensureTable(table)
|
|
145
227
|
|
|
146
|
-
let querySQL = "SELECT
|
|
228
|
+
let querySQL = "SELECT value FROM \(table) WHERE key = ? LIMIT 1"
|
|
147
229
|
var statement: OpaquePointer?
|
|
230
|
+
defer { sqlite3_finalize(statement) }
|
|
148
231
|
|
|
149
232
|
guard sqlite3_prepare_v2(db, querySQL, -1, &statement, nil) == SQLITE_OK,
|
|
150
|
-
sqlite3_bind_text(statement, 1, key, -1,
|
|
151
|
-
sqlite3_finalize(statement)
|
|
233
|
+
sqlite3_bind_text(statement, 1, key, -1, SQLITE_TRANSIENT) == SQLITE_OK else {
|
|
152
234
|
return nil
|
|
153
235
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
"created": created,
|
|
165
|
-
"updated": updated
|
|
166
|
-
]
|
|
167
|
-
|
|
168
|
-
if sqlite3_column_type(statement, 4) != SQLITE_NULL {
|
|
169
|
-
result!["expires"] = sqlite3_column_int64(statement, 4)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if sqlite3_column_type(statement, 5) != SQLITE_NULL {
|
|
173
|
-
let tagsData = Data(bytes: sqlite3_column_blob(statement, 5), count: Int(sqlite3_column_bytes(statement, 5)))
|
|
174
|
-
if let tags = try? JSONSerialization.jsonObject(with: tagsData, options: []) {
|
|
175
|
-
result!["tags"] = tags
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if sqlite3_column_type(statement, 6) != SQLITE_NULL {
|
|
180
|
-
let metadataData = Data(bytes: sqlite3_column_blob(statement, 6), count: Int(sqlite3_column_bytes(statement, 6)))
|
|
181
|
-
if let metadata = try? JSONSerialization.jsonObject(with: metadataData, options: []) {
|
|
182
|
-
result!["metadata"] = metadata
|
|
183
|
-
}
|
|
184
|
-
}
|
|
236
|
+
|
|
237
|
+
guard sqlite3_step(statement) == SQLITE_ROW,
|
|
238
|
+
let blob = sqlite3_column_blob(statement, 0) else {
|
|
239
|
+
return nil
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let valueData = Data(bytes: blob, count: Int(sqlite3_column_bytes(statement, 0)))
|
|
243
|
+
// The wrapper is always a JSON object. Anything else = legacy/corrupt → miss.
|
|
244
|
+
guard let wrapper = try? JSONSerialization.jsonObject(with: valueData, options: []) as? [String: Any] else {
|
|
245
|
+
return nil
|
|
185
246
|
}
|
|
186
|
-
|
|
187
|
-
sqlite3_finalize(statement)
|
|
188
|
-
return result
|
|
247
|
+
return wrapper
|
|
189
248
|
}
|
|
190
|
-
|
|
191
|
-
@objc public func remove(key: String) -> Bool {
|
|
192
|
-
guard db
|
|
249
|
+
|
|
250
|
+
@objc public func remove(table: String, key: String) -> Bool {
|
|
251
|
+
guard let db = db else {
|
|
193
252
|
os_log("Database not initialized", log: logger, type: .error)
|
|
194
253
|
return false
|
|
195
254
|
}
|
|
255
|
+
try? ensureTable(table)
|
|
196
256
|
|
|
197
|
-
let deleteSQL = "DELETE FROM \(
|
|
257
|
+
let deleteSQL = "DELETE FROM \(table) WHERE key = ?"
|
|
198
258
|
var statement: OpaquePointer?
|
|
259
|
+
defer { sqlite3_finalize(statement) }
|
|
199
260
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return result
|
|
261
|
+
guard sqlite3_prepare_v2(db, deleteSQL, -1, &statement, nil) == SQLITE_OK,
|
|
262
|
+
sqlite3_bind_text(statement, 1, key, -1, SQLITE_TRANSIENT) == SQLITE_OK else {
|
|
263
|
+
return false
|
|
264
|
+
}
|
|
265
|
+
return sqlite3_step(statement) == SQLITE_DONE
|
|
206
266
|
}
|
|
207
|
-
|
|
208
|
-
@objc public func clear(prefix: String? = nil) -> Bool {
|
|
209
|
-
guard db
|
|
267
|
+
|
|
268
|
+
@objc public func clear(table: String, prefix: String? = nil) -> Bool {
|
|
269
|
+
guard let db = db else {
|
|
210
270
|
os_log("Database not initialized", log: logger, type: .error)
|
|
211
271
|
return false
|
|
212
272
|
}
|
|
273
|
+
try? ensureTable(table)
|
|
213
274
|
|
|
214
275
|
let deleteSQL: String
|
|
215
|
-
if
|
|
216
|
-
deleteSQL = "DELETE FROM \(
|
|
276
|
+
if prefix != nil {
|
|
277
|
+
deleteSQL = "DELETE FROM \(table) WHERE key LIKE ?"
|
|
217
278
|
} else {
|
|
218
|
-
deleteSQL = "DELETE FROM \(
|
|
279
|
+
deleteSQL = "DELETE FROM \(table)"
|
|
219
280
|
}
|
|
220
281
|
|
|
221
282
|
var statement: OpaquePointer?
|
|
222
|
-
|
|
283
|
+
defer { sqlite3_finalize(statement) }
|
|
223
284
|
|
|
224
|
-
|
|
225
|
-
|
|
285
|
+
guard sqlite3_prepare_v2(db, deleteSQL, -1, &statement, nil) == SQLITE_OK else {
|
|
286
|
+
return false
|
|
226
287
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
288
|
+
if let prefix = prefix {
|
|
289
|
+
guard sqlite3_bind_text(statement, 1, "\(prefix)%", -1, SQLITE_TRANSIENT) == SQLITE_OK else {
|
|
290
|
+
return false
|
|
291
|
+
}
|
|
230
292
|
}
|
|
231
|
-
|
|
232
|
-
sqlite3_finalize(statement)
|
|
233
|
-
return result
|
|
293
|
+
return sqlite3_step(statement) == SQLITE_DONE
|
|
234
294
|
}
|
|
235
|
-
|
|
236
|
-
@objc public func keys(pattern: String? = nil) -> [String] {
|
|
237
|
-
guard db
|
|
295
|
+
|
|
296
|
+
@objc public func keys(table: String, pattern: String? = nil) -> [String] {
|
|
297
|
+
guard let db = db else {
|
|
238
298
|
os_log("Database not initialized", log: logger, type: .error)
|
|
239
299
|
return []
|
|
240
300
|
}
|
|
301
|
+
try? ensureTable(table)
|
|
241
302
|
|
|
242
303
|
let querySQL: String
|
|
243
|
-
if
|
|
244
|
-
querySQL = "SELECT key FROM \(
|
|
304
|
+
if pattern != nil {
|
|
305
|
+
querySQL = "SELECT key FROM \(table) WHERE key LIKE ?"
|
|
245
306
|
} else {
|
|
246
|
-
querySQL = "SELECT key FROM \(
|
|
307
|
+
querySQL = "SELECT key FROM \(table)"
|
|
247
308
|
}
|
|
248
309
|
|
|
249
310
|
var statement: OpaquePointer?
|
|
311
|
+
defer { sqlite3_finalize(statement) }
|
|
250
312
|
var keys: [String] = []
|
|
251
313
|
|
|
252
314
|
if sqlite3_prepare_v2(db, querySQL, -1, &statement, nil) == SQLITE_OK {
|
|
253
315
|
if let pattern = pattern {
|
|
254
|
-
//
|
|
255
|
-
sqlite3_bind_text(statement, 1, "%\(pattern)%", -1,
|
|
316
|
+
// `%pattern%` mirrors the contains-matching used by other backends.
|
|
317
|
+
sqlite3_bind_text(statement, 1, "%\(pattern)%", -1, SQLITE_TRANSIENT)
|
|
256
318
|
}
|
|
257
|
-
|
|
258
319
|
while sqlite3_step(statement) == SQLITE_ROW {
|
|
259
320
|
if let key = sqlite3_column_text(statement, 0) {
|
|
260
321
|
keys.append(String(cString: key))
|
|
261
322
|
}
|
|
262
323
|
}
|
|
263
324
|
}
|
|
264
|
-
|
|
265
|
-
sqlite3_finalize(statement)
|
|
266
325
|
return keys
|
|
267
326
|
}
|
|
268
|
-
|
|
269
|
-
@objc public func size() throws -> (total: Int, count: Int) {
|
|
270
|
-
guard db != nil else {
|
|
271
|
-
throw NSError(
|
|
272
|
-
domain: "StrataStorage.SQLiteStorage",
|
|
273
|
-
code: 1000,
|
|
274
|
-
userInfo: [NSLocalizedDescriptionKey: "Database not initialized"]
|
|
275
|
-
)
|
|
276
|
-
}
|
|
277
327
|
|
|
278
|
-
|
|
328
|
+
/**
|
|
329
|
+
* Aggregate sizes for `table`.
|
|
330
|
+
*
|
|
331
|
+
* Returns `count`, `total` (= `keys` + `values`), and per-segment byte
|
|
332
|
+
* sums: `keys` = SUM(LENGTH(key)), `values` = SUM(LENGTH(value)),
|
|
333
|
+
* `metadata` = SUM(LENGTH(metadata)) treating NULL as 0. The plugin decides
|
|
334
|
+
* whether to surface the `detailed` segment based on the `detailed` flag.
|
|
335
|
+
*/
|
|
336
|
+
@objc public func size(table: String) throws -> [String: Int] {
|
|
337
|
+
guard let db = db else { throw notInitializedError() }
|
|
338
|
+
try ensureTable(table)
|
|
339
|
+
|
|
340
|
+
let querySQL = """
|
|
341
|
+
SELECT COUNT(*),
|
|
342
|
+
COALESCE(SUM(LENGTH(key)), 0),
|
|
343
|
+
COALESCE(SUM(LENGTH(value)), 0),
|
|
344
|
+
COALESCE(SUM(LENGTH(metadata)), 0)
|
|
345
|
+
FROM \(table)
|
|
346
|
+
"""
|
|
279
347
|
var statement: OpaquePointer?
|
|
348
|
+
defer { sqlite3_finalize(statement) }
|
|
280
349
|
|
|
281
|
-
var totalSize = 0
|
|
282
350
|
var count = 0
|
|
351
|
+
var keysSize = 0
|
|
352
|
+
var valuesSize = 0
|
|
353
|
+
var metadataSize = 0
|
|
354
|
+
|
|
355
|
+
if sqlite3_prepare_v2(db, querySQL, -1, &statement, nil) == SQLITE_OK,
|
|
356
|
+
sqlite3_step(statement) == SQLITE_ROW {
|
|
357
|
+
count = Int(sqlite3_column_int(statement, 0))
|
|
358
|
+
keysSize = Int(sqlite3_column_int64(statement, 1))
|
|
359
|
+
valuesSize = Int(sqlite3_column_int64(statement, 2))
|
|
360
|
+
metadataSize = Int(sqlite3_column_int64(statement, 3))
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return [
|
|
364
|
+
"count": count,
|
|
365
|
+
"keys": keysSize,
|
|
366
|
+
"values": valuesSize,
|
|
367
|
+
"metadata": metadataSize,
|
|
368
|
+
"total": keysSize + valuesSize
|
|
369
|
+
]
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Returns every row as `{ key }`. The JS SqliteAdapter applies the real
|
|
374
|
+
* query `condition` by re-fetching and filtering each key, so this native
|
|
375
|
+
* side only needs to enumerate candidate keys. The `condition` argument is
|
|
376
|
+
* accepted for forward-compatibility but is not yet pushed down into SQL.
|
|
377
|
+
*/
|
|
378
|
+
@objc public func query(table: String, condition: [String: Any]) -> [[String: Any]] {
|
|
379
|
+
guard let db = db else {
|
|
380
|
+
os_log("Database not initialized", log: logger, type: .error)
|
|
381
|
+
return []
|
|
382
|
+
}
|
|
383
|
+
try? ensureTable(table)
|
|
384
|
+
|
|
385
|
+
let querySQL = "SELECT key FROM \(table)"
|
|
386
|
+
var statement: OpaquePointer?
|
|
387
|
+
defer { sqlite3_finalize(statement) }
|
|
388
|
+
var rows: [[String: Any]] = []
|
|
283
389
|
|
|
284
390
|
if sqlite3_prepare_v2(db, querySQL, -1, &statement, nil) == SQLITE_OK {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
391
|
+
while sqlite3_step(statement) == SQLITE_ROW {
|
|
392
|
+
guard let keyCString = sqlite3_column_text(statement, 0) else { continue }
|
|
393
|
+
rows.append(["key": String(cString: keyCString)])
|
|
288
394
|
}
|
|
289
395
|
}
|
|
396
|
+
return rows
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Caches one open `SQLiteStorage` (and thus one DB handle) per database
|
|
402
|
+
* filename, so the database is opened once per file instead of on every
|
|
403
|
+
* bridge call. Access is serialized with a lock because Capacitor may dispatch
|
|
404
|
+
* plugin calls on a background queue.
|
|
405
|
+
*/
|
|
406
|
+
final class SQLiteStorageManager {
|
|
407
|
+
static let shared = SQLiteStorageManager()
|
|
408
|
+
|
|
409
|
+
private var stores: [String: SQLiteStorage] = [:]
|
|
410
|
+
private let lock = NSLock()
|
|
290
411
|
|
|
291
|
-
|
|
292
|
-
|
|
412
|
+
private init() {}
|
|
413
|
+
|
|
414
|
+
/// Returns the cached store for `fileName` (e.g. `storage.db`), creating it
|
|
415
|
+
/// on first use. `fileName` MUST already be sanitized.
|
|
416
|
+
func store(forFile fileName: String) -> SQLiteStorage {
|
|
417
|
+
lock.lock()
|
|
418
|
+
defer { lock.unlock() }
|
|
419
|
+
if let existing = stores[fileName] {
|
|
420
|
+
return existing
|
|
421
|
+
}
|
|
422
|
+
let created = SQLiteStorage(dbName: fileName)
|
|
423
|
+
stores[fileName] = created
|
|
424
|
+
return created
|
|
293
425
|
}
|
|
294
|
-
}
|
|
426
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
#import <Capacitor/Capacitor.h>
|
|
3
|
+
|
|
4
|
+
// Registers the StrataStorage plugin and its methods with the Capacitor
|
|
5
|
+
// bridge. Without this macro block the @objc Swift methods are NOT exposed
|
|
6
|
+
// to the JavaScript layer and every call would fail with "method not
|
|
7
|
+
// implemented". The method names and the parameter list below MUST match
|
|
8
|
+
// the @objc func names in StrataStoragePlugin.swift and the JS plugin
|
|
9
|
+
// contract in src/plugin/definitions.ts.
|
|
10
|
+
CAP_PLUGIN(StrataStoragePlugin, "StrataStorage",
|
|
11
|
+
CAP_PLUGIN_METHOD(isAvailable, CAPPluginReturnPromise);
|
|
12
|
+
CAP_PLUGIN_METHOD(get, CAPPluginReturnPromise);
|
|
13
|
+
CAP_PLUGIN_METHOD(set, CAPPluginReturnPromise);
|
|
14
|
+
CAP_PLUGIN_METHOD(remove, CAPPluginReturnPromise);
|
|
15
|
+
CAP_PLUGIN_METHOD(clear, CAPPluginReturnPromise);
|
|
16
|
+
CAP_PLUGIN_METHOD(keys, CAPPluginReturnPromise);
|
|
17
|
+
CAP_PLUGIN_METHOD(size, CAPPluginReturnPromise);
|
|
18
|
+
CAP_PLUGIN_METHOD(query, CAPPluginReturnPromise);
|
|
19
|
+
CAP_PLUGIN_METHOD(getUserDefaults, CAPPluginReturnPromise);
|
|
20
|
+
CAP_PLUGIN_METHOD(setUserDefaults, CAPPluginReturnPromise);
|
|
21
|
+
CAP_PLUGIN_METHOD(getKeychain, CAPPluginReturnPromise);
|
|
22
|
+
CAP_PLUGIN_METHOD(setKeychain, CAPPluginReturnPromise);
|
|
23
|
+
)
|