@_nazmiforreal/flutter-ota 0.1.11 → 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.
Files changed (176) hide show
  1. package/README.md +1 -1
  2. package/bin/flutter-ota-linux-x64 +0 -0
  3. package/dart-src/packages/cli-tools/.dart_tool/package_config.json +46 -76
  4. package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +140 -166
  5. package/dart-src/packages/cli-tools/README.md +1 -1
  6. package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +30 -17
  7. package/dart-src/packages/cli-tools/lib/src/backend.dart +30 -31
  8. package/dart-src/packages/cli-tools/lib/src/cli_base.dart +3 -1
  9. package/dart-src/packages/cli-tools/lib/src/commands/build.dart +44 -43
  10. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +265 -239
  11. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +75 -66
  12. package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +39 -35
  13. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +30 -27
  14. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +88 -72
  15. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +66 -29
  16. package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +10 -6
  17. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +299 -190
  18. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +24 -23
  19. package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +111 -97
  20. package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +237 -0
  21. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +43 -34
  22. package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +57 -53
  23. package/dart-src/packages/cli-tools/lib/src/config.dart +227 -93
  24. package/dart-src/packages/cli-tools/lib/src/operations.dart +9 -8
  25. package/dart-src/packages/cli-tools/lib/src/pack.dart +26 -25
  26. package/dart-src/packages/cli-tools/lib/src/pocketbase/data_bootstrap.dart +39 -0
  27. package/dart-src/packages/cli-tools/lib/src/pocketbase/hooks/bundles.pb.js +56 -0
  28. package/dart-src/packages/cli-tools/lib/src/pocketbase/installer.dart +189 -0
  29. package/dart-src/packages/cli-tools/lib/src/pocketbase/process_manager.dart +123 -0
  30. package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +237 -0
  31. package/dart-src/packages/cli-tools/lib/src/runner.dart +36 -15
  32. package/dart-src/packages/cli-tools/lib/src/sign.dart +13 -5
  33. package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +10 -13
  34. package/dart-src/packages/cli-tools/lib/src/util.dart +3 -1
  35. package/dart-src/packages/cli-tools/pubspec.lock +69 -116
  36. package/dart-src/packages/cli-tools/pubspec.yaml +25 -7
  37. package/dart-src/packages/cli-tools/test/backends_test.dart +62 -32
  38. package/dart-src/packages/cli-tools/test/build_pack_test.dart +4 -4
  39. package/dart-src/packages/cli-tools/test/cli_test.dart +89 -42
  40. package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +8 -13
  41. package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +23 -18
  42. package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +197 -0
  43. package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +70 -46
  44. package/dart-src/packages/cli-tools/test/pocketbase_installer_test.dart +72 -0
  45. package/dart-src/packages/cli-tools/test/pocketbase_schema_test.dart +107 -0
  46. package/dart-src/packages/cli-tools/test/pocketbase_smoke_test.dart +59 -0
  47. package/dart-src/packages/core/README.md +2 -2
  48. package/dart-src/packages/core/example/lib/main.dart +69 -0
  49. package/dart-src/packages/core/lib/flutter_ota_kit_core.dart +1 -1
  50. package/dart-src/packages/core/lib/src/app_update_info.dart +17 -15
  51. package/dart-src/packages/core/lib/src/bundle.dart +102 -94
  52. package/dart-src/packages/core/lib/src/bundle_artifacts.dart +1 -2
  53. package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +15 -16
  54. package/dart-src/packages/core/lib/src/changed_asset.dart +24 -22
  55. package/dart-src/packages/core/lib/src/get_bundles_args.dart +13 -13
  56. package/dart-src/packages/core/lib/src/metadata.dart +9 -7
  57. package/dart-src/packages/core/lib/src/patch_info.dart +8 -7
  58. package/dart-src/packages/core/lib/src/platform.dart +4 -3
  59. package/dart-src/packages/core/lib/src/rollout.dart +14 -11
  60. package/dart-src/packages/core/lib/src/semver_range.dart +49 -39
  61. package/dart-src/packages/core/lib/src/semver_version.dart +8 -7
  62. package/dart-src/packages/core/lib/src/status.dart +4 -3
  63. package/dart-src/packages/core/lib/src/update_bundle_params.dart +7 -7
  64. package/dart-src/packages/core/lib/src/uuid.dart +1 -2
  65. package/dart-src/packages/core/pubspec.yaml +6 -7
  66. package/dart-src/packages/core/test/rollout_test.dart +15 -8
  67. package/dart-src/packages/core/test/semver_test.dart +5 -2
  68. package/dart-src/packages/core/test/types_test.dart +26 -24
  69. package/dart-src/packages/core/test/uuid_test.dart +3 -1
  70. package/dart-src/packages/core/tool/dbg.dart +1 -0
  71. package/dart-src/plugins/aws/CHANGELOG.md +6 -0
  72. package/dart-src/plugins/aws/example/lib/main.dart +60 -0
  73. package/dart-src/plugins/aws/lib/flutter_ota_kit_aws.dart +5 -1
  74. package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +11 -16
  75. package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +7 -2
  76. package/dart-src/plugins/aws/lib/src/aws_database.dart +14 -18
  77. package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +23 -18
  78. package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +5 -4
  79. package/dart-src/plugins/aws/lib/src/aws_storage.dart +2 -3
  80. package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +10 -16
  81. package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +6 -9
  82. package/dart-src/plugins/aws/pubspec.yaml +15 -10
  83. package/dart-src/plugins/aws/test/aws_database_test.dart +51 -57
  84. package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +8 -13
  85. package/dart-src/plugins/cloudflare/CHANGELOG.md +7 -0
  86. package/dart-src/plugins/cloudflare/example/lib/main.dart +62 -0
  87. package/dart-src/plugins/cloudflare/lib/flutter_ota_kit_cloudflare.dart +4 -2
  88. package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +3 -3
  89. package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +3 -2
  90. package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +24 -27
  91. package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +22 -30
  92. package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +1 -1
  93. package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +21 -29
  94. package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +7 -7
  95. package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +7 -15
  96. package/dart-src/plugins/cloudflare/pubspec.yaml +14 -9
  97. package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +46 -42
  98. package/dart-src/plugins/cloudflare/test/d1_database_test.dart +100 -90
  99. package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +23 -18
  100. package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +8 -13
  101. package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +1 -4
  102. package/dart-src/plugins/plugin-core/CHANGELOG.md +16 -0
  103. package/dart-src/plugins/plugin-core/example/lib/main.dart +61 -0
  104. package/dart-src/plugins/plugin-core/lib/flutter_ota_kit_plugin_core.dart +3 -7
  105. package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +5 -5
  106. package/dart-src/plugins/plugin-core/lib/src/bundle_helpers.dart +23 -0
  107. package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +16 -10
  108. package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +31 -26
  109. package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +5 -1
  110. package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +1 -1
  111. package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +78 -71
  112. package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +56 -35
  113. package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +10 -13
  114. package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +30 -17
  115. package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +3 -1
  116. package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +5 -3
  117. package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +2 -4
  118. package/dart-src/plugins/plugin-core/lib/src/storage_helpers.dart +18 -0
  119. package/dart-src/plugins/plugin-core/lib/src/types.dart +7 -6
  120. package/dart-src/plugins/plugin-core/pubspec.yaml +12 -9
  121. package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +13 -9
  122. package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +28 -15
  123. package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +254 -122
  124. package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +36 -32
  125. package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +18 -15
  126. package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +8 -3
  127. package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +682 -693
  128. package/dart-src/plugins/plugin-core/test/helpers_test.dart +45 -21
  129. package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +76 -13
  130. package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +20 -9
  131. package/dart-src/plugins/pocketbase/CHANGELOG.md +8 -0
  132. package/dart-src/plugins/pocketbase/LICENSE +21 -0
  133. package/dart-src/plugins/pocketbase/README.md +54 -0
  134. package/dart-src/plugins/pocketbase/example/lib/main.dart +62 -0
  135. package/dart-src/plugins/pocketbase/lib/flutter_ota_kit_pocketbase.dart +28 -0
  136. package/dart-src/plugins/pocketbase/lib/src/pocketbase_bundle_mapper.dart +121 -0
  137. package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +349 -0
  138. package/dart-src/plugins/pocketbase/lib/src/pocketbase_config.dart +61 -0
  139. package/dart-src/plugins/pocketbase/lib/src/pocketbase_database.dart +278 -0
  140. package/dart-src/plugins/pocketbase/lib/src/pocketbase_storage.dart +255 -0
  141. package/dart-src/plugins/pocketbase/pubspec.yaml +26 -0
  142. package/dart-src/plugins/pocketbase/test/pb_uri_test.dart +12 -0
  143. package/dart-src/plugins/postgres/CHANGELOG.md +6 -0
  144. package/dart-src/plugins/postgres/example/lib/main.dart +67 -0
  145. package/dart-src/plugins/postgres/lib/flutter_ota_kit_postgres.dart +3 -6
  146. package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +32 -52
  147. package/dart-src/plugins/postgres/lib/src/postgres_config.dart +3 -1
  148. package/dart-src/plugins/postgres/lib/src/postgres_database.dart +28 -38
  149. package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +16 -14
  150. package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +26 -14
  151. package/dart-src/plugins/postgres/lib/src/postgres_types.dart +27 -28
  152. package/dart-src/plugins/postgres/pubspec.yaml +14 -9
  153. package/dart-src/plugins/postgres/test/mock_postgres_client.dart +62 -39
  154. package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +3 -4
  155. package/dart-src/plugins/postgres/test/postgres_database_test.dart +102 -73
  156. package/dart-src/plugins/postgres/test/postgres_storage_runtime_test.dart +60 -0
  157. package/dart-src/plugins/supabase/CHANGELOG.md +17 -0
  158. package/dart-src/plugins/supabase/README.md +2 -2
  159. package/dart-src/plugins/supabase/example/lib/main.dart +63 -0
  160. package/dart-src/plugins/supabase/lib/flutter_ota_kit_supabase.dart +2 -0
  161. package/dart-src/plugins/supabase/lib/src/error_message.dart +11 -0
  162. package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +34 -58
  163. package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +3 -13
  164. package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +34 -52
  165. package/dart-src/plugins/supabase/lib/src/supabase_database.dart +38 -30
  166. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +21 -29
  167. package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +7 -19
  168. package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +18 -29
  169. package/dart-src/plugins/supabase/lib/src/types.dart +26 -26
  170. package/dart-src/plugins/supabase/pubspec.yaml +14 -9
  171. package/dart-src/plugins/supabase/test/mock_supabase_client.dart +57 -57
  172. package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +30 -27
  173. package/dart-src/plugins/supabase/test/supabase_database_test.dart +26 -35
  174. package/dart-src/plugins/supabase/test/supabase_storage_test.dart +27 -34
  175. package/package.json +2 -2
  176. 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
- ? data
110
- : <int>[];
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) ? [{'1': 1}] : [];
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()..sort((a, b) => (a['id'] as String).compareTo(b['id'] as String));
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.map((r) => r['channel'] as String).toSet().toList();
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.where((p) => ids.contains(p['bundle_id'])).toList();
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((r) => r['id'] == row['id']);
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 ? [{'id': id}] : [];
257
+ return existed
258
+ ? [
259
+ {'id': id},
260
+ ]
261
+ : [];
252
262
  }
253
263
 
254
- List<Map<String, dynamic>> _targetAppVersionList(Map<String, dynamic> params) {
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((r) => r['platform'] == platform && (r['id'] as String).compareTo(min) >= 0)
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(Map<String, dynamic> params) {
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 = (params['target_app_version_list'] as List?)?.cast<String>() ?? [];
280
- final row = _newestMatch((r) =>
281
- r['platform'] == platform &&
282
- r['enabled'] == true &&
283
- r['channel'] == channel &&
284
- (r['id'] as String).compareTo(min) >= 0 &&
285
- list.contains(r['target_app_version']));
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(Map<String, dynamic> params) {
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((r) =>
296
- r['platform'] == platform &&
297
- r['enabled'] == true &&
298
- r['channel'] == channel &&
299
- (r['id'] as String).compareTo(min) >= 0 &&
300
- r['fingerprint_hash'] == fp);
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
- 'id': row['id'],
307
- 'should_force_update': row['should_force_update'],
308
- 'message': row['message'],
309
- 'status': 'UPDATE',
310
- 'storage_uri': row['storage_uri'],
311
- 'file_hash': row['file_hash'],
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
- host: 'mock',
338
- database: 'mock',
339
- clientFactory: (_) => MockPostgresClient(store),
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/flutter_ota_kit) OTA toolkit. It has **no Flutter dependency** and is shared by every other package (client, plugin-core, and all four backends).
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/flutter_ota_kit/blob/main/LICENSE).
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
+ }
@@ -26,4 +26,4 @@ export 'src/uuid.dart';
26
26
  // Bundle artifact helpers.
27
27
  export 'src/bundle_artifacts.dart';
28
28
 
29
- export 'src/patch_info.dart';
29
+ export 'src/patch_info.dart';
@@ -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((k, v) => MapEntry(
72
- k as String,
73
- ChangedAsset.fromJson((v as Map).cast<String, dynamic>()),
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
- 'id': id,
80
- 'shouldForceUpdate': shouldForceUpdate,
81
- 'message': message,
82
- 'status': status.value,
83
- 'fileUrl': fileUrl,
84
- 'fileHash': fileHash,
85
- if (signature != null) 'signature': signature,
86
- if (manifestUrl != null) 'manifestUrl': manifestUrl,
87
- if (manifestFileHash != null) 'manifestFileHash': manifestFileHash,
88
- if (changedAssets != null) 'changedAssets': changedAssets,
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`.