strata-storage 2.5.0 → 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/android/AGENTS.md +24 -7
- package/android/CLAUDE.md +42 -4
- 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 +243 -221
- package/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +202 -78
- package/dist/android/AGENTS.md +24 -7
- package/dist/android/CLAUDE.md +42 -4
- 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 +243 -221
- package/dist/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +202 -78
- package/dist/ios/AGENTS.md +19 -4
- package/dist/ios/CLAUDE.md +39 -4
- package/dist/ios/Plugin/FilesystemStorage.swift +218 -0
- package/dist/ios/Plugin/SQLiteStorage.swift +265 -173
- package/dist/ios/Plugin/StrataStoragePlugin.swift +100 -42
- package/dist/package.json +1 -1
- package/ios/AGENTS.md +19 -4
- package/ios/CLAUDE.md +39 -4
- package/ios/Plugin/FilesystemStorage.swift +218 -0
- package/ios/Plugin/SQLiteStorage.swift +265 -173
- package/ios/Plugin/StrataStoragePlugin.swift +100 -42
- package/package.json +1 -1
|
@@ -8,10 +8,11 @@ import com.getcapacitor.PluginMethod;
|
|
|
8
8
|
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
9
9
|
import com.strata.storage.SharedPreferencesStorage;
|
|
10
10
|
import com.strata.storage.EncryptedStorage;
|
|
11
|
+
import com.strata.storage.FilesystemStorage;
|
|
11
12
|
import com.strata.storage.SQLiteStorage;
|
|
12
13
|
import android.util.Log;
|
|
13
14
|
import org.json.JSONArray;
|
|
14
|
-
import
|
|
15
|
+
import java.util.HashMap;
|
|
15
16
|
import java.util.List;
|
|
16
17
|
import java.util.Map;
|
|
17
18
|
|
|
@@ -19,15 +20,24 @@ import java.util.Map;
|
|
|
19
20
|
* Main Capacitor plugin for Strata Storage
|
|
20
21
|
* Coordinates between different storage types on Android.
|
|
21
22
|
*
|
|
22
|
-
* Storage types with a real native backend: preferences, secure, sqlite
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* Storage types with a real native backend: preferences, secure, sqlite,
|
|
24
|
+
* filesystem.
|
|
25
|
+
*
|
|
26
|
+
* SQLite supports multi-store: the `database` option selects the DB file and
|
|
27
|
+
* `table` selects the table within it. SQLite helper instances are cached per
|
|
28
|
+
* database name so the connection is not reopened per call.
|
|
25
29
|
*/
|
|
26
30
|
@CapacitorPlugin(name = "StrataStorage")
|
|
27
31
|
public class StrataStoragePlugin extends Plugin {
|
|
32
|
+
private static final String DEFAULT_DATABASE = "strata_storage";
|
|
33
|
+
private static final String DEFAULT_TABLE = "storage";
|
|
34
|
+
|
|
28
35
|
private SharedPreferencesStorage sharedPrefsStorage;
|
|
29
36
|
private EncryptedStorage encryptedStorage;
|
|
30
|
-
private
|
|
37
|
+
private FilesystemStorage filesystemStorage;
|
|
38
|
+
|
|
39
|
+
/** SQLite helper cache keyed by resolved database name (without ".db"). */
|
|
40
|
+
private final Map<String, SQLiteStorage> sqliteStores = new HashMap<>();
|
|
31
41
|
|
|
32
42
|
@Override
|
|
33
43
|
public void load() {
|
|
@@ -44,18 +54,43 @@ public class StrataStoragePlugin extends Plugin {
|
|
|
44
54
|
Log.e("StrataStorage", "Failed to initialize encrypted storage", e);
|
|
45
55
|
}
|
|
46
56
|
try {
|
|
47
|
-
|
|
57
|
+
filesystemStorage = new FilesystemStorage(getContext());
|
|
48
58
|
} catch (Exception e) {
|
|
49
|
-
Log.e("StrataStorage", "Failed to initialize
|
|
59
|
+
Log.e("StrataStorage", "Failed to initialize filesystem storage", e);
|
|
50
60
|
}
|
|
51
61
|
}
|
|
52
62
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
)
|
|
63
|
+
/**
|
|
64
|
+
* Sanitize a database name to a safe file stem ({@code ^[A-Za-z0-9_]+$})
|
|
65
|
+
* and return the resolved stem. The ".db" suffix is appended when opening.
|
|
66
|
+
*/
|
|
67
|
+
private static String sanitizeDatabase(String database) {
|
|
68
|
+
if (database == null || database.isEmpty()) {
|
|
69
|
+
return DEFAULT_DATABASE;
|
|
70
|
+
}
|
|
71
|
+
String cleaned = database.replaceAll("[^A-Za-z0-9_]", "");
|
|
72
|
+
return cleaned.isEmpty() ? DEFAULT_DATABASE : cleaned;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Return (creating + caching on first use) the SQLite helper for the given
|
|
77
|
+
* database name. The cache is keyed by the sanitized DB stem so the
|
|
78
|
+
* connection is reused across bridge calls. May return {@code null} if the
|
|
79
|
+
* helper cannot be constructed.
|
|
80
|
+
*/
|
|
81
|
+
private synchronized SQLiteStorage getSqliteStore(String database) {
|
|
82
|
+
String dbStem = sanitizeDatabase(database);
|
|
83
|
+
SQLiteStorage store = sqliteStores.get(dbStem);
|
|
84
|
+
if (store == null) {
|
|
85
|
+
try {
|
|
86
|
+
store = new SQLiteStorage(getContext(), dbStem + ".db");
|
|
87
|
+
sqliteStores.put(dbStem, store);
|
|
88
|
+
} catch (Exception e) {
|
|
89
|
+
Log.e("StrataStorage", "Failed to open SQLite database " + dbStem, e);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return store;
|
|
59
94
|
}
|
|
60
95
|
|
|
61
96
|
/**
|
|
@@ -71,11 +106,10 @@ public class StrataStoragePlugin extends Plugin {
|
|
|
71
106
|
available = encryptedStorage != null;
|
|
72
107
|
break;
|
|
73
108
|
case "sqlite":
|
|
74
|
-
available =
|
|
109
|
+
available = getSqliteStore(call.getString("database", DEFAULT_DATABASE)) != null;
|
|
75
110
|
break;
|
|
76
111
|
case "filesystem":
|
|
77
|
-
|
|
78
|
-
available = false;
|
|
112
|
+
available = filesystemStorage != null;
|
|
79
113
|
break;
|
|
80
114
|
case "preferences":
|
|
81
115
|
default:
|
|
@@ -89,7 +123,10 @@ public class StrataStoragePlugin extends Plugin {
|
|
|
89
123
|
}
|
|
90
124
|
|
|
91
125
|
/**
|
|
92
|
-
* Get value from storage
|
|
126
|
+
* Get value from storage.
|
|
127
|
+
*
|
|
128
|
+
* For sqlite/filesystem the resolved `value` is the full wrapper object
|
|
129
|
+
* (parsed back from JSON). A miss resolves `value` = null.
|
|
93
130
|
*/
|
|
94
131
|
@PluginMethod
|
|
95
132
|
public void get(PluginCall call) {
|
|
@@ -102,46 +139,70 @@ public class StrataStoragePlugin extends Plugin {
|
|
|
102
139
|
String storage = call.getString("storage", "preferences");
|
|
103
140
|
|
|
104
141
|
try {
|
|
105
|
-
Object value = null;
|
|
106
|
-
|
|
107
142
|
switch (storage) {
|
|
108
|
-
case "secure":
|
|
143
|
+
case "secure": {
|
|
109
144
|
if (encryptedStorage == null) {
|
|
110
145
|
call.reject("Encrypted storage not available");
|
|
111
146
|
return;
|
|
112
147
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
148
|
+
resolveValue(call, encryptedStorage.get(key));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
case "sqlite": {
|
|
152
|
+
SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
|
|
153
|
+
if (store == null) {
|
|
117
154
|
call.reject("SQLite storage not available");
|
|
118
155
|
return;
|
|
119
156
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
case "filesystem":
|
|
123
|
-
rejectUnsupportedFilesystem(call);
|
|
157
|
+
String table = call.getString("table", DEFAULT_TABLE);
|
|
158
|
+
resolveValue(call, store.get(table, key));
|
|
124
159
|
return;
|
|
160
|
+
}
|
|
161
|
+
case "filesystem": {
|
|
162
|
+
if (filesystemStorage == null) {
|
|
163
|
+
call.reject("Filesystem storage not available");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
resolveValue(call, filesystemStorage.get(key));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
125
169
|
case "preferences":
|
|
126
|
-
default:
|
|
170
|
+
default: {
|
|
127
171
|
if (sharedPrefsStorage == null) {
|
|
128
172
|
call.reject("Preferences storage not available");
|
|
129
173
|
return;
|
|
130
174
|
}
|
|
131
|
-
|
|
132
|
-
|
|
175
|
+
resolveValue(call, sharedPrefsStorage.get(key));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
133
178
|
}
|
|
134
|
-
|
|
135
|
-
JSObject result = new JSObject();
|
|
136
|
-
result.put("value", value);
|
|
137
|
-
call.resolve(result);
|
|
138
179
|
} catch (Exception e) {
|
|
139
180
|
call.reject("Failed to get value", e);
|
|
140
181
|
}
|
|
141
182
|
}
|
|
142
183
|
|
|
143
184
|
/**
|
|
144
|
-
*
|
|
185
|
+
* Resolve a get() call, mapping a Java {@code null} to a JS {@code null}
|
|
186
|
+
* value so the TS adapters treat it as a miss.
|
|
187
|
+
*/
|
|
188
|
+
private void resolveValue(PluginCall call, Object value) {
|
|
189
|
+
JSObject result = new JSObject();
|
|
190
|
+
if (value == null) {
|
|
191
|
+
result.put("value", JSObject.NULL);
|
|
192
|
+
} else {
|
|
193
|
+
result.put("value", value);
|
|
194
|
+
}
|
|
195
|
+
call.resolve(result);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Set value in storage.
|
|
200
|
+
*
|
|
201
|
+
* For sqlite/filesystem the `value` is the FULL wrapper object
|
|
202
|
+
* ({ value, created, updated, expires?, tags?, metadata? }); it is read via
|
|
203
|
+
* call.getObject("value") and stored verbatim (with TTL/query columns
|
|
204
|
+
* extracted for sqlite). For preferences/secure the raw value is forwarded
|
|
205
|
+
* unchanged.
|
|
145
206
|
*/
|
|
146
207
|
@PluginMethod
|
|
147
208
|
public void set(PluginCall call) {
|
|
@@ -151,7 +212,6 @@ public class StrataStoragePlugin extends Plugin {
|
|
|
151
212
|
return;
|
|
152
213
|
}
|
|
153
214
|
|
|
154
|
-
Object value = call.getData().opt("value");
|
|
155
215
|
String storage = call.getString("storage", "preferences");
|
|
156
216
|
|
|
157
217
|
try {
|
|
@@ -162,25 +222,42 @@ public class StrataStoragePlugin extends Plugin {
|
|
|
162
222
|
call.reject("Encrypted storage not available");
|
|
163
223
|
return;
|
|
164
224
|
}
|
|
165
|
-
ok = encryptedStorage.set(key, value);
|
|
225
|
+
ok = encryptedStorage.set(key, call.getData().opt("value"));
|
|
166
226
|
break;
|
|
167
|
-
case "sqlite":
|
|
168
|
-
|
|
227
|
+
case "sqlite": {
|
|
228
|
+
SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
|
|
229
|
+
if (store == null) {
|
|
169
230
|
call.reject("SQLite storage not available");
|
|
170
231
|
return;
|
|
171
232
|
}
|
|
172
|
-
|
|
233
|
+
JSObject wrapper = call.getObject("value");
|
|
234
|
+
if (wrapper == null) {
|
|
235
|
+
call.reject("A wrapper object 'value' is required for sqlite storage");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
ok = store.set(call.getString("table", DEFAULT_TABLE), key, wrapper);
|
|
173
239
|
break;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
240
|
+
}
|
|
241
|
+
case "filesystem": {
|
|
242
|
+
if (filesystemStorage == null) {
|
|
243
|
+
call.reject("Filesystem storage not available");
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
JSObject wrapper = call.getObject("value");
|
|
247
|
+
if (wrapper == null) {
|
|
248
|
+
call.reject("A wrapper object 'value' is required for filesystem storage");
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
ok = filesystemStorage.set(key, wrapper);
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
177
254
|
case "preferences":
|
|
178
255
|
default:
|
|
179
256
|
if (sharedPrefsStorage == null) {
|
|
180
257
|
call.reject("Preferences storage not available");
|
|
181
258
|
return;
|
|
182
259
|
}
|
|
183
|
-
ok = sharedPrefsStorage.set(key, value);
|
|
260
|
+
ok = sharedPrefsStorage.set(key, call.getData().opt("value"));
|
|
184
261
|
break;
|
|
185
262
|
}
|
|
186
263
|
|
|
@@ -216,16 +293,22 @@ public class StrataStoragePlugin extends Plugin {
|
|
|
216
293
|
}
|
|
217
294
|
encryptedStorage.remove(key);
|
|
218
295
|
break;
|
|
219
|
-
case "sqlite":
|
|
220
|
-
|
|
296
|
+
case "sqlite": {
|
|
297
|
+
SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
|
|
298
|
+
if (store == null) {
|
|
221
299
|
call.reject("SQLite storage not available");
|
|
222
300
|
return;
|
|
223
301
|
}
|
|
224
|
-
|
|
302
|
+
store.remove(call.getString("table", DEFAULT_TABLE), key);
|
|
225
303
|
break;
|
|
304
|
+
}
|
|
226
305
|
case "filesystem":
|
|
227
|
-
|
|
228
|
-
|
|
306
|
+
if (filesystemStorage == null) {
|
|
307
|
+
call.reject("Filesystem storage not available");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
filesystemStorage.remove(key);
|
|
311
|
+
break;
|
|
229
312
|
case "preferences":
|
|
230
313
|
default:
|
|
231
314
|
if (sharedPrefsStorage == null) {
|
|
@@ -263,16 +346,22 @@ public class StrataStoragePlugin extends Plugin {
|
|
|
263
346
|
}
|
|
264
347
|
encryptedStorage.clear(prefix);
|
|
265
348
|
break;
|
|
266
|
-
case "sqlite":
|
|
267
|
-
|
|
349
|
+
case "sqlite": {
|
|
350
|
+
SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
|
|
351
|
+
if (store == null) {
|
|
268
352
|
call.reject("SQLite storage not available");
|
|
269
353
|
return;
|
|
270
354
|
}
|
|
271
|
-
|
|
355
|
+
store.clear(call.getString("table", DEFAULT_TABLE), prefix);
|
|
272
356
|
break;
|
|
357
|
+
}
|
|
273
358
|
case "filesystem":
|
|
274
|
-
|
|
275
|
-
|
|
359
|
+
if (filesystemStorage == null) {
|
|
360
|
+
call.reject("Filesystem storage not available");
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
filesystemStorage.clear(prefix);
|
|
364
|
+
break;
|
|
276
365
|
case "preferences":
|
|
277
366
|
default:
|
|
278
367
|
if (sharedPrefsStorage == null) {
|
|
@@ -298,7 +387,7 @@ public class StrataStoragePlugin extends Plugin {
|
|
|
298
387
|
String pattern = call.getString("pattern");
|
|
299
388
|
|
|
300
389
|
try {
|
|
301
|
-
List<String> keys
|
|
390
|
+
List<String> keys;
|
|
302
391
|
|
|
303
392
|
switch (storage) {
|
|
304
393
|
case "secure":
|
|
@@ -308,16 +397,22 @@ public class StrataStoragePlugin extends Plugin {
|
|
|
308
397
|
}
|
|
309
398
|
keys = encryptedStorage.keys(pattern);
|
|
310
399
|
break;
|
|
311
|
-
case "sqlite":
|
|
312
|
-
|
|
400
|
+
case "sqlite": {
|
|
401
|
+
SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
|
|
402
|
+
if (store == null) {
|
|
313
403
|
call.reject("SQLite storage not available");
|
|
314
404
|
return;
|
|
315
405
|
}
|
|
316
|
-
keys =
|
|
406
|
+
keys = store.keys(call.getString("table", DEFAULT_TABLE), pattern);
|
|
317
407
|
break;
|
|
408
|
+
}
|
|
318
409
|
case "filesystem":
|
|
319
|
-
|
|
320
|
-
|
|
410
|
+
if (filesystemStorage == null) {
|
|
411
|
+
call.reject("Filesystem storage not available");
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
keys = filesystemStorage.keys(pattern);
|
|
415
|
+
break;
|
|
321
416
|
case "preferences":
|
|
322
417
|
default:
|
|
323
418
|
if (sharedPrefsStorage == null) {
|
|
@@ -337,17 +432,21 @@ public class StrataStoragePlugin extends Plugin {
|
|
|
337
432
|
}
|
|
338
433
|
|
|
339
434
|
/**
|
|
340
|
-
* Get storage size information
|
|
435
|
+
* Get storage size information.
|
|
436
|
+
*
|
|
437
|
+
* When `detailed` is true the result also carries a byte breakdown
|
|
438
|
+
* { detailed: { keys, values, metadata } }.
|
|
341
439
|
*/
|
|
342
440
|
@PluginMethod
|
|
343
441
|
public void size(PluginCall call) {
|
|
344
442
|
String storage = call.getString("storage", "preferences");
|
|
443
|
+
boolean detailed = Boolean.TRUE.equals(call.getBoolean("detailed", false));
|
|
345
444
|
|
|
346
445
|
try {
|
|
347
446
|
JSObject result = new JSObject();
|
|
348
447
|
|
|
349
448
|
switch (storage) {
|
|
350
|
-
case "secure":
|
|
449
|
+
case "secure": {
|
|
351
450
|
if (encryptedStorage == null) {
|
|
352
451
|
call.reject("Encrypted storage not available");
|
|
353
452
|
return;
|
|
@@ -356,20 +455,29 @@ public class StrataStoragePlugin extends Plugin {
|
|
|
356
455
|
result.put("total", encryptedSizeInfo.total);
|
|
357
456
|
result.put("count", encryptedSizeInfo.count);
|
|
358
457
|
break;
|
|
359
|
-
|
|
360
|
-
|
|
458
|
+
}
|
|
459
|
+
case "sqlite": {
|
|
460
|
+
SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
|
|
461
|
+
if (store == null) {
|
|
361
462
|
call.reject("SQLite storage not available");
|
|
362
463
|
return;
|
|
363
464
|
}
|
|
364
|
-
SQLiteStorage.SizeInfo sqliteSizeInfo =
|
|
365
|
-
|
|
366
|
-
result
|
|
465
|
+
SQLiteStorage.SizeInfo sqliteSizeInfo =
|
|
466
|
+
store.size(call.getString("table", DEFAULT_TABLE), detailed);
|
|
467
|
+
putSizeInfo(result, sqliteSizeInfo);
|
|
367
468
|
break;
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
469
|
+
}
|
|
470
|
+
case "filesystem": {
|
|
471
|
+
if (filesystemStorage == null) {
|
|
472
|
+
call.reject("Filesystem storage not available");
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
SQLiteStorage.SizeInfo fsSizeInfo = filesystemStorage.size(detailed);
|
|
476
|
+
putSizeInfo(result, fsSizeInfo);
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
371
479
|
case "preferences":
|
|
372
|
-
default:
|
|
480
|
+
default: {
|
|
373
481
|
if (sharedPrefsStorage == null) {
|
|
374
482
|
call.reject("Preferences storage not available");
|
|
375
483
|
return;
|
|
@@ -378,6 +486,7 @@ public class StrataStoragePlugin extends Plugin {
|
|
|
378
486
|
result.put("total", prefsSizeInfo.total);
|
|
379
487
|
result.put("count", prefsSizeInfo.count);
|
|
380
488
|
break;
|
|
489
|
+
}
|
|
381
490
|
}
|
|
382
491
|
|
|
383
492
|
call.resolve(result);
|
|
@@ -386,12 +495,27 @@ public class StrataStoragePlugin extends Plugin {
|
|
|
386
495
|
}
|
|
387
496
|
}
|
|
388
497
|
|
|
498
|
+
/**
|
|
499
|
+
* Populate a size result, including the detailed byte breakdown when the
|
|
500
|
+
* {@link SQLiteStorage.SizeInfo} carries one.
|
|
501
|
+
*/
|
|
502
|
+
private void putSizeInfo(JSObject result, SQLiteStorage.SizeInfo info) {
|
|
503
|
+
result.put("total", info.total);
|
|
504
|
+
result.put("count", info.count);
|
|
505
|
+
if (info.detailed) {
|
|
506
|
+
JSObject breakdown = new JSObject();
|
|
507
|
+
breakdown.put("keys", info.keysBytes);
|
|
508
|
+
breakdown.put("values", info.valuesBytes);
|
|
509
|
+
breakdown.put("metadata", info.metadataBytes);
|
|
510
|
+
result.put("detailed", breakdown);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
389
514
|
/**
|
|
390
515
|
* Query SQLite-backed storage.
|
|
391
516
|
* Matches the optional `query` method in the JS contract:
|
|
392
|
-
* resolves { results: [{ key
|
|
393
|
-
*
|
|
394
|
-
* rows.
|
|
517
|
+
* resolves { results: [{ key }] }. The JS SqliteAdapter applies the real
|
|
518
|
+
* condition by re-fetching/filtering, so this enumerates candidate keys.
|
|
395
519
|
*/
|
|
396
520
|
@PluginMethod
|
|
397
521
|
public void query(PluginCall call) {
|
|
@@ -400,18 +524,18 @@ public class StrataStoragePlugin extends Plugin {
|
|
|
400
524
|
call.reject("Query is only supported for the 'sqlite' storage type");
|
|
401
525
|
return;
|
|
402
526
|
}
|
|
403
|
-
|
|
527
|
+
SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
|
|
528
|
+
if (store == null) {
|
|
404
529
|
call.reject("SQLite storage not available");
|
|
405
530
|
return;
|
|
406
531
|
}
|
|
407
532
|
|
|
408
533
|
try {
|
|
409
|
-
List<Map<String, Object>> rows =
|
|
534
|
+
List<Map<String, Object>> rows = store.query(call.getString("table", DEFAULT_TABLE));
|
|
410
535
|
JSArray results = new JSArray();
|
|
411
536
|
for (Map<String, Object> row : rows) {
|
|
412
537
|
JSObject obj = new JSObject();
|
|
413
538
|
obj.put("key", row.get("key"));
|
|
414
|
-
obj.put("value", row.get("value"));
|
|
415
539
|
results.put(obj);
|
|
416
540
|
}
|
|
417
541
|
JSObject result = new JSObject();
|
package/dist/ios/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AGENTS.md — ios/
|
|
2
2
|
|
|
3
|
-
Last Updated: 2026-
|
|
3
|
+
Last Updated: 2026-05-27
|
|
4
4
|
|
|
5
5
|
> Agent instructions for iOS native development.
|
|
6
6
|
|
|
@@ -11,7 +11,16 @@ Last Updated: 2026-04-03
|
|
|
11
11
|
| `Plugin/StrataStoragePlugin.swift` | Capacitor plugin entry |
|
|
12
12
|
| `Plugin/UserDefaultsStorage.swift` | General key-value storage |
|
|
13
13
|
| `Plugin/KeychainStorage.swift` | Secure storage |
|
|
14
|
-
| `Plugin/SQLiteStorage.swift` |
|
|
14
|
+
| `Plugin/SQLiteStorage.swift` | SQLite database storage — multi-store (v2.6.0), `database`/`table` options honoured, full `StorageValue` wrapper round-trip, `size(detailed)` supported |
|
|
15
|
+
| `Plugin/FilesystemStorage.swift` | File-per-key native storage under `NSDocumentsDirectory/strata_storage/` (new in v2.6.0); atomic writes via staging rename; `isAvailable()` returns `true` |
|
|
16
|
+
|
|
17
|
+
## v2.6.0 Notes
|
|
18
|
+
|
|
19
|
+
- **SQLite multi-store:** `database` option → distinct `.db` file. `table` option → sanitised table identifier `[A-Za-z0-9_]`. Previously both were ignored and all adapters shared one table.
|
|
20
|
+
- **SQLite value shape:** `get` now returns the full `StorageValue` wrapper (`value`, `created`, `updated`, `expires`, `tags`, `metadata`). Corrupt rows are treated as a miss.
|
|
21
|
+
- **SQLite bind safety:** text/blob binds use `SQLITE_TRANSIENT` — removes a latent use-after-free for transient Swift buffers.
|
|
22
|
+
- **FilesystemStorage.swift:** new file. Writes are atomic (staging temp → rename). `keys()` excludes `.staging/` artifacts. `size(detailed: true)` returns `{ keys, values, metadata }`.
|
|
23
|
+
- **Pending on-device verification** — see `docs/guides/platforms/device-verification.md`.
|
|
15
24
|
|
|
16
25
|
## Agent Rules
|
|
17
26
|
|
|
@@ -20,14 +29,20 @@ Last Updated: 2026-04-03
|
|
|
20
29
|
- NEVER store secrets in `UserDefaultsStorage`
|
|
21
30
|
|
|
22
31
|
### SQL Safety
|
|
23
|
-
- ALWAYS use parameterized queries in SQLiteStorage
|
|
32
|
+
- ALWAYS use parameterized queries in `SQLiteStorage`
|
|
24
33
|
- NEVER string-interpolate SQL values
|
|
34
|
+
- Use `SQLITE_TRANSIENT` for text/blob binds
|
|
35
|
+
|
|
36
|
+
### Filesystem Safety
|
|
37
|
+
- NEVER read from `strata_storage/.staging/` — staging files are transient
|
|
38
|
+
- Key sanitisation must be consistent between `set`, `get`, `remove`, and `keys`
|
|
25
39
|
|
|
26
40
|
### Plugin Architecture
|
|
27
41
|
- Methods exposed through `StrataStoragePlugin.swift`
|
|
28
42
|
- Each storage backend is a separate Swift file
|
|
29
43
|
- Bridges Capacitor JS calls to native Swift
|
|
44
|
+
- The `CAP_PLUGIN` macro must include every callable method name
|
|
30
45
|
|
|
31
46
|
### Before Modifying
|
|
32
47
|
- Understand Capacitor plugin protocol
|
|
33
|
-
- Test on iOS simulator after changes
|
|
48
|
+
- Test on iOS simulator after changes; follow device-verification guide
|
package/dist/ios/CLAUDE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CLAUDE.md — ios/
|
|
2
2
|
|
|
3
|
-
Last Updated: 2026-
|
|
3
|
+
Last Updated: 2026-05-27
|
|
4
4
|
|
|
5
5
|
## iOS Native Plugin
|
|
6
6
|
|
|
@@ -13,7 +13,8 @@ Swift implementation of native storage backends for Capacitor.
|
|
|
13
13
|
| `Plugin/StrataStoragePlugin.swift` | Main Capacitor plugin entry point |
|
|
14
14
|
| `Plugin/UserDefaultsStorage.swift` | UserDefaults-based general storage |
|
|
15
15
|
| `Plugin/KeychainStorage.swift` | Keychain-based secure storage |
|
|
16
|
-
| `Plugin/SQLiteStorage.swift` | SQLite database storage (
|
|
16
|
+
| `Plugin/SQLiteStorage.swift` | SQLite database storage (multi-store, v2.6.0) |
|
|
17
|
+
| `Plugin/FilesystemStorage.swift` | File-per-key storage under `NSDocumentsDirectory/strata_storage/` (new in v2.6.0) |
|
|
17
18
|
|
|
18
19
|
### Configuration
|
|
19
20
|
|
|
@@ -21,6 +22,30 @@ Swift implementation of native storage backends for Capacitor.
|
|
|
21
22
|
- Minimum iOS version: defined in podspec
|
|
22
23
|
- Language: Swift
|
|
23
24
|
|
|
25
|
+
## v2.6.0 Changes
|
|
26
|
+
|
|
27
|
+
### SQLite multi-store
|
|
28
|
+
`SQLiteStorage.swift` now accepts `database` and `table` parameters from the
|
|
29
|
+
Capacitor call options. Each unique `(database, table)` pair opens a separate
|
|
30
|
+
`.db` file in the app's documents directory. Table identifiers are sanitised to
|
|
31
|
+
`[A-Za-z0-9_]` before use in SQL. The full `StorageValue` wrapper is serialised
|
|
32
|
+
to JSON and stored in a single `value` column; native `get` deserialises and
|
|
33
|
+
returns the full wrapper so TTL, tags, and metadata survive the round-trip.
|
|
34
|
+
Text/blob binds use `SQLITE_TRANSIENT` (removes a latent use-after-free for
|
|
35
|
+
transient Swift buffers). `size(detailed: true)` returns per-column byte
|
|
36
|
+
breakdown.
|
|
37
|
+
|
|
38
|
+
### FilesystemStorage.swift (new)
|
|
39
|
+
Stores each key as `NSDocumentsDirectory/strata_storage/<sanitised-key>.json`.
|
|
40
|
+
Writes are atomic: the value is first written to
|
|
41
|
+
`strata_storage/.staging/<key>.tmp`, then renamed into place. Temp files live in
|
|
42
|
+
the staging subdirectory, so they are never exposed by `keys()` and are not
|
|
43
|
+
deleted by a targeted `remove()`. `isAvailable()` returns `true` on device.
|
|
44
|
+
`size(detailed: true)` supported.
|
|
45
|
+
|
|
46
|
+
> **Pending on-device verification** — native code is complete and reviewed; see
|
|
47
|
+
> `docs/guides/platforms/device-verification.md` for the test matrix.
|
|
48
|
+
|
|
24
49
|
## Rules
|
|
25
50
|
|
|
26
51
|
### Security (IRON-SOLID)
|
|
@@ -32,15 +57,25 @@ Swift implementation of native storage backends for Capacitor.
|
|
|
32
57
|
- All native methods are exposed through `StrataStoragePlugin.swift`
|
|
33
58
|
- Plugin bridges Capacitor JS calls to native Swift implementations
|
|
34
59
|
- Each storage backend is a separate Swift file
|
|
60
|
+
- The `CAP_PLUGIN` macro must list every callable method; missing entries cause
|
|
61
|
+
silent `undefined` returns on the JS side
|
|
35
62
|
|
|
36
63
|
### SQLite
|
|
37
|
-
- `SQLiteStorage.swift`
|
|
64
|
+
- `SQLiteStorage.swift` opens or creates the `.db` file identified by the
|
|
65
|
+
`database` call option (default `strata_storage.db`)
|
|
38
66
|
- Handles table creation, migrations, and CRUD operations
|
|
39
67
|
- Use parameterized queries — NEVER string-interpolate SQL
|
|
68
|
+
- Use `SQLITE_TRANSIENT` for text/blob binds
|
|
69
|
+
|
|
70
|
+
### Filesystem
|
|
71
|
+
- Key strings are sanitised before use as filenames (`/` → `_`, etc.)
|
|
72
|
+
- Staging renames ensure atomic writes; never read from `.staging/`
|
|
73
|
+
- `keys()` lists `.json` files only, ignoring staging artifacts
|
|
40
74
|
|
|
41
75
|
### Testing
|
|
42
76
|
- Native code is tested through the Capacitor bridge
|
|
43
|
-
- Test via the demo app on iOS simulator
|
|
77
|
+
- Test via the demo app on iOS simulator following
|
|
78
|
+
`docs/guides/platforms/device-verification.md`
|
|
44
79
|
- Verify all adapter methods work end-to-end
|
|
45
80
|
|
|
46
81
|
### Before Modifying
|