@_nazmiforreal/flutter-ota 0.1.12 → 0.1.13
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 +1 -1
- package/bin/flutter-ota-linux-x64 +0 -0
- package/dart-src/packages/cli-tools/.dart_tool/package_config.json +46 -76
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +140 -166
- package/dart-src/packages/cli-tools/README.md +1 -1
- package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +30 -17
- package/dart-src/packages/cli-tools/lib/src/backend.dart +30 -31
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +3 -1
- package/dart-src/packages/cli-tools/lib/src/commands/build.dart +44 -43
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +265 -239
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +75 -66
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +39 -35
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +30 -27
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +88 -72
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +66 -29
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +10 -6
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +290 -181
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +24 -23
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +111 -97
- package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +237 -0
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +43 -34
- package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +57 -53
- package/dart-src/packages/cli-tools/lib/src/config.dart +227 -93
- package/dart-src/packages/cli-tools/lib/src/operations.dart +9 -8
- package/dart-src/packages/cli-tools/lib/src/pack.dart +26 -25
- package/dart-src/packages/cli-tools/lib/src/pocketbase/data_bootstrap.dart +39 -0
- package/dart-src/packages/cli-tools/lib/src/pocketbase/hooks/bundles.pb.js +56 -0
- package/dart-src/packages/cli-tools/lib/src/pocketbase/installer.dart +189 -0
- package/dart-src/packages/cli-tools/lib/src/pocketbase/process_manager.dart +123 -0
- package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +237 -0
- package/dart-src/packages/cli-tools/lib/src/runner.dart +36 -15
- package/dart-src/packages/cli-tools/lib/src/sign.dart +13 -5
- package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +10 -13
- package/dart-src/packages/cli-tools/lib/src/util.dart +3 -1
- package/dart-src/packages/cli-tools/pubspec.lock +69 -116
- package/dart-src/packages/cli-tools/pubspec.yaml +25 -7
- package/dart-src/packages/cli-tools/test/backends_test.dart +62 -32
- package/dart-src/packages/cli-tools/test/build_pack_test.dart +4 -4
- package/dart-src/packages/cli-tools/test/cli_test.dart +89 -42
- package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +8 -13
- package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +23 -18
- package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +197 -0
- package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +70 -46
- package/dart-src/packages/cli-tools/test/pocketbase_installer_test.dart +72 -0
- package/dart-src/packages/cli-tools/test/pocketbase_schema_test.dart +107 -0
- package/dart-src/packages/cli-tools/test/pocketbase_smoke_test.dart +59 -0
- package/dart-src/packages/core/README.md +2 -2
- package/dart-src/packages/core/example/lib/main.dart +69 -0
- package/dart-src/packages/core/lib/flutter_ota_kit_core.dart +1 -1
- package/dart-src/packages/core/lib/src/app_update_info.dart +17 -15
- package/dart-src/packages/core/lib/src/bundle.dart +102 -94
- package/dart-src/packages/core/lib/src/bundle_artifacts.dart +1 -2
- package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +15 -16
- package/dart-src/packages/core/lib/src/changed_asset.dart +24 -22
- package/dart-src/packages/core/lib/src/get_bundles_args.dart +13 -13
- package/dart-src/packages/core/lib/src/metadata.dart +9 -7
- package/dart-src/packages/core/lib/src/patch_info.dart +8 -7
- package/dart-src/packages/core/lib/src/platform.dart +4 -3
- package/dart-src/packages/core/lib/src/rollout.dart +14 -11
- package/dart-src/packages/core/lib/src/semver_range.dart +49 -39
- package/dart-src/packages/core/lib/src/semver_version.dart +8 -7
- package/dart-src/packages/core/lib/src/status.dart +4 -3
- package/dart-src/packages/core/lib/src/update_bundle_params.dart +7 -7
- package/dart-src/packages/core/lib/src/uuid.dart +1 -2
- package/dart-src/packages/core/pubspec.yaml +6 -7
- package/dart-src/packages/core/test/rollout_test.dart +15 -8
- package/dart-src/packages/core/test/semver_test.dart +5 -2
- package/dart-src/packages/core/test/types_test.dart +26 -24
- package/dart-src/packages/core/test/uuid_test.dart +3 -1
- package/dart-src/packages/core/tool/dbg.dart +1 -0
- package/dart-src/plugins/aws/CHANGELOG.md +6 -0
- package/dart-src/plugins/aws/example/lib/main.dart +60 -0
- package/dart-src/plugins/aws/lib/flutter_ota_kit_aws.dart +5 -1
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +11 -16
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +7 -2
- package/dart-src/plugins/aws/lib/src/aws_database.dart +14 -18
- package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +23 -18
- package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +5 -4
- package/dart-src/plugins/aws/lib/src/aws_storage.dart +2 -3
- package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +10 -16
- package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +6 -9
- package/dart-src/plugins/aws/pubspec.yaml +15 -10
- package/dart-src/plugins/aws/test/aws_database_test.dart +51 -57
- package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +8 -13
- package/dart-src/plugins/cloudflare/CHANGELOG.md +7 -0
- package/dart-src/plugins/cloudflare/example/lib/main.dart +62 -0
- package/dart-src/plugins/cloudflare/lib/flutter_ota_kit_cloudflare.dart +4 -2
- package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +3 -3
- package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +3 -2
- package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +24 -27
- package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +22 -30
- package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +21 -29
- package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +7 -7
- package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +7 -15
- package/dart-src/plugins/cloudflare/pubspec.yaml +14 -9
- package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +46 -42
- package/dart-src/plugins/cloudflare/test/d1_database_test.dart +100 -90
- package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +23 -18
- package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +8 -13
- package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +1 -4
- package/dart-src/plugins/plugin-core/CHANGELOG.md +16 -0
- package/dart-src/plugins/plugin-core/example/lib/main.dart +61 -0
- package/dart-src/plugins/plugin-core/lib/flutter_ota_kit_plugin_core.dart +3 -7
- package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +5 -5
- package/dart-src/plugins/plugin-core/lib/src/bundle_helpers.dart +23 -0
- package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +16 -10
- package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +31 -26
- package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +5 -1
- package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +78 -71
- package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +56 -35
- package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +10 -13
- package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +30 -17
- package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +3 -1
- package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +5 -3
- package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +2 -4
- package/dart-src/plugins/plugin-core/lib/src/storage_helpers.dart +18 -0
- package/dart-src/plugins/plugin-core/lib/src/types.dart +7 -6
- package/dart-src/plugins/plugin-core/pubspec.yaml +12 -9
- package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +13 -9
- package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +28 -15
- package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +254 -122
- package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +36 -32
- package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +18 -15
- package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +8 -3
- package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +682 -693
- package/dart-src/plugins/plugin-core/test/helpers_test.dart +45 -21
- package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +76 -13
- package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +20 -9
- package/dart-src/plugins/pocketbase/CHANGELOG.md +8 -0
- package/dart-src/plugins/pocketbase/LICENSE +21 -0
- package/dart-src/plugins/pocketbase/README.md +54 -0
- package/dart-src/plugins/pocketbase/example/lib/main.dart +62 -0
- package/dart-src/plugins/pocketbase/lib/flutter_ota_kit_pocketbase.dart +28 -0
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_bundle_mapper.dart +121 -0
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +349 -0
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_config.dart +61 -0
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_database.dart +278 -0
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_storage.dart +255 -0
- package/dart-src/plugins/pocketbase/pubspec.yaml +26 -0
- package/dart-src/plugins/pocketbase/test/pb_uri_test.dart +12 -0
- package/dart-src/plugins/postgres/CHANGELOG.md +6 -0
- package/dart-src/plugins/postgres/example/lib/main.dart +67 -0
- package/dart-src/plugins/postgres/lib/flutter_ota_kit_postgres.dart +3 -6
- package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +32 -52
- package/dart-src/plugins/postgres/lib/src/postgres_config.dart +3 -1
- package/dart-src/plugins/postgres/lib/src/postgres_database.dart +28 -38
- package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +16 -14
- package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +26 -14
- package/dart-src/plugins/postgres/lib/src/postgres_types.dart +27 -28
- package/dart-src/plugins/postgres/pubspec.yaml +14 -9
- package/dart-src/plugins/postgres/test/mock_postgres_client.dart +62 -39
- package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +3 -4
- package/dart-src/plugins/postgres/test/postgres_database_test.dart +102 -73
- package/dart-src/plugins/postgres/test/postgres_storage_runtime_test.dart +60 -0
- package/dart-src/plugins/supabase/CHANGELOG.md +17 -0
- package/dart-src/plugins/supabase/README.md +2 -2
- package/dart-src/plugins/supabase/example/lib/main.dart +63 -0
- package/dart-src/plugins/supabase/lib/flutter_ota_kit_supabase.dart +2 -0
- package/dart-src/plugins/supabase/lib/src/error_message.dart +11 -0
- package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +34 -58
- package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +3 -13
- package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +34 -52
- package/dart-src/plugins/supabase/lib/src/supabase_database.dart +38 -30
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +21 -29
- package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +7 -19
- package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +18 -29
- package/dart-src/plugins/supabase/lib/src/types.dart +26 -26
- package/dart-src/plugins/supabase/pubspec.yaml +14 -9
- package/dart-src/plugins/supabase/test/mock_supabase_client.dart +57 -57
- package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +30 -27
- package/dart-src/plugins/supabase/test/supabase_database_test.dart +26 -35
- package/dart-src/plugins/supabase/test/supabase_storage_test.dart +27 -34
- package/package.json +3 -3
- package/scripts/vendor.sh +1 -1
|
@@ -54,10 +54,7 @@ class MockPostgresClient implements PostgresClientLike {
|
|
|
54
54
|
@override
|
|
55
55
|
Future<void> close() async {}
|
|
56
56
|
|
|
57
|
-
List<Map<String, dynamic>> _handle(
|
|
58
|
-
String sql,
|
|
59
|
-
Map<String, dynamic> params,
|
|
60
|
-
) {
|
|
57
|
+
List<Map<String, dynamic>> _handle(String sql, Map<String, dynamic> params) {
|
|
61
58
|
if (sql.contains('get_target_app_version_list')) {
|
|
62
59
|
return _targetAppVersionList(params);
|
|
63
60
|
}
|
|
@@ -106,13 +103,17 @@ class MockPostgresClient implements PostgresClientLike {
|
|
|
106
103
|
store.storageObjects[key] = data is List<int>
|
|
107
104
|
? data
|
|
108
105
|
: data is Uint8List
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
? data
|
|
107
|
+
: <int>[];
|
|
111
108
|
return [];
|
|
112
109
|
}
|
|
113
110
|
if (sql.contains('SELECT 1 FROM flutter_ota_kit_storage')) {
|
|
114
111
|
final key = params['@key'] as String;
|
|
115
|
-
return store.storageObjects.containsKey(key)
|
|
112
|
+
return store.storageObjects.containsKey(key)
|
|
113
|
+
? [
|
|
114
|
+
{'1': 1},
|
|
115
|
+
]
|
|
116
|
+
: [];
|
|
116
117
|
}
|
|
117
118
|
if (sql.contains('DELETE FROM flutter_ota_kit_storage')) {
|
|
118
119
|
final key = params['@key'] as String;
|
|
@@ -131,10 +132,7 @@ class MockPostgresClient implements PostgresClientLike {
|
|
|
131
132
|
final prefix = (params['@prefix'] as String?) ?? '';
|
|
132
133
|
return store.storageObjects.keys
|
|
133
134
|
.where((k) => k.startsWith(prefix))
|
|
134
|
-
.map((k) => {
|
|
135
|
-
'key': k,
|
|
136
|
-
'size': store.storageObjects[k]!.length,
|
|
137
|
-
})
|
|
135
|
+
.map((k) => {'key': k, 'size': store.storageObjects[k]!.length})
|
|
138
136
|
.toList();
|
|
139
137
|
}
|
|
140
138
|
return [];
|
|
@@ -176,7 +174,8 @@ class MockPostgresClient implements PostgresClientLike {
|
|
|
176
174
|
var rows = _filterBundles(sql, params);
|
|
177
175
|
|
|
178
176
|
if (sql.contains('ORDER BY id asc')) {
|
|
179
|
-
rows = rows.toList()
|
|
177
|
+
rows = rows.toList()
|
|
178
|
+
..sort((a, b) => (a['id'] as String).compareTo(b['id'] as String));
|
|
180
179
|
} else {
|
|
181
180
|
rows = rows.toList()
|
|
182
181
|
..sort((a, b) => (b['id'] as String).compareTo(a['id'] as String));
|
|
@@ -200,13 +199,18 @@ class MockPostgresClient implements PostgresClientLike {
|
|
|
200
199
|
}
|
|
201
200
|
|
|
202
201
|
List<Map<String, dynamic>> _channels() {
|
|
203
|
-
final channels = store.bundleRows
|
|
202
|
+
final channels = store.bundleRows
|
|
203
|
+
.map((r) => r['channel'] as String)
|
|
204
|
+
.toSet()
|
|
205
|
+
.toList();
|
|
204
206
|
return channels.map((c) => {'channel': c}).toList();
|
|
205
207
|
}
|
|
206
208
|
|
|
207
209
|
List<Map<String, dynamic>> _patchesForIds(Map<String, dynamic> params) {
|
|
208
210
|
final ids = (params['bundle_ids'] as List).cast<String>();
|
|
209
|
-
return store.bundlePatchRows
|
|
211
|
+
return store.bundlePatchRows
|
|
212
|
+
.where((p) => ids.contains(p['bundle_id']))
|
|
213
|
+
.toList();
|
|
210
214
|
}
|
|
211
215
|
|
|
212
216
|
void _upsertBundle(Map<String, dynamic> params) {
|
|
@@ -223,7 +227,9 @@ class MockPostgresClient implements PostgresClientLike {
|
|
|
223
227
|
void _upsertPatch(Map<String, dynamic> params) {
|
|
224
228
|
final row = <String, dynamic>{};
|
|
225
229
|
params.forEach((k, v) => row[k] = v);
|
|
226
|
-
final existing = store.bundlePatchRows.indexWhere(
|
|
230
|
+
final existing = store.bundlePatchRows.indexWhere(
|
|
231
|
+
(r) => r['id'] == row['id'],
|
|
232
|
+
);
|
|
227
233
|
if (existing >= 0) {
|
|
228
234
|
store.bundlePatchRows[existing] = row;
|
|
229
235
|
} else {
|
|
@@ -248,14 +254,24 @@ class MockPostgresClient implements PostgresClientLike {
|
|
|
248
254
|
final existed = store.bundleRows.any((r) => r['id'] == id);
|
|
249
255
|
store.bundleRows.removeWhere((r) => r['id'] == id);
|
|
250
256
|
store.bundlePatchRows.removeWhere((p) => p['bundle_id'] == id);
|
|
251
|
-
return existed
|
|
257
|
+
return existed
|
|
258
|
+
? [
|
|
259
|
+
{'id': id},
|
|
260
|
+
]
|
|
261
|
+
: [];
|
|
252
262
|
}
|
|
253
263
|
|
|
254
|
-
List<Map<String, dynamic>> _targetAppVersionList(
|
|
264
|
+
List<Map<String, dynamic>> _targetAppVersionList(
|
|
265
|
+
Map<String, dynamic> params,
|
|
266
|
+
) {
|
|
255
267
|
final platform = params['app_platform'];
|
|
256
268
|
final min = params['min_bundle_id'] as String? ?? '';
|
|
257
269
|
final versions = store.bundleRows
|
|
258
|
-
.where(
|
|
270
|
+
.where(
|
|
271
|
+
(r) =>
|
|
272
|
+
r['platform'] == platform &&
|
|
273
|
+
(r['id'] as String).compareTo(min) >= 0,
|
|
274
|
+
)
|
|
259
275
|
.map((r) => r['target_app_version'])
|
|
260
276
|
.where((v) => v != null)
|
|
261
277
|
.toSet()
|
|
@@ -272,44 +288,53 @@ class MockPostgresClient implements PostgresClientLike {
|
|
|
272
288
|
return matches.isEmpty ? null : matches.first;
|
|
273
289
|
}
|
|
274
290
|
|
|
275
|
-
List<Map<String, dynamic>> _updateInfoByAppVersion(
|
|
291
|
+
List<Map<String, dynamic>> _updateInfoByAppVersion(
|
|
292
|
+
Map<String, dynamic> params,
|
|
293
|
+
) {
|
|
276
294
|
final platform = params['app_platform'];
|
|
277
295
|
final channel = params['target_channel'];
|
|
278
296
|
final min = params['min_bundle_id'] as String? ?? '';
|
|
279
|
-
final list =
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
297
|
+
final list =
|
|
298
|
+
(params['target_app_version_list'] as List?)?.cast<String>() ?? [];
|
|
299
|
+
final row = _newestMatch(
|
|
300
|
+
(r) =>
|
|
301
|
+
r['platform'] == platform &&
|
|
302
|
+
r['enabled'] == true &&
|
|
303
|
+
r['channel'] == channel &&
|
|
304
|
+
(r['id'] as String).compareTo(min) >= 0 &&
|
|
305
|
+
list.contains(r['target_app_version']),
|
|
306
|
+
);
|
|
286
307
|
if (row == null) return [];
|
|
287
308
|
return [_infoRow(row)];
|
|
288
309
|
}
|
|
289
310
|
|
|
290
|
-
List<Map<String, dynamic>> _updateInfoByFingerprint(
|
|
311
|
+
List<Map<String, dynamic>> _updateInfoByFingerprint(
|
|
312
|
+
Map<String, dynamic> params,
|
|
313
|
+
) {
|
|
291
314
|
final platform = params['app_platform'];
|
|
292
315
|
final channel = params['target_channel'];
|
|
293
316
|
final min = params['min_bundle_id'] as String? ?? '';
|
|
294
317
|
final fp = params['target_fingerprint_hash'];
|
|
295
|
-
final row = _newestMatch(
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
318
|
+
final row = _newestMatch(
|
|
319
|
+
(r) =>
|
|
320
|
+
r['platform'] == platform &&
|
|
321
|
+
r['enabled'] == true &&
|
|
322
|
+
r['channel'] == channel &&
|
|
323
|
+
(r['id'] as String).compareTo(min) >= 0 &&
|
|
324
|
+
r['fingerprint_hash'] == fp,
|
|
325
|
+
);
|
|
301
326
|
if (row == null) return [];
|
|
302
327
|
return [_infoRow(row)];
|
|
303
328
|
}
|
|
304
329
|
|
|
305
330
|
Map<String, dynamic> _infoRow(Map<String, dynamic> row) => {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
331
|
+
'id': row['id'],
|
|
332
|
+
'should_force_update': row['should_force_update'],
|
|
333
|
+
'message': row['message'],
|
|
334
|
+
'status': 'UPDATE',
|
|
335
|
+
'storage_uri': row['storage_uri'],
|
|
336
|
+
'file_hash': row['file_hash'],
|
|
337
|
+
};
|
|
313
338
|
}
|
|
314
339
|
|
|
315
340
|
class _TxClient implements PostgresClientLike {
|
|
@@ -321,8 +346,7 @@ class _TxClient implements PostgresClientLike {
|
|
|
321
346
|
Future<List<Map<String, dynamic>>> execute(
|
|
322
347
|
String sql,
|
|
323
348
|
Map<String, dynamic> parameters,
|
|
324
|
-
) =>
|
|
325
|
-
_parent.execute(sql, parameters);
|
|
349
|
+
) => _parent.execute(sql, parameters);
|
|
326
350
|
|
|
327
351
|
@override
|
|
328
352
|
Future<T> runTx<T>(Future<T> Function(PostgresClientLike tx) fn) =>
|
|
@@ -334,10 +358,10 @@ class _TxClient implements PostgresClientLike {
|
|
|
334
358
|
|
|
335
359
|
/// Build a [PostgresConfig] wired to [MockPostgresClient].
|
|
336
360
|
PostgresConfig mockPostgresConfig(Store store) => PostgresConfig(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
361
|
+
host: 'mock',
|
|
362
|
+
database: 'mock',
|
|
363
|
+
clientFactory: (_) => MockPostgresClient(store),
|
|
364
|
+
);
|
|
341
365
|
|
|
342
366
|
/// Create a [DatabasePlugin] backed by the mock store.
|
|
343
367
|
DatabasePlugin newPlugin({Store? store}) {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import 'dart:io';
|
|
2
|
+
|
|
3
|
+
import 'package:flutter_ota_kit_cli/flutter_ota_kit_cli.dart';
|
|
4
|
+
import 'package:path/path.dart' as p;
|
|
5
|
+
import 'package:test/test.dart';
|
|
6
|
+
|
|
7
|
+
void main() {
|
|
8
|
+
group('PocketBase install paths', () {
|
|
9
|
+
test('resolve returns a stable, versioned path', () {
|
|
10
|
+
final a = PocketBaseInstallPaths.resolve(version: '0.22.21');
|
|
11
|
+
final b = PocketBaseInstallPaths.resolve(version: '0.22.21');
|
|
12
|
+
expect(a.installDir.path, b.installDir.path);
|
|
13
|
+
expect(a.installDir.path, endsWith(p.join('pocketbase', '0.22.21')));
|
|
14
|
+
expect(
|
|
15
|
+
a.binaryPath.path,
|
|
16
|
+
anyOf(
|
|
17
|
+
endsWith(p.join('0.22.21', 'pocketbase')),
|
|
18
|
+
endsWith(p.join('0.22.21', 'pocketbase.exe')),
|
|
19
|
+
),
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('resolve uses custom root', () {
|
|
24
|
+
final tmp = Directory.systemTemp.createTempSync('fp-pb-paths-');
|
|
25
|
+
final a = PocketBaseInstallPaths.resolve(version: '0.22.21', root: tmp);
|
|
26
|
+
expect(a.installDir.path, tmp.path);
|
|
27
|
+
tmp.deleteSync(recursive: true);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
group('PocketBaseDataBootstrap', () {
|
|
32
|
+
test('copies .pb.js hooks on first run, leaves user edits alone', () async {
|
|
33
|
+
final tmp = Directory.systemTemp.createTempSync('fp-pb-data-');
|
|
34
|
+
final source = Directory(p.join(tmp.path, 'src'))
|
|
35
|
+
..createSync(recursive: true);
|
|
36
|
+
File(p.join(source.path, 'bundles.pb.js'))
|
|
37
|
+
.writeAsStringSync('// hook v1');
|
|
38
|
+
final dataDir = Directory(p.join(tmp.path, 'data'))..createSync();
|
|
39
|
+
|
|
40
|
+
final bootstrap = PocketBaseDataBootstrap(
|
|
41
|
+
dataDir: dataDir,
|
|
42
|
+
hooksSourceDir: source,
|
|
43
|
+
);
|
|
44
|
+
final installed = await bootstrap.install();
|
|
45
|
+
expect(installed, ['bundles.pb.js']);
|
|
46
|
+
final copied = File(p.join(dataDir.path, 'pb_hooks', 'bundles.pb.js'));
|
|
47
|
+
expect(copied.existsSync(), isTrue);
|
|
48
|
+
expect(copied.readAsStringSync(), '// hook v1');
|
|
49
|
+
|
|
50
|
+
// User edits the copy; second install must not overwrite.
|
|
51
|
+
copied.writeAsStringSync('// user edit');
|
|
52
|
+
final second = await bootstrap.install();
|
|
53
|
+
expect(second, isEmpty);
|
|
54
|
+
expect(copied.readAsStringSync(), '// user edit');
|
|
55
|
+
|
|
56
|
+
tmp.deleteSync(recursive: true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('no-op when source dir does not exist', () async {
|
|
60
|
+
final tmp = Directory.systemTemp.createTempSync('fp-pb-data-');
|
|
61
|
+
final source = Directory(p.join(tmp.path, 'missing'));
|
|
62
|
+
final dataDir = Directory(p.join(tmp.path, 'data'))..createSync();
|
|
63
|
+
final bootstrap = PocketBaseDataBootstrap(
|
|
64
|
+
dataDir: dataDir,
|
|
65
|
+
hooksSourceDir: source,
|
|
66
|
+
);
|
|
67
|
+
final installed = await bootstrap.install();
|
|
68
|
+
expect(installed, isEmpty);
|
|
69
|
+
tmp.deleteSync(recursive: true);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import 'dart:async';
|
|
2
|
+
import 'dart:convert';
|
|
3
|
+
import 'dart:io';
|
|
4
|
+
|
|
5
|
+
import 'package:flutter_ota_kit_cli/flutter_ota_kit_cli.dart';
|
|
6
|
+
import 'package:test/test.dart';
|
|
7
|
+
|
|
8
|
+
/// Minimal in-process HTTP server that simulates a PocketBase admin API.
|
|
9
|
+
class _MockPocketBase {
|
|
10
|
+
_MockPocketBase._(this._server, this.baseUrl);
|
|
11
|
+
|
|
12
|
+
final HttpServer _server;
|
|
13
|
+
final String baseUrl;
|
|
14
|
+
final List<String> createdCollections = [];
|
|
15
|
+
List<String> existingCollections = const ['admins'];
|
|
16
|
+
bool authCalled = false;
|
|
17
|
+
String? lastAuthEmail;
|
|
18
|
+
|
|
19
|
+
static Future<_MockPocketBase> start() async {
|
|
20
|
+
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
|
21
|
+
final mock = _MockPocketBase._(server, 'http://127.0.0.1:${server.port}');
|
|
22
|
+
server.listen(mock._handle);
|
|
23
|
+
return mock;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Future<void> stop() async {
|
|
27
|
+
await _server.close(force: true);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Future<void> _handle(HttpRequest req) async {
|
|
31
|
+
final path = req.uri.path;
|
|
32
|
+
if (path == '/api/admins/auth-with-password') {
|
|
33
|
+
authCalled = true;
|
|
34
|
+
final body = jsonDecode(await utf8.decoder.bind(req).join()) as Map;
|
|
35
|
+
lastAuthEmail = body['identity'] as String?;
|
|
36
|
+
req.response.headers.contentType = ContentType.json;
|
|
37
|
+
req.response.write(jsonEncode({'token': 'mock-token'}));
|
|
38
|
+
await req.response.close();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (path == '/api/collections' && req.method == 'GET') {
|
|
42
|
+
req.response.headers.contentType = ContentType.json;
|
|
43
|
+
req.response.write(
|
|
44
|
+
jsonEncode({
|
|
45
|
+
'items': [
|
|
46
|
+
for (final n in existingCollections) {'name': n},
|
|
47
|
+
],
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
await req.response.close();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (path == '/api/collections' && req.method == 'POST') {
|
|
54
|
+
final body = jsonDecode(await utf8.decoder.bind(req).join()) as Map;
|
|
55
|
+
createdCollections.add(body['name'] as String? ?? '?');
|
|
56
|
+
req.response.statusCode = 200;
|
|
57
|
+
req.response.write(jsonEncode({'id': 'mock'}));
|
|
58
|
+
await req.response.close();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
req.response.statusCode = 404;
|
|
62
|
+
await req.response.close();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
void main() {
|
|
67
|
+
late _MockPocketBase mock;
|
|
68
|
+
setUp(() async {
|
|
69
|
+
mock = await _MockPocketBase.start();
|
|
70
|
+
});
|
|
71
|
+
tearDown(() async {
|
|
72
|
+
await mock.stop();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test(
|
|
76
|
+
'schema installer creates the three collections on a fresh PB',
|
|
77
|
+
() async {
|
|
78
|
+
final installer = PocketBaseSchemaInstaller(
|
|
79
|
+
url: mock.baseUrl,
|
|
80
|
+
adminEmail: 'admin@x.com',
|
|
81
|
+
adminPassword: 'secret',
|
|
82
|
+
);
|
|
83
|
+
final result = await installer.install();
|
|
84
|
+
expect(mock.authCalled, isTrue);
|
|
85
|
+
expect(mock.lastAuthEmail, 'admin@x.com');
|
|
86
|
+
expect(result.created, containsAll(['bundles', 'channels', 'audit_log']));
|
|
87
|
+
expect(result.skipped, isEmpty);
|
|
88
|
+
expect(
|
|
89
|
+
mock.createdCollections,
|
|
90
|
+
containsAll(['bundles', 'channels', 'audit_log']),
|
|
91
|
+
);
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
test('schema installer skips collections that already exist', () async {
|
|
96
|
+
mock.existingCollections = ['bundles', 'channels', 'audit_log'];
|
|
97
|
+
final installer = PocketBaseSchemaInstaller(
|
|
98
|
+
url: mock.baseUrl,
|
|
99
|
+
adminEmail: 'admin@x.com',
|
|
100
|
+
adminPassword: 'secret',
|
|
101
|
+
);
|
|
102
|
+
final result = await installer.install();
|
|
103
|
+
expect(result.skipped, containsAll(['bundles', 'channels', 'audit_log']));
|
|
104
|
+
expect(result.created, isEmpty);
|
|
105
|
+
expect(mock.createdCollections, isEmpty);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import 'package:flutter_ota_kit_pocketbase/flutter_ota_kit_pocketbase.dart';
|
|
2
|
+
import 'package:test/test.dart';
|
|
3
|
+
|
|
4
|
+
import 'mocks/mock_pocketbase_client.dart' as pb;
|
|
5
|
+
|
|
6
|
+
void main() {
|
|
7
|
+
test('parsePocketBaseStorageUri handles empty bucket', () {
|
|
8
|
+
final result = parsePocketBaseStorageUri('pb:///abc/file.zip');
|
|
9
|
+
expect(result, isNotNull);
|
|
10
|
+
expect(result!.recordId, 'abc');
|
|
11
|
+
expect(result.filename, 'file.zip');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('parsePocketBaseStorageUri handles non-empty bucket', () {
|
|
15
|
+
final result = parsePocketBaseStorageUri('pb://bundles/abc/file.zip');
|
|
16
|
+
expect(result, isNotNull);
|
|
17
|
+
expect(result!.recordId, 'abc');
|
|
18
|
+
expect(result.filename, 'file.zip');
|
|
19
|
+
expect(result.bucket, 'bundles');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('MockPocketBaseClient upload + exists round-trips', () async {
|
|
23
|
+
final store = pb.PocketBaseStore();
|
|
24
|
+
final client = pb.MockPocketBaseClient(store);
|
|
25
|
+
client.adminCredentials('admin@x.com', 'pw');
|
|
26
|
+
|
|
27
|
+
// Create a record.
|
|
28
|
+
await client.createRecord<dynamic>('bundles', {
|
|
29
|
+
'id': 'abc',
|
|
30
|
+
'channel': 'pending',
|
|
31
|
+
'platform': 'android',
|
|
32
|
+
'enabled': false,
|
|
33
|
+
}, (j) => j);
|
|
34
|
+
|
|
35
|
+
// Upload a file to it.
|
|
36
|
+
await client.uploadFile<dynamic>('bundles', 'abc', 'artifact', 'test.zip', [
|
|
37
|
+
1,
|
|
38
|
+
2,
|
|
39
|
+
3,
|
|
40
|
+
], fromJson: (j) => j);
|
|
41
|
+
|
|
42
|
+
// Check exists.
|
|
43
|
+
expect(await client.recordExists('bundles', 'abc'), isTrue);
|
|
44
|
+
expect(await client.recordExists('bundles', 'nope'), isFalse);
|
|
45
|
+
|
|
46
|
+
// Duplicate create should fail (real PB would return 409).
|
|
47
|
+
expect(
|
|
48
|
+
() => client.createRecord<dynamic>('bundles', {
|
|
49
|
+
'id': 'abc',
|
|
50
|
+
'channel': 'production',
|
|
51
|
+
}, (j) => j),
|
|
52
|
+
throwsA(isA<PocketBaseException>()),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Download the file.
|
|
56
|
+
final bytes = await client.downloadFile('pb://mock/bundles/abc/test.zip');
|
|
57
|
+
expect(bytes, [1, 2, 3]);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# flutter_ota_kit_core
|
|
2
2
|
|
|
3
|
-
Pure-Dart core of the [flutter_ota_kit](https://github.com/HYPER12755/
|
|
3
|
+
Pure-Dart core of the [flutter_ota_kit](https://github.com/HYPER12755/flutter-ota-kit) OTA toolkit. It has **no Flutter dependency** and is shared by every other package (client, plugin-core, and all four backends).
|
|
4
4
|
|
|
5
5
|
## What's inside
|
|
6
6
|
|
|
@@ -20,4 +20,4 @@ final ok = semverSatisfies('1.0.0', '>=1.0.0 <2.0.0'); // true
|
|
|
20
20
|
|
|
21
21
|
## License
|
|
22
22
|
|
|
23
|
-
MIT — see the [project LICENSE](https://github.com/HYPER12755/
|
|
23
|
+
MIT — see the [project LICENSE](https://github.com/HYPER12755/flutter-ota-kit/blob/main/LICENSE).
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// flutter_ota_kit_core example
|
|
2
|
+
//
|
|
3
|
+
// `flutter_ota_kit_core` is the framework-agnostic data model shared by every
|
|
4
|
+
// backend plugin in the `flutter_ota_kit` family. It defines the canonical
|
|
5
|
+
// shapes for bundles, patches, and rollouts.
|
|
6
|
+
//
|
|
7
|
+
// Most apps don't import this package directly — they use `flutter_ota_kit`
|
|
8
|
+
// (the umbrella package) which re-exports it. This file shows the types you
|
|
9
|
+
// might encounter in API responses, in `flutter_ota_kit_cli` tool output, or
|
|
10
|
+
// while writing a custom backend.
|
|
11
|
+
//
|
|
12
|
+
// Run: `dart run example/main.dart`
|
|
13
|
+
|
|
14
|
+
import 'package:flutter_ota_kit_core/flutter_ota_kit_core.dart';
|
|
15
|
+
|
|
16
|
+
void main() {
|
|
17
|
+
// ── 1. Bundle: the central database row / manifest object. ─────────
|
|
18
|
+
// Every backend stores this shape (Postgres, Supabase, Cloudflare D1,
|
|
19
|
+
// AWS DynamoDB, PocketBase). The wire format is camelCase, the SQL
|
|
20
|
+
// format is snake_case — `fromJson` tolerates both.
|
|
21
|
+
final bundle = Bundle(
|
|
22
|
+
id: '01a059a6-7d3c-4f3e-8a2c-9c2e3a4b5d6e',
|
|
23
|
+
platform: Platform.android,
|
|
24
|
+
shouldForceUpdate: false,
|
|
25
|
+
enabled: true,
|
|
26
|
+
fileHash: '7c4a8d09ca3762af61e59520943dc26494f8941b',
|
|
27
|
+
storageUri: 'supabase-storage://flutter-ota-bundles/01a059a6/patch.zip',
|
|
28
|
+
channel: 'production',
|
|
29
|
+
targetAppVersion: '1.0.0',
|
|
30
|
+
metadata: const BundleMetadata(
|
|
31
|
+
appVersion: '1.0.1',
|
|
32
|
+
signature: 'ed25519:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08',
|
|
33
|
+
),
|
|
34
|
+
rolloutCohortCount: 100, // 10% of devices (per-mille 0..1000).
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
print('Bundle ${bundle.id} on ${bundle.platform.value}:');
|
|
38
|
+
print(' storageUri = ${bundle.storageUri}');
|
|
39
|
+
print(' targetAppVersion = ${bundle.targetAppVersion}');
|
|
40
|
+
print(' metadata.appVersion = ${bundle.metadata?.appVersion}');
|
|
41
|
+
|
|
42
|
+
// ── 2. Wire format conversion. ─────────────────────────────────────
|
|
43
|
+
// `toJson()` produces the camelCase wire payload sent over the
|
|
44
|
+
// hot-updater-compatible update-check HTTP contract.
|
|
45
|
+
final wire = bundle.toJson();
|
|
46
|
+
print('Wire payload keys: ${wire.keys.toList()}');
|
|
47
|
+
|
|
48
|
+
// `toSqlJson()` produces the snake_case row format used by SQL
|
|
49
|
+
// backends (Postgres / Supabase / D1 / DynamoDB).
|
|
50
|
+
final sql = bundle.toSqlJson();
|
|
51
|
+
print('SQL row keys: ${sql.keys.toList()}');
|
|
52
|
+
|
|
53
|
+
// ── 3. Rollout: per-mille 0..1000 cohort bucketing. ────────────────
|
|
54
|
+
// The same device fingerprint always lands in the same bucket across
|
|
55
|
+
// cold starts, so a 10% rollout stays at 10% even as users churn.
|
|
56
|
+
const bundleId = '01a059a6-7d3c-4f3e-8a2c-9c2e3a4b5d6e';
|
|
57
|
+
const rolloutCohortCount = 100; // 10% (per-mille).
|
|
58
|
+
|
|
59
|
+
for (final userId in const ['alice', 'bob', 'carol', 'dave', 'eve']) {
|
|
60
|
+
final cohort = getDefaultNumericCohort(userId);
|
|
61
|
+
final eligible = isCohortEligibleForUpdate(
|
|
62
|
+
bundleId,
|
|
63
|
+
cohort,
|
|
64
|
+
rolloutCohortCount,
|
|
65
|
+
null, // targetCohorts (null = percentage rollout)
|
|
66
|
+
);
|
|
67
|
+
print('user=$userId cohort=$cohort in 10% rollout? $eligible');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -68,25 +68,27 @@ class AppUpdateAvailableInfo extends AppUpdateInfo {
|
|
|
68
68
|
signature: j['signature'] as String?,
|
|
69
69
|
manifestUrl: j['manifestUrl'] as String?,
|
|
70
70
|
manifestFileHash: j['manifestFileHash'] as String?,
|
|
71
|
-
changedAssets: (j['changedAssets'] as Map?)?.map(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
)),
|
|
71
|
+
changedAssets: (j['changedAssets'] as Map?)?.map(
|
|
72
|
+
(k, v) => MapEntry(
|
|
73
|
+
k as String,
|
|
74
|
+
ChangedAsset.fromJson((v as Map).cast<String, dynamic>()),
|
|
75
|
+
),
|
|
76
|
+
),
|
|
75
77
|
);
|
|
76
78
|
|
|
77
79
|
@override
|
|
78
80
|
Map<String, dynamic> toJson() => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
'id': id,
|
|
82
|
+
'shouldForceUpdate': shouldForceUpdate,
|
|
83
|
+
'message': message,
|
|
84
|
+
'status': status.value,
|
|
85
|
+
'fileUrl': fileUrl,
|
|
86
|
+
'fileHash': fileHash,
|
|
87
|
+
if (signature != null) 'signature': signature,
|
|
88
|
+
if (manifestUrl != null) 'manifestUrl': manifestUrl,
|
|
89
|
+
if (manifestFileHash != null) 'manifestFileHash': manifestFileHash,
|
|
90
|
+
if (changedAssets != null) 'changedAssets': changedAssets,
|
|
91
|
+
};
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
/// Device-facing "nothing to do" — hot-updater `AppUpToDateInfo`.
|