@_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
@@ -25,20 +25,19 @@ Bundle _makeBundle({
25
25
  String? targetAppVersion,
26
26
  String? message,
27
27
  List<BundlePatchArtifact>? patches,
28
- }) =>
29
- Bundle(
30
- id: id,
31
- channel: channel,
32
- enabled: enabled,
33
- shouldForceUpdate: false,
34
- fileHash: 'h-$id',
35
- platform: Platform.android,
36
- targetAppVersion: targetAppVersion,
37
- storageUri: 's3://$id',
38
- fingerprintHash: fingerprintHash,
39
- message: message,
40
- patches: patches,
41
- );
28
+ }) => Bundle(
29
+ id: id,
30
+ channel: channel,
31
+ enabled: enabled,
32
+ shouldForceUpdate: false,
33
+ fileHash: 'h-$id',
34
+ platform: Platform.android,
35
+ targetAppVersion: targetAppVersion,
36
+ storageUri: 's3://$id',
37
+ fingerprintHash: fingerprintHash,
38
+ message: message,
39
+ patches: patches,
40
+ );
42
41
 
43
42
  void main() {
44
43
  group('cloudflare d1Database', () {
@@ -61,48 +60,51 @@ void main() {
61
60
  expect(channels, containsAll(['production', 'staging']));
62
61
  });
63
62
 
64
- test('getBundles returns bundles and pagination; filters by channel',
65
- () async {
66
- for (final id in ['b1', 'b2', 'b3']) {
67
- await plugin.appendBundle(_makeBundle(id: id));
68
- }
69
- await plugin.commitBundle();
70
-
71
- final res = await plugin.getBundles(
72
- DatabaseBundleQueryOptions(
73
- where: const DatabaseBundleQueryWhere(),
74
- limit: 2,
75
- offset: 0,
76
- orderBy: const DatabaseBundleQueryOrder(direction: 'desc'),
77
- ),
78
- );
79
- expect(res.data, hasLength(2));
80
- expect(res.pagination.total, 3);
81
- expect(res.pagination.hasNextPage, isTrue);
82
-
83
- final staging = await plugin.getBundles(
84
- DatabaseBundleQueryOptions(
85
- where: const DatabaseBundleQueryWhere(channel: 'staging'),
86
- limit: 10,
87
- offset: 0,
88
- ),
89
- );
90
- expect(staging.data, isEmpty);
91
- });
92
-
93
- test('getBundleById returns null for missing, bundle for existing',
94
- () async {
95
- await plugin.appendBundle(_makeBundle(id: 'b1'));
96
- await plugin.commitBundle();
63
+ test(
64
+ 'getBundles returns bundles and pagination; filters by channel',
65
+ () async {
66
+ for (final id in ['b1', 'b2', 'b3']) {
67
+ await plugin.appendBundle(_makeBundle(id: id));
68
+ }
69
+ await plugin.commitBundle();
70
+
71
+ final res = await plugin.getBundles(
72
+ DatabaseBundleQueryOptions(
73
+ where: const DatabaseBundleQueryWhere(),
74
+ limit: 2,
75
+ offset: 0,
76
+ orderBy: const DatabaseBundleQueryOrder(direction: 'desc'),
77
+ ),
78
+ );
79
+ expect(res.data, hasLength(2));
80
+ expect(res.pagination.total, 3);
81
+ expect(res.pagination.hasNextPage, isTrue);
82
+
83
+ final staging = await plugin.getBundles(
84
+ DatabaseBundleQueryOptions(
85
+ where: const DatabaseBundleQueryWhere(channel: 'staging'),
86
+ limit: 10,
87
+ offset: 0,
88
+ ),
89
+ );
90
+ expect(staging.data, isEmpty);
91
+ },
92
+ );
97
93
 
98
- expect(await plugin.getBundleById('missing'), isNull);
99
- final got = await plugin.getBundleById('b1');
100
- expect(got, isNotNull);
101
- expect(got!.id, 'b1');
102
- });
94
+ test(
95
+ 'getBundleById returns null for missing, bundle for existing',
96
+ () async {
97
+ await plugin.appendBundle(_makeBundle(id: 'b1'));
98
+ await plugin.commitBundle();
99
+
100
+ expect(await plugin.getBundleById('missing'), isNull);
101
+ final got = await plugin.getBundleById('b1');
102
+ expect(got, isNotNull);
103
+ expect(got!.id, 'b1');
104
+ },
105
+ );
103
106
 
104
- test('commitBundle append -> update -> delete reflects in store',
105
- () async {
107
+ test('commitBundle append -> update -> delete reflects in store', () async {
106
108
  await plugin.appendBundle(_makeBundle(id: 'b1', message: 'original'));
107
109
  await plugin.commitBundle();
108
110
  expect((await plugin.getBundleById('b1'))!.message, 'original');
@@ -118,43 +120,51 @@ void main() {
118
120
  expect(await plugin.getBundleById('b1'), isNull);
119
121
  });
120
122
 
121
- test('getUpdateInfo (appVersion) returns UpdateInfo for compatible bundle',
122
- () async {
123
- await plugin.appendBundle(_makeBundle(id: 'b1', targetAppVersion: '1.0.0'));
124
- await plugin.commitBundle();
125
-
126
- final info = await plugin.getUpdateInfo(
127
- AppVersionGetBundlesArgs(
128
- platform: Platform.android,
129
- bundleId: nilUuid,
130
- appVersion: '1.0.0',
131
- channel: 'production',
132
- minBundleId: nilUuid,
133
- ),
134
- );
135
-
136
- expect(info, isNotNull);
137
- expect(info!.id, 'b1');
138
- expect(info.status, UpdateStatus.update);
139
- });
140
-
141
- test('getUpdateInfo (appVersion) returns null when no compatible bundle',
142
- () async {
143
- await plugin.appendBundle(_makeBundle(id: 'b1', targetAppVersion: '1.0.0'));
144
- await plugin.commitBundle();
145
-
146
- final info = await plugin.getUpdateInfo(
147
- AppVersionGetBundlesArgs(
148
- platform: Platform.android,
149
- bundleId: nilUuid,
150
- appVersion: '9.9.9',
151
- channel: 'production',
152
- minBundleId: nilUuid,
153
- ),
154
- );
123
+ test(
124
+ 'getUpdateInfo (appVersion) returns UpdateInfo for compatible bundle',
125
+ () async {
126
+ await plugin.appendBundle(
127
+ _makeBundle(id: 'b1', targetAppVersion: '1.0.0'),
128
+ );
129
+ await plugin.commitBundle();
130
+
131
+ final info = await plugin.getUpdateInfo(
132
+ AppVersionGetBundlesArgs(
133
+ platform: Platform.android,
134
+ bundleId: nilUuid,
135
+ appVersion: '1.0.0',
136
+ channel: 'production',
137
+ minBundleId: nilUuid,
138
+ ),
139
+ );
140
+
141
+ expect(info, isNotNull);
142
+ expect(info!.id, 'b1');
143
+ expect(info.status, UpdateStatus.update);
144
+ },
145
+ );
155
146
 
156
- expect(info, isNull);
157
- });
147
+ test(
148
+ 'getUpdateInfo (appVersion) returns null when no compatible bundle',
149
+ () async {
150
+ await plugin.appendBundle(
151
+ _makeBundle(id: 'b1', targetAppVersion: '1.0.0'),
152
+ );
153
+ await plugin.commitBundle();
154
+
155
+ final info = await plugin.getUpdateInfo(
156
+ AppVersionGetBundlesArgs(
157
+ platform: Platform.android,
158
+ bundleId: nilUuid,
159
+ appVersion: '9.9.9',
160
+ channel: 'production',
161
+ minBundleId: nilUuid,
162
+ ),
163
+ );
164
+
165
+ expect(info, isNull);
166
+ },
167
+ );
158
168
 
159
169
  test('getUpdateInfo (fingerprint) returns matching bundle', () async {
160
170
  await plugin.appendBundle(_makeBundle(id: 'fp', fingerprintHash: 'fp1'));
@@ -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
+ )();
@@ -29,8 +29,7 @@ class MockR2Client implements R2S3ClientLike {
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 MockR2Client implements R2S3ClientLike {
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 [R2S3StorageConfig] wired to an in-memory [Store].
77
72
  R2S3StorageConfig mockR2Config(Store store) => R2S3StorageConfig(
78
- accountId: 'test-account',
79
- bucketName: 'test-bucket',
80
- accessKeyId: 'test-access-key',
81
- secretAccessKey: 'test-secret',
82
- clientFactory: (_) => MockR2Client(store),
83
- );
73
+ accountId: 'test-account',
74
+ bucketName: 'test-bucket',
75
+ accessKeyId: 'test-access-key',
76
+ secretAccessKey: 'test-secret',
77
+ clientFactory: (_) => MockR2Client(store),
78
+ );
@@ -35,10 +35,7 @@ void main() {
35
35
  test('node upload + exists + downloadFile + delete', () async {
36
36
  final node = plugin.profiles.node!;
37
37
  final res = await node.upload('v1', tmpFile);
38
- expect(
39
- res['storageUri'],
40
- 'r2://test-bucket/v1/${basename(tmpFile)}',
41
- );
38
+ expect(res['storageUri'], 'r2://test-bucket/v1/${basename(tmpFile)}');
42
39
  expect(await node.exists(res['storageUri']!), isTrue);
43
40
 
44
41
  await node.downloadFile(res['storageUri']!, outFile);
@@ -1,3 +1,19 @@
1
+ ## 0.0.2
2
+
3
+ ### Changed
4
+
5
+ - **Shared previously-duplicated backend helpers in one place.** Extracted
6
+ `ensureExpectedBucket` (the S3/R2 bucket-mismatch guard, formerly copied
7
+ byte-for-byte in the cloudflare + aws storage profiles), `normalizeMetadata`
8
+ (bundle `metadata` JSON-string/object normalization, formerly in the supabase +
9
+ postgres mappers and cloudflare's `parseMetadata`), `buildBundlePatchId`
10
+ (the `${bundleId}:${baseBundleId}` patch primary key, formerly in three mappers),
11
+ and `updateJsonKeyRegex` (the `update.json` key matcher, formerly duplicated in
12
+ the aws blob database + `createBlobDatabasePlugin`). The four backend plugins
13
+ now import these from `plugin-core` instead of maintaining their own copies.
14
+ Public API of the backend packages is unchanged (cloudflare still re-exports
15
+ `buildBundlePatchId` / `parseMetadata` as aliases of the shared helpers).
16
+
1
17
  ## 0.0.1
2
18
 
3
19
  - Initial release of `flutter_ota_kit_plugin_core` as part of the flutter_ota_kit OTA toolkit.
@@ -0,0 +1,61 @@
1
+ // flutter_ota_kit_plugin_core example
2
+ //
3
+ // `flutter_ota_kit_plugin_core` provides the shared abstractions that
4
+ // every backend plugin is built on top of: `DatabasePlugin`,
5
+ // `StoragePlugin`, `Bundle` filtering, storage-URI parsing, pagination,
6
+ // and the "blob database" factory used by the AWS, Cloudflare, and
7
+ // PocketBase backends.
8
+ //
9
+ // Most apps don't import this package directly; the SDK's update
10
+ // pipeline does. This file shows the standalone helpers so you can
11
+ // use them while writing a custom backend or a CLI tool.
12
+ //
13
+ // Run: `dart run example/main.dart`
14
+
15
+ import 'package:flutter_ota_kit_plugin_core/flutter_ota_kit_plugin_core.dart';
16
+
17
+ void main() {
18
+ // ── 1. Storage URI parser. ───────────────────────────────────────
19
+ // `parseStorageUri` validates the protocol and returns the bucket +
20
+ // key. Throws on unknown protocols so misuse is caught early.
21
+ const supabaseUri =
22
+ 'supabase-storage://flutter-ota-bundles/'
23
+ '01a059a6/patch.zip';
24
+ final parsed = parseStorageUri(supabaseUri, 'supabase-storage');
25
+ print('Parsed protocol: ${parsed.protocol}');
26
+ print('Parsed bucket: ${parsed.bucket}');
27
+ print('Parsed key: ${parsed.key}');
28
+
29
+ // ── 2. Bundle storage keys. ──────────────────────────────────────
30
+ // `createBundleStorageKey` returns a flat key for the patch blob
31
+ // inside a storage bucket. The default layout is `{id}/patch.zip`;
32
+ // for assets it's `{id}/assets/{path}`.
33
+ final patchKey = createBundleStorageKey('01a059a6');
34
+ print('Patch key: $patchKey');
35
+
36
+ final assetKey = createBundleStorageKey('01a059a6', [
37
+ 'assets',
38
+ 'images',
39
+ 'logo.png',
40
+ ]);
41
+ print('Asset key: $assetKey');
42
+
43
+ // ── 3. Patch ID helper. ──────────────────────────────────────────
44
+ // The device SDK reports the currently-installed patch id back to
45
+ // the server (so the server can compute diffs). For patches with a
46
+ // base, the id is `{currentId}-h{patchCount}`; for standalone
47
+ // patches, the id is the current id.
48
+ final patchId = buildBundlePatchId(
49
+ '01a059a6-7d3c-4f3e-8a2c-9c2e3a4b5d6e',
50
+ '00000000-0000-0000-0000-000000000000',
51
+ );
52
+ print('Patch id: $patchId');
53
+
54
+ // ── 4. Pagination math. ──────────────────────────────────────────
55
+ // `calculatePagination` returns total/page/hasNext/hasPrev info
56
+ // used by the CLI's `bundle list` command.
57
+ final p = calculatePagination(247, limit: 20, offset: 40);
58
+ print('Page ${p.currentPage} of ${p.totalPages} (${p.total} items)');
59
+ print(' hasNextPage: ${p.hasNextPage}');
60
+ print(' hasPreviousPage: ${p.hasPreviousPage}');
61
+ }
@@ -2,17 +2,14 @@ library;
2
2
 
3
3
  // Re-export core types for convenience.
4
4
  export 'package:flutter_ota_kit_core/flutter_ota_kit_core.dart'
5
- show
6
- Bundle,
7
- GetBundlesArgs,
8
- Platform,
9
- UpdateInfo,
10
- semverSatisfies;
5
+ show Bundle, GetBundlesArgs, Platform, UpdateInfo, semverSatisfies;
11
6
 
12
7
  // Plugin-core types.
13
8
  export 'src/types.dart';
14
9
 
15
10
  // Helpers.
11
+ export 'src/storage_helpers.dart';
12
+ export 'src/bundle_helpers.dart';
16
13
  export 'src/calculate_pagination.dart';
17
14
  export 'src/query_bundles.dart';
18
15
  export 'src/paginate_bundles.dart';
@@ -39,4 +36,3 @@ export 'src/generate_min_bundle_id.dart';
39
36
  export 'src/uuidv7.dart';
40
37
  export 'src/get_update_info.dart';
41
38
  export 'src/resolve_update_info_from_bundles.dart';
42
- export 'src/resolve_update_info_from_bundles.dart';
@@ -1,5 +1,6 @@
1
1
  import 'content_addressed_assets.dart' show getContentAddressedAssetStoragePath;
2
- import 'legacy_asset_storage_layout.dart' show getLegacyManifestAssetStoragePath;
2
+ import 'legacy_asset_storage_layout.dart'
3
+ show getLegacyManifestAssetStoragePath;
3
4
 
4
5
  /// The two supported asset storage layout strategies.
5
6
  typedef AssetStorageLayout = String;
@@ -27,8 +28,7 @@ String createStorageUriWithRelativePath({
27
28
  required String relativePath,
28
29
  }) {
29
30
  final storageUrl = Uri.parse(baseStorageUri);
30
- final normalizedBasePath =
31
- storageUrl.path.replaceAll(RegExp(r'/+$'), '');
31
+ final normalizedBasePath = storageUrl.path.replaceAll(RegExp(r'/+$'), '');
32
32
  final normalizedRelativePath = relativePath
33
33
  .replaceAll('\\', '/')
34
34
  .split('/')
@@ -46,8 +46,8 @@ String createStorageUriWithRelativePath({
46
46
  /// Returns `'content-addressed'` if the URI path ends with `/assets`,
47
47
  /// otherwise `'legacy-files'`.
48
48
  AssetStorageLayout getAssetStorageLayout(String assetBaseStorageUri) {
49
- final pathname =
50
- Uri.parse(assetBaseStorageUri).path.replaceAll(RegExp(r'/+$'), '');
49
+ final pathname = Uri.parse(assetBaseStorageUri).path
50
+ .replaceAll(RegExp(r'/+$'), '');
51
51
  return pathname.endsWith('/assets') || pathname == '/assets'
52
52
  ? assetLayoutContentAddressed
53
53
  : assetLayoutLegacyFiles;
@@ -0,0 +1,23 @@
1
+ import 'dart:convert' show jsonDecode;
2
+
3
+ /// Normalize bundle `metadata` from various shapes (a JSON string or an
4
+ /// already-decoded object) to a plain map. Identical across every backend
5
+ /// mapper (supabase / postgres / cloudflare), so it lives here once.
6
+ Map<String, Object?>? normalizeMetadata(Object? value) {
7
+ if (value == null) return null;
8
+ if (value is String) {
9
+ try {
10
+ final parsed = jsonDecode(value);
11
+ return normalizeMetadata(parsed);
12
+ } catch (_) {
13
+ return null;
14
+ }
15
+ }
16
+ if (value is Map) return value.cast<String, Object?>();
17
+ return null;
18
+ }
19
+
20
+ /// Compound primary key for a patch row: `${bundleId}:${baseBundleId}`.
21
+ /// Shared by every backend mapper.
22
+ String buildBundlePatchId(String bundleId, String baseBundleId) =>
23
+ '$bundleId:$baseBundleId';
@@ -1,16 +1,21 @@
1
1
  /// The top-level prefix for bundle storage keys.
2
2
  const String bundleStoragePrefix = 'bundles';
3
3
 
4
- /// Build a storage key for a bundle, e.g. `bundles/<bundleId>/patch.zip`.
5
- String createBundleStorageKey(String bundleId, [List<String> relativePaths = const []]) {
6
- return [bundleStoragePrefix, bundleId, ...relativePaths]
7
- .where((s) => s.isNotEmpty)
8
- .join('/');
4
+ /// Build a storage key for a bundle, e.g. `bundles/{bundleId}/patch.zip`.
5
+ String createBundleStorageKey(
6
+ String bundleId, [
7
+ List<String> relativePaths = const [],
8
+ ]) {
9
+ return [
10
+ bundleStoragePrefix,
11
+ bundleId,
12
+ ...relativePaths,
13
+ ].where((s) => s.isNotEmpty).join('/');
9
14
  }
10
15
 
11
16
  /// Replace the bundle-id segment in a storage URI with a relative path.
12
17
  ///
13
- /// For example, given a storage URI containing `.../bundles/<bundleId>/patch.zip`
18
+ /// For example, given a storage URI containing `.../bundles/{bundleId}/patch.zip`
14
19
  /// and a [relativePath] of `files/manifest.json`, this returns a URI pointing
15
20
  /// to `.../files/manifest.json`.
16
21
  String createStorageRootUriWithPath(
@@ -19,12 +24,13 @@ String createStorageRootUriWithPath(
19
24
  String relativePath,
20
25
  ) {
21
26
  final storageUrl = Uri.parse(storageUri);
22
- final segments = storageUrl.path.split('/').where((s) => s.isNotEmpty).toList();
27
+ final segments = storageUrl.path
28
+ .split('/')
29
+ .where((s) => s.isNotEmpty)
30
+ .toList();
23
31
  final bundleIndex = segments.lastIndexOf(bundleId);
24
32
  if (bundleIndex < 0) {
25
- throw StateError(
26
- 'Storage URI does not contain bundle id: $bundleId',
27
- );
33
+ throw StateError('Storage URI does not contain bundle id: $bundleId');
28
34
  }
29
35
 
30
36
  final rootSegments = segments.sublist(0, bundleIndex);
@@ -105,9 +105,7 @@ class BundleUnitOfWork {
105
105
  return const TrackedBundleNotFound();
106
106
  }
107
107
  final entry = _entries[bundleId];
108
- return TrackedBundleFound(
109
- entry is _PresentEntry ? entry.bundle : null,
110
- );
108
+ return TrackedBundleFound(entry is _PresentEntry ? entry.bundle : null);
111
109
  }
112
110
 
113
111
  /// Deduped async lookup: returns cached entry, coalesces concurrent loads,
@@ -124,19 +122,23 @@ class BundleUnitOfWork {
124
122
  final pending = _pendingLoads[bundleId];
125
123
  if (pending != null) return pending;
126
124
 
127
- final load = loadBundleById().then<Bundle?>((bundle) {
128
- _pendingLoads.remove(bundleId);
129
- final currentEntry = _entries[bundleId];
130
- if (currentEntry != null) {
131
- return currentEntry is _PresentEntry ? currentEntry.bundle : null;
132
- }
133
- _entries[bundleId] =
134
- bundle != null ? _PresentEntry(bundle) : const _MissingEntry();
135
- return bundle;
136
- }, onError: (Object error) {
137
- _pendingLoads.remove(bundleId);
138
- throw error;
139
- });
125
+ final load = loadBundleById().then<Bundle?>(
126
+ (bundle) {
127
+ _pendingLoads.remove(bundleId);
128
+ final currentEntry = _entries[bundleId];
129
+ if (currentEntry != null) {
130
+ return currentEntry is _PresentEntry ? currentEntry.bundle : null;
131
+ }
132
+ _entries[bundleId] = bundle != null
133
+ ? _PresentEntry(bundle)
134
+ : const _MissingEntry();
135
+ return bundle;
136
+ },
137
+ onError: (Object error) {
138
+ _pendingLoads.remove(bundleId);
139
+ throw error;
140
+ },
141
+ );
140
142
  _pendingLoads[bundleId] = load;
141
143
  return load;
142
144
  }
@@ -177,9 +179,7 @@ class BundleUnitOfWork {
177
179
  }
178
180
  }
179
181
 
180
- return sortBundles(dataById.values.toList(), orderBy)
181
- .take(limit)
182
- .toList();
182
+ return sortBundles(dataById.values.toList(), orderBy).take(limit).toList();
183
183
  }
184
184
 
185
185
  void markInsert(Bundle bundle) {
@@ -199,7 +199,8 @@ class BundleUnitOfWork {
199
199
  _changes[bundle.id] = _TrackedBundleChange(
200
200
  operation: op,
201
201
  data: bundle,
202
- before: prev?.before ??
202
+ before:
203
+ prev?.before ??
203
204
  (prevEntry is _PresentEntry ? prevEntry.bundle : null),
204
205
  );
205
206
  }
@@ -211,7 +212,8 @@ class BundleUnitOfWork {
211
212
  _changes[bundle.id] = _TrackedBundleChange(
212
213
  operation: BundleChangeOperation.delete,
213
214
  data: bundle,
214
- before: prev?.before ??
215
+ before:
216
+ prev?.before ??
215
217
  (prevEntry is _PresentEntry ? prevEntry.bundle : bundle),
216
218
  );
217
219
  }
@@ -226,9 +228,11 @@ class BundleUnitOfWork {
226
228
 
227
229
  int listFetchExtraCount() {
228
230
  return _changes.values
229
- .where((c) =>
230
- c.operation == BundleChangeOperation.update ||
231
- c.operation == BundleChangeOperation.delete)
231
+ .where(
232
+ (c) =>
233
+ c.operation == BundleChangeOperation.update ||
234
+ c.operation == BundleChangeOperation.delete,
235
+ )
232
236
  .length;
233
237
  }
234
238
 
@@ -237,10 +241,11 @@ class BundleUnitOfWork {
237
241
  for (final change in _changes.values) {
238
242
  if (change.operation == BundleChangeOperation.insert) continue;
239
243
  final matchedBefore =
240
- change.before != null && bundleMatchesQueryWhere(change.before!, where);
244
+ change.before != null &&
245
+ bundleMatchesQueryWhere(change.before!, where);
241
246
  final matchesAfter =
242
247
  change.operation == BundleChangeOperation.update &&
243
- bundleMatchesQueryWhere(change.data, where);
248
+ bundleMatchesQueryWhere(change.data, where);
244
249
  if (matchedBefore && !matchesAfter) {
245
250
  total -= 1;
246
251
  } else if (!matchedBefore && matchesAfter) {