@_nazmiforreal/flutter-ota 0.1.0 → 0.1.2

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 (167) hide show
  1. package/bin/flutter-patcher.js +48 -39
  2. package/dart-src/packages/cli-tools/.gitkeep +0 -0
  3. package/dart-src/packages/cli-tools/bin/flutter_patcher.dart +9 -0
  4. package/dart-src/packages/cli-tools/lib/flutter_patcher_cli.dart +73 -0
  5. package/dart-src/packages/cli-tools/lib/src/backend.dart +162 -0
  6. package/dart-src/packages/cli-tools/lib/src/cli_base.dart +56 -0
  7. package/dart-src/packages/cli-tools/lib/src/commands/build.dart +99 -0
  8. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +151 -0
  9. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +140 -0
  10. package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +113 -0
  11. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +55 -0
  12. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +95 -0
  13. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +48 -0
  14. package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +29 -0
  15. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +82 -0
  16. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +55 -0
  17. package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +132 -0
  18. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +41 -0
  19. package/dart-src/packages/cli-tools/lib/src/config.dart +715 -0
  20. package/dart-src/packages/cli-tools/lib/src/operations.dart +206 -0
  21. package/dart-src/packages/cli-tools/lib/src/pack.dart +417 -0
  22. package/dart-src/packages/cli-tools/lib/src/sign.dart +60 -0
  23. package/dart-src/packages/cli-tools/lib/src/util.dart +86 -0
  24. package/dart-src/packages/cli-tools/migrations/cloudflare/0001_hot-updater_init.sql +16 -0
  25. package/dart-src/packages/cli-tools/migrations/cloudflare/0002_hot-updater_0.13.0.sql +8 -0
  26. package/dart-src/packages/cli-tools/migrations/cloudflare/0003_hot-updater_0.18.0.sql +38 -0
  27. package/dart-src/packages/cli-tools/migrations/cloudflare/0004_hot-updater_0.29.0.sql +11 -0
  28. package/dart-src/packages/cli-tools/migrations/cloudflare/0005_hot-updater_0.31.0.sql +24 -0
  29. package/dart-src/packages/cli-tools/migrations/cloudflare/bundles.sql +42 -0
  30. package/dart-src/packages/cli-tools/migrations/supabase/20250103114225_init.sql +228 -0
  31. package/dart-src/packages/cli-tools/migrations/supabase/20250314000000_hot-updater_0.13.0.sql +134 -0
  32. package/dart-src/packages/cli-tools/migrations/supabase/20250516000000_hot-updater_0.18.0.sql +194 -0
  33. package/dart-src/packages/cli-tools/migrations/supabase/20251014000000_hot-updater_0.21.0.sql +184 -0
  34. package/dart-src/packages/cli-tools/migrations/supabase/20260401000000_hot-updater_0.29.0.sql +503 -0
  35. package/dart-src/packages/cli-tools/migrations/supabase/20260414000000_hot-updater_0.30.0.sql +50 -0
  36. package/dart-src/packages/cli-tools/migrations/supabase/20260422000000_hot-updater_0.31.0.sql +20 -0
  37. package/dart-src/packages/cli-tools/migrations/supabase/20260520014100_hot-updater_rls.sql +48 -0
  38. package/dart-src/packages/cli-tools/pubspec.yaml +37 -0
  39. package/dart-src/packages/core/lib/flutter_patcher_core.dart +29 -0
  40. package/dart-src/packages/core/lib/src/app_update_info.dart +111 -0
  41. package/dart-src/packages/core/lib/src/bundle.dart +191 -0
  42. package/dart-src/packages/core/lib/src/bundle_artifacts.dart +39 -0
  43. package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +47 -0
  44. package/dart-src/packages/core/lib/src/changed_asset.dart +77 -0
  45. package/dart-src/packages/core/lib/src/get_bundles_args.dart +78 -0
  46. package/dart-src/packages/core/lib/src/metadata.dart +31 -0
  47. package/dart-src/packages/core/lib/src/patch_info.dart +284 -0
  48. package/dart-src/packages/core/lib/src/platform.dart +15 -0
  49. package/dart-src/packages/core/lib/src/rollout.dart +209 -0
  50. package/dart-src/packages/core/lib/src/semver.dart +70 -0
  51. package/dart-src/packages/core/lib/src/semver_range.dart +358 -0
  52. package/dart-src/packages/core/lib/src/semver_version.dart +94 -0
  53. package/dart-src/packages/core/lib/src/status.dart +24 -0
  54. package/dart-src/packages/core/lib/src/strategy.dart +10 -0
  55. package/dart-src/packages/core/lib/src/update_bundle_params.dart +30 -0
  56. package/dart-src/packages/core/lib/src/uuid.dart +45 -0
  57. package/dart-src/packages/core/pubspec.yaml +9 -0
  58. package/dart-src/packages/core/tool/dbg.dart +6 -0
  59. package/dart-src/plugins/aws/.gitkeep +0 -0
  60. package/dart-src/plugins/aws/lib/flutter_patcher_aws.dart +12 -0
  61. package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +207 -0
  62. package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +130 -0
  63. package/dart-src/plugins/aws/lib/src/aws_config.dart +59 -0
  64. package/dart-src/plugins/aws/lib/src/aws_database.dart +198 -0
  65. package/dart-src/plugins/aws/lib/src/aws_lambda_edge_storage.dart +72 -0
  66. package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +312 -0
  67. package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +125 -0
  68. package/dart-src/plugins/aws/lib/src/aws_storage.dart +20 -0
  69. package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +123 -0
  70. package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +136 -0
  71. package/dart-src/plugins/aws/pubspec.yaml +22 -0
  72. package/dart-src/plugins/cloudflare/.gitkeep +0 -0
  73. package/dart-src/plugins/cloudflare/lib/flutter_patcher_cloudflare.dart +18 -0
  74. package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +29 -0
  75. package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +93 -0
  76. package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +131 -0
  77. package/dart-src/plugins/cloudflare/lib/src/d1_client.dart +66 -0
  78. package/dart-src/plugins/cloudflare/lib/src/d1_config.dart +30 -0
  79. package/dart-src/plugins/cloudflare/lib/src/d1_database.dart +16 -0
  80. package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +314 -0
  81. package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +50 -0
  82. package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +287 -0
  83. package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +23 -0
  84. package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +110 -0
  85. package/dart-src/plugins/cloudflare/migrations/0001_hot-updater_init.sql +16 -0
  86. package/dart-src/plugins/cloudflare/migrations/0002_hot-updater_0.13.0.sql +8 -0
  87. package/dart-src/plugins/cloudflare/migrations/0003_hot-updater_0.18.0.sql +38 -0
  88. package/dart-src/plugins/cloudflare/migrations/0004_hot-updater_0.29.0.sql +11 -0
  89. package/dart-src/plugins/cloudflare/migrations/0005_hot-updater_0.31.0.sql +24 -0
  90. package/dart-src/plugins/cloudflare/pubspec.yaml +20 -0
  91. package/dart-src/plugins/cloudflare/sql/bundles.sql +42 -0
  92. package/dart-src/plugins/plugin-core/lib/flutter_patcher_plugin_core.dart +42 -0
  93. package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +94 -0
  94. package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +44 -0
  95. package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +259 -0
  96. package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work_store.dart +20 -0
  97. package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +29 -0
  98. package/dart-src/plugins/plugin-core/lib/src/compression_format.dart +63 -0
  99. package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +22 -0
  100. package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +853 -0
  101. package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +525 -0
  102. package/dart-src/plugins/plugin-core/lib/src/create_storage_key_builder.dart +15 -0
  103. package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +130 -0
  104. package/dart-src/plugins/plugin-core/lib/src/filter_compatible_app_versions.dart +18 -0
  105. package/dart-src/plugins/plugin-core/lib/src/generate_min_bundle_id.dart +19 -0
  106. package/dart-src/plugins/plugin-core/lib/src/get_update_info.dart +241 -0
  107. package/dart-src/plugins/plugin-core/lib/src/is_object.dart +5 -0
  108. package/dart-src/plugins/plugin-core/lib/src/legacy_asset_storage_layout.dart +9 -0
  109. package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +112 -0
  110. package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +44 -0
  111. package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +70 -0
  112. package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +62 -0
  113. package/dart-src/plugins/plugin-core/lib/src/resolve_update_info_from_bundles.dart +43 -0
  114. package/dart-src/plugins/plugin-core/lib/src/storage_profile.dart +27 -0
  115. package/dart-src/plugins/plugin-core/lib/src/types.dart +454 -0
  116. package/dart-src/plugins/plugin-core/lib/src/uuidv7.dart +41 -0
  117. package/dart-src/plugins/plugin-core/pubspec.yaml +15 -0
  118. package/dart-src/plugins/postgres/.gitkeep +0 -0
  119. package/dart-src/plugins/postgres/lib/flutter_patcher_postgres.dart +17 -0
  120. package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +130 -0
  121. package/dart-src/plugins/postgres/lib/src/postgres_client.dart +90 -0
  122. package/dart-src/plugins/postgres/lib/src/postgres_config.dart +35 -0
  123. package/dart-src/plugins/postgres/lib/src/postgres_database.dart +302 -0
  124. package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +120 -0
  125. package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +205 -0
  126. package/dart-src/plugins/postgres/lib/src/postgres_types.dart +132 -0
  127. package/dart-src/plugins/postgres/pubspec.yaml +18 -0
  128. package/dart-src/plugins/postgres/sql/bundles.sql +45 -0
  129. package/dart-src/plugins/postgres/sql/get_target_app_version_list.sql +21 -0
  130. package/dart-src/plugins/postgres/sql/get_update_info.spec.ts +171 -0
  131. package/dart-src/plugins/postgres/sql/get_update_info_by_app_version.sql +126 -0
  132. package/dart-src/plugins/postgres/sql/get_update_info_by_fingerprint_hash.sql +125 -0
  133. package/dart-src/plugins/postgres/sql/hash_user_id.sql +30 -0
  134. package/dart-src/plugins/postgres/sql/is_device_eligible.sql +237 -0
  135. package/dart-src/plugins/postgres/sql/prepareSql.ts +11 -0
  136. package/dart-src/plugins/standalone/.gitkeep +0 -0
  137. package/dart-src/plugins/standalone/lib/flutter_patcher_standalone.dart +12 -0
  138. package/dart-src/plugins/standalone/lib/src/standalone_config.dart +107 -0
  139. package/dart-src/plugins/standalone/lib/src/standalone_database.dart +233 -0
  140. package/dart-src/plugins/standalone/lib/src/standalone_storage.dart +175 -0
  141. package/dart-src/plugins/standalone/pubspec.yaml +18 -0
  142. package/dart-src/plugins/supabase/lib/edge.dart +10 -0
  143. package/dart-src/plugins/supabase/lib/flutter_patcher_supabase.dart +21 -0
  144. package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +129 -0
  145. package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +154 -0
  146. package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +516 -0
  147. package/dart-src/plugins/supabase/lib/src/supabase_config.dart +34 -0
  148. package/dart-src/plugins/supabase/lib/src/supabase_database.dart +422 -0
  149. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_database.dart +32 -0
  150. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +99 -0
  151. package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +145 -0
  152. package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +298 -0
  153. package/dart-src/plugins/supabase/lib/src/types.dart +130 -0
  154. package/dart-src/plugins/supabase/pubspec.yaml +19 -0
  155. package/dart-src/plugins/supabase/supabase/edge-functions/index.ts +44 -0
  156. package/dart-src/plugins/supabase/supabase/edge-functions/runtime.docker.integration.spec.ts +968 -0
  157. package/dart-src/plugins/supabase/supabase/migrations/20250103114225_init.sql +228 -0
  158. package/dart-src/plugins/supabase/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +134 -0
  159. package/dart-src/plugins/supabase/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +194 -0
  160. package/dart-src/plugins/supabase/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +184 -0
  161. package/dart-src/plugins/supabase/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +503 -0
  162. package/dart-src/plugins/supabase/supabase/migrations/20260414000000_hot-updater_0.30.0.sql +50 -0
  163. package/dart-src/plugins/supabase/supabase/migrations/20260422000000_hot-updater_0.31.0.sql +20 -0
  164. package/dart-src/plugins/supabase/supabase/migrations/20260520014100_hot-updater_rls.sql +48 -0
  165. package/package.json +2 -1
  166. package/scripts/postinstall.js +53 -25
  167. /package/bin/{flutter-patcher-linux → flutter-patcher-linux-x64} +0 -0
@@ -0,0 +1,136 @@
1
+ import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
2
+ show
3
+ RuntimeStorageProfile,
4
+ StoragePlugin,
5
+ StoragePluginProfiles;
6
+
7
+ import 'aws_cloudfront_signer.dart' show cloudfrontSignedUrl;
8
+ import 'aws_ssm_client.dart' show AwsSsmClient;
9
+
10
+ const int _oneYearInSeconds = 60 * 60 * 24 * 365;
11
+
12
+ /// Configuration for wrapping a storage plugin with CloudFront signed URLs.
13
+ ///
14
+ /// Faithful port of hot-updater `WithCloudFrontSignedUrlOptions` (minus the
15
+ /// `TContext` generic, which the Dart runtime profile does not expose).
16
+ class WithCloudFrontSignedUrlOptions {
17
+ const WithCloudFrontSignedUrlOptions({
18
+ required this.keyPairId,
19
+ this.publicBaseUrl,
20
+ this.publicBaseUrlResolver,
21
+ this.getPrivateKey,
22
+ this.ssmParameterName,
23
+ this.ssmRegion,
24
+ this.expiresSeconds,
25
+ this.accessKeyId,
26
+ this.secretAccessKey,
27
+ this.sessionToken,
28
+ });
29
+
30
+ final String keyPairId;
31
+ final String? publicBaseUrl;
32
+ final Future<String> Function()? publicBaseUrlResolver;
33
+ final Future<String> Function()? getPrivateKey;
34
+ final String? ssmParameterName;
35
+ final String? ssmRegion;
36
+ final int? expiresSeconds;
37
+ final String? accessKeyId;
38
+ final String? secretAccessKey;
39
+ final String? sessionToken;
40
+ }
41
+
42
+ /// Wrap a storage plugin factory so that `s3://` download URLs are rewritten as
43
+ /// CloudFront signed URLs. Faithful port of `withCloudFrontSignedUrl.ts`.
44
+ StoragePlugin Function() withCloudFrontSignedUrl(
45
+ StoragePlugin Function() storageFactory,
46
+ WithCloudFrontSignedUrlOptions config,
47
+ ) {
48
+ return () {
49
+ final base = storageFactory();
50
+ final runtime = base.profiles.runtime == null
51
+ ? null
52
+ : _CloudFrontSignedRuntimeProfile(base.profiles.runtime!, config);
53
+ return _CloudFrontSignedStoragePlugin(base, runtime);
54
+ };
55
+ }
56
+
57
+ class _CloudFrontSignedStoragePlugin implements StoragePlugin {
58
+ _CloudFrontSignedStoragePlugin(this._base, this._runtime);
59
+
60
+ final StoragePlugin _base;
61
+ final RuntimeStorageProfile? _runtime;
62
+
63
+ @override
64
+ String get name => '${_base.name}WithCloudFrontSignedUrl';
65
+
66
+ @override
67
+ String get supportedProtocol => _base.supportedProtocol;
68
+
69
+ @override
70
+ StoragePluginProfiles get profiles => StoragePluginProfiles(
71
+ node: _base.profiles.node,
72
+ runtime: _runtime,
73
+ );
74
+ }
75
+
76
+ class _CloudFrontSignedRuntimeProfile implements RuntimeStorageProfile {
77
+ _CloudFrontSignedRuntimeProfile(this._base, this._config);
78
+
79
+ final RuntimeStorageProfile _base;
80
+ final WithCloudFrontSignedUrlOptions _config;
81
+
82
+ @override
83
+ Future<Map<String, String>> getDownloadUrl(String storageUri) async {
84
+ final parsed = Uri.parse(storageUri);
85
+ if (parsed.scheme != 's3') {
86
+ return _base.getDownloadUrl(storageUri);
87
+ }
88
+
89
+ final privateKey = await _resolvePrivateKey();
90
+ final publicBaseUrl = await _resolvePublicBaseUrl();
91
+ final url = Uri.parse(publicBaseUrl);
92
+ final signedUrl = cloudfrontSignedUrl(
93
+ url: url.replace(path: parsed.path, query: null).toString(),
94
+ keyPairId: _config.keyPairId,
95
+ privateKeyPem: privateKey,
96
+ dateLessThan: DateTime.now().add(
97
+ Duration(seconds: _config.expiresSeconds ?? _oneYearInSeconds),
98
+ ),
99
+ );
100
+ return {'fileUrl': signedUrl};
101
+ }
102
+
103
+ @override
104
+ Future<String?> readText(String storageUri) => _base.readText(storageUri);
105
+
106
+ Future<String> _resolvePrivateKey() async {
107
+ if (_config.getPrivateKey != null) return _config.getPrivateKey!();
108
+ if (_config.ssmParameterName != null && _config.ssmRegion != null) {
109
+ final ssm = AwsSsmClient(
110
+ region: _config.ssmRegion!,
111
+ accessKeyId: _config.accessKeyId ?? '',
112
+ secretAccessKey: _config.secretAccessKey ?? '',
113
+ sessionToken: _config.sessionToken,
114
+ );
115
+ return ssm.getParameter(_config.ssmParameterName!);
116
+ }
117
+ throw StateError(
118
+ 'withCloudFrontSignedUrl: no private key source configured '
119
+ '(provide getPrivateKey or ssmParameterName/ssmRegion).',
120
+ );
121
+ }
122
+
123
+ Future<String> _resolvePublicBaseUrl() async {
124
+ if (_config.publicBaseUrlResolver != null) {
125
+ final value = await _config.publicBaseUrlResolver!();
126
+ if (value.isEmpty) {
127
+ throw StateError('CloudFront publicBaseUrl resolver returned empty URL');
128
+ }
129
+ return value;
130
+ }
131
+ if (_config.publicBaseUrl != null && _config.publicBaseUrl!.isNotEmpty) {
132
+ return _config.publicBaseUrl!;
133
+ }
134
+ throw StateError('CloudFront publicBaseUrl is empty');
135
+ }
136
+ }
@@ -0,0 +1,22 @@
1
+ name: flutter_patcher_aws
2
+ description: AWS S3 storage + S3/DynamoDB database plugins for flutter_patcher (faithful port of hot-updater plugins/aws).
3
+ version: 0.1.0
4
+ publish_to: none
5
+
6
+ environment:
7
+ sdk: ">=3.3.0 <4.0.0"
8
+
9
+ dependencies:
10
+ flutter_patcher_core:
11
+ path: ../../packages/core
12
+ flutter_patcher_plugin_core:
13
+ path: ../../plugins/plugin-core
14
+ crypto: ^3.0.0
15
+ http: ^1.2.0
16
+ path: ^1.9.0
17
+ pointycastle: ^3.9.1
18
+ asn1lib: ^1.5.4
19
+
20
+ dev_dependencies:
21
+ test: ^1.25.0
22
+ lints: ^4.0.0
File without changes
@@ -0,0 +1,18 @@
1
+ library;
2
+
3
+ export 'src/d1_bundle_mapper.dart'
4
+ show
5
+ buildBundlePatchId,
6
+ bundleToPatchRows,
7
+ defaultRolloutCohortCount,
8
+ parseMetadata,
9
+ parseTargetCohorts,
10
+ transformRowToBundle;
11
+ export 'src/d1_client.dart' show D1Client, D1ClientLike;
12
+ export 'src/d1_config.dart' show D1ClientFactory, D1DatabaseConfig, resolveD1Client;
13
+ export 'src/d1_database.dart' show d1Database;
14
+ export 'src/cloudflare_worker_database.dart'
15
+ show CloudflareWorkerDatabaseConfig, cloudflareWorkerDatabase;
16
+ export 'src/r2_config.dart' show R2S3ClientFactory, R2S3StorageConfig, resolveR2Client;
17
+ export 'src/r2_s3_client.dart' show R2S3Client, R2S3ClientLike;
18
+ export 'src/r2_storage.dart' show r2Storage;
@@ -0,0 +1,29 @@
1
+ import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
2
+ show createDatabasePlugin;
3
+
4
+ import 'd1_client.dart' show D1ClientLike;
5
+ import 'd1_database_plugin.dart' show D1DatabasePlugin;
6
+
7
+ /// Configuration for the Cloudflare Workers D1 database plugin.
8
+ ///
9
+ /// Mirrors hot-updater's `CloudflareWorkerDatabaseConfig`: instead of talking
10
+ /// to D1 over the REST API, the plugin uses a `D1Like` binding supplied by
11
+ /// [getDb] (the Workers runtime `env.DB`). In the Dart port the binding is
12
+ /// represented by a [D1ClientLike], which makes it trivially testable by
13
+ /// injecting a mock.
14
+ class CloudflareWorkerDatabaseConfig {
15
+ const CloudflareWorkerDatabaseConfig({required this.getDb});
16
+
17
+ final D1ClientLike Function() getDb;
18
+ }
19
+
20
+ /// Cloudflare Workers D1 database plugin (faithful port of hot-updater's
21
+ /// `cloudflareWorkerDatabase`).
22
+ ///
23
+ /// Shares the exact same SQL/update-info logic as [d1Database]; only the
24
+ /// client source differs (an in-process D1 binding instead of the REST API).
25
+ final cloudflareWorkerDatabase =
26
+ createDatabasePlugin<CloudflareWorkerDatabaseConfig>(
27
+ name: 'd1WorkerDatabase',
28
+ factory: (config) => D1DatabasePlugin(config.getDb()),
29
+ );
@@ -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
+ );