@_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,90 @@
1
+ import 'package:postgres/postgres.dart';
2
+
3
+ import 'postgres_config.dart';
4
+
5
+ /// Minimal Postgres session contract used by the plugin.
6
+ ///
7
+ /// Mirrors the surface the Dart port needs from `package:postgres`
8
+ /// (`Session`/`SessionExecutor`): parameterized `execute` returning column maps,
9
+ /// `runTx` for transactional `commitBundle`, and `close`.
10
+ ///
11
+ /// A mock implementation backs the unit tests (see `test/mock_postgres_client.dart`).
12
+ abstract class PostgresClientLike {
13
+ /// Runs [sql] with named [parameters] (e.g. `@id`) and returns rows as
14
+ /// column-name → value maps.
15
+ Future<List<Map<String, dynamic>>> execute(
16
+ String sql,
17
+ Map<String, dynamic> parameters,
18
+ );
19
+
20
+ /// Runs [fn] inside a transaction. The argument is a client bound to the
21
+ /// transaction session (only `execute` is supported on it; nested
22
+ /// transactions are not supported).
23
+ Future<T> runTx<T>(Future<T> Function(PostgresClientLike tx) fn);
24
+
25
+ /// Closes the underlying connection.
26
+ Future<void> close();
27
+ }
28
+
29
+ /// Real [PostgresClientLike] backed by `package:postgres`.
30
+ ///
31
+ /// The connection is opened lazily on first use (so `createDatabasePlugin`'s
32
+ /// synchronous `factory` can build this client without `await`).
33
+ class PostgresClient implements PostgresClientLike {
34
+ PostgresClient._(this._config, this._session, this._executor);
35
+
36
+ final PostgresConfig _config;
37
+ Session? _session;
38
+ SessionExecutor? _executor;
39
+
40
+ /// Creates a client that will open a real connection on first use.
41
+ factory PostgresClient.connect(PostgresConfig config) =>
42
+ PostgresClient._(config, null, null);
43
+
44
+ Future<Session> get _activeSession async {
45
+ if (_session != null) return _session!;
46
+ final endpoint = Endpoint(
47
+ host: _config.host,
48
+ port: _config.port,
49
+ database: _config.database,
50
+ username: _config.username,
51
+ password: _config.password,
52
+ );
53
+ final connection = await Connection.open(
54
+ endpoint,
55
+ settings: ConnectionSettings(sslMode: _config.sslMode),
56
+ );
57
+ _session = connection;
58
+ _executor = connection;
59
+ return connection;
60
+ }
61
+
62
+ @override
63
+ Future<List<Map<String, dynamic>>> execute(
64
+ String sql,
65
+ Map<String, dynamic> parameters,
66
+ ) async {
67
+ final session = await _activeSession;
68
+ final result = await session.execute(sql, parameters: parameters);
69
+ return result.map((row) => row.toColumnMap()).toList();
70
+ }
71
+
72
+ @override
73
+ Future<T> runTx<T>(Future<T> Function(PostgresClientLike tx) fn) async {
74
+ final executor = _executor;
75
+ if (executor == null) {
76
+ throw UnsupportedError('Nested transactions are not supported');
77
+ }
78
+ return executor.runTx((tx) => fn(PostgresClient._(_config, tx, null)));
79
+ }
80
+
81
+ @override
82
+ Future<void> close() async {
83
+ final executor = _executor;
84
+ if (executor != null) {
85
+ await executor.close();
86
+ _session = null;
87
+ _executor = null;
88
+ }
89
+ }
90
+ }
@@ -0,0 +1,35 @@
1
+ import 'package:postgres/postgres.dart';
2
+
3
+ import 'postgres_client.dart';
4
+
5
+ /// Connection configuration for the Postgres backend.
6
+ ///
7
+ /// Faithful port of hot-updater `PostgresConfig extends PoolConfig` (the `pg`
8
+ /// connection options). Mirrors the Supabase plugin's `clientFactory` seam:
9
+ /// when [clientFactory] is provided, the plugin uses the injected client instead
10
+ /// of opening a real connection — this is what makes the plugin unit-testable
11
+ /// without a live database.
12
+ class PostgresConfig {
13
+ const PostgresConfig({
14
+ required this.host,
15
+ this.port = 5432,
16
+ required this.database,
17
+ this.username,
18
+ this.password,
19
+ this.sslMode,
20
+ this.clientFactory,
21
+ });
22
+
23
+ final String host;
24
+ final int port;
25
+ final String database;
26
+ final String? username;
27
+ final String? password;
28
+ final SslMode? sslMode;
29
+
30
+ /// Optional factory for injecting a [PostgresClientLike] (tests / custom pools).
31
+ final PostgresClientFactory? clientFactory;
32
+ }
33
+
34
+ /// Builds a [PostgresClientLike] from a [PostgresConfig].
35
+ typedef PostgresClientFactory = PostgresClientLike Function(PostgresConfig config);
@@ -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
+ }