@_nazmiforreal/flutter-ota 0.1.0 → 0.1.1

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 (177) hide show
  1. package/bin/flutter-patcher.js +37 -39
  2. package/dart-src/packages/cli-tools/.dart_tool/package_config.json +496 -0
  3. package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +743 -0
  4. package/dart-src/packages/cli-tools/.gitkeep +0 -0
  5. package/dart-src/packages/cli-tools/bin/flutter_patcher.dart +9 -0
  6. package/dart-src/packages/cli-tools/lib/flutter_patcher_cli.dart +73 -0
  7. package/dart-src/packages/cli-tools/lib/src/backend.dart +162 -0
  8. package/dart-src/packages/cli-tools/lib/src/cli_base.dart +56 -0
  9. package/dart-src/packages/cli-tools/lib/src/commands/build.dart +99 -0
  10. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +151 -0
  11. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +140 -0
  12. package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +113 -0
  13. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +55 -0
  14. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +95 -0
  15. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +48 -0
  16. package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +29 -0
  17. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +82 -0
  18. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +55 -0
  19. package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +132 -0
  20. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +41 -0
  21. package/dart-src/packages/cli-tools/lib/src/config.dart +715 -0
  22. package/dart-src/packages/cli-tools/lib/src/operations.dart +206 -0
  23. package/dart-src/packages/cli-tools/lib/src/pack.dart +417 -0
  24. package/dart-src/packages/cli-tools/lib/src/sign.dart +60 -0
  25. package/dart-src/packages/cli-tools/lib/src/util.dart +86 -0
  26. package/dart-src/packages/cli-tools/migrations/cloudflare/0001_hot-updater_init.sql +16 -0
  27. package/dart-src/packages/cli-tools/migrations/cloudflare/0002_hot-updater_0.13.0.sql +8 -0
  28. package/dart-src/packages/cli-tools/migrations/cloudflare/0003_hot-updater_0.18.0.sql +38 -0
  29. package/dart-src/packages/cli-tools/migrations/cloudflare/0004_hot-updater_0.29.0.sql +11 -0
  30. package/dart-src/packages/cli-tools/migrations/cloudflare/0005_hot-updater_0.31.0.sql +24 -0
  31. package/dart-src/packages/cli-tools/migrations/cloudflare/bundles.sql +42 -0
  32. package/dart-src/packages/cli-tools/migrations/supabase/20250103114225_init.sql +228 -0
  33. package/dart-src/packages/cli-tools/migrations/supabase/20250314000000_hot-updater_0.13.0.sql +134 -0
  34. package/dart-src/packages/cli-tools/migrations/supabase/20250516000000_hot-updater_0.18.0.sql +194 -0
  35. package/dart-src/packages/cli-tools/migrations/supabase/20251014000000_hot-updater_0.21.0.sql +184 -0
  36. package/dart-src/packages/cli-tools/migrations/supabase/20260401000000_hot-updater_0.29.0.sql +503 -0
  37. package/dart-src/packages/cli-tools/migrations/supabase/20260414000000_hot-updater_0.30.0.sql +50 -0
  38. package/dart-src/packages/cli-tools/migrations/supabase/20260422000000_hot-updater_0.31.0.sql +20 -0
  39. package/dart-src/packages/cli-tools/migrations/supabase/20260520014100_hot-updater_rls.sql +48 -0
  40. package/dart-src/packages/cli-tools/pubspec.lock +638 -0
  41. package/dart-src/packages/cli-tools/pubspec.yaml +37 -0
  42. package/dart-src/packages/core/lib/flutter_patcher_core.dart +29 -0
  43. package/dart-src/packages/core/lib/src/app_update_info.dart +111 -0
  44. package/dart-src/packages/core/lib/src/bundle.dart +191 -0
  45. package/dart-src/packages/core/lib/src/bundle_artifacts.dart +39 -0
  46. package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +47 -0
  47. package/dart-src/packages/core/lib/src/changed_asset.dart +77 -0
  48. package/dart-src/packages/core/lib/src/get_bundles_args.dart +78 -0
  49. package/dart-src/packages/core/lib/src/metadata.dart +31 -0
  50. package/dart-src/packages/core/lib/src/patch_info.dart +284 -0
  51. package/dart-src/packages/core/lib/src/platform.dart +15 -0
  52. package/dart-src/packages/core/lib/src/rollout.dart +209 -0
  53. package/dart-src/packages/core/lib/src/semver.dart +70 -0
  54. package/dart-src/packages/core/lib/src/semver_range.dart +358 -0
  55. package/dart-src/packages/core/lib/src/semver_version.dart +94 -0
  56. package/dart-src/packages/core/lib/src/status.dart +24 -0
  57. package/dart-src/packages/core/lib/src/strategy.dart +10 -0
  58. package/dart-src/packages/core/lib/src/update_bundle_params.dart +30 -0
  59. package/dart-src/packages/core/lib/src/uuid.dart +45 -0
  60. package/dart-src/packages/core/pubspec.lock +381 -0
  61. package/dart-src/packages/core/pubspec.yaml +9 -0
  62. package/dart-src/packages/core/tool/dbg.dart +6 -0
  63. package/dart-src/plugins/aws/.gitkeep +0 -0
  64. package/dart-src/plugins/aws/lib/flutter_patcher_aws.dart +12 -0
  65. package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +207 -0
  66. package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +130 -0
  67. package/dart-src/plugins/aws/lib/src/aws_config.dart +59 -0
  68. package/dart-src/plugins/aws/lib/src/aws_database.dart +198 -0
  69. package/dart-src/plugins/aws/lib/src/aws_lambda_edge_storage.dart +72 -0
  70. package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +312 -0
  71. package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +125 -0
  72. package/dart-src/plugins/aws/lib/src/aws_storage.dart +20 -0
  73. package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +123 -0
  74. package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +136 -0
  75. package/dart-src/plugins/aws/pubspec.lock +435 -0
  76. package/dart-src/plugins/aws/pubspec.yaml +22 -0
  77. package/dart-src/plugins/cloudflare/.gitkeep +0 -0
  78. package/dart-src/plugins/cloudflare/lib/flutter_patcher_cloudflare.dart +18 -0
  79. package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +29 -0
  80. package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +93 -0
  81. package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +131 -0
  82. package/dart-src/plugins/cloudflare/lib/src/d1_client.dart +66 -0
  83. package/dart-src/plugins/cloudflare/lib/src/d1_config.dart +30 -0
  84. package/dart-src/plugins/cloudflare/lib/src/d1_database.dart +16 -0
  85. package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +314 -0
  86. package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +50 -0
  87. package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +287 -0
  88. package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +23 -0
  89. package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +110 -0
  90. package/dart-src/plugins/cloudflare/migrations/0001_hot-updater_init.sql +16 -0
  91. package/dart-src/plugins/cloudflare/migrations/0002_hot-updater_0.13.0.sql +8 -0
  92. package/dart-src/plugins/cloudflare/migrations/0003_hot-updater_0.18.0.sql +38 -0
  93. package/dart-src/plugins/cloudflare/migrations/0004_hot-updater_0.29.0.sql +11 -0
  94. package/dart-src/plugins/cloudflare/migrations/0005_hot-updater_0.31.0.sql +24 -0
  95. package/dart-src/plugins/cloudflare/pubspec.lock +411 -0
  96. package/dart-src/plugins/cloudflare/pubspec.yaml +20 -0
  97. package/dart-src/plugins/cloudflare/sql/bundles.sql +42 -0
  98. package/dart-src/plugins/plugin-core/lib/flutter_patcher_plugin_core.dart +42 -0
  99. package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +94 -0
  100. package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +44 -0
  101. package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +259 -0
  102. package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work_store.dart +20 -0
  103. package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +29 -0
  104. package/dart-src/plugins/plugin-core/lib/src/compression_format.dart +63 -0
  105. package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +22 -0
  106. package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +853 -0
  107. package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +525 -0
  108. package/dart-src/plugins/plugin-core/lib/src/create_storage_key_builder.dart +15 -0
  109. package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +130 -0
  110. package/dart-src/plugins/plugin-core/lib/src/filter_compatible_app_versions.dart +18 -0
  111. package/dart-src/plugins/plugin-core/lib/src/generate_min_bundle_id.dart +19 -0
  112. package/dart-src/plugins/plugin-core/lib/src/get_update_info.dart +241 -0
  113. package/dart-src/plugins/plugin-core/lib/src/is_object.dart +5 -0
  114. package/dart-src/plugins/plugin-core/lib/src/legacy_asset_storage_layout.dart +9 -0
  115. package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +112 -0
  116. package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +44 -0
  117. package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +70 -0
  118. package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +62 -0
  119. package/dart-src/plugins/plugin-core/lib/src/resolve_update_info_from_bundles.dart +43 -0
  120. package/dart-src/plugins/plugin-core/lib/src/storage_profile.dart +27 -0
  121. package/dart-src/plugins/plugin-core/lib/src/types.dart +454 -0
  122. package/dart-src/plugins/plugin-core/lib/src/uuidv7.dart +41 -0
  123. package/dart-src/plugins/plugin-core/pubspec.lock +396 -0
  124. package/dart-src/plugins/plugin-core/pubspec.yaml +15 -0
  125. package/dart-src/plugins/postgres/.gitkeep +0 -0
  126. package/dart-src/plugins/postgres/lib/flutter_patcher_postgres.dart +17 -0
  127. package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +130 -0
  128. package/dart-src/plugins/postgres/lib/src/postgres_client.dart +90 -0
  129. package/dart-src/plugins/postgres/lib/src/postgres_config.dart +35 -0
  130. package/dart-src/plugins/postgres/lib/src/postgres_database.dart +302 -0
  131. package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +120 -0
  132. package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +205 -0
  133. package/dart-src/plugins/postgres/lib/src/postgres_types.dart +132 -0
  134. package/dart-src/plugins/postgres/pubspec.lock +427 -0
  135. package/dart-src/plugins/postgres/pubspec.yaml +18 -0
  136. package/dart-src/plugins/postgres/sql/bundles.sql +45 -0
  137. package/dart-src/plugins/postgres/sql/get_target_app_version_list.sql +21 -0
  138. package/dart-src/plugins/postgres/sql/get_update_info.spec.ts +171 -0
  139. package/dart-src/plugins/postgres/sql/get_update_info_by_app_version.sql +126 -0
  140. package/dart-src/plugins/postgres/sql/get_update_info_by_fingerprint_hash.sql +125 -0
  141. package/dart-src/plugins/postgres/sql/hash_user_id.sql +30 -0
  142. package/dart-src/plugins/postgres/sql/is_device_eligible.sql +237 -0
  143. package/dart-src/plugins/postgres/sql/prepareSql.ts +11 -0
  144. package/dart-src/plugins/standalone/.gitkeep +0 -0
  145. package/dart-src/plugins/standalone/lib/flutter_patcher_standalone.dart +12 -0
  146. package/dart-src/plugins/standalone/lib/src/standalone_config.dart +107 -0
  147. package/dart-src/plugins/standalone/lib/src/standalone_database.dart +233 -0
  148. package/dart-src/plugins/standalone/lib/src/standalone_storage.dart +175 -0
  149. package/dart-src/plugins/standalone/pubspec.lock +411 -0
  150. package/dart-src/plugins/standalone/pubspec.yaml +18 -0
  151. package/dart-src/plugins/supabase/lib/edge.dart +10 -0
  152. package/dart-src/plugins/supabase/lib/flutter_patcher_supabase.dart +21 -0
  153. package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +129 -0
  154. package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +154 -0
  155. package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +516 -0
  156. package/dart-src/plugins/supabase/lib/src/supabase_config.dart +34 -0
  157. package/dart-src/plugins/supabase/lib/src/supabase_database.dart +422 -0
  158. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_database.dart +32 -0
  159. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +99 -0
  160. package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +145 -0
  161. package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +298 -0
  162. package/dart-src/plugins/supabase/lib/src/types.dart +130 -0
  163. package/dart-src/plugins/supabase/pubspec.lock +515 -0
  164. package/dart-src/plugins/supabase/pubspec.yaml +19 -0
  165. package/dart-src/plugins/supabase/supabase/edge-functions/index.ts +44 -0
  166. package/dart-src/plugins/supabase/supabase/edge-functions/runtime.docker.integration.spec.ts +968 -0
  167. package/dart-src/plugins/supabase/supabase/migrations/20250103114225_init.sql +228 -0
  168. package/dart-src/plugins/supabase/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +134 -0
  169. package/dart-src/plugins/supabase/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +194 -0
  170. package/dart-src/plugins/supabase/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +184 -0
  171. package/dart-src/plugins/supabase/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +503 -0
  172. package/dart-src/plugins/supabase/supabase/migrations/20260414000000_hot-updater_0.30.0.sql +50 -0
  173. package/dart-src/plugins/supabase/supabase/migrations/20260422000000_hot-updater_0.31.0.sql +20 -0
  174. package/dart-src/plugins/supabase/supabase/migrations/20260520014100_hot-updater_rls.sql +48 -0
  175. package/package.json +2 -1
  176. package/scripts/postinstall.js +32 -23
  177. /package/bin/{flutter-patcher-linux → flutter-patcher-linux-x64} +0 -0
@@ -0,0 +1,93 @@
1
+ import 'dart:convert' show jsonEncode;
2
+
3
+ import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
4
+ show DatabaseBundleQueryWhere;
5
+
6
+ /// Result of building a WHERE clause: SQL fragment plus positional params.
7
+ class _BuildWhereResult {
8
+ const _BuildWhereResult(this.sql, this.params);
9
+
10
+ final String sql;
11
+ final List<dynamic> params;
12
+ }
13
+
14
+ /// Build a SQLite WHERE clause with positional `?` params, mirroring
15
+ /// `buildWhereClause` from hot-updater's `d1Database.ts`.
16
+ _BuildWhereResult buildWhereClause(DatabaseBundleQueryWhere conditions) {
17
+ final clauses = <String>[];
18
+ final params = <dynamic>[];
19
+
20
+ String jsonEachIn(String columnName, List<String> values) {
21
+ if (values.isEmpty) return '1 = 0';
22
+ params.add(jsonEncode(values));
23
+ return '$columnName IN (SELECT value FROM json_each(?))';
24
+ }
25
+
26
+ if (conditions.channel != null) {
27
+ clauses.add('channel = ?');
28
+ params.add(conditions.channel);
29
+ }
30
+
31
+ if (conditions.platform != null) {
32
+ clauses.add('platform = ?');
33
+ params.add(conditions.platform!.value);
34
+ }
35
+
36
+ if (conditions.enabled != null) {
37
+ clauses.add('enabled = ?');
38
+ params.add(conditions.enabled! ? 1 : 0);
39
+ }
40
+
41
+ if (conditions.id?.ins != null) {
42
+ clauses.add(jsonEachIn('id', conditions.id!.ins!));
43
+ }
44
+
45
+ if (conditions.id?.eq != null) {
46
+ clauses.add('id = ?');
47
+ params.add(conditions.id!.eq);
48
+ }
49
+
50
+ if (conditions.id?.gt != null) {
51
+ clauses.add('id > ?');
52
+ params.add(conditions.id!.gt);
53
+ }
54
+
55
+ if (conditions.id?.gte != null) {
56
+ clauses.add('id >= ?');
57
+ params.add(conditions.id!.gte);
58
+ }
59
+
60
+ if (conditions.id?.lt != null) {
61
+ clauses.add('id < ?');
62
+ params.add(conditions.id!.lt);
63
+ }
64
+
65
+ if (conditions.id?.lte != null) {
66
+ clauses.add('id <= ?');
67
+ params.add(conditions.id!.lte);
68
+ }
69
+
70
+ if (conditions.targetAppVersionNotNull == true) {
71
+ clauses.add('target_app_version IS NOT NULL');
72
+ }
73
+
74
+ if (conditions.targetAppVersion != null) {
75
+ clauses.add('target_app_version = ?');
76
+ params.add(conditions.targetAppVersion);
77
+ }
78
+
79
+ if (conditions.targetAppVersionIn != null) {
80
+ clauses.add(
81
+ jsonEachIn('target_app_version', conditions.targetAppVersionIn!),
82
+ );
83
+ }
84
+
85
+ if (conditions.fingerprintHash != null) {
86
+ clauses.add('fingerprint_hash = ?');
87
+ params.add(conditions.fingerprintHash);
88
+ }
89
+
90
+ final whereClause =
91
+ clauses.isNotEmpty ? ' WHERE ${clauses.join(' AND ')}' : '';
92
+ return _BuildWhereResult(whereClause, params);
93
+ }
@@ -0,0 +1,131 @@
1
+ import 'dart:convert' show jsonDecode;
2
+
3
+ import 'package:flutter_patcher_core/flutter_patcher_core.dart'
4
+ show Bundle, BundleMetadata, BundlePatchArtifact, Platform, getBundlePatches;
5
+
6
+ /// Default rollout cohort count — full rollout in the Dart port's per-mille
7
+ /// scale (matches core's `defaultRolloutCohortCount` = `numericCohortSize`).
8
+ const int defaultRolloutCohortCount = 1000;
9
+
10
+ /// Compound patch id `${bundleId}:${baseBundleId}` (matches D1 schema).
11
+ String buildBundlePatchId(String bundleId, String baseBundleId) =>
12
+ '$bundleId:$baseBundleId';
13
+
14
+ /// Parse `target_cohorts` which is stored as a JSON string (or already a list).
15
+ List<String>? parseTargetCohorts(dynamic value) {
16
+ if (value == null) return null;
17
+ if (value is List) {
18
+ return [
19
+ for (final v in value)
20
+ if (v is String) v,
21
+ ];
22
+ }
23
+ if (value is String) {
24
+ try {
25
+ final parsed = jsonDecode(value);
26
+ if (parsed is List) {
27
+ return [
28
+ for (final v in parsed)
29
+ if (v is String) v,
30
+ ];
31
+ }
32
+ } catch (_) {
33
+ return null;
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+
39
+ /// Parse `metadata` which may be a JSON string or an already-decoded object.
40
+ Map<String, Object?>? parseMetadata(dynamic value) {
41
+ if (value == null) return null;
42
+ if (value is String) {
43
+ try {
44
+ return parseMetadata(jsonDecode(value));
45
+ } catch (_) {
46
+ return null;
47
+ }
48
+ }
49
+ if (value is Map) {
50
+ return value.cast<String, Object?>();
51
+ }
52
+ return null;
53
+ }
54
+
55
+ /// Convert a [Bundle] into the rows inserted into `bundle_patches`.
56
+ List<Map<String, dynamic>> bundleToPatchRows(Bundle bundle) =>
57
+ getBundlePatches(bundle).asMap().entries.map((entry) {
58
+ final patch = entry.value;
59
+ return {
60
+ 'id': buildBundlePatchId(bundle.id, patch.baseBundleId),
61
+ 'bundle_id': bundle.id,
62
+ 'base_bundle_id': patch.baseBundleId,
63
+ 'base_file_hash': patch.baseFileHash,
64
+ 'patch_file_hash': patch.patchFileHash,
65
+ 'patch_storage_uri': patch.patchStorageUri,
66
+ 'order_index': entry.key,
67
+ };
68
+ }).toList();
69
+
70
+ /// Rebuild a [Bundle] from a D1 `bundles` row plus its `bundle_patches` rows.
71
+ Bundle transformRowToBundle(
72
+ Map<String, dynamic> row, [
73
+ List<Map<String, dynamic>> patchRows = const [],
74
+ ]) {
75
+ final rawMetadata = parseMetadata(row['metadata']);
76
+ final metadata = rawMetadata == null
77
+ ? null
78
+ : BundleMetadata.fromJson(rawMetadata.cast<String, dynamic>());
79
+ final patches = patchRows
80
+ .toList()
81
+ ..sort(
82
+ (left, right) =>
83
+ (left['order_index'] as num? ?? 0)
84
+ .compareTo(right['order_index'] as num? ?? 0) +
85
+ (left['base_bundle_id'] as String? ?? '')
86
+ .compareTo(right['base_bundle_id'] as String? ?? ''),
87
+ );
88
+
89
+ final bundlePatches = patches.map((patch) {
90
+ return BundlePatchArtifact(
91
+ baseBundleId: patch['base_bundle_id'] as String,
92
+ baseFileHash: patch['base_file_hash'] as String,
93
+ patchFileHash: patch['patch_file_hash'] as String,
94
+ patchStorageUri: patch['patch_storage_uri'] as String,
95
+ );
96
+ }).toList();
97
+
98
+ final primaryPatch = bundlePatches.isNotEmpty ? bundlePatches.first : null;
99
+
100
+ final enabled = row['enabled'];
101
+ final shouldForceUpdate = row['should_force_update'];
102
+
103
+ return Bundle(
104
+ id: row['id'] as String,
105
+ channel: row['channel'] as String,
106
+ enabled: enabled is bool ? enabled : (enabled as num? ?? 0) != 0,
107
+ shouldForceUpdate: shouldForceUpdate is bool
108
+ ? shouldForceUpdate
109
+ : (shouldForceUpdate as num? ?? 0) != 0,
110
+ fileHash: row['file_hash'] as String,
111
+ gitCommitHash: row['git_commit_hash'] as String?,
112
+ message: row['message'] as String?,
113
+ platform: Platform.fromValue(row['platform'] as String),
114
+ targetAppVersion: row['target_app_version'] as String?,
115
+ storageUri: row['storage_uri'] as String,
116
+ fingerprintHash: row['fingerprint_hash'] as String?,
117
+ metadata: metadata,
118
+ manifestStorageUri: row['manifest_storage_uri'] as String?,
119
+ manifestFileHash: row['manifest_file_hash'] as String?,
120
+ assetBaseStorageUri: row['asset_base_storage_uri'] as String?,
121
+ patches: bundlePatches,
122
+ patchBaseBundleId: primaryPatch?.baseBundleId,
123
+ patchBaseFileHash: primaryPatch?.baseFileHash,
124
+ patchFileHash: primaryPatch?.patchFileHash,
125
+ patchStorageUri: primaryPatch?.patchStorageUri,
126
+ rolloutCohortCount:
127
+ (row['rollout_cohort_count'] as num? ?? defaultRolloutCohortCount)
128
+ .toInt(),
129
+ targetCohorts: parseTargetCohorts(row['target_cohorts']),
130
+ );
131
+ }
@@ -0,0 +1,66 @@
1
+ import 'dart:convert' show jsonDecode, jsonEncode;
2
+
3
+ import 'package:http/http.dart' show Client;
4
+
5
+ import 'd1_config.dart' show D1DatabaseConfig;
6
+
7
+ /// A thin client over a Cloudflare D1 database.
8
+ ///
9
+ /// Mirrors the `cf.d1.database.query(databaseId, { account_id, sql, params })`
10
+ /// call from hot-updater's cloudflare plugin: each call issues a single SQL
11
+ /// statement with positional `?` parameters and returns the flattened result
12
+ /// rows as JSON maps.
13
+ abstract class D1ClientLike {
14
+ const D1ClientLike();
15
+
16
+ Future<List<Map<String, dynamic>>> query(
17
+ String sql, [
18
+ List<dynamic> params = const [],
19
+ ]);
20
+ }
21
+
22
+ /// Real D1 client backed by the Cloudflare REST API.
23
+ class D1Client implements D1ClientLike {
24
+ D1Client(this.config, {Client? http}) : _http = http ?? Client();
25
+
26
+ final D1DatabaseConfig config;
27
+ final Client _http;
28
+
29
+ @override
30
+ Future<List<Map<String, dynamic>>> query(
31
+ String sql, [
32
+ List<dynamic> params = const [],
33
+ ]) async {
34
+ final uri = Uri.https(
35
+ 'api.cloudflare.com',
36
+ '/client/v4/accounts/${config.accountId}/d1/database/${config.databaseId}/query',
37
+ );
38
+
39
+ final response = await _http.post(
40
+ uri,
41
+ headers: {
42
+ 'Authorization': 'Bearer ${config.cloudflareApiToken}',
43
+ 'Content-Type': 'application/json',
44
+ },
45
+ body: jsonEncode({'sql': sql, 'params': params}),
46
+ );
47
+
48
+ if (response.statusCode < 200 || response.statusCode >= 300) {
49
+ throw Exception(
50
+ 'Cloudflare D1 query failed (${response.statusCode}): ${response.body}',
51
+ );
52
+ }
53
+
54
+ final decoded = jsonDecode(response.body) as Map<String, dynamic>;
55
+ final result = (decoded['result'] as List?) ?? const <dynamic>[];
56
+
57
+ final rows = <Map<String, dynamic>>[];
58
+ for (final page in result.cast<Map<String, dynamic>>()) {
59
+ final pageResults = (page['results'] as List?) ?? const <dynamic>[];
60
+ for (final row in pageResults) {
61
+ rows.add((row as Map).cast<String, dynamic>());
62
+ }
63
+ }
64
+ return rows;
65
+ }
66
+ }
@@ -0,0 +1,30 @@
1
+ import 'd1_client.dart' show D1Client, D1ClientLike;
2
+
3
+ /// Configuration for the Cloudflare D1 database plugin.
4
+ class D1DatabaseConfig {
5
+ const D1DatabaseConfig({
6
+ required this.databaseId,
7
+ required this.accountId,
8
+ required this.cloudflareApiToken,
9
+ this.clientFactory,
10
+ });
11
+
12
+ /// Cloudflare D1 database ID.
13
+ final String databaseId;
14
+
15
+ /// Cloudflare account ID.
16
+ final String accountId;
17
+
18
+ /// Cloudflare API token with D1 read/write permissions.
19
+ final String cloudflareApiToken;
20
+
21
+ /// Optional factory used to inject a [D1ClientLike] in tests.
22
+ final D1ClientFactory? clientFactory;
23
+ }
24
+
25
+ /// Builds a [D1ClientLike] for the given [D1DatabaseConfig].
26
+ typedef D1ClientFactory = D1ClientLike Function(D1DatabaseConfig config);
27
+
28
+ /// Resolves the client for a config, using the injected factory when present.
29
+ D1ClientLike resolveD1Client(D1DatabaseConfig config) =>
30
+ config.clientFactory?.call(config) ?? D1Client(config);
@@ -0,0 +1,16 @@
1
+ import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
2
+ show createDatabasePlugin;
3
+
4
+ import 'd1_config.dart' show D1DatabaseConfig, resolveD1Client;
5
+ import 'd1_database_plugin.dart' show D1DatabasePlugin;
6
+
7
+ /// Cloudflare D1 database plugin over the REST API (faithful port of
8
+ /// hot-updater's `d1Database`).
9
+ ///
10
+ /// Issues raw SQLite statements against Cloudflare D1 over the REST API and
11
+ /// resolves update information in-process via [resolveUpdateInfoFromBundles]
12
+ /// (no SQL stored procedures, unlike the Supabase/Postgres backends).
13
+ final d1Database = createDatabasePlugin<D1DatabaseConfig>(
14
+ name: 'd1Database',
15
+ factory: (config) => D1DatabasePlugin(resolveD1Client(config)),
16
+ );
@@ -0,0 +1,314 @@
1
+ import 'dart:convert' show jsonEncode;
2
+
3
+ import 'package:flutter_patcher_core/flutter_patcher_core.dart'
4
+ show
5
+ AppVersionGetBundlesArgs,
6
+ Bundle,
7
+ FingerprintGetBundlesArgs,
8
+ GetBundlesArgs,
9
+ UpdateInfo,
10
+ UpdateStrategy;
11
+ import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
12
+ show
13
+ AbstractDatabasePlugin,
14
+ BundleChange,
15
+ BundleChangeOperation,
16
+ DatabaseBundleIdFilter,
17
+ DatabaseBundleQueryOptions,
18
+ DatabaseBundleQueryOrder,
19
+ DatabaseBundleQueryWhere,
20
+ Paginated,
21
+ calculatePagination,
22
+ filterCompatibleAppVersions,
23
+ resolveUpdateInfoFromBundles,
24
+ ResolveUpdateInfoFromBundlesOptions;
25
+
26
+ import 'd1_bundle_mapper.dart'
27
+ show
28
+ bundleToPatchRows,
29
+ defaultRolloutCohortCount,
30
+ transformRowToBundle;
31
+ import 'd1_build_where.dart' show buildWhereClause;
32
+ import 'd1_client.dart' show D1ClientLike;
33
+
34
+ /// Shared D1 database plugin logic used by both [d1Database] (REST API client)
35
+ /// and [cloudflareWorkerDatabase] (in-process D1 binding).
36
+ ///
37
+ /// Issues raw SQLite statements and resolves update information in-process via
38
+ /// [resolveUpdateInfoFromBundles] (no SQL stored procedures, unlike the
39
+ /// Supabase/Postgres backends).
40
+ class D1DatabasePlugin implements AbstractDatabasePlugin {
41
+ D1DatabasePlugin(this._client);
42
+
43
+ final D1ClientLike _client;
44
+
45
+ Future<Map<String, List<Map<String, dynamic>>>> _getPatchMap(
46
+ List<String> bundleIds,
47
+ ) async {
48
+ final patchMap = <String, List<Map<String, dynamic>>>{};
49
+ if (bundleIds.isEmpty) return patchMap;
50
+
51
+ final rows = await _client.query(
52
+ 'SELECT * FROM bundle_patches '
53
+ 'WHERE bundle_id IN (SELECT value FROM json_each(?)) '
54
+ 'ORDER BY order_index ASC, base_bundle_id ASC',
55
+ [jsonEncode(bundleIds)],
56
+ );
57
+
58
+ for (final row in rows) {
59
+ (patchMap[row['bundle_id'] as String] ??= []).add(row);
60
+ }
61
+ return patchMap;
62
+ }
63
+
64
+ Future<int> _getTotalCount(DatabaseBundleQueryWhere conditions) async {
65
+ final where = buildWhereClause(conditions);
66
+ final rows = await _client.query(
67
+ 'SELECT COUNT(*) as total FROM bundles${where.sql}',
68
+ where.params,
69
+ );
70
+ return (rows.firstOrNull?['total'] as num?)?.toInt() ?? 0;
71
+ }
72
+
73
+ Future<List<Bundle>> _getPaginatedBundles(
74
+ DatabaseBundleQueryWhere conditions,
75
+ int limit,
76
+ int offset,
77
+ DatabaseBundleQueryOrder? orderBy,
78
+ ) async {
79
+ final where = buildWhereClause(conditions);
80
+ final orderBySql =
81
+ orderBy?.direction == 'asc' ? 'ORDER BY id ASC' : 'ORDER BY id DESC';
82
+
83
+ final rows = await _client.query(
84
+ 'SELECT * FROM bundles ${where.sql} $orderBySql LIMIT ? OFFSET ?',
85
+ [...where.params, limit, offset],
86
+ );
87
+
88
+ final patchMap = await _getPatchMap(
89
+ rows.map((row) => row['id'] as String).toList(),
90
+ );
91
+ return rows
92
+ .map((row) => transformRowToBundle(row, patchMap[row['id']] ?? const []))
93
+ .toList();
94
+ }
95
+
96
+ Future<List<Bundle>> _queryBundlesForUpdateInfo(
97
+ DatabaseBundleQueryWhere conditions,
98
+ ) async {
99
+ final where = buildWhereClause(conditions);
100
+ final rows = await _client.query(
101
+ 'SELECT * FROM bundles${where.sql}',
102
+ where.params,
103
+ );
104
+ final patchMap = await _getPatchMap(
105
+ rows.map((row) => row['id'] as String).toList(),
106
+ );
107
+ return rows
108
+ .map((row) => transformRowToBundle(row, patchMap[row['id']] ?? const []))
109
+ .toList();
110
+ }
111
+
112
+ Future<List<String>> _getTargetAppVersionsForUpdateInfo({
113
+ required String channel,
114
+ required String platform,
115
+ required String minBundleId,
116
+ }) async {
117
+ final rows = await _client.query(
118
+ 'SELECT target_app_version FROM bundles '
119
+ 'WHERE channel = ? AND platform = ? AND enabled = 1 '
120
+ 'AND id >= ? AND target_app_version IS NOT NULL '
121
+ 'GROUP BY target_app_version',
122
+ [channel, platform, minBundleId],
123
+ );
124
+ return rows
125
+ .map((row) => row['target_app_version'] as String)
126
+ .toList();
127
+ }
128
+
129
+ @override
130
+ Future<UpdateInfo?> getUpdateInfo(GetBundlesArgs args) async {
131
+ final channel = args.channel;
132
+ final minBundleId = args.minBundleId;
133
+ final platform = args.platform.value;
134
+
135
+ if (args.updateStrategy == UpdateStrategy.appVersion) {
136
+ final appVersion = (args as AppVersionGetBundlesArgs).appVersion;
137
+ final targetAppVersions = await _getTargetAppVersionsForUpdateInfo(
138
+ channel: channel,
139
+ platform: platform,
140
+ minBundleId: minBundleId,
141
+ );
142
+ final compatibleAppVersions = filterCompatibleAppVersions(
143
+ targetAppVersions,
144
+ appVersion,
145
+ );
146
+ final bundles = compatibleAppVersions.isNotEmpty
147
+ ? await _queryBundlesForUpdateInfo(
148
+ DatabaseBundleQueryWhere(
149
+ enabled: true,
150
+ platform: args.platform,
151
+ channel: channel,
152
+ id: DatabaseBundleIdFilter(gte: minBundleId),
153
+ targetAppVersionIn: compatibleAppVersions,
154
+ ),
155
+ )
156
+ : const <Bundle>[];
157
+
158
+ return resolveUpdateInfoFromBundles(
159
+ ResolveUpdateInfoFromBundlesOptions(args: args, bundles: bundles),
160
+ );
161
+ }
162
+
163
+ final fingerprintHash = args is FingerprintGetBundlesArgs
164
+ ? args.fingerprintHash
165
+ : null;
166
+
167
+ final bundles = await _queryBundlesForUpdateInfo(
168
+ DatabaseBundleQueryWhere(
169
+ enabled: true,
170
+ platform: args.platform,
171
+ channel: channel,
172
+ id: DatabaseBundleIdFilter(gte: minBundleId),
173
+ fingerprintHash: fingerprintHash,
174
+ ),
175
+ );
176
+
177
+ return resolveUpdateInfoFromBundles(
178
+ ResolveUpdateInfoFromBundlesOptions(args: args, bundles: bundles),
179
+ );
180
+ }
181
+
182
+ @override
183
+ Future<Bundle?> getBundleById(String bundleId) async {
184
+ final rows = await _client.query(
185
+ 'SELECT * FROM bundles WHERE id = ? LIMIT 1',
186
+ [bundleId],
187
+ );
188
+ if (rows.isEmpty) return null;
189
+
190
+ final patchMap = await _getPatchMap([bundleId]);
191
+ return transformRowToBundle(rows.first, patchMap[bundleId] ?? const []);
192
+ }
193
+
194
+ @override
195
+ Future<Paginated<List<Bundle>>> getBundles(
196
+ DatabaseBundleQueryOptions options,
197
+ ) async {
198
+ final where = options.where ?? const DatabaseBundleQueryWhere();
199
+ final limit = options.limit;
200
+ final offset = options.offset ??
201
+ (options.page != null ? (options.page! - 1) * limit : 0);
202
+
203
+ final totalCount = await _getTotalCount(where);
204
+ final bundles = await _getPaginatedBundles(
205
+ where,
206
+ limit,
207
+ offset,
208
+ options.orderBy,
209
+ );
210
+
211
+ final pagination = calculatePagination(
212
+ totalCount,
213
+ limit: limit,
214
+ offset: offset,
215
+ );
216
+
217
+ return Paginated<List<Bundle>>(data: bundles, pagination: pagination);
218
+ }
219
+
220
+ @override
221
+ Future<List<String>> getChannels() async {
222
+ final rows = await _client.query(
223
+ 'SELECT channel FROM bundles GROUP BY channel',
224
+ );
225
+ return rows.map((row) => row['channel'] as String).toList();
226
+ }
227
+
228
+ @override
229
+ bool get supportsCursorPagination => false;
230
+
231
+ @override
232
+ Future<void> onUnmount() async {}
233
+
234
+ @override
235
+ Future<void> commitBundle({
236
+ required List<BundleChange> changedSets,
237
+ }) async {
238
+ if (changedSets.isEmpty) return;
239
+
240
+ for (final op in changedSets) {
241
+ if (op.operation == BundleChangeOperation.delete) {
242
+ await _client.query(
243
+ 'DELETE FROM bundle_patches WHERE bundle_id = ?',
244
+ [op.data.id],
245
+ );
246
+ await _client.query(
247
+ 'DELETE FROM bundle_patches WHERE base_bundle_id = ?',
248
+ [op.data.id],
249
+ );
250
+ await _client.query(
251
+ 'DELETE FROM bundles WHERE id = ?',
252
+ [op.data.id],
253
+ );
254
+ } else {
255
+ final bundle = op.data;
256
+ await _client.query(
257
+ 'INSERT OR REPLACE INTO bundles ('
258
+ 'id, channel, enabled, should_force_update, file_hash, '
259
+ 'git_commit_hash, message, platform, target_app_version, '
260
+ 'storage_uri, fingerprint_hash, metadata, manifest_storage_uri, '
261
+ 'manifest_file_hash, asset_base_storage_uri, rollout_cohort_count, '
262
+ 'target_cohorts) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
263
+ [
264
+ bundle.id,
265
+ bundle.channel,
266
+ bundle.enabled ? 1 : 0,
267
+ bundle.shouldForceUpdate ? 1 : 0,
268
+ bundle.fileHash,
269
+ bundle.gitCommitHash,
270
+ bundle.message,
271
+ bundle.platform.value,
272
+ bundle.targetAppVersion,
273
+ bundle.storageUri,
274
+ bundle.fingerprintHash,
275
+ jsonEncode(
276
+ bundle.metadata?.toJson() ?? <String, dynamic>{},
277
+ ),
278
+ bundle.manifestStorageUri,
279
+ bundle.manifestFileHash,
280
+ bundle.assetBaseStorageUri,
281
+ bundle.rolloutCohortCount ?? defaultRolloutCohortCount,
282
+ bundle.targetCohorts != null
283
+ ? jsonEncode(bundle.targetCohorts)
284
+ : null,
285
+ ],
286
+ );
287
+
288
+ await _client.query(
289
+ 'DELETE FROM bundle_patches WHERE bundle_id = ?',
290
+ [bundle.id],
291
+ );
292
+
293
+ final patchRows = bundleToPatchRows(bundle);
294
+ for (final patchRow in patchRows) {
295
+ await _client.query(
296
+ 'INSERT OR REPLACE INTO bundle_patches ('
297
+ 'id, bundle_id, base_bundle_id, base_file_hash, '
298
+ 'patch_file_hash, patch_storage_uri, order_index) '
299
+ 'VALUES (?, ?, ?, ?, ?, ?, ?)',
300
+ [
301
+ patchRow['id'],
302
+ patchRow['bundle_id'],
303
+ patchRow['base_bundle_id'],
304
+ patchRow['base_file_hash'],
305
+ patchRow['patch_file_hash'],
306
+ patchRow['patch_storage_uri'],
307
+ patchRow['order_index'],
308
+ ],
309
+ );
310
+ }
311
+ }
312
+ }
313
+ }
314
+ }
@@ -0,0 +1,50 @@
1
+ import 'r2_s3_client.dart' show R2S3Client, R2S3ClientLike;
2
+
3
+ /// Configuration for the Cloudflare R2 S3-compatible storage plugin.
4
+ ///
5
+ /// Mirrors hot-updater's `R2S3StorageConfig` (a thin wrapper over the AWS S3
6
+ /// client config). Credentials are passed directly (access/secret keys) and
7
+ /// the client signs requests with AWS Signature V4 against the R2 endpoint.
8
+ class R2S3StorageConfig {
9
+ const R2S3StorageConfig({
10
+ required this.accountId,
11
+ required this.bucketName,
12
+ required this.accessKeyId,
13
+ required this.secretAccessKey,
14
+ this.basePath,
15
+ this.region = 'auto',
16
+ this.endpoint,
17
+ this.clientFactory,
18
+ });
19
+
20
+ /// Cloudflare account ID (used to build the R2 endpoint host).
21
+ final String accountId;
22
+
23
+ /// Target R2 bucket name.
24
+ final String bucketName;
25
+
26
+ /// S3 access key ID.
27
+ final String accessKeyId;
28
+
29
+ /// S3 secret access key.
30
+ final String secretAccessKey;
31
+
32
+ /// Optional base path prepended to all object keys.
33
+ final String? basePath;
34
+
35
+ /// R2 region (defaults to "auto").
36
+ final String region;
37
+
38
+ /// Override endpoint (defaults to `https://<accountId>.r2.cloudflarestorage.com`).
39
+ final String? endpoint;
40
+
41
+ /// Optional factory used to inject an [R2S3ClientLike] in tests.
42
+ final R2S3ClientFactory? clientFactory;
43
+ }
44
+
45
+ /// Builds an [R2S3ClientLike] for the given [R2S3StorageConfig].
46
+ typedef R2S3ClientFactory = R2S3ClientLike Function(R2S3StorageConfig config);
47
+
48
+ /// Resolves the client for a config, using the injected factory when present.
49
+ R2S3ClientLike resolveR2Client(R2S3StorageConfig config) =>
50
+ config.clientFactory?.call(config) ?? R2S3Client(config);