@_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
@@ -118,8 +118,7 @@ class MockBuilder
118
118
  bool _ascending = false;
119
119
 
120
120
  @override
121
- SupabaseFilterBuilderLike select(String columns,
122
- {bool? count, bool? head}) =>
121
+ SupabaseFilterBuilderLike select(String columns, {bool? count, bool? head}) =>
123
122
  _asSelect(count: count, head: head);
124
123
 
125
124
  MockBuilder _asSelect({bool? count, bool? head}) {
@@ -175,14 +174,18 @@ class MockBuilder
175
174
  @override
176
175
  SupabaseFilterBuilderLike isFilter(String column, Object? value) => this;
177
176
  @override
178
- SupabaseFilterBuilderLike not(String column, String operator, Object? value) =>
179
- this;
177
+ SupabaseFilterBuilderLike not(
178
+ String column,
179
+ String operator,
180
+ Object? value,
181
+ ) => this;
180
182
  @override
181
183
  SupabaseFilterBuilderLike order(String column, {bool? ascending}) {
182
184
  _order = column;
183
185
  _ascending = ascending ?? false;
184
186
  return this;
185
187
  }
188
+
186
189
  @override
187
190
  SupabaseFilterBuilderLike limit(int value) => this;
188
191
  @override
@@ -207,8 +210,9 @@ class MockBuilder
207
210
  }
208
211
  // select
209
212
  if (_head && _count) {
210
- final total =
211
- _table == 'bundles' ? _client._bundles.length : _client._patches.length;
213
+ final total = _table == 'bundles'
214
+ ? _client._bundles.length
215
+ : _client._patches.length;
212
216
  return MockResponse(data: const [], count: total, error: null);
213
217
  }
214
218
  if (_table == 'bundle_patches') {
@@ -225,8 +229,11 @@ class MockBuilder
225
229
  rows = rows.where((r) => r['id'] == _eq['id']).toList();
226
230
  }
227
231
  if (_table == 'bundles' && _order == 'id') {
228
- rows.sort((a, b) => (_ascending ? 1 : -1) *
229
- (a['id'] as String).compareTo(b['id'] as String));
232
+ rows.sort(
233
+ (a, b) =>
234
+ (_ascending ? 1 : -1) *
235
+ (a['id'] as String).compareTo(b['id'] as String),
236
+ );
230
237
  }
231
238
  if (_single) {
232
239
  return MockResponse(data: rows.isEmpty ? null : rows.first, error: null);
@@ -241,22 +248,34 @@ class MockBucket implements SupabaseStorageBucketLike {
241
248
  final String _bucket;
242
249
 
243
250
  @override
244
- Future<SupabaseSignedUrlResult> createSignedUrl(String path, int expiresIn) async =>
245
- MockSignedUrlResult(signedUrl: 'https://signed/$_bucket/$path', error: null);
251
+ Future<SupabaseSignedUrlResult> createSignedUrl(
252
+ String path,
253
+ int expiresIn,
254
+ ) async => MockSignedUrlResult(
255
+ signedUrl: 'https://signed/$_bucket/$path',
256
+ error: null,
257
+ );
246
258
 
247
259
  @override
248
260
  Future<SupabaseSignedUrlListResult> createSignedUrls(
249
- List<String> paths, int expiresIn) async =>
250
- MockSignedUrlListResult(
251
- data: paths
252
- .map((p) =>
253
- MockSignedUrlResult(signedUrl: 'https://signed/$_bucket/$p'))
254
- .toList(),
255
- error: null);
256
-
257
- @override
258
- Future<SupabaseUploadResult> upload(String path, List<int> fileBytes,
259
- {String? contentType, String? cacheControl}) async {
261
+ List<String> paths,
262
+ int expiresIn,
263
+ ) async => MockSignedUrlListResult(
264
+ data: paths
265
+ .map(
266
+ (p) => MockSignedUrlResult(signedUrl: 'https://signed/$_bucket/$p'),
267
+ )
268
+ .toList(),
269
+ error: null,
270
+ );
271
+
272
+ @override
273
+ Future<SupabaseUploadResult> upload(
274
+ String path,
275
+ List<int> fileBytes, {
276
+ String? contentType,
277
+ String? cacheControl,
278
+ }) async {
260
279
  _client._storage[path] = Uint8List.fromList(fileBytes);
261
280
  return MockUploadResult(data: {'Key': path}, error: null);
262
281
  }
@@ -291,8 +310,7 @@ class MockStorageClient implements SupabaseStorageClientLike {
291
310
  MockStorageClient(this._client);
292
311
  final MockSupabaseClient _client;
293
312
  @override
294
- SupabaseStorageBucketLike from(String bucket) =>
295
- MockBucket(_client, bucket);
313
+ SupabaseStorageBucketLike from(String bucket) => MockBucket(_client, bucket);
296
314
  }
297
315
 
298
316
  class MockSupabaseClient implements SupabaseClientLike {
@@ -307,8 +325,10 @@ class MockSupabaseClient implements SupabaseClientLike {
307
325
  SupabaseStorageClientLike get storage => MockStorageClient(this);
308
326
 
309
327
  @override
310
- Future<SupabaseRpcResponse> rpc(String functionName,
311
- {Map<String, dynamic>? params}) async {
328
+ Future<SupabaseRpcResponse> rpc(
329
+ String functionName, {
330
+ Map<String, dynamic>? params,
331
+ }) async {
312
332
  if (functionName == 'get_channels') {
313
333
  return const MockRpcResponse(
314
334
  data: [
@@ -327,16 +347,16 @@ class MockSupabaseClient implements SupabaseClientLike {
327
347
  // ---------------------------------------------------------------------------
328
348
 
329
349
  FlutterPatcherConfig _config() => FlutterPatcherConfig(
330
- provider: 'supabase',
331
- supabase: SupabaseConfigJson(
332
- url: 'https://demo.supabase.co',
333
- serviceRoleKey: 'service-role-key',
334
- bucket: 'bundles',
335
- ),
336
- channel: 'production',
337
- platform: 'android',
338
- source: './dist',
339
- );
350
+ provider: 'supabase',
351
+ supabase: SupabaseConfigJson(
352
+ url: 'https://demo.supabase.co',
353
+ serviceRoleKey: 'service-role-key',
354
+ bucket: 'bundles',
355
+ ),
356
+ channel: 'production',
357
+ platform: 'android',
358
+ source: './dist',
359
+ );
340
360
 
341
361
  Backend _backend(MockSupabaseClient mock) =>
342
362
  resolveBackend(_config(), supabaseClientFactory: (url, key) => mock);
@@ -357,7 +377,11 @@ void main() {
357
377
  await File(p.join(dir.path, 'main.dart')).writeAsString('print(1);');
358
378
  final bundle = await deployBundle(
359
379
  backend,
360
- DeployOptions(source: dir.path, channel: 'production', platform: 'android'),
380
+ DeployOptions(
381
+ source: dir.path,
382
+ channel: 'production',
383
+ platform: 'android',
384
+ ),
361
385
  );
362
386
  expect(bundle.enabled, isTrue);
363
387
  expect(mock._bundles.containsKey(bundle.id), isTrue);
@@ -374,9 +398,16 @@ void main() {
374
398
  await File(p.join(dir.path, 'a.txt')).writeAsString('x');
375
399
  final deployed = await deployBundle(
376
400
  backend,
377
- DeployOptions(source: dir.path, channel: 'production', platform: 'android'),
401
+ DeployOptions(
402
+ source: dir.path,
403
+ channel: 'production',
404
+ platform: 'android',
405
+ ),
406
+ );
407
+ final res = await listBundles(
408
+ backend,
409
+ ListOptions(channel: 'production'),
378
410
  );
379
- final res = await listBundles(backend, ListOptions(channel: 'production'));
380
411
  expect(res.data.map((b) => b.id), contains(deployed.id));
381
412
  } finally {
382
413
  await dir.delete(recursive: true);
@@ -394,7 +425,11 @@ void main() {
394
425
  await File(p.join(dir.path, 'a.txt')).writeAsString('x');
395
426
  final deployed = await deployBundle(
396
427
  backend,
397
- DeployOptions(source: dir.path, channel: 'production', platform: 'android'),
428
+ DeployOptions(
429
+ source: dir.path,
430
+ channel: 'production',
431
+ platform: 'android',
432
+ ),
398
433
  );
399
434
  await deleteBundle(backend, deployed.id);
400
435
  expect(mock._bundles.containsKey(deployed.id), isFalse);
@@ -409,11 +444,19 @@ void main() {
409
444
  await File(p.join(dir.path, 'a.txt')).writeAsString('x');
410
445
  final first = await deployBundle(
411
446
  backend,
412
- DeployOptions(source: dir.path, channel: 'production', platform: 'android'),
447
+ DeployOptions(
448
+ source: dir.path,
449
+ channel: 'production',
450
+ platform: 'android',
451
+ ),
413
452
  );
414
453
  final second = await deployBundle(
415
454
  backend,
416
- DeployOptions(source: dir.path, channel: 'production', platform: 'android'),
455
+ DeployOptions(
456
+ source: dir.path,
457
+ channel: 'production',
458
+ platform: 'android',
459
+ ),
417
460
  );
418
461
  final disabled = await rollbackChannel(backend, 'production');
419
462
  expect(disabled, equals(second.id));
@@ -430,7 +473,11 @@ void main() {
430
473
  await File(p.join(dir.path, 'a.txt')).writeAsString('x');
431
474
  final b = await deployBundle(
432
475
  backend,
433
- DeployOptions(source: dir.path, channel: 'production', platform: 'android'),
476
+ DeployOptions(
477
+ source: dir.path,
478
+ channel: 'production',
479
+ platform: 'android',
480
+ ),
434
481
  );
435
482
  await promoteBundle(backend, b.id, 'staging');
436
483
  expect(mock._bundles[b.id]!['channel'], equals('staging'));
@@ -29,8 +29,7 @@ class MockAwsS3Client implements AwsS3ClientLike {
29
29
  }
30
30
 
31
31
  @override
32
- Future<bool> headObject(String key) async =>
33
- store.objects.containsKey(key);
32
+ Future<bool> headObject(String key) async => store.objects.containsKey(key);
34
33
 
35
34
  @override
36
35
  Future<List<int>> getObjectAsBytes(String key) async {
@@ -64,20 +63,16 @@ class MockAwsS3Client implements AwsS3ClientLike {
64
63
  }
65
64
 
66
65
  @override
67
- Future<void> putObject(
68
- String key,
69
- List<int> body,
70
- String contentType,
71
- ) async {
66
+ Future<void> putObject(String key, List<int> body, String contentType) async {
72
67
  store.objects[key] = body;
73
68
  }
74
69
  }
75
70
 
76
71
  /// Build an [AwsS3StorageConfig] wired to an in-memory [Store].
77
72
  AwsS3StorageConfig mockConfig(Store store) => AwsS3StorageConfig(
78
- bucketName: 'test-bucket',
79
- region: 'us-east-1',
80
- accessKeyId: 'test-access-key',
81
- secretAccessKey: 'test-secret',
82
- clientFactory: (_) => MockAwsS3Client(store),
83
- );
73
+ bucketName: 'test-bucket',
74
+ region: 'us-east-1',
75
+ accessKeyId: 'test-access-key',
76
+ secretAccessKey: 'test-secret',
77
+ clientFactory: (_) => MockAwsS3Client(store),
78
+ );
@@ -76,7 +76,9 @@ class MockD1Client implements D1ClientLike {
76
76
  if (where.contains('target_app_version = ?')) {
77
77
  if (row['target_app_version'] != params[p++]) return false;
78
78
  }
79
- if (where.contains('target_app_version IN (SELECT value FROM json_each(?))')) {
79
+ if (where.contains(
80
+ 'target_app_version IN (SELECT value FROM json_each(?))',
81
+ )) {
80
82
  if (!_asList(params[p++]).contains(row['target_app_version'])) {
81
83
  return false;
82
84
  }
@@ -140,7 +142,9 @@ class MockD1Client implements D1ClientLike {
140
142
  final channels = {
141
143
  for (final row in store.bundleRows) row['channel'] as String,
142
144
  };
143
- return [for (final c in channels) {'channel': c}];
145
+ return [
146
+ for (final c in channels) {'channel': c},
147
+ ];
144
148
  }
145
149
 
146
150
  if (sql.contains('SELECT target_app_version FROM bundles')) {
@@ -164,9 +168,11 @@ class MockD1Client implements D1ClientLike {
164
168
  .where((row) => ids.contains(row['bundle_id']))
165
169
  .toList();
166
170
  rows.sort(
167
- (a, b) => (a['order_index'] as num).compareTo(b['order_index'] as num) +
168
- (a['base_bundle_id'] as String)
169
- .compareTo(b['base_bundle_id'] as String),
171
+ (a, b) =>
172
+ (a['order_index'] as num).compareTo(b['order_index'] as num) +
173
+ (a['base_bundle_id'] as String).compareTo(
174
+ b['base_bundle_id'] as String,
175
+ ),
170
176
  );
171
177
  return rows;
172
178
  }
@@ -195,8 +201,7 @@ class MockD1Client implements D1ClientLike {
195
201
  'rollout_cohort_count': params[15],
196
202
  'target_cohorts': params[16],
197
203
  };
198
- store.bundleRows
199
- .removeWhere((r) => r['id'] == row['id']);
204
+ store.bundleRows.removeWhere((r) => r['id'] == row['id']);
200
205
  store.bundleRows.add(row);
201
206
  return const [];
202
207
  }
@@ -219,8 +224,9 @@ class MockD1Client implements D1ClientLike {
219
224
  if (sql.contains('DELETE FROM bundles')) {
220
225
  final id = params[0];
221
226
  store.bundleRows.removeWhere((r) => r['id'] == id);
222
- store.patchRows
223
- .removeWhere((r) => r['bundle_id'] == id || r['base_bundle_id'] == id);
227
+ store.patchRows.removeWhere(
228
+ (r) => r['bundle_id'] == id || r['base_bundle_id'] == id,
229
+ );
224
230
  return const [];
225
231
  }
226
232
 
@@ -240,17 +246,16 @@ class MockD1Client implements D1ClientLike {
240
246
 
241
247
  /// Build a [D1DatabaseConfig] wired to an in-memory [Store].
242
248
  D1DatabaseConfig mockConfig(Store store) => D1DatabaseConfig(
243
- databaseId: 'test-db',
244
- accountId: 'test-account',
245
- cloudflareApiToken: 'test-token',
246
- clientFactory: (_) => MockD1Client(store),
247
- );
249
+ databaseId: 'test-db',
250
+ accountId: 'test-account',
251
+ cloudflareApiToken: 'test-token',
252
+ clientFactory: (_) => MockD1Client(store),
253
+ );
248
254
 
249
255
  /// Construct the plugin with an in-memory store.
250
256
  DatabasePlugin newPlugin(Store store) => d1Database(mockConfig(store))();
251
257
 
252
258
  /// Construct the Workers D1 variant with an in-memory store.
253
- DatabasePlugin newWorkerPlugin(Store store) =>
254
- cloudflareWorkerDatabase(
255
- CloudflareWorkerDatabaseConfig(getDb: () => MockD1Client(store)),
256
- )();
259
+ DatabasePlugin newWorkerPlugin(Store store) => cloudflareWorkerDatabase(
260
+ CloudflareWorkerDatabaseConfig(getDb: () => MockD1Client(store)),
261
+ )();
@@ -0,0 +1,197 @@
1
+ /// In-memory mock of [PocketBaseClient] for tests.
2
+ ///
3
+ /// Implements the same surface as the real client but stores everything in
4
+ /// Dart maps. Used by the `pocketbase backend` lifecycle test in
5
+ /// `backends_test.dart`.
6
+ library;
7
+
8
+ import 'package:flutter_ota_kit_pocketbase/flutter_ota_kit_pocketbase.dart';
9
+
10
+ class MockPocketBaseClient implements PocketBaseClient {
11
+ MockPocketBaseClient(this.store);
12
+
13
+ final PocketBaseStore store;
14
+
15
+ bool _closed = false;
16
+ bool get isClosed => _closed;
17
+ String? _email;
18
+ String? _password;
19
+
20
+ @override
21
+ void adminCredentials(String email, String password) {
22
+ _email = email;
23
+ _password = password;
24
+ }
25
+
26
+ @override
27
+ Future<PocketBaseClient> authenticate(String email, String password) async {
28
+ _email = email;
29
+ _password = password;
30
+ return this;
31
+ }
32
+
33
+ void _checkAuth() {
34
+ if (_email == null || _password == null) {
35
+ throw const PocketBaseException('not authenticated');
36
+ }
37
+ }
38
+
39
+ @override
40
+ Future<bool> health() async => true;
41
+
42
+ @override
43
+ Future<bool> recordExists(String collection, String id) async {
44
+ _checkAuth();
45
+ return store.records(collection).containsKey(id);
46
+ }
47
+
48
+ @override
49
+ Future<T?> getRecord<T>(
50
+ String collection,
51
+ String id,
52
+ T Function(Map<String, dynamic>) fromJson,
53
+ ) async {
54
+ _checkAuth();
55
+ final r = store.records(collection)[id];
56
+ if (r == null) return null;
57
+ return fromJson(Map<String, dynamic>.from(r));
58
+ }
59
+
60
+ @override
61
+ Future<PocketBaseList<T>> listRecords<T>(
62
+ String collection,
63
+ T Function(Map<String, dynamic>) fromJson, {
64
+ String? filter,
65
+ String? sort,
66
+ int page = 1,
67
+ int perPage = 50,
68
+ }) async {
69
+ _checkAuth();
70
+ // Sort the underlying JSON maps BEFORE decoding to typed objects, so the
71
+ // sort field is always available regardless of the decoded bean shape.
72
+ final rawList = store.records(collection).values.toList();
73
+ if (sort != null && sort.isNotEmpty) {
74
+ final desc = sort.startsWith('-');
75
+ final field = desc ? sort.substring(1) : sort;
76
+ rawList.sort((a, b) {
77
+ final av = a[field]?.toString() ?? '';
78
+ final bv = b[field]?.toString() ?? '';
79
+ final cmp = av.compareTo(bv);
80
+ return desc ? -cmp : cmp;
81
+ });
82
+ }
83
+ final all = rawList
84
+ .map((j) => fromJson(Map<String, dynamic>.from(j)))
85
+ .toList();
86
+ final start = (page - 1) * perPage;
87
+ final end = (start + perPage).clamp(0, all.length);
88
+ final items = start >= all.length ? <T>[] : all.sublist(start, end);
89
+ final totalPages = all.isEmpty ? 0 : (all.length / perPage).ceil();
90
+ return PocketBaseList<T>(
91
+ items: items,
92
+ page: page,
93
+ perPage: perPage,
94
+ totalItems: all.length,
95
+ totalPages: totalPages,
96
+ );
97
+ }
98
+
99
+ @override
100
+ Future<T> createRecord<T>(
101
+ String collection,
102
+ Map<String, dynamic> body,
103
+ T Function(Map<String, dynamic>) fromJson,
104
+ ) async {
105
+ _checkAuth();
106
+ final id =
107
+ (body['id'] as String?) ??
108
+ DateTime.now().microsecondsSinceEpoch.toString();
109
+ final existing = store.records(collection)[id];
110
+ if (existing != null) {
111
+ // PB returns 409 on duplicate id; the mock simulates the real API by
112
+ // throwing. Callers should use updateRecord() to patch existing rows.
113
+ throw const PocketBaseException('mock: record id already exists');
114
+ }
115
+ store.records(collection)[id] = Map<String, dynamic>.from(body);
116
+ return fromJson(store.records(collection)[id]!);
117
+ }
118
+
119
+ @override
120
+ Future<T> updateRecord<T>(
121
+ String collection,
122
+ String id,
123
+ Map<String, dynamic> body,
124
+ T Function(Map<String, dynamic>) fromJson,
125
+ ) async {
126
+ _checkAuth();
127
+ final existing = store.records(collection);
128
+ final current = existing[id];
129
+ if (current == null) {
130
+ throw PocketBaseException('not_found: $id');
131
+ }
132
+ current.addAll(body);
133
+ return fromJson(Map<String, dynamic>.from(current));
134
+ }
135
+
136
+ @override
137
+ Future<void> deleteRecord(String collection, String id) async {
138
+ _checkAuth();
139
+ store.records(collection).remove(id);
140
+ }
141
+
142
+ @override
143
+ Future<T> uploadFile<T>(
144
+ String collection,
145
+ String recordId,
146
+ String fieldName,
147
+ String filename,
148
+ List<int> bytes, {
149
+ Map<String, String>? extraFields,
150
+ required T Function(Map<String, dynamic>) fromJson,
151
+ }) async {
152
+ _checkAuth();
153
+ final rec = store.records(collection)[recordId];
154
+ if (rec == null) throw PocketBaseException('not_found: $recordId');
155
+ rec[fieldName] = filename;
156
+ store.fileStorage['$recordId/$filename'] = List<int>.from(bytes);
157
+ return fromJson(Map<String, dynamic>.from(rec));
158
+ }
159
+
160
+ @override
161
+ Future<String> getFileToken(String recordId, String filename) async => 'mock';
162
+
163
+ @override
164
+ String fileUrl(
165
+ String collection,
166
+ String recordId,
167
+ String filename, [
168
+ String? token,
169
+ ]) => 'pb://mock/$collection/$recordId/$filename';
170
+
171
+ @override
172
+ Future<List<int>> downloadFile(String url) async {
173
+ final match = RegExp(r'pb://mock/[^/]+/([^/]+)/(.+)$').firstMatch(url);
174
+ if (match == null) throw PocketBaseException('bad url: $url');
175
+ final key = '${match.group(1)}/${match.group(2)}';
176
+ final bytes = store.fileStorage[key];
177
+ if (bytes == null) throw PocketBaseException('not found: $key');
178
+ return bytes;
179
+ }
180
+
181
+ @override
182
+ void close() {
183
+ _closed = true;
184
+ }
185
+ }
186
+
187
+ /// In-memory store backing [MockPocketBaseClient].
188
+ class PocketBaseStore {
189
+ final Map<String, Map<String, Map<String, dynamic>>> _records = {};
190
+ final Map<String, List<int>> _files = {};
191
+
192
+ Map<String, Map<String, dynamic>> records(String collection) =>
193
+ _records.putIfAbsent(collection, () => <String, Map<String, dynamic>>{});
194
+
195
+ Map<String, List<int>> get fileStorage => _files;
196
+ Map<String, Map<String, Map<String, dynamic>>> get allRecords => _records;
197
+ }