@_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,302 @@
1
+ /// Faithful port of hot-updater `plugins/postgres/src/postgres.ts`.
2
+ library;
3
+
4
+ import 'package:flutter_patcher_core/flutter_patcher_core.dart'
5
+ show Bundle, UpdateInfo;
6
+ import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
7
+ show
8
+ AbstractDatabasePlugin,
9
+ BundleChange,
10
+ BundleChangeOperation,
11
+ DatabaseBundleQueryOptions,
12
+ DatabaseBundleQueryOrder,
13
+ DatabaseBundleQueryWhere,
14
+ DatabasePlugin,
15
+ DatabasePluginHooks,
16
+ GetBundlesArgs,
17
+ Paginated,
18
+ calculatePagination,
19
+ createDatabasePlugin;
20
+
21
+ import 'postgres_bundle_mapper.dart';
22
+ import 'postgres_client.dart';
23
+ import 'postgres_config.dart';
24
+ import 'postgres_get_update_info.dart' as pg_ui;
25
+ import 'postgres_types.dart';
26
+
27
+ /// Postgres-backed [DatabasePlugin] factory.
28
+ ///
29
+ /// Mirrors `postgres` from hot-updater: a `createDatabasePlugin` wrapper whose
30
+ /// inner factory builds a Postgres client and returns an [AbstractDatabasePlugin]
31
+ /// implementation talking to the `bundles` / `bundle_patches` tables and the
32
+ /// update-info RPCs.
33
+ DatabasePlugin Function() Function(
34
+ PostgresConfig config, [
35
+ DatabasePluginHooks? hooks,
36
+ ]) postgresDatabase = createDatabasePlugin<PostgresConfig>(
37
+ name: 'postgresDatabase',
38
+ factory: (config) {
39
+ final client = config.clientFactory != null
40
+ ? config.clientFactory!(config)
41
+ : PostgresClient.connect(config);
42
+
43
+ Future<Map<String, List<PostgresBundlePatchRow>>> fetchPatchMap(
44
+ List<String> bundleIds,
45
+ ) async {
46
+ final patchMap = <String, List<PostgresBundlePatchRow>>{};
47
+ if (bundleIds.isEmpty) return patchMap;
48
+
49
+ final rows = await client.execute(
50
+ 'SELECT * FROM bundle_patches '
51
+ 'WHERE bundle_id = ANY(@bundle_ids) ORDER BY order_index ASC',
52
+ {'bundle_ids': bundleIds},
53
+ );
54
+
55
+ for (final raw in rows) {
56
+ final row = PostgresBundlePatchRow.fromJson(raw);
57
+ final current = patchMap[row.bundleId] ?? [];
58
+ current.add(row);
59
+ patchMap[row.bundleId] = current;
60
+ }
61
+ return patchMap;
62
+ }
63
+
64
+ return _PostgresDatabasePlugin(
65
+ client: client,
66
+ fetchPatchMap: fetchPatchMap,
67
+ );
68
+ },
69
+ );
70
+
71
+ /// Build a SQL `WHERE` fragment (with `@pN` placeholders) from [where].
72
+ (String where, Map<String, dynamic> params) _buildWhere(
73
+ DatabaseBundleQueryWhere? where,
74
+ ) {
75
+ final conditions = <String>[];
76
+ final params = <String, dynamic>{};
77
+ if (where == null) return ('', params);
78
+
79
+ var index = 0;
80
+ String next() => '@p${index++}';
81
+
82
+ void add(String sql, dynamic value) {
83
+ final p = next();
84
+ conditions.add(sql.replaceAll('?', p));
85
+ params[p] = value;
86
+ }
87
+
88
+ if (where.channel != null) {
89
+ add('channel = ?', where.channel);
90
+ }
91
+ if (where.platform != null) {
92
+ add('platform = ?', where.platform!.value);
93
+ }
94
+ if (where.enabled != null) {
95
+ add('enabled = ?', where.enabled);
96
+ }
97
+ if (where.fingerprintHash != null) {
98
+ if (where.fingerprintHash == null) {
99
+ conditions.add('fingerprint_hash IS NULL');
100
+ } else {
101
+ add('fingerprint_hash = ?', where.fingerprintHash);
102
+ }
103
+ }
104
+ if (where.targetAppVersion != null) {
105
+ if (where.targetAppVersion == null) {
106
+ conditions.add('target_app_version IS NULL');
107
+ } else {
108
+ add('target_app_version = ?', where.targetAppVersion);
109
+ }
110
+ }
111
+ if (where.targetAppVersionIn != null) {
112
+ add('target_app_version = ANY(?)', where.targetAppVersionIn);
113
+ }
114
+ if (where.targetAppVersionNotNull == true) {
115
+ conditions.add('target_app_version IS NOT NULL');
116
+ }
117
+ if (where.id != null) {
118
+ final id = where.id!;
119
+ if (id.eq != null) add('id = ?', id.eq);
120
+ if (id.gt != null) add('id > ?', id.gt);
121
+ if (id.gte != null) add('id >= ?', id.gte);
122
+ if (id.lt != null) add('id < ?', id.lt);
123
+ if (id.lte != null) add('id <= ?', id.lte);
124
+ if (id.ins != null) add('id = ANY(?)', id.ins);
125
+ }
126
+
127
+ return (
128
+ conditions.isEmpty ? '' : 'WHERE ${conditions.join(' AND ')}',
129
+ params,
130
+ );
131
+ }
132
+
133
+ class _PostgresDatabasePlugin implements AbstractDatabasePlugin {
134
+ final PostgresClientLike client;
135
+ final Future<Map<String, List<PostgresBundlePatchRow>>> Function(
136
+ List<String> bundleIds,
137
+ ) fetchPatchMap;
138
+
139
+ _PostgresDatabasePlugin({
140
+ required this.client,
141
+ required this.fetchPatchMap,
142
+ });
143
+
144
+ @override
145
+ bool get supportsCursorPagination => false;
146
+
147
+ @override
148
+ Future<UpdateInfo?> getUpdateInfo(GetBundlesArgs args) =>
149
+ pg_ui.getUpdateInfo(client, args);
150
+
151
+ @override
152
+ Future<Bundle?> getBundleById(String bundleId) async {
153
+ final results = await Future.wait([
154
+ client.execute(
155
+ 'SELECT * FROM bundles WHERE id = @p0',
156
+ {'p0': bundleId},
157
+ ),
158
+ fetchPatchMap([bundleId]),
159
+ ]);
160
+
161
+ final data = results[0] as List<Map<String, dynamic>>;
162
+ if (data.isEmpty) return null;
163
+ final row = PostgresBundleRow.fromJson(data.first);
164
+ final patches = (results[1] as Map<String, List<PostgresBundlePatchRow>>)[bundleId] ?? [];
165
+ return mapRowToBundle(row, patches);
166
+ }
167
+
168
+ @override
169
+ Future<Paginated<List<Bundle>>> getBundles(
170
+ DatabaseBundleQueryOptions options,
171
+ ) async {
172
+ final where = options.where;
173
+ final limit = options.limit;
174
+ final orderBy =
175
+ options.orderBy ?? const DatabaseBundleQueryOrder(direction: 'desc');
176
+ final offset =
177
+ options.offset ?? (options.page != null ? (options.page! - 1) * limit : 0);
178
+
179
+ if ((where?.targetAppVersionIn != null && where!.targetAppVersionIn!.isEmpty) ||
180
+ (where?.id?.ins != null && where!.id!.ins!.isEmpty)) {
181
+ return Paginated(
182
+ data: const [],
183
+ pagination: calculatePagination(0, limit: limit, offset: offset),
184
+ );
185
+ }
186
+
187
+ final (whereSql, whereParams) = _buildWhere(where);
188
+
189
+ final countRows = await client.execute(
190
+ 'SELECT COUNT(*) AS total FROM bundles $whereSql',
191
+ whereParams,
192
+ );
193
+ final total = countRows.isEmpty
194
+ ? 0
195
+ : (countRows.first['total'] as num?)?.toInt() ?? 0;
196
+
197
+ final selectParams = <String, dynamic>{...whereParams};
198
+ if (limit > 0) selectParams['@limit'] = limit;
199
+ if (offset > 0) selectParams['@offset'] = offset;
200
+
201
+ final dataRows = await client.execute(
202
+ 'SELECT * FROM bundles $whereSql '
203
+ 'ORDER BY id ${orderBy.direction == 'asc' ? 'asc' : 'desc'}'
204
+ '${limit > 0 ? ' LIMIT @limit' : ''}'
205
+ '${offset > 0 ? ' OFFSET @offset' : ''}',
206
+ selectParams,
207
+ );
208
+
209
+ final patchMap = await fetchPatchMap(
210
+ dataRows.map((row) => row['id'] as String).toList(),
211
+ );
212
+ final bundles = dataRows.map((row) {
213
+ final bundleRow = PostgresBundleRow.fromJson(row);
214
+ return mapRowToBundle(bundleRow, patchMap[bundleRow.id] ?? []);
215
+ }).toList();
216
+
217
+ return Paginated(
218
+ data: bundles,
219
+ pagination: calculatePagination(total, limit: limit, offset: offset),
220
+ );
221
+ }
222
+
223
+ @override
224
+ Future<List<String>> getChannels() async {
225
+ final rows = await client.execute(
226
+ 'SELECT channel FROM bundles GROUP BY channel',
227
+ {},
228
+ );
229
+ return rows.map((row) => row['channel'] as String).toList();
230
+ }
231
+
232
+ @override
233
+ Future<void> commitBundle({
234
+ required List<BundleChange> changedSets,
235
+ }) async {
236
+ if (changedSets.isEmpty) return;
237
+
238
+ await client.runTx((tx) async {
239
+ for (final op in changedSets) {
240
+ if (op.operation == BundleChangeOperation.delete) {
241
+ await tx.execute(
242
+ 'DELETE FROM bundle_patches WHERE bundle_id = @p0',
243
+ {'p0': op.data.id},
244
+ );
245
+ await tx.execute(
246
+ 'DELETE FROM bundle_patches WHERE base_bundle_id = @p0',
247
+ {'p0': op.data.id},
248
+ );
249
+ final result = await tx.execute(
250
+ 'DELETE FROM bundles WHERE id = @p0 RETURNING id',
251
+ {'p0': op.data.id},
252
+ );
253
+ if (result.isEmpty) {
254
+ throw StateError('Bundle with id ${op.data.id} not found');
255
+ }
256
+ } else {
257
+ final bundle = op.data;
258
+ final values = bundleToRowValues(bundle);
259
+ final columns = values.keys.toList();
260
+ final insertCols = columns.join(', ');
261
+ final placeholders =
262
+ columns.map((c) => '@${c.replaceAll('.', '_')}').join(', ');
263
+ final updateSets = columns
264
+ .where((c) => c != 'id')
265
+ .map((c) => '$c = EXCLUDED.$c')
266
+ .join(', ');
267
+ await tx.execute(
268
+ 'INSERT INTO bundles ($insertCols) VALUES ($placeholders) '
269
+ 'ON CONFLICT (id) DO UPDATE SET $updateSets',
270
+ {for (final c in columns) c.replaceAll('.', '_'): values[c]},
271
+ );
272
+
273
+ await tx.execute(
274
+ 'DELETE FROM bundle_patches WHERE bundle_id = @p0',
275
+ {'p0': bundle.id},
276
+ );
277
+ final patchRows = bundleToPatchRows(bundle);
278
+ if (patchRows.isNotEmpty) {
279
+ for (final pr in patchRows) {
280
+ final patchCols = pr.keys.toList();
281
+ final patchInsert = patchCols.join(', ');
282
+ final patchPlaceholders =
283
+ patchCols.map((c) => '@${c.replaceAll('.', '_')}').join(', ');
284
+ await tx.execute(
285
+ 'INSERT INTO bundle_patches ($patchInsert) '
286
+ 'VALUES ($patchPlaceholders) '
287
+ 'ON CONFLICT (id) DO UPDATE SET '
288
+ '${patchCols.where((c) => c != 'id').map((c) => '$c = EXCLUDED.$c').join(', ')}',
289
+ {for (final c in patchCols) c.replaceAll('.', '_'): pr[c]},
290
+ );
291
+ }
292
+ }
293
+ }
294
+ }
295
+ });
296
+ }
297
+
298
+ @override
299
+ Future<void> onUnmount() async {
300
+ await client.close();
301
+ }
302
+ }
@@ -0,0 +1,120 @@
1
+ /// Faithful port of hot-updater `plugins/postgres/src/getUpdateInfo.ts`.
2
+ ///
3
+ /// Calls the PL/pgSQL RPCs (`get_target_app_version_list`,
4
+ /// `get_update_info_by_app_version`, `get_update_info_by_fingerprint_hash`)
5
+ /// defined in `plugins/postgres/sql/*.sql`. The DB must have those functions
6
+ /// installed (see `supabase/migrations` parity work / Phase 5).
7
+ library;
8
+
9
+ import 'package:flutter_patcher_core/flutter_patcher_core.dart'
10
+ show
11
+ FingerprintGetBundlesArgs,
12
+ GetBundlesArgs,
13
+ AppVersionGetBundlesArgs,
14
+ UpdateInfo,
15
+ UpdateStatus,
16
+ UpdateStrategy;
17
+ import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
18
+ show filterCompatibleAppVersions;
19
+
20
+ import 'postgres_client.dart';
21
+
22
+ /// Row shape returned by the update-info RPCs.
23
+ class _UpdateInfoRow {
24
+ const _UpdateInfoRow({
25
+ required this.id,
26
+ required this.shouldForceUpdate,
27
+ required this.message,
28
+ required this.status,
29
+ required this.storageUri,
30
+ required this.fileHash,
31
+ });
32
+
33
+ factory _UpdateInfoRow.fromJson(Map<String, dynamic> row) => _UpdateInfoRow(
34
+ id: row['id'] as String,
35
+ shouldForceUpdate: row['should_force_update'] as bool,
36
+ message: row['message'] as String?,
37
+ status: row['status'] as String,
38
+ storageUri: row['storage_uri'] as String?,
39
+ fileHash: row['file_hash'] as String?,
40
+ );
41
+
42
+ final String id;
43
+ final bool shouldForceUpdate;
44
+ final String? message;
45
+ final String status;
46
+ final String? storageUri;
47
+ final String? fileHash;
48
+ }
49
+
50
+ UpdateInfo mapUpdateInfoRow(_UpdateInfoRow row) => UpdateInfo(
51
+ id: row.id,
52
+ shouldForceUpdate: row.shouldForceUpdate,
53
+ message: row.message,
54
+ status: row.status == 'ROLLBACK' ? UpdateStatus.rollback : UpdateStatus.update,
55
+ storageUri: row.storageUri,
56
+ fileHash: row.fileHash,
57
+ );
58
+
59
+ /// Resolve update info via the Postgres RPCs.
60
+ Future<UpdateInfo?> getUpdateInfo(
61
+ PostgresClientLike client,
62
+ GetBundlesArgs args,
63
+ ) async {
64
+ if (args.updateStrategy == UpdateStrategy.appVersion) {
65
+ final appArgs = args as AppVersionGetBundlesArgs;
66
+ final targetRes = await client.execute(
67
+ 'SELECT target_app_version '
68
+ 'FROM get_target_app_version_list(@app_platform, @min_bundle_id)',
69
+ {
70
+ 'app_platform': appArgs.platform.value,
71
+ 'min_bundle_id': appArgs.minBundleId,
72
+ },
73
+ );
74
+
75
+ final targetAppVersionList = filterCompatibleAppVersions(
76
+ targetRes
77
+ .map((row) => row['target_app_version'] as String?)
78
+ .where((value) => value != null)
79
+ .cast<String>()
80
+ .toList(),
81
+ appArgs.appVersion,
82
+ );
83
+
84
+ final res = await client.execute(
85
+ 'SELECT * FROM get_update_info_by_app_version('
86
+ '@app_platform, @app_version, @bundle_id, @min_bundle_id, '
87
+ '@target_channel, @target_app_version_list, @cohort)',
88
+ {
89
+ 'app_platform': appArgs.platform.value,
90
+ 'app_version': appArgs.appVersion,
91
+ 'bundle_id': appArgs.bundleId,
92
+ 'min_bundle_id': appArgs.minBundleId,
93
+ 'target_channel': appArgs.channel,
94
+ 'target_app_version_list': targetAppVersionList,
95
+ 'cohort': appArgs.cohort,
96
+ },
97
+ );
98
+
99
+ if (res.isEmpty) return null;
100
+ return mapUpdateInfoRow(_UpdateInfoRow.fromJson(res.first));
101
+ }
102
+
103
+ final fpArgs = args as FingerprintGetBundlesArgs;
104
+ final res = await client.execute(
105
+ 'SELECT * FROM get_update_info_by_fingerprint_hash('
106
+ '@app_platform, @bundle_id, @min_bundle_id, '
107
+ '@target_channel, @target_fingerprint_hash, @cohort)',
108
+ {
109
+ 'app_platform': fpArgs.platform.value,
110
+ 'bundle_id': fpArgs.bundleId,
111
+ 'min_bundle_id': fpArgs.minBundleId,
112
+ 'target_channel': fpArgs.channel,
113
+ 'target_fingerprint_hash': fpArgs.fingerprintHash,
114
+ 'cohort': fpArgs.cohort,
115
+ },
116
+ );
117
+
118
+ if (res.isEmpty) return null;
119
+ return mapUpdateInfoRow(_UpdateInfoRow.fromJson(res.first));
120
+ }
@@ -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
+ );