@_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,205 @@
1
+ import 'dart:convert';
2
+ import 'dart:io';
3
+ import 'dart:typed_data';
4
+
5
+ import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart';
6
+
7
+ import 'postgres_client.dart';
8
+ import 'postgres_config.dart';
9
+
10
+ /// Configuration for the Postgres bytea storage plugin.
11
+ ///
12
+ /// Reuses the same [PostgresConfig] connection as the database plugin. Blob
13
+ /// bytes are stored in a `flutter_patcher_storage(key text primary key, data
14
+ /// bytea, content_type text)` table. Because Postgres cannot serve bytes over
15
+ /// HTTP itself, [servingBaseUrl] may point at a thin proxy that reads from this
16
+ /// table so device update-checks can download over HTTP (the CLI/console supply
17
+ /// one; tests mount a local server).
18
+ class PostgresStorageConfig {
19
+ const PostgresStorageConfig({
20
+ required this.db,
21
+ this.basePath,
22
+ this.servingBaseUrl,
23
+ });
24
+
25
+ final PostgresConfig db;
26
+ final String? basePath;
27
+ final String? servingBaseUrl;
28
+ }
29
+
30
+ /// Resolves the storage client for a config (test seam via [PostgresConfig.clientFactory]).
31
+ PostgresClientLike resolvePostgresStorageClient(PostgresStorageConfig config) =>
32
+ config.db.clientFactory?.call(config.db) ?? PostgresClient.connect(config.db);
33
+
34
+ String _storageUri(String key, String? servingBaseUrl) {
35
+ if (servingBaseUrl != null && servingBaseUrl.isNotEmpty) {
36
+ final base = servingBaseUrl.endsWith('/')
37
+ ? servingBaseUrl.substring(0, servingBaseUrl.length - 1)
38
+ : servingBaseUrl;
39
+ return '$base/$key';
40
+ }
41
+ return 'postgres://$key';
42
+ }
43
+
44
+ String _contentType(String key) {
45
+ if (key.endsWith('.json')) return 'application/json';
46
+ if (key.endsWith('.zip')) return 'application/zip';
47
+ return 'application/octet-stream';
48
+ }
49
+
50
+ String _strip(String storageUri, String? servingBaseUrl) {
51
+ if (storageUri.startsWith('postgres://')) {
52
+ return storageUri.substring('postgres://'.length);
53
+ }
54
+ if (servingBaseUrl != null && storageUri.startsWith(servingBaseUrl)) {
55
+ final base =
56
+ servingBaseUrl.endsWith('/') ? servingBaseUrl : '$servingBaseUrl/';
57
+ return storageUri.startsWith(base)
58
+ ? storageUri.substring(base.length)
59
+ : storageUri;
60
+ }
61
+ return storageUri;
62
+ }
63
+
64
+ Uint8List _toBytes(Object? data) {
65
+ if (data is Uint8List) return data;
66
+ if (data is List) return Uint8List.fromList(data.cast<int>());
67
+ return Uint8List(0);
68
+ }
69
+
70
+ NodeStorageProfile createPostgresNodeProfile(
71
+ PostgresClientLike client,
72
+ String? basePath,
73
+ String? servingBaseUrl,
74
+ ) =>
75
+ _PostgresNodeProfile(client, basePath, servingBaseUrl);
76
+
77
+ class _PostgresNodeProfile implements NodeStorageProfile {
78
+ _PostgresNodeProfile(this._client, this._basePath, this._servingBaseUrl);
79
+
80
+ final PostgresClientLike _client;
81
+ final String? _basePath;
82
+ final String? _servingBaseUrl;
83
+
84
+ String _withBase(String key) =>
85
+ [_basePath, key].where((s) => s != null && s.isNotEmpty).join('/');
86
+
87
+ @override
88
+ Future<Map<String, String>> upload(String key, String filePath) async {
89
+ final fullKey = _withBase(key);
90
+ final bytes = await File(filePath).readAsBytes();
91
+ await _client.execute(
92
+ 'INSERT INTO flutter_patcher_storage (key, data, content_type) '
93
+ 'VALUES (@key, @data, @ct) '
94
+ 'ON CONFLICT (key) DO UPDATE SET data = EXCLUDED.data, content_type = EXCLUDED.content_type',
95
+ {
96
+ '@key': fullKey,
97
+ '@data': bytes,
98
+ '@ct': _contentType(fullKey),
99
+ },
100
+ );
101
+ return {'storageUri': _storageUri(fullKey, _servingBaseUrl)};
102
+ }
103
+
104
+ @override
105
+ Future<bool> exists(String storageUri) async {
106
+ final key = _strip(storageUri, _servingBaseUrl);
107
+ final rows = await _client.execute(
108
+ 'SELECT 1 FROM flutter_patcher_storage WHERE key = @key',
109
+ {'@key': key},
110
+ );
111
+ return rows.isNotEmpty;
112
+ }
113
+
114
+ @override
115
+ Future<void> delete(String storageUri) async {
116
+ await _client.execute(
117
+ 'DELETE FROM flutter_patcher_storage WHERE key = @key',
118
+ {'@key': _strip(storageUri, _servingBaseUrl)},
119
+ );
120
+ }
121
+
122
+ @override
123
+ Future<void> downloadFile(String storageUri, String filePath) async {
124
+ final key = _strip(storageUri, _servingBaseUrl);
125
+ final rows = await _client.execute(
126
+ 'SELECT data FROM flutter_patcher_storage WHERE key = @key',
127
+ {'@key': key},
128
+ );
129
+ if (rows.isEmpty) {
130
+ throw StateError('Object not found: $key');
131
+ }
132
+ await File(filePath).writeAsBytes(_toBytes(rows.first['data']));
133
+ }
134
+
135
+ @override
136
+ Future<List<StorageObject>> listObjects([String? prefix]) async {
137
+ final fullPrefix = prefix == null ? null : _withBase(prefix);
138
+ final rows = await _client.execute(
139
+ 'SELECT key, octet_length(data) AS size FROM flutter_patcher_storage '
140
+ 'WHERE key LIKE @prefix',
141
+ {'@prefix': '${fullPrefix ?? ''}%'},
142
+ );
143
+ return rows.map((r) {
144
+ final key = r['key'] as String;
145
+ return StorageObject(
146
+ key: key,
147
+ storageUri: _storageUri(key, _servingBaseUrl),
148
+ size: (r['size'] as int?) ?? 0,
149
+ );
150
+ }).toList();
151
+ }
152
+
153
+ @override
154
+ Future<void> deleteObjects(List<String> keys) async {
155
+ for (final k in keys) {
156
+ await _client.execute(
157
+ 'DELETE FROM flutter_patcher_storage WHERE key = @key',
158
+ {'@key': _strip(k, _servingBaseUrl)},
159
+ );
160
+ }
161
+ }
162
+ }
163
+
164
+ RuntimeStorageProfile createPostgresRuntimeProfile(
165
+ PostgresClientLike client,
166
+ String? servingBaseUrl,
167
+ ) =>
168
+ _PostgresRuntimeProfile(client, servingBaseUrl);
169
+
170
+ class _PostgresRuntimeProfile implements RuntimeStorageProfile {
171
+ _PostgresRuntimeProfile(this._client, this._servingBaseUrl);
172
+
173
+ final PostgresClientLike _client;
174
+ final String? _servingBaseUrl;
175
+
176
+ @override
177
+ Future<Map<String, String>> getDownloadUrl(String storageUri) async {
178
+ return {'fileUrl': _storageUri(_strip(storageUri, _servingBaseUrl), _servingBaseUrl)};
179
+ }
180
+
181
+ @override
182
+ Future<String?> readText(String storageUri) async {
183
+ final key = _strip(storageUri, _servingBaseUrl);
184
+ final rows = await _client.execute(
185
+ 'SELECT data FROM flutter_patcher_storage WHERE key = @key',
186
+ {'@key': key},
187
+ );
188
+ if (rows.isEmpty) return null;
189
+ return utf8.decode(_toBytes(rows.first['data']));
190
+ }
191
+ }
192
+
193
+ /// Postgres bytea storage plugin (name `postgresStorage`, protocol `postgres`).
194
+ StoragePlugin Function(PostgresStorageConfig) postgresStorage =
195
+ createUniversalStoragePlugin<PostgresStorageConfig>(
196
+ name: 'postgresStorage',
197
+ supportedProtocol: 'postgres',
198
+ factory: (cfg) {
199
+ final client = resolvePostgresStorageClient(cfg);
200
+ return (
201
+ node: createPostgresNodeProfile(client, cfg.basePath, cfg.servingBaseUrl),
202
+ runtime: createPostgresRuntimeProfile(client, cfg.servingBaseUrl),
203
+ );
204
+ },
205
+ );
@@ -0,0 +1,132 @@
1
+ import 'package:flutter_patcher_core/flutter_patcher_core.dart'
2
+ show Platform;
3
+
4
+ /// Database row for the `bundles` table (snake_case columns).
5
+ ///
6
+ /// Faithful port of hot-updater `postgres/src/types.ts` `PostgresBundleRow`.
7
+ class PostgresBundleRow {
8
+ const PostgresBundleRow({
9
+ required this.id,
10
+ required this.channel,
11
+ required this.enabled,
12
+ required this.shouldForceUpdate,
13
+ required this.fileHash,
14
+ this.gitCommitHash,
15
+ this.message,
16
+ required this.platform,
17
+ this.targetAppVersion,
18
+ required this.storageUri,
19
+ this.fingerprintHash,
20
+ this.metadata,
21
+ this.manifestStorageUri,
22
+ this.manifestFileHash,
23
+ this.assetBaseStorageUri,
24
+ this.rolloutCohortCount,
25
+ this.targetCohorts,
26
+ });
27
+
28
+ factory PostgresBundleRow.fromJson(Map<String, dynamic> json) {
29
+ return PostgresBundleRow(
30
+ id: json['id'] as String,
31
+ channel: json['channel'] as String,
32
+ enabled: json['enabled'] as bool,
33
+ shouldForceUpdate: json['should_force_update'] as bool,
34
+ fileHash: json['file_hash'] as String,
35
+ gitCommitHash: json['git_commit_hash'] as String?,
36
+ message: json['message'] as String?,
37
+ platform: Platform.fromValue(json['platform'] as String),
38
+ targetAppVersion: json['target_app_version'] as String?,
39
+ storageUri: json['storage_uri'] as String,
40
+ fingerprintHash: json['fingerprint_hash'] as String?,
41
+ metadata: json['metadata'],
42
+ manifestStorageUri: json['manifest_storage_uri'] as String?,
43
+ manifestFileHash: json['manifest_file_hash'] as String?,
44
+ assetBaseStorageUri: json['asset_base_storage_uri'] as String?,
45
+ rolloutCohortCount: json['rollout_cohort_count'] as int?,
46
+ targetCohorts: (json['target_cohorts'] as List?)?.cast<String>(),
47
+ );
48
+ }
49
+
50
+ final String id;
51
+ final String channel;
52
+ final bool enabled;
53
+ final bool shouldForceUpdate;
54
+ final String fileHash;
55
+ final String? gitCommitHash;
56
+ final String? message;
57
+ final Platform platform;
58
+ final String? targetAppVersion;
59
+ final String storageUri;
60
+ final String? fingerprintHash;
61
+ final dynamic metadata;
62
+ final String? manifestStorageUri;
63
+ final String? manifestFileHash;
64
+ final String? assetBaseStorageUri;
65
+ final int? rolloutCohortCount;
66
+ final List<String>? targetCohorts;
67
+
68
+ Map<String, dynamic> toJson() => {
69
+ 'id': id,
70
+ 'channel': channel,
71
+ 'enabled': enabled,
72
+ 'should_force_update': shouldForceUpdate,
73
+ 'file_hash': fileHash,
74
+ 'git_commit_hash': gitCommitHash,
75
+ 'message': message,
76
+ 'platform': platform.name,
77
+ 'target_app_version': targetAppVersion,
78
+ 'storage_uri': storageUri,
79
+ 'fingerprint_hash': fingerprintHash,
80
+ 'metadata': metadata ?? const {},
81
+ 'manifest_storage_uri': manifestStorageUri,
82
+ 'manifest_file_hash': manifestFileHash,
83
+ 'asset_base_storage_uri': assetBaseStorageUri,
84
+ 'rollout_cohort_count': rolloutCohortCount,
85
+ 'target_cohorts': targetCohorts,
86
+ };
87
+ }
88
+
89
+ /// Database row for the `bundle_patches` table (snake_case columns).
90
+ ///
91
+ /// Faithful port of hot-updater `postgres/src/types.ts` `PostgresBundlePatchRow`.
92
+ class PostgresBundlePatchRow {
93
+ const PostgresBundlePatchRow({
94
+ required this.id,
95
+ required this.bundleId,
96
+ required this.baseBundleId,
97
+ required this.baseFileHash,
98
+ required this.patchFileHash,
99
+ required this.patchStorageUri,
100
+ required this.orderIndex,
101
+ });
102
+
103
+ factory PostgresBundlePatchRow.fromJson(Map<String, dynamic> json) {
104
+ return PostgresBundlePatchRow(
105
+ id: json['id'] as String,
106
+ bundleId: json['bundle_id'] as String,
107
+ baseBundleId: json['base_bundle_id'] as String,
108
+ baseFileHash: json['base_file_hash'] as String,
109
+ patchFileHash: json['patch_file_hash'] as String,
110
+ patchStorageUri: json['patch_storage_uri'] as String,
111
+ orderIndex: json['order_index'] as int,
112
+ );
113
+ }
114
+
115
+ final String id;
116
+ final String bundleId;
117
+ final String baseBundleId;
118
+ final String baseFileHash;
119
+ final String patchFileHash;
120
+ final String patchStorageUri;
121
+ final int orderIndex;
122
+
123
+ Map<String, dynamic> toJson() => {
124
+ 'id': id,
125
+ 'bundle_id': bundleId,
126
+ 'base_bundle_id': baseBundleId,
127
+ 'base_file_hash': baseFileHash,
128
+ 'patch_file_hash': patchFileHash,
129
+ 'patch_storage_uri': patchStorageUri,
130
+ 'order_index': orderIndex,
131
+ };
132
+ }
@@ -0,0 +1,18 @@
1
+ name: flutter_patcher_postgres
2
+ description: Postgres plugin for flutter_patcher — database backend.
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
+ postgres: ^3.4.0
15
+
16
+ dev_dependencies:
17
+ test: ^1.25.0
18
+ lints: ^4.0.0
@@ -0,0 +1,45 @@
1
+ -- HotUpdater.bundles
2
+
3
+ CREATE TYPE platforms AS ENUM ('ios', 'android');
4
+
5
+ CREATE TABLE bundles (
6
+ id uuid PRIMARY KEY,
7
+ platform platforms NOT NULL,
8
+ should_force_update boolean NOT NULL,
9
+ enabled boolean NOT NULL,
10
+ file_hash text NOT NULL,
11
+ git_commit_hash text,
12
+ message text,
13
+ channel text NOT NULL DEFAULT 'production',
14
+ storage_uri text NOT NULL,
15
+ target_app_version text,
16
+ fingerprint_hash text,
17
+ CONSTRAINT check_version_or_fingerprint CHECK (
18
+ (target_app_version IS NOT NULL) OR (fingerprint_hash IS NOT NULL)
19
+ ),
20
+ metadata jsonb DEFAULT '{}'::jsonb,
21
+ manifest_storage_uri text,
22
+ manifest_file_hash text,
23
+ asset_base_storage_uri text,
24
+ rollout_cohort_count INTEGER DEFAULT 1000
25
+ CHECK (rollout_cohort_count >= 0 AND rollout_cohort_count <= 1000),
26
+ target_cohorts TEXT[]
27
+ );
28
+
29
+ CREATE TABLE bundle_patches (
30
+ id text PRIMARY KEY,
31
+ bundle_id uuid NOT NULL REFERENCES bundles(id) ON DELETE CASCADE,
32
+ base_bundle_id uuid NOT NULL REFERENCES bundles(id) ON DELETE CASCADE,
33
+ base_file_hash text NOT NULL,
34
+ patch_file_hash text NOT NULL,
35
+ patch_storage_uri text NOT NULL,
36
+ order_index integer NOT NULL DEFAULT 0
37
+ );
38
+
39
+ CREATE INDEX bundles_target_app_version_idx ON bundles(target_app_version);
40
+ CREATE INDEX bundles_fingerprint_hash_idx ON bundles(fingerprint_hash);
41
+ CREATE INDEX bundles_channel_idx ON bundles(channel);
42
+ CREATE INDEX bundles_rollout_idx ON bundles(rollout_cohort_count);
43
+ CREATE INDEX bundles_target_cohorts_idx ON bundles USING GIN (target_cohorts);
44
+ CREATE INDEX bundle_patches_bundle_id_idx ON bundle_patches(bundle_id);
45
+ CREATE INDEX bundle_patches_base_bundle_id_idx ON bundle_patches(base_bundle_id);
@@ -0,0 +1,21 @@
1
+ -- HotUpdater.get_target_app_version_list
2
+
3
+ CREATE OR REPLACE FUNCTION get_target_app_version_list (
4
+ app_platform platforms,
5
+ min_bundle_id uuid
6
+ )
7
+ RETURNS TABLE (
8
+ target_app_version text
9
+ )
10
+ LANGUAGE plpgsql
11
+ AS
12
+ $$
13
+ BEGIN
14
+ RETURN QUERY
15
+ SELECT b.target_app_version
16
+ FROM bundles b
17
+ WHERE b.platform = app_platform
18
+ AND b.id >= min_bundle_id
19
+ GROUP BY b.target_app_version;
20
+ END;
21
+ $$;
@@ -0,0 +1,171 @@
1
+ import { PGlite } from "@electric-sql/pglite";
2
+ import {
3
+ type Bundle,
4
+ type GetBundlesArgs,
5
+ NIL_UUID,
6
+ type UpdateInfo,
7
+ } from "@hot-updater/core";
8
+ import { filterCompatibleAppVersions } from "@hot-updater/js";
9
+ import { setupGetUpdateInfoTestSuite } from "@hot-updater/test-utils";
10
+ import camelcaseKeys from "camelcase-keys";
11
+ import { afterAll, beforeEach, describe } from "vitest";
12
+
13
+ import { prepareSql } from "./prepareSql";
14
+
15
+ const createInsertBundleQuery = (bundle: Bundle) => {
16
+ const rolloutCohortCount = bundle.rolloutCohortCount ?? 1000;
17
+ const targetCohorts = bundle.targetCohorts
18
+ ? `ARRAY[${bundle.targetCohorts.map((id) => `'${id}'`).join(",")}]::TEXT[]`
19
+ : "NULL";
20
+
21
+ return `
22
+ INSERT INTO bundles (
23
+ id, file_hash, platform, target_app_version,
24
+ should_force_update, enabled, git_commit_hash, message, channel, storage_uri, fingerprint_hash,
25
+ rollout_cohort_count, target_cohorts
26
+ ) VALUES (
27
+ '${bundle.id}',
28
+ '${bundle.fileHash}',
29
+ '${bundle.platform}',
30
+ ${bundle.targetAppVersion ? `'${bundle.targetAppVersion}'` : "null"},
31
+ ${bundle.shouldForceUpdate},
32
+ ${bundle.enabled},
33
+ ${bundle.gitCommitHash ? `'${bundle.gitCommitHash}'` : "null"},
34
+ ${bundle.message ? `'${bundle.message}'` : "null"},
35
+ '${bundle.channel}',
36
+ '${bundle.storageUri}',
37
+ ${bundle.fingerprintHash ? `'${bundle.fingerprintHash}'` : "null"},
38
+ ${rolloutCohortCount},
39
+ ${targetCohorts}
40
+ ) ON CONFLICT (id) DO UPDATE SET
41
+ file_hash = EXCLUDED.file_hash,
42
+ platform = EXCLUDED.platform,
43
+ target_app_version = EXCLUDED.target_app_version,
44
+ should_force_update = EXCLUDED.should_force_update,
45
+ enabled = EXCLUDED.enabled,
46
+ git_commit_hash = EXCLUDED.git_commit_hash,
47
+ message = EXCLUDED.message,
48
+ channel = EXCLUDED.channel,
49
+ storage_uri = EXCLUDED.storage_uri,
50
+ fingerprint_hash = EXCLUDED.fingerprint_hash,
51
+ rollout_cohort_count = EXCLUDED.rollout_cohort_count,
52
+ target_cohorts = EXCLUDED.target_cohorts;
53
+ `;
54
+ };
55
+
56
+ const createGetUpdateInfo =
57
+ (db: PGlite) =>
58
+ async (
59
+ bundles: Bundle[],
60
+ args: GetBundlesArgs,
61
+ ): Promise<UpdateInfo | null> => {
62
+ const {
63
+ bundleId,
64
+ platform,
65
+ minBundleId = NIL_UUID,
66
+ channel = "production",
67
+ _updateStrategy,
68
+ } = args;
69
+ await db.exec(createInsertBundleQuerys(bundles));
70
+
71
+ if (_updateStrategy === "fingerprint") {
72
+ const fingerprintHash = args.fingerprintHash;
73
+ const cohort = args.cohort;
74
+ const cohortSql = cohort ? `'${cohort}'` : "NULL";
75
+ const result = await db.query<{
76
+ id: string;
77
+ should_force_update: boolean;
78
+ message: string;
79
+ status: string;
80
+ storage_uri: string | null;
81
+ file_hash: string | null;
82
+ }>(
83
+ `
84
+ SELECT * FROM get_update_info_by_fingerprint_hash(
85
+ '${platform}',
86
+ '${bundleId}',
87
+ '${minBundleId}',
88
+ '${channel}',
89
+ '${fingerprintHash}',
90
+ ${cohortSql}
91
+ );
92
+ `,
93
+ );
94
+
95
+ if (!result.rows[0]) {
96
+ return null;
97
+ }
98
+
99
+ const row = result.rows[0];
100
+ return camelcaseKeys(row) as UpdateInfo;
101
+ }
102
+
103
+ const appVersion = args.appVersion;
104
+ const { rows: appVersionList } = await db.query<{
105
+ target_app_version: string;
106
+ }>(
107
+ `
108
+ SELECT target_app_version FROM get_target_app_version_list('${platform}', '${minBundleId}');
109
+ `,
110
+ );
111
+
112
+ const targetAppVersionList = filterCompatibleAppVersions(
113
+ appVersionList?.map((group) => group.target_app_version) ?? [],
114
+ appVersion,
115
+ );
116
+
117
+ const cohort = args.cohort;
118
+ const cohortSql = cohort ? `'${cohort}'` : "NULL";
119
+ const result = await db.query<{
120
+ id: string;
121
+ should_force_update: boolean;
122
+ message: string;
123
+ status: string;
124
+ storage_uri: string | null;
125
+ file_hash: string | null;
126
+ }>(
127
+ `
128
+ SELECT * FROM get_update_info_by_app_version(
129
+ '${platform}',
130
+ '${appVersion}',
131
+ '${bundleId}',
132
+ '${minBundleId ?? NIL_UUID}',
133
+ '${channel}',
134
+ ARRAY[${targetAppVersionList.map((v) => `'${v}'`).join(",")}]::text[],
135
+ ${cohortSql}
136
+ );
137
+ `,
138
+ );
139
+
140
+ if (!result.rows[0]) {
141
+ return null;
142
+ }
143
+
144
+ const row = result.rows[0];
145
+ return camelcaseKeys(row) as UpdateInfo;
146
+ };
147
+
148
+ const createInsertBundleQuerys = (bundles: Bundle[]) => {
149
+ return bundles.map(createInsertBundleQuery).join("\n");
150
+ };
151
+
152
+ const db = new PGlite();
153
+
154
+ const sql = await prepareSql();
155
+ await db.exec(sql);
156
+ const getUpdateInfo = createGetUpdateInfo(db);
157
+
158
+ describe("getUpdateInfo", () => {
159
+ beforeEach(async () => {
160
+ await db.exec("DELETE FROM bundle_patches");
161
+ await db.exec("DELETE FROM bundles");
162
+ });
163
+
164
+ afterAll(async () => {
165
+ await db.close();
166
+ });
167
+
168
+ setupGetUpdateInfoTestSuite({
169
+ getUpdateInfo: getUpdateInfo,
170
+ });
171
+ });