@_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,422 @@
1
+ /// Faithful port of hot-updater `plugins/supabase/src/supabaseDatabase.ts`.
2
+ library;
3
+
4
+ import 'dart:convert';
5
+
6
+ import 'package:flutter_patcher_core/flutter_patcher_core.dart';
7
+ import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart';
8
+
9
+ import 'supabase_bundle_mapper.dart';
10
+ import 'supabase_client_adapter.dart';
11
+ import 'supabase_client_http.dart';
12
+ import 'supabase_config.dart';
13
+ import 'types.dart';
14
+
15
+ /// Configuration for the Supabase database plugin.
16
+ typedef SupabaseDatabaseConfig = SupabaseServiceRoleConfig;
17
+
18
+ /// Row shape returned by the update-info RPCs.
19
+ class _SupabaseUpdateInfoRow {
20
+ final String id;
21
+ final bool shouldForceUpdate;
22
+ final String? message;
23
+ final String status;
24
+ final String? storageUri;
25
+ final String? fileHash;
26
+
27
+ const _SupabaseUpdateInfoRow({
28
+ required this.id,
29
+ required this.shouldForceUpdate,
30
+ required this.message,
31
+ required this.status,
32
+ required this.storageUri,
33
+ required this.fileHash,
34
+ });
35
+ }
36
+
37
+ /// Row shape returned by `get_target_app_version_list`.
38
+ class _SupabaseTargetAppVersionRow {
39
+ final String? targetAppVersion;
40
+
41
+ const _SupabaseTargetAppVersionRow(this.targetAppVersion);
42
+ }
43
+
44
+ /// Build a throwable error from an unknown Supabase error value.
45
+ Object createSupabaseError(Object error) {
46
+ if (error is Error) return error;
47
+ if (error is Exception) return error;
48
+
49
+ if (error is Map) {
50
+ final props = <String, Object?>{};
51
+ error.forEach((key, value) {
52
+ props[key.toString()] = value;
53
+ });
54
+ return StateError(jsonEncode(props));
55
+ }
56
+ return StateError(error.toString());
57
+ }
58
+
59
+ UpdateInfo mapUpdateInfoRow(_SupabaseUpdateInfoRow row) => UpdateInfo(
60
+ id: row.id,
61
+ shouldForceUpdate: row.shouldForceUpdate,
62
+ message: row.message,
63
+ status: row.status == 'ROLLBACK' ? UpdateStatus.rollback : UpdateStatus.update,
64
+ storageUri: row.storageUri,
65
+ fileHash: row.fileHash,
66
+ );
67
+
68
+ /// Supabase-backed [DatabasePlugin] factory.
69
+ ///
70
+ /// Mirrors `supabaseDatabase` from hot-updater: a `createDatabasePlugin`
71
+ /// wrapper whose inner factory builds a Supabase client and returns an
72
+ /// [AbstractDatabasePlugin] implementation.
73
+ DatabasePlugin Function() Function(
74
+ SupabaseDatabaseConfig config, [
75
+ DatabasePluginHooks? hooks,
76
+ ]) supabaseDatabase = createDatabasePlugin<SupabaseDatabaseConfig>(
77
+ name: 'supabaseDatabase',
78
+ factory: (config) {
79
+ final supabase = (config.clientFactory != null
80
+ ? config.clientFactory!(config.supabaseUrl, config.resolveKey())
81
+ : createSupabaseHttpClient(
82
+ config.supabaseUrl,
83
+ config.resolveKey(),
84
+ ));
85
+
86
+ Future<Map<String, List<SupabaseBundlePatchRow>>> fetchPatchMap(
87
+ List<String> bundleIds,
88
+ ) async {
89
+ final patchMap = <String, List<SupabaseBundlePatchRow>>{};
90
+ if (bundleIds.isEmpty) return patchMap;
91
+
92
+ final res = await supabase
93
+ .from('bundle_patches')
94
+ .select('*')
95
+ .in_('bundle_id', bundleIds)
96
+ .order('order_index', ascending: true)
97
+ .execute();
98
+
99
+ if (res.error != null) {
100
+ throw createSupabaseError(res.error!);
101
+ }
102
+
103
+ final data = (res.data as List?) ?? [];
104
+ for (final raw in data) {
105
+ final row = SupabaseBundlePatchRow.fromJson(
106
+ (raw as Map).cast<String, dynamic>(),
107
+ );
108
+ final current = patchMap[row.bundleId] ?? [];
109
+ current.add(row);
110
+ patchMap[row.bundleId] = current;
111
+ }
112
+ return patchMap;
113
+ }
114
+
115
+ return _SupabaseDatabasePlugin(
116
+ supabase: supabase,
117
+ fetchPatchMap: fetchPatchMap,
118
+ );
119
+ },
120
+ );
121
+
122
+ class _SupabaseDatabasePlugin implements AbstractDatabasePlugin {
123
+ final SupabaseClientLike supabase;
124
+ final Future<Map<String, List<SupabaseBundlePatchRow>>> Function(
125
+ List<String> bundleIds,
126
+ ) fetchPatchMap;
127
+
128
+ _SupabaseDatabasePlugin({
129
+ required this.supabase,
130
+ required this.fetchPatchMap,
131
+ });
132
+
133
+ @override
134
+ bool get supportsCursorPagination => false;
135
+
136
+ @override
137
+ Future<UpdateInfo?> getUpdateInfo(GetBundlesArgs args) async {
138
+ final channel = args.channel;
139
+ final minBundleId = args.minBundleId;
140
+
141
+ if (args.updateStrategy == UpdateStrategy.appVersion) {
142
+ final appArgs = args as AppVersionGetBundlesArgs;
143
+ final targetRes = await supabase.rpc(
144
+ 'get_target_app_version_list',
145
+ params: {
146
+ 'app_platform': appArgs.platform.value,
147
+ 'min_bundle_id': minBundleId,
148
+ },
149
+ );
150
+ if (targetRes.error != null) {
151
+ throw createSupabaseError(targetRes.error!);
152
+ }
153
+ final targetAppVersionList = filterCompatibleAppVersions(
154
+ ((targetRes.data as List?) ?? [])
155
+ .map((row) =>
156
+ _SupabaseTargetAppVersionRow((row as Map)['target_app_version']))
157
+ .where((row) => row.targetAppVersion != null)
158
+ .map((row) => row.targetAppVersion!)
159
+ .toList(),
160
+ appArgs.appVersion,
161
+ );
162
+
163
+ final res = await supabase.rpc(
164
+ 'get_update_info_by_app_version',
165
+ params: {
166
+ 'app_platform': appArgs.platform.value,
167
+ 'app_version': appArgs.appVersion,
168
+ 'bundle_id': appArgs.bundleId,
169
+ 'min_bundle_id': minBundleId,
170
+ 'target_channel': channel,
171
+ 'target_app_version_list': targetAppVersionList,
172
+ 'cohort': appArgs.cohort,
173
+ },
174
+ );
175
+ if (res.error != null) {
176
+ throw createSupabaseError(res.error!);
177
+ }
178
+ final list = (res.data as List?) ?? [];
179
+ if (list.isEmpty) return null;
180
+ return mapUpdateInfoRow(_rowToUpdateInfo(list.first));
181
+ }
182
+
183
+ final fpArgs = args as FingerprintGetBundlesArgs;
184
+ final res = await supabase.rpc(
185
+ 'get_update_info_by_fingerprint_hash',
186
+ params: {
187
+ 'app_platform': fpArgs.platform.value,
188
+ 'bundle_id': fpArgs.bundleId,
189
+ 'min_bundle_id': minBundleId,
190
+ 'target_channel': channel,
191
+ 'target_fingerprint_hash': fpArgs.fingerprintHash,
192
+ 'cohort': fpArgs.cohort,
193
+ },
194
+ );
195
+ if (res.error != null) {
196
+ throw createSupabaseError(res.error!);
197
+ }
198
+ final list = (res.data as List?) ?? [];
199
+ if (list.isEmpty) return null;
200
+ return mapUpdateInfoRow(_rowToUpdateInfo(list.first));
201
+ }
202
+
203
+ _SupabaseUpdateInfoRow _rowToUpdateInfo(Object raw) {
204
+ final row = (raw as Map).cast<String, dynamic>();
205
+ return _SupabaseUpdateInfoRow(
206
+ id: row['id'] as String,
207
+ shouldForceUpdate: row['should_force_update'] as bool,
208
+ message: row['message'] as String?,
209
+ status: row['status'] as String,
210
+ storageUri: row['storage_uri'] as String?,
211
+ fileHash: row['file_hash'] as String?,
212
+ );
213
+ }
214
+
215
+ @override
216
+ Future<Bundle?> getBundleById(String bundleId) async {
217
+ final results = await Future.wait([
218
+ supabase
219
+ .from('bundles')
220
+ .select(bundleSelectColumns)
221
+ .eq('id', bundleId)
222
+ .single()
223
+ .execute(),
224
+ fetchPatchMap([bundleId]),
225
+ ]);
226
+
227
+ final res = results[0] as SupabaseResponseLike;
228
+ if (res.data == null || res.error != null) return null;
229
+ final row = SupabaseBundleRow.fromJson(
230
+ (res.data as Map).cast<String, dynamic>(),
231
+ );
232
+ final patches = (results[1] as Map<String, List<SupabaseBundlePatchRow>>)[bundleId] ?? [];
233
+ return mapRowToBundle(row, patches);
234
+ }
235
+
236
+ @override
237
+ Future<Paginated<List<Bundle>>> getBundles(
238
+ DatabaseBundleQueryOptions options,
239
+ ) async {
240
+ final where = options.where;
241
+ final limit = options.limit;
242
+ final orderBy = options.orderBy ??
243
+ const DatabaseBundleQueryOrder(direction: 'desc');
244
+ final offset = options.offset ??
245
+ (options.page != null ? (options.page! - 1) * limit : 0);
246
+
247
+ if ((where?.targetAppVersionIn != null &&
248
+ where!.targetAppVersionIn!.isEmpty) ||
249
+ (where?.id?.ins != null && where!.id!.ins!.isEmpty)) {
250
+ return Paginated(
251
+ data: const [],
252
+ pagination: calculatePagination(0, limit: limit, offset: offset),
253
+ );
254
+ }
255
+
256
+ var countQuery = supabase
257
+ .from('bundles')
258
+ .select('*', count: true, head: true);
259
+ countQuery = _applyWhereFilters(countQuery, where);
260
+
261
+ final countRes = await countQuery.execute();
262
+ final total = countRes.count ?? 0;
263
+
264
+ var query = supabase
265
+ .from('bundles')
266
+ .select(bundleSelectColumns)
267
+ .order('id', ascending: orderBy.direction == 'asc');
268
+ query = _applyWhereFilters(query, where);
269
+
270
+ if (limit > 0) query = query.limit(limit);
271
+ if (offset > 0) query = query.range(offset, offset + limit - 1);
272
+
273
+ final res = await query.execute();
274
+ if (res.error != null) {
275
+ throw createSupabaseError(res.error!);
276
+ }
277
+ final data = (res.data as List?) ?? [];
278
+ final patchMap = await fetchPatchMap(
279
+ data.map((b) => (b as Map)['id'] as String).toList(),
280
+ );
281
+ final bundles = data.map((b) {
282
+ final row = SupabaseBundleRow.fromJson((b as Map).cast<String, dynamic>());
283
+ return mapRowToBundle(row, patchMap[row.id] ?? []);
284
+ }).toList();
285
+
286
+ return Paginated(
287
+ data: bundles,
288
+ pagination: calculatePagination(total, limit: limit, offset: offset),
289
+ );
290
+ }
291
+
292
+ SupabaseFilterBuilderLike _applyWhereFilters(
293
+ SupabaseFilterBuilderLike query,
294
+ DatabaseBundleQueryWhere? where,
295
+ ) {
296
+ if (where == null) return query;
297
+ if (where.channel != null) {
298
+ query = query.eq('channel', where.channel!);
299
+ }
300
+ if (where.platform != null) {
301
+ query = query.eq('platform', where.platform!.value);
302
+ }
303
+ if (where.enabled != null) {
304
+ query = query.eq('enabled', where.enabled!);
305
+ }
306
+ if (where.fingerprintHash != null) {
307
+ query = where.fingerprintHash == null
308
+ ? query.isFilter('fingerprint_hash', null)
309
+ : query.eq('fingerprint_hash', where.fingerprintHash!);
310
+ }
311
+ if (where.targetAppVersion != null) {
312
+ query = where.targetAppVersion == null
313
+ ? query.isFilter('target_app_version', null)
314
+ : query.eq('target_app_version', where.targetAppVersion!);
315
+ }
316
+ if (where.targetAppVersionIn != null) {
317
+ query = query.in_('target_app_version', where.targetAppVersionIn!);
318
+ }
319
+ if (where.targetAppVersionNotNull == true) {
320
+ query = query.not('target_app_version', 'is', null);
321
+ }
322
+ if (where.id != null) {
323
+ if (where.id!.eq != null) query = query.eq('id', where.id!.eq!);
324
+ if (where.id!.gt != null) query = query.gt('id', where.id!.gt!);
325
+ if (where.id!.gte != null) query = query.gte('id', where.id!.gte!);
326
+ if (where.id!.lt != null) query = query.lt('id', where.id!.lt!);
327
+ if (where.id!.lte != null) query = query.lte('id', where.id!.lte!);
328
+ if (where.id!.ins != null) {
329
+ query = query.in_('id', where.id!.ins!);
330
+ }
331
+ }
332
+ return query;
333
+ }
334
+
335
+ @override
336
+ Future<List<String>> getChannels() async {
337
+ final res = await supabase.rpc('get_channels');
338
+ if (res.error != null) {
339
+ throw createSupabaseError(res.error!);
340
+ }
341
+ final data = (res.data as List?) ?? [];
342
+ return data
343
+ .map((row) => (row as Map)['channel'] as String)
344
+ .toList();
345
+ }
346
+
347
+ @override
348
+ Future<void> commitBundle({
349
+ required List<BundleChange> changedSets,
350
+ }) async {
351
+ if (changedSets.isEmpty) return;
352
+
353
+ for (final op in changedSets) {
354
+ if (op.operation == BundleChangeOperation.delete) {
355
+ final id = op.data.id;
356
+ final patchDelete = await supabase
357
+ .from('bundle_patches')
358
+ .delete()
359
+ .eq('bundle_id', id)
360
+ .execute();
361
+ if (patchDelete.error != null) {
362
+ throw StateError(
363
+ 'Failed to delete bundle patches: ${patchDelete.error}',
364
+ );
365
+ }
366
+ final basePatchDelete = await supabase
367
+ .from('bundle_patches')
368
+ .delete()
369
+ .eq('base_bundle_id', id)
370
+ .execute();
371
+ if (basePatchDelete.error != null) {
372
+ throw StateError(
373
+ 'Failed to delete base bundle patches: ${basePatchDelete.error}',
374
+ );
375
+ }
376
+ final deleteRes = await supabase
377
+ .from('bundles')
378
+ .delete()
379
+ .eq('id', id)
380
+ .execute();
381
+ if (deleteRes.error != null) {
382
+ throw StateError('Failed to delete bundle: ${deleteRes.error}');
383
+ }
384
+ } else {
385
+ final bundle = op.data;
386
+ final patchRows = bundleToPatchRows(bundle);
387
+ final upsertRes = await supabase
388
+ .from('bundles')
389
+ .upsert(bundleToRow(bundle), onConflict: 'id');
390
+ if (upsertRes is SupabaseResponseLike && upsertRes.error != null) {
391
+ throw createSupabaseError(upsertRes.error!);
392
+ } else if (upsertRes is Map && upsertRes['error'] != null) {
393
+ throw createSupabaseError(upsertRes['error']!);
394
+ }
395
+
396
+ final patchDelete = await supabase
397
+ .from('bundle_patches')
398
+ .delete()
399
+ .eq('bundle_id', bundle.id)
400
+ .execute();
401
+ if (patchDelete.error != null) {
402
+ throw createSupabaseError(patchDelete.error!);
403
+ }
404
+
405
+ if (patchRows.isNotEmpty) {
406
+ final patchUpsert = await supabase
407
+ .from('bundle_patches')
408
+ .upsert(patchRows.map((p) => p.toJson()).toList(),
409
+ onConflict: 'id');
410
+ if (patchUpsert is SupabaseResponseLike && patchUpsert.error != null) {
411
+ throw createSupabaseError(patchUpsert.error!);
412
+ } else if (patchUpsert is Map && patchUpsert['error'] != null) {
413
+ throw createSupabaseError(patchUpsert['error']!);
414
+ }
415
+ }
416
+ }
417
+ }
418
+ }
419
+
420
+ @override
421
+ Future<void> onUnmount() async {}
422
+ }
@@ -0,0 +1,32 @@
1
+ /// Faithful port of hot-updater `plugins/supabase/src/supabaseEdgeFunctionDatabase.ts`.
2
+ library;
3
+
4
+ import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart';
5
+
6
+ import 'supabase_database.dart';
7
+
8
+ /// Config for the Supabase edge-function database plugin.
9
+ class SupabaseEdgeFunctionDatabaseConfig {
10
+ final String supabaseUrl;
11
+ final String supabaseServiceRoleKey;
12
+
13
+ const SupabaseEdgeFunctionDatabaseConfig({
14
+ required this.supabaseUrl,
15
+ required this.supabaseServiceRoleKey,
16
+ });
17
+ }
18
+
19
+ /// Edge-function variant of [supabaseDatabase]: builds the same database plugin
20
+ /// from a url + service-role key pair (no anon key).
21
+ DatabasePlugin Function() supabaseEdgeFunctionDatabase(
22
+ SupabaseEdgeFunctionDatabaseConfig config, [
23
+ DatabasePluginHooks? hooks,
24
+ ]) {
25
+ return supabaseDatabase(
26
+ SupabaseDatabaseConfig(
27
+ supabaseUrl: config.supabaseUrl,
28
+ supabaseServiceRoleKey: config.supabaseServiceRoleKey,
29
+ ),
30
+ hooks,
31
+ );
32
+ }
@@ -0,0 +1,99 @@
1
+ /// Faithful port of hot-updater `plugins/supabase/src/supabaseEdgeFunctionStorage.ts`.
2
+ library;
3
+
4
+ import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart';
5
+
6
+ import 'supabase_client_adapter.dart';
7
+ import 'supabase_client_http.dart';
8
+ import 'supabase_signed_url_batcher.dart';
9
+
10
+ /// Config for the Supabase edge-function storage plugin.
11
+ class SupabaseEdgeFunctionStorageConfig {
12
+ final String supabaseUrl;
13
+ final String supabaseServiceRoleKey;
14
+ final int? signedUrlExpiresIn;
15
+
16
+ const SupabaseEdgeFunctionStorageConfig({
17
+ required this.supabaseUrl,
18
+ required this.supabaseServiceRoleKey,
19
+ this.signedUrlExpiresIn,
20
+ });
21
+
22
+ SupabaseClientFactory get clientFactory =>
23
+ (url, key) => createSupabaseHttpClient(url, key);
24
+ }
25
+
26
+ /// Parse a `supabase-storage://` URI into bucket + key.
27
+ ParsedStorageUri parseSupabaseEdgeStorageUri(String storageUri) =>
28
+ parseStorageUri(storageUri, 'supabase-storage');
29
+
30
+ /// Edge-function variant of [supabaseStorage]: a runtime-only storage plugin
31
+ /// (no node profile) backed by the Supabase client.
32
+ final supabaseEdgeFunctionStorage =
33
+ createRuntimeStoragePlugin<SupabaseEdgeFunctionStorageConfig>(
34
+ name: 'supabaseEdgeFunctionStorage',
35
+ supportedProtocol: 'supabase-storage',
36
+ factory: (config) {
37
+ final supabase = config.clientFactory(
38
+ config.supabaseUrl,
39
+ config.supabaseServiceRoleKey,
40
+ );
41
+ final resolveSignedUrl = createSupabaseSignedUrlBatcher(
42
+ createSignedUrls: (bucketName, keys, expiresIn) =>
43
+ supabase.storage.from(bucketName).createSignedUrls(keys, expiresIn),
44
+ expiresIn: config.signedUrlExpiresIn ?? 3600,
45
+ formatObjectPath: (bucketName, key) => '$bucketName/$key',
46
+ );
47
+
48
+ return RuntimeStorageProfileImplEdge(
49
+ supabase: supabase,
50
+ resolveSignedUrl: resolveSignedUrl,
51
+ );
52
+ },
53
+ );
54
+
55
+ class RuntimeStorageProfileImplEdge implements RuntimeStorageProfile {
56
+ RuntimeStorageProfileImplEdge({
57
+ required this.supabase,
58
+ required this.resolveSignedUrl,
59
+ });
60
+
61
+ final SupabaseClientLike supabase;
62
+ final ResolveSignedUrl resolveSignedUrl;
63
+
64
+ @override
65
+ Future<String?> readText(String storageUri) async {
66
+ final parsed = parseSupabaseEdgeStorageUri(storageUri);
67
+ if (parsed.bucket.isEmpty || parsed.key.isEmpty) {
68
+ throw StateError('Invalid Supabase storage URI');
69
+ }
70
+ final res = await supabase.storage.from(parsed.bucket).download(parsed.key);
71
+ if (res.error != null) {
72
+ final msg = (res.message ?? _errorToString(res.error))
73
+ .toString();
74
+ if (msg.contains('not found')) return null;
75
+ throw StateError('Failed to read storage text: $msg');
76
+ }
77
+ if (res.data == null) return null;
78
+ return String.fromCharCodes(res.data!);
79
+ }
80
+
81
+ @override
82
+ Future<Map<String, String>> getDownloadUrl(String storageUri) async {
83
+ final parsed = parseSupabaseEdgeStorageUri(storageUri);
84
+ if (parsed.bucket.isEmpty || parsed.key.isEmpty) {
85
+ throw StateError('Invalid Supabase storage URI');
86
+ }
87
+ final signedUrl = await resolveSignedUrl(parsed.bucket, parsed.key);
88
+ return {'fileUrl': signedUrl};
89
+ }
90
+
91
+ String _errorToString(Object? error) {
92
+ if (error is Error) return error.toString();
93
+ if (error is Exception) return error.toString();
94
+ if (error is Map && error['message'] is String) {
95
+ return error['message'] as String;
96
+ }
97
+ return error.toString();
98
+ }
99
+ }
@@ -0,0 +1,145 @@
1
+ /// Faithful port of hot-updater `plugins/supabase/src/supabaseSignedUrlBatcher.ts`.
2
+ library;
3
+
4
+ import 'dart:async';
5
+
6
+ import 'supabase_client_adapter.dart';
7
+
8
+ /// A pending signed URL request waiting for the next flush.
9
+ class _PendingSignedUrl {
10
+ final String key;
11
+ final void Function(Object error) reject;
12
+ final void Function(String signedUrl) resolve;
13
+
14
+ _PendingSignedUrl({
15
+ required this.key,
16
+ required this.reject,
17
+ required this.resolve,
18
+ });
19
+ }
20
+
21
+ /// Extract a human-readable message from an unknown error value.
22
+ String _getErrorMessage(Object? error) {
23
+ if (error is Error) return error.toString();
24
+ if (error is Exception) return error.toString();
25
+ if (error is Map && error['message'] is String) {
26
+ return error['message'] as String;
27
+ }
28
+ return error.toString();
29
+ }
30
+
31
+ /// Signature of the batch signed URL generator. Returns the same shape as the
32
+ /// Supabase storage `createSignedUrls` call: a list of results aligned to the
33
+ /// input key order.
34
+ typedef CreateSignedUrls = Future<SupabaseSignedUrlListResult> Function(
35
+ String bucketName,
36
+ List<String> keys,
37
+ int expiresIn,
38
+ );
39
+
40
+ /// Creates a function that batches signed URL generation requests per
41
+ /// bucket and flushes them on the next microtask, mirroring Supabase's
42
+ /// `createSignedUrls` batch API.
43
+ ///
44
+ /// [createSignedUrls] must take a bucket name, list of keys, and expiry,
45
+ /// and return a [SupabaseSignedUrlListResult] whose `data` entries are aligned
46
+ /// to the input key order.
47
+ /// [formatObjectPath] builds the human-readable object path used in errors.
48
+ ResolveSignedUrl createSupabaseSignedUrlBatcher({
49
+ required CreateSignedUrls createSignedUrls,
50
+ required int expiresIn,
51
+ required String Function(String bucketName, String key) formatObjectPath,
52
+ }) {
53
+ var pendingByBucket = <String, List<_PendingSignedUrl>>{};
54
+ var flushScheduled = false;
55
+
56
+ Object createSignedUrlError(
57
+ String bucketName,
58
+ String key,
59
+ Object error,
60
+ ) =>
61
+ StateError(
62
+ 'Failed to generate download URL for '
63
+ '"${formatObjectPath(bucketName, key)}": ${_getErrorMessage(error)}',
64
+ );
65
+
66
+ Future<void> flush() async {
67
+ final batches = pendingByBucket;
68
+ pendingByBucket = <String, List<_PendingSignedUrl>>{};
69
+ flushScheduled = false;
70
+
71
+ await Future.wait(
72
+ batches.entries.map((entry) async {
73
+ final bucketName = entry.key;
74
+ final pending = entry.value;
75
+
76
+ try {
77
+ final result = await createSignedUrls(
78
+ bucketName,
79
+ pending.map((p) => p.key).toList(),
80
+ expiresIn,
81
+ );
82
+
83
+ final data = result.data;
84
+ if (result.error != null || data == null) {
85
+ final batchError =
86
+ result.error ?? StateError('missing signed URL response');
87
+ for (final request in pending) {
88
+ request.reject(
89
+ createSignedUrlError(bucketName, request.key, batchError),
90
+ );
91
+ }
92
+ return;
93
+ }
94
+
95
+ for (var i = 0; i < pending.length; i++) {
96
+ final request = pending[i];
97
+ final row = data[i];
98
+ if (row.error == null && row.signedUrl != null) {
99
+ request.resolve(row.signedUrl!);
100
+ } else {
101
+ request.reject(
102
+ createSignedUrlError(
103
+ bucketName,
104
+ request.key,
105
+ row.error ?? StateError('missing signed URL'),
106
+ ),
107
+ );
108
+ }
109
+ }
110
+ } catch (error) {
111
+ for (final request in pending) {
112
+ request.reject(createSignedUrlError(bucketName, request.key, error));
113
+ }
114
+ }
115
+ }),
116
+ );
117
+ }
118
+
119
+ return (String bucketName, String key) {
120
+ final completer = Completer<String>();
121
+ final pending =
122
+ pendingByBucket[bucketName] ?? <_PendingSignedUrl>[];
123
+ pending.add(
124
+ _PendingSignedUrl(
125
+ key: key,
126
+ reject: completer.completeError,
127
+ resolve: completer.complete,
128
+ ),
129
+ );
130
+ pendingByBucket[bucketName] = pending;
131
+
132
+ if (!flushScheduled) {
133
+ flushScheduled = true;
134
+ Future.microtask(() => flush());
135
+ }
136
+
137
+ return completer.future;
138
+ };
139
+ }
140
+
141
+ /// Resolved function returned by [createSupabaseSignedUrlBatcher].
142
+ typedef ResolveSignedUrl = Future<String> Function(
143
+ String bucketName,
144
+ String key,
145
+ );