@_nazmiforreal/flutter-ota 0.1.5 → 0.1.7
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.
- package/README.md +13 -13
- package/bin/{flutter-patcher-linux-x64 → flutter-ota-linux-x64} +0 -0
- package/bin/{flutter-patcher.js → flutter-ota.js} +4 -4
- package/dart-src/packages/cli-tools/bin/{flutter_patcher.dart → flutter_ota_kit.dart} +2 -2
- package/dart-src/packages/cli-tools/lib/{flutter_patcher_cli.dart → flutter_ota_kit_cli.dart} +5 -3
- package/dart-src/packages/cli-tools/lib/src/backend.dart +6 -25
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +10 -4
- package/dart-src/packages/cli-tools/lib/src/commands/build.dart +5 -3
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +279 -10
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +7 -7
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +5 -5
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +4 -4
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +4 -4
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +3 -3
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +294 -45
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +218 -69
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +5 -5
- package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +117 -0
- package/dart-src/packages/cli-tools/lib/src/config.dart +29 -78
- package/dart-src/packages/cli-tools/lib/src/operations.dart +4 -4
- package/dart-src/packages/cli-tools/lib/src/pack.dart +12 -6
- package/dart-src/packages/cli-tools/lib/src/runner.dart +25 -6
- package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +5 -3
- package/dart-src/packages/cli-tools/lib/src/verbose.dart +3 -0
- package/dart-src/packages/cli-tools/migrations/aws/README.md +7 -0
- package/dart-src/packages/cli-tools/migrations/postgres/bundles.sql +45 -0
- package/dart-src/packages/cli-tools/migrations/postgres/get_target_app_version_list.sql +21 -0
- package/dart-src/packages/cli-tools/migrations/postgres/get_update_info_by_app_version.sql +126 -0
- package/dart-src/packages/cli-tools/migrations/postgres/get_update_info_by_fingerprint_hash.sql +125 -0
- package/dart-src/packages/cli-tools/migrations/postgres/hash_user_id.sql +30 -0
- package/dart-src/packages/cli-tools/migrations/postgres/is_device_eligible.sql +237 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260520014100_hot-updater_rls.sql +11 -0
- package/dart-src/packages/cli-tools/pubspec.yaml +9 -11
- package/dart-src/packages/cli-tools/test/backends_test.dart +4 -4
- package/dart-src/packages/cli-tools/test/build_pack_test.dart +1 -1
- package/dart-src/packages/cli-tools/test/cli_test.dart +3 -3
- package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +2 -2
- package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +2 -2
- package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +7 -7
- package/dart-src/packages/core/lib/{flutter_patcher_core.dart → flutter_ota_kit_core.dart} +1 -1
- package/dart-src/packages/core/pubspec.yaml +2 -2
- package/dart-src/packages/core/test/rollout_test.dart +1 -1
- package/dart-src/packages/core/test/semver_test.dart +1 -1
- package/dart-src/packages/core/test/types_test.dart +2 -2
- package/dart-src/packages/core/test/uuid_test.dart +1 -1
- package/dart-src/packages/core/tool/dbg.dart +1 -1
- package/dart-src/plugins/aws/lib/{flutter_patcher_aws.dart → flutter_ota_kit_aws.dart} +0 -2
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +1 -1
- package/dart-src/plugins/aws/lib/src/aws_database.dart +1 -1
- package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +1 -1
- package/dart-src/plugins/aws/lib/src/aws_storage.dart +1 -1
- package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +25 -3
- package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +1 -1
- package/dart-src/plugins/aws/pubspec.yaml +4 -4
- package/dart-src/plugins/aws/test/aws_database_test.dart +3 -3
- package/dart-src/plugins/aws/test/aws_storage_test.dart +2 -2
- package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +2 -2
- package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/d1_database.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +2 -2
- package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +1 -1
- package/dart-src/plugins/cloudflare/pubspec.yaml +4 -4
- package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +2 -2
- package/dart-src/plugins/cloudflare/test/d1_bundle_mapper_test.dart +2 -2
- package/dart-src/plugins/cloudflare/test/d1_database_test.dart +2 -2
- package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +2 -2
- package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +2 -2
- package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +2 -2
- package/dart-src/plugins/plugin-core/lib/{flutter_patcher_plugin_core.dart → flutter_ota_kit_plugin_core.dart} +1 -1
- package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/filter_compatible_app_versions.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/get_update_info.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/resolve_update_info_from_bundles.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/types.dart +2 -2
- package/dart-src/plugins/plugin-core/pubspec.yaml +3 -3
- package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +3 -3
- package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_store_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +5 -5
- package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/helpers_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/request_update_bundle_state_test.dart +3 -3
- package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +1 -1
- package/dart-src/plugins/postgres/lib/{flutter_patcher_postgres.dart → flutter_ota_kit_postgres.dart} +1 -1
- package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +1 -1
- package/dart-src/plugins/postgres/lib/src/postgres_database.dart +2 -2
- package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +2 -2
- package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +9 -9
- package/dart-src/plugins/postgres/lib/src/postgres_types.dart +1 -1
- package/dart-src/plugins/postgres/pubspec.yaml +4 -4
- package/dart-src/plugins/postgres/test/mock_postgres_client.dart +2 -2
- package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +2 -2
- package/dart-src/plugins/postgres/test/postgres_database_test.dart +2 -2
- package/dart-src/plugins/supabase/lib/edge.dart +1 -1
- package/dart-src/plugins/supabase/lib/{flutter_patcher_supabase.dart → flutter_ota_kit_supabase.dart} +1 -1
- package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +1 -1
- package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +60 -20
- package/dart-src/plugins/supabase/lib/src/supabase_database.dart +2 -2
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_database.dart +1 -1
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +1 -1
- package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +2 -5
- package/dart-src/plugins/supabase/pubspec.yaml +4 -4
- package/dart-src/plugins/supabase/test/mock_supabase_client.dart +5 -5
- package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +3 -3
- package/dart-src/plugins/supabase/test/supabase_database_test.dart +3 -3
- package/dart-src/plugins/supabase/test/supabase_storage_test.dart +2 -2
- package/migrations/aws/README.md +7 -0
- package/migrations/cloudflare/0001_hot-updater_init.sql +16 -0
- package/migrations/cloudflare/0002_hot-updater_0.13.0.sql +8 -0
- package/migrations/cloudflare/0003_hot-updater_0.18.0.sql +38 -0
- package/migrations/cloudflare/0004_hot-updater_0.29.0.sql +11 -0
- package/migrations/cloudflare/0005_hot-updater_0.31.0.sql +24 -0
- package/migrations/cloudflare/bundles.sql +42 -0
- package/migrations/postgres/bundles.sql +45 -0
- package/migrations/postgres/get_target_app_version_list.sql +21 -0
- package/migrations/postgres/get_update_info_by_app_version.sql +126 -0
- package/migrations/postgres/get_update_info_by_fingerprint_hash.sql +125 -0
- package/migrations/postgres/hash_user_id.sql +30 -0
- package/migrations/postgres/is_device_eligible.sql +237 -0
- package/package.json +4 -4
- package/scripts/postinstall.js +15 -15
- package/scripts/vendor.sh +1 -1
- package/dart-src/packages/cli-tools/.dart_tool/package_config.json +0 -496
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +0 -743
- package/dart-src/packages/cli-tools/pubspec.lock +0 -638
- package/dart-src/packages/cli-tools/test/standalone_backend_test.dart +0 -315
- package/dart-src/plugins/aws/lib/src/aws_lambda_edge_storage.dart +0 -72
- package/dart-src/plugins/aws/test/aws_lambda_edge_storage_test.dart +0 -190
- package/dart-src/plugins/standalone/.gitkeep +0 -0
- package/dart-src/plugins/standalone/lib/flutter_patcher_standalone.dart +0 -12
- package/dart-src/plugins/standalone/lib/src/standalone_config.dart +0 -107
- package/dart-src/plugins/standalone/lib/src/standalone_database.dart +0 -233
- package/dart-src/plugins/standalone/lib/src/standalone_storage.dart +0 -175
- package/dart-src/plugins/standalone/pubspec.yaml +0 -18
- package/dart-src/plugins/standalone/test/standalone_test.dart +0 -264
- /package/dart-src/plugins/cloudflare/lib/{flutter_patcher_cloudflare.dart → flutter_ota_kit_cloudflare.dart} +0 -0
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import 'dart:convert';
|
|
2
2
|
import 'dart:io';
|
|
3
3
|
|
|
4
|
-
import 'package:
|
|
4
|
+
import 'package:flutter_ota_kit_aws/flutter_ota_kit_aws.dart';
|
|
5
5
|
import 'package:postgres/postgres.dart';
|
|
6
|
-
import 'package:
|
|
7
|
-
import 'package:
|
|
8
|
-
|
|
9
|
-
import 'package:
|
|
6
|
+
import 'package:flutter_ota_kit_cloudflare/flutter_ota_kit_cloudflare.dart';
|
|
7
|
+
import 'package:flutter_ota_kit_postgres/flutter_ota_kit_postgres.dart';
|
|
8
|
+
|
|
9
|
+
import 'package:flutter_ota_kit_supabase/flutter_ota_kit_supabase.dart';
|
|
10
10
|
import 'package:path/path.dart' as p;
|
|
11
11
|
|
|
12
|
-
/// Resolved configuration for the
|
|
12
|
+
/// Resolved configuration for the flutter_ota_kit CLI.
|
|
13
13
|
///
|
|
14
14
|
/// Mirrors hot-updater's config precedence: explicit flags > environment
|
|
15
|
-
/// variables > the project `.
|
|
16
|
-
/// `~/.
|
|
15
|
+
/// variables > the project `.otat/config.json` (falling back to the global
|
|
16
|
+
/// `~/.otat/config.json`). All CLI working state lives under `.otat/` so it can
|
|
17
|
+
/// be git-ignored as a single unit.
|
|
17
18
|
class FlutterPatcherConfig {
|
|
18
19
|
FlutterPatcherConfig({
|
|
19
20
|
required this.provider,
|
|
@@ -21,7 +22,6 @@ class FlutterPatcherConfig {
|
|
|
21
22
|
this.postgres = const PostgresConfigJson(),
|
|
22
23
|
this.cloudflare = const CloudflareConfigJson(),
|
|
23
24
|
this.aws = const AwsConfigJson(),
|
|
24
|
-
this.standalone = const StandaloneConfigJson(),
|
|
25
25
|
this.channel = 'production',
|
|
26
26
|
this.platform = 'android',
|
|
27
27
|
this.source = './dist',
|
|
@@ -43,9 +43,6 @@ class FlutterPatcherConfig {
|
|
|
43
43
|
aws: AwsConfigJson.fromJson(
|
|
44
44
|
(json['aws'] as Map?)?.cast<String, dynamic>() ?? {},
|
|
45
45
|
),
|
|
46
|
-
standalone: StandaloneConfigJson.fromJson(
|
|
47
|
-
(json['standalone'] as Map?)?.cast<String, dynamic>() ?? {},
|
|
48
|
-
),
|
|
49
46
|
channel: json['channel'] as String? ?? 'production',
|
|
50
47
|
platform: json['platform'] as String? ?? 'android',
|
|
51
48
|
source: json['source'] as String? ?? './dist',
|
|
@@ -57,7 +54,6 @@ class FlutterPatcherConfig {
|
|
|
57
54
|
final PostgresConfigJson postgres;
|
|
58
55
|
final CloudflareConfigJson cloudflare;
|
|
59
56
|
final AwsConfigJson aws;
|
|
60
|
-
final StandaloneConfigJson standalone;
|
|
61
57
|
final String channel;
|
|
62
58
|
final String platform;
|
|
63
59
|
final String source;
|
|
@@ -69,7 +65,6 @@ class FlutterPatcherConfig {
|
|
|
69
65
|
'postgres': postgres.toJson(),
|
|
70
66
|
'cloudflare': cloudflare.toJson(),
|
|
71
67
|
'aws': aws.toJson(),
|
|
72
|
-
'standalone': standalone.toJson(),
|
|
73
68
|
'channel': channel,
|
|
74
69
|
'platform': platform,
|
|
75
70
|
'source': source,
|
|
@@ -82,7 +77,6 @@ class FlutterPatcherConfig {
|
|
|
82
77
|
PostgresConfigJson? postgres,
|
|
83
78
|
CloudflareConfigJson? cloudflare,
|
|
84
79
|
AwsConfigJson? aws,
|
|
85
|
-
StandaloneConfigJson? standalone,
|
|
86
80
|
String? channel,
|
|
87
81
|
String? platform,
|
|
88
82
|
String? source,
|
|
@@ -94,7 +88,6 @@ class FlutterPatcherConfig {
|
|
|
94
88
|
postgres: postgres ?? this.postgres,
|
|
95
89
|
cloudflare: cloudflare ?? this.cloudflare,
|
|
96
90
|
aws: aws ?? this.aws,
|
|
97
|
-
standalone: standalone ?? this.standalone,
|
|
98
91
|
channel: channel ?? this.channel,
|
|
99
92
|
platform: platform ?? this.platform,
|
|
100
93
|
source: source ?? this.source,
|
|
@@ -110,6 +103,8 @@ class SupabaseConfigJson {
|
|
|
110
103
|
this.anonKey,
|
|
111
104
|
this.bucket,
|
|
112
105
|
this.basePath,
|
|
106
|
+
this.managementKey,
|
|
107
|
+
this.databaseUrl,
|
|
113
108
|
});
|
|
114
109
|
|
|
115
110
|
factory SupabaseConfigJson.fromJson(Map<String, dynamic> json) =>
|
|
@@ -119,6 +114,8 @@ class SupabaseConfigJson {
|
|
|
119
114
|
anonKey: json['anonKey'] as String?,
|
|
120
115
|
bucket: json['bucket'] as String?,
|
|
121
116
|
basePath: json['basePath'] as String?,
|
|
117
|
+
managementKey: json['managementKey'] as String?,
|
|
118
|
+
databaseUrl: json['databaseUrl'] as String?,
|
|
122
119
|
);
|
|
123
120
|
|
|
124
121
|
final String? url;
|
|
@@ -126,6 +123,8 @@ class SupabaseConfigJson {
|
|
|
126
123
|
final String? anonKey;
|
|
127
124
|
final String? bucket;
|
|
128
125
|
final String? basePath;
|
|
126
|
+
final String? managementKey;
|
|
127
|
+
final String? databaseUrl;
|
|
129
128
|
|
|
130
129
|
Map<String, dynamic> toJson() => {
|
|
131
130
|
if (url != null) 'url': url,
|
|
@@ -133,16 +132,20 @@ class SupabaseConfigJson {
|
|
|
133
132
|
if (anonKey != null) 'anonKey': anonKey,
|
|
134
133
|
if (bucket != null) 'bucket': bucket,
|
|
135
134
|
if (basePath != null) 'basePath': basePath,
|
|
135
|
+
if (managementKey != null) 'managementKey': managementKey,
|
|
136
|
+
if (databaseUrl != null) 'databaseUrl': databaseUrl,
|
|
136
137
|
};
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
/// Search order for a config file on disk.
|
|
140
|
+
/// Search order for a config file on disk. Both the project config and the
|
|
141
|
+
/// global config live under a `.otat/` directory so all CLI state is grouped
|
|
142
|
+
/// in one place (and can be git-ignored together).
|
|
140
143
|
List<File> configCandidates() {
|
|
141
|
-
final cwd = File(p.join(Directory.current.path, '.
|
|
144
|
+
final cwd = File(p.join(Directory.current.path, '.otat', 'config.json'));
|
|
142
145
|
final home = Platform.environment['HOME'] ??
|
|
143
146
|
Platform.environment['USERPROFILE'] ??
|
|
144
147
|
'';
|
|
145
|
-
final global = File(p.join(home, '.
|
|
148
|
+
final global = File(p.join(home, '.otat', 'config.json'));
|
|
146
149
|
return [cwd, global];
|
|
147
150
|
}
|
|
148
151
|
|
|
@@ -162,7 +165,7 @@ FlutterPatcherConfig? loadConfig() {
|
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
/// Persist a config. By default writes the project file (cwd); pass
|
|
165
|
-
/// [global: true] to write the global `~/.
|
|
168
|
+
/// [global: true] to write the global `~/.flutter_ota_kit/config.json`.
|
|
166
169
|
void saveConfig(FlutterPatcherConfig config, {bool global = false}) {
|
|
167
170
|
final file = global ? configCandidates().last : configCandidates().first;
|
|
168
171
|
file.parent.createSync(recursive: true);
|
|
@@ -228,7 +231,7 @@ SupabaseServiceRoleConfig resolveSupabaseConfig(
|
|
|
228
231
|
if (resolvedUrl == null || resolvedUrl.isEmpty) {
|
|
229
232
|
throw StateError(
|
|
230
233
|
'Supabase URL is required. Set it via --url, SUPABASE_URL, or '
|
|
231
|
-
'`
|
|
234
|
+
'`flutter_ota_kit config set supabase.url <url>`.',
|
|
232
235
|
);
|
|
233
236
|
}
|
|
234
237
|
|
|
@@ -368,7 +371,7 @@ PostgresConfig resolvePostgresDatabaseConfig(
|
|
|
368
371
|
if (resolvedHost == null || resolvedHost.isEmpty) {
|
|
369
372
|
throw StateError(
|
|
370
373
|
'Postgres host is required. Set --pg-host, POSTGRES_HOST, or '
|
|
371
|
-
'`
|
|
374
|
+
'`flutter_ota_kit config set postgres.host <host>`.',
|
|
372
375
|
);
|
|
373
376
|
}
|
|
374
377
|
|
|
@@ -485,7 +488,7 @@ D1DatabaseConfig resolveCloudflareDatabaseConfig(
|
|
|
485
488
|
resolvedToken == null) {
|
|
486
489
|
throw StateError(
|
|
487
490
|
'Cloudflare requires accountId, d1DatabaseId and apiToken. Set them via '
|
|
488
|
-
'flags, CLOUDFLARE_* env vars, or `
|
|
491
|
+
'flags, CLOUDFLARE_* env vars, or `flutter_ota_kit config set cloudflare.*`.',
|
|
489
492
|
);
|
|
490
493
|
}
|
|
491
494
|
|
|
@@ -526,7 +529,7 @@ R2S3StorageConfig resolveCloudflareStorageConfig(
|
|
|
526
529
|
resolvedSecret == null) {
|
|
527
530
|
throw StateError(
|
|
528
531
|
'Cloudflare R2 requires accountId, accessKeyId and secretAccessKey. Set '
|
|
529
|
-
'them via flags, R2_* env vars, or `
|
|
532
|
+
'them via flags, R2_* env vars, or `flutter_ota_kit config set cloudflare.*`.',
|
|
530
533
|
);
|
|
531
534
|
}
|
|
532
535
|
|
|
@@ -612,7 +615,7 @@ S3DatabaseConfig resolveAwsDatabaseConfig(
|
|
|
612
615
|
resolvedSecret == null) {
|
|
613
616
|
throw StateError(
|
|
614
617
|
'AWS requires bucket, region, accessKeyId and secretAccessKey. Set them '
|
|
615
|
-
'via flags, AWS_* env vars, or `
|
|
618
|
+
'via flags, AWS_* env vars, or `flutter_ota_kit config set aws.*`.',
|
|
616
619
|
);
|
|
617
620
|
}
|
|
618
621
|
|
|
@@ -656,7 +659,7 @@ AwsS3StorageConfig resolveAwsStorageConfig(
|
|
|
656
659
|
resolvedSecret == null) {
|
|
657
660
|
throw StateError(
|
|
658
661
|
'AWS requires bucket, region, accessKeyId and secretAccessKey. Set them '
|
|
659
|
-
'via flags, AWS_* env vars, or `
|
|
662
|
+
'via flags, AWS_* env vars, or `flutter_ota_kit config set aws.*`.',
|
|
660
663
|
);
|
|
661
664
|
}
|
|
662
665
|
|
|
@@ -671,55 +674,3 @@ AwsS3StorageConfig resolveAwsStorageConfig(
|
|
|
671
674
|
clientFactory: clientFactory ?? defaultAwsS3ClientFactory,
|
|
672
675
|
);
|
|
673
676
|
}
|
|
674
|
-
|
|
675
|
-
/// Standalone (self-hosted) section of [FlutterPatcherConfig].
|
|
676
|
-
class StandaloneConfigJson {
|
|
677
|
-
const StandaloneConfigJson({this.baseUrl});
|
|
678
|
-
|
|
679
|
-
factory StandaloneConfigJson.fromJson(Map<String, dynamic> json) =>
|
|
680
|
-
StandaloneConfigJson(baseUrl: json['baseUrl'] as String?);
|
|
681
|
-
|
|
682
|
-
final String? baseUrl;
|
|
683
|
-
|
|
684
|
-
Map<String, dynamic> toJson() =>
|
|
685
|
-
{if (baseUrl != null) 'baseUrl': baseUrl};
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
/// Resolve the standalone repository (database) config.
|
|
689
|
-
StandaloneRepositoryConfig resolveStandaloneDatabaseConfig(
|
|
690
|
-
FlutterPatcherConfig config, {
|
|
691
|
-
String? baseUrl,
|
|
692
|
-
StandaloneClientFactory? clientFactory,
|
|
693
|
-
}) {
|
|
694
|
-
final env = Platform.environment;
|
|
695
|
-
final resolvedBaseUrl =
|
|
696
|
-
baseUrl ?? env['STANDALONE_BASE_URL'] ?? config.standalone.baseUrl;
|
|
697
|
-
if (resolvedBaseUrl == null || resolvedBaseUrl.isEmpty) {
|
|
698
|
-
throw StateError(
|
|
699
|
-
'Standalone baseUrl is required. Set --base-url, STANDALONE_BASE_URL, '
|
|
700
|
-
'or `flutter_patcher config set standalone.baseUrl <url>`.',
|
|
701
|
-
);
|
|
702
|
-
}
|
|
703
|
-
return StandaloneRepositoryConfig(
|
|
704
|
-
baseUrl: resolvedBaseUrl,
|
|
705
|
-
clientFactory: clientFactory,
|
|
706
|
-
);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
/// Resolve the standalone storage config (shares the same self-hosted base).
|
|
710
|
-
StandaloneStorageConfig resolveStandaloneStorageConfig(
|
|
711
|
-
FlutterPatcherConfig config, {
|
|
712
|
-
String? baseUrl,
|
|
713
|
-
StandaloneClientFactory? clientFactory,
|
|
714
|
-
}) {
|
|
715
|
-
final db = resolveStandaloneDatabaseConfig(
|
|
716
|
-
config,
|
|
717
|
-
baseUrl: baseUrl,
|
|
718
|
-
clientFactory: clientFactory,
|
|
719
|
-
);
|
|
720
|
-
return StandaloneStorageConfig(
|
|
721
|
-
baseUrl: db.baseUrl,
|
|
722
|
-
commonHeaders: db.commonHeaders,
|
|
723
|
-
clientFactory: clientFactory,
|
|
724
|
-
);
|
|
725
|
-
}
|
|
@@ -2,8 +2,8 @@ import 'dart:convert';
|
|
|
2
2
|
import 'dart:io';
|
|
3
3
|
|
|
4
4
|
import 'package:crypto/crypto.dart';
|
|
5
|
-
import 'package:
|
|
6
|
-
import 'package:
|
|
5
|
+
import 'package:flutter_ota_kit_core/flutter_ota_kit_core.dart';
|
|
6
|
+
import 'package:flutter_ota_kit_plugin_core/flutter_ota_kit_plugin_core.dart';
|
|
7
7
|
|
|
8
8
|
import 'backend.dart';
|
|
9
9
|
import 'sign.dart';
|
|
@@ -61,7 +61,7 @@ Future<Bundle> deployBundle(Backend backend, DeployOptions opts) async {
|
|
|
61
61
|
);
|
|
62
62
|
}
|
|
63
63
|
// Resolve the artifact to upload. If the source is already a zip (e.g. the
|
|
64
|
-
// `patch.zip` produced by `
|
|
64
|
+
// `patch.zip` produced by `flutter_ota_kit build`/`pack`), upload it directly.
|
|
65
65
|
// Otherwise, if the source directory contains a `patch.zip`, use that. As a
|
|
66
66
|
// fallback, zip the source directory (for pre-built bundle layouts).
|
|
67
67
|
final sourceFile = File(opts.source);
|
|
@@ -93,7 +93,7 @@ Future<Bundle> deployBundle(Backend backend, DeployOptions opts) async {
|
|
|
93
93
|
: null;
|
|
94
94
|
|
|
95
95
|
final bundleId = opts.bundleId ?? uuidV7();
|
|
96
|
-
final key =
|
|
96
|
+
final key = bundleId;
|
|
97
97
|
final uploaded = await backend.storage.upload(key, zipPath);
|
|
98
98
|
final storageUri = uploaded['storageUri'];
|
|
99
99
|
if (storageUri == null || storageUri.isEmpty) {
|
|
@@ -33,7 +33,7 @@ class PackResult {
|
|
|
33
33
|
/// Pack a release APK into a device-compatible OTA patch (`patch.zip`).
|
|
34
34
|
///
|
|
35
35
|
/// Mirrors the standalone `bin/pack.dart` CLI, but as a library usable by the
|
|
36
|
-
/// `
|
|
36
|
+
/// `flutter_ota_kit build` command. Includes **every** ABI found in the APK so
|
|
37
37
|
/// a single bundle serves all device ABIs (the device SDK picks its ABI).
|
|
38
38
|
Future<PackResult> packPatch({
|
|
39
39
|
required String apkPath,
|
|
@@ -73,6 +73,7 @@ Future<PackResult> packPatch({
|
|
|
73
73
|
version: version,
|
|
74
74
|
targetVersionCode: targetVersionCode,
|
|
75
75
|
requestedAssets: requestedAssets,
|
|
76
|
+
abi: abi,
|
|
76
77
|
onProgress: onProgress,
|
|
77
78
|
);
|
|
78
79
|
return result;
|
|
@@ -85,8 +86,13 @@ PackResult _writePatchPackage({
|
|
|
85
86
|
required String version,
|
|
86
87
|
required int targetVersionCode,
|
|
87
88
|
required List<String> requestedAssets,
|
|
89
|
+
String? abi,
|
|
88
90
|
void Function(int done, int total, String label)? onProgress,
|
|
89
91
|
}) {
|
|
92
|
+
// When an ABI is requested, restrict the patch to only that ABI instead of
|
|
93
|
+
// shipping every architecture found in the APK.
|
|
94
|
+
final effectiveLibs =
|
|
95
|
+
abi == null ? libs : libs.where((e) => e.$1 == abi).toList();
|
|
90
96
|
final patchFiles = <String, _PatchAssetFile>{};
|
|
91
97
|
final operations = <Map<String, dynamic>>[];
|
|
92
98
|
var baseManifestSize = 0;
|
|
@@ -144,7 +150,7 @@ PackResult _writePatchPackage({
|
|
|
144
150
|
}
|
|
145
151
|
|
|
146
152
|
final libManifest = <String, dynamic>{
|
|
147
|
-
for (final (currentAbi, soBytes) in
|
|
153
|
+
for (final (currentAbi, soBytes) in effectiveLibs) currentAbi: {
|
|
148
154
|
'path': 'lib/$currentAbi/libapp.so',
|
|
149
155
|
'md5': md5.convert(soBytes).toString(),
|
|
150
156
|
},
|
|
@@ -172,10 +178,10 @@ PackResult _writePatchPackage({
|
|
|
172
178
|
|
|
173
179
|
final package = Archive()
|
|
174
180
|
..addFile(_jsonArchiveFile('manifest.json', zipManifest));
|
|
175
|
-
final progressTotal =
|
|
181
|
+
final progressTotal = effectiveLibs.length + 1;
|
|
176
182
|
onProgress?.call(0, progressTotal, 'libapp.so');
|
|
177
183
|
var progressDone = 0;
|
|
178
|
-
for (final (currentAbi, soBytes) in
|
|
184
|
+
for (final (currentAbi, soBytes) in effectiveLibs) {
|
|
179
185
|
package.addFile(
|
|
180
186
|
ArchiveFile('lib/$currentAbi/libapp.so', soBytes.length, soBytes));
|
|
181
187
|
progressDone++;
|
|
@@ -210,7 +216,7 @@ PackResult _writePatchPackage({
|
|
|
210
216
|
'version': version,
|
|
211
217
|
'md5': payloadMd5,
|
|
212
218
|
'targetVersionCode': targetVersionCode,
|
|
213
|
-
'abis':
|
|
219
|
+
'abis': effectiveLibs.map((e) => e.$1).toList(),
|
|
214
220
|
'payload': 'patch.zip',
|
|
215
221
|
};
|
|
216
222
|
_writeJson(File('${outDir.path}/manifest.json'), outerManifest);
|
|
@@ -218,7 +224,7 @@ PackResult _writePatchPackage({
|
|
|
218
224
|
return PackResult(
|
|
219
225
|
payloadPath: outZip.path,
|
|
220
226
|
manifestPath: '${outDir.path}/manifest.json',
|
|
221
|
-
abis:
|
|
227
|
+
abis: effectiveLibs.map((e) => e.$1).toList(),
|
|
222
228
|
md5: payloadMd5,
|
|
223
229
|
version: version,
|
|
224
230
|
targetVersionCode: targetVersionCode,
|
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
import 'dart:io';
|
|
2
2
|
|
|
3
|
+
import 'package:args/args.dart';
|
|
3
4
|
import 'package:args/command_runner.dart';
|
|
4
5
|
import 'ui/ui.dart';
|
|
6
|
+
import 'verbose.dart';
|
|
5
7
|
|
|
6
8
|
/// Custom [CommandRunner] that renders a clean, colorized top-level help.
|
|
7
9
|
class FlutterPatcherRunner extends CommandRunner<int> {
|
|
8
|
-
FlutterPatcherRunner(
|
|
9
|
-
|
|
10
|
+
FlutterPatcherRunner(super.name, super.description) {
|
|
11
|
+
argParser.addFlag(
|
|
12
|
+
'verbose',
|
|
13
|
+
abbr: 'v',
|
|
14
|
+
help: 'Show full error stack traces.',
|
|
15
|
+
negatable: false,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@override
|
|
20
|
+
Future<int?> runCommand(ArgResults results) {
|
|
21
|
+
verboseMode = results['verbose'] as bool? ?? false;
|
|
22
|
+
return super.runCommand(results);
|
|
23
|
+
}
|
|
10
24
|
|
|
11
25
|
@override
|
|
12
26
|
String get usage {
|
|
13
27
|
final names = commands.keys.toList()..sort();
|
|
14
28
|
var maxName = 0;
|
|
15
|
-
for (final n in names)
|
|
29
|
+
for (final n in names) {
|
|
30
|
+
maxName = maxName < n.length ? n.length : maxName;
|
|
31
|
+
}
|
|
16
32
|
final buf = StringBuffer();
|
|
17
33
|
buf.writeln(' ${cyan('▶')} ${cyan(bold(executableName))} '
|
|
18
34
|
'${dim('·')} ${bold(description)}');
|
|
@@ -28,6 +44,9 @@ class FlutterPatcherRunner extends CommandRunner<int> {
|
|
|
28
44
|
}
|
|
29
45
|
buf.writeln('');
|
|
30
46
|
buf.writeln(' ${dim('Run "$executableName help <command>" for more about a command.')}');
|
|
47
|
+
buf.writeln('');
|
|
48
|
+
buf.writeln(' ${bold('GLOBAL OPTIONS')}');
|
|
49
|
+
buf.writeln(' ${cyan('-v, --verbose')} ${dim('Show full error stack traces.')}');
|
|
31
50
|
return buf.toString();
|
|
32
51
|
}
|
|
33
52
|
|
|
@@ -47,13 +66,13 @@ String _fullName(Command<int> command) {
|
|
|
47
66
|
return parts.join(' ');
|
|
48
67
|
}
|
|
49
68
|
|
|
50
|
-
/// Base class for
|
|
51
|
-
/// per-command help (used by `
|
|
69
|
+
/// Base class for flutter_ota_kit commands; renders a clean, colorized
|
|
70
|
+
/// per-command help (used by `flutter_ota_kit <cmd> --help`).
|
|
52
71
|
abstract class FlutterPatcherCommand extends Command<int> {
|
|
53
72
|
@override
|
|
54
73
|
String get usage {
|
|
55
74
|
final buf = StringBuffer();
|
|
56
|
-
buf.writeln(' ${cyan(bold(name))} ${dim('·')} $
|
|
75
|
+
buf.writeln(' ${cyan(bold(name))} ${dim('·')} $description');
|
|
57
76
|
buf.writeln('');
|
|
58
77
|
buf.writeln(' ${bold('USAGE')}');
|
|
59
78
|
buf.writeln(' ${_fullName(this)} [arguments]');
|
|
@@ -4,7 +4,7 @@ import 'dart:async';
|
|
|
4
4
|
import 'dart:io';
|
|
5
5
|
import 'dart:math' as math;
|
|
6
6
|
|
|
7
|
-
/// Terminal UI toolkit for the
|
|
7
|
+
/// Terminal UI toolkit for the flutter_ota_kit CLI: colored text, banners,
|
|
8
8
|
/// bordered boxes, step logs, dynamic spinners and progress bars.
|
|
9
9
|
///
|
|
10
10
|
/// Everything degrades gracefully to plain text when stdout is not a TTY or the
|
|
@@ -61,7 +61,7 @@ String kv(String k, String v) {
|
|
|
61
61
|
|
|
62
62
|
void banner(String name) {
|
|
63
63
|
_echo('');
|
|
64
|
-
_echo(' ${cyan('▶')} ${cyan(bold('
|
|
64
|
+
_echo(' ${cyan('▶')} ${cyan(bold('flutter_ota_kit'))} ${dim('·')} ${bold(name)}');
|
|
65
65
|
_echo(' ${dim('─' * math.min(_cols - 4, 60))}');
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -173,7 +173,9 @@ void box(String title, List<String> lines) {
|
|
|
173
173
|
final width = _boxWidth(lines, title);
|
|
174
174
|
final inner = width - 4;
|
|
175
175
|
final wrapped = <String>[];
|
|
176
|
-
for (final l in lines)
|
|
176
|
+
for (final l in lines) {
|
|
177
|
+
wrapped.addAll(_wrap(l, inner));
|
|
178
|
+
}
|
|
177
179
|
|
|
178
180
|
final titleText = ' $title ';
|
|
179
181
|
final tVisible = _dispWidth(titleText);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# AWS migrations
|
|
2
|
+
|
|
3
|
+
The AWS backend stores bundle metadata as JSON in S3 (blob database), not in a
|
|
4
|
+
relational database. There are **no SQL migrations** to apply — the bucket and
|
|
5
|
+
object prefix are created on first `deploy`.
|
|
6
|
+
|
|
7
|
+
If you front downloads with CloudFront, provision the distribution separately.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
-- HotUpdater.bundles
|
|
2
|
+
|
|
3
|
+
CREATE TYPE platforms AS ENUM ('ios', 'android');
|
|
4
|
+
|
|
5
|
+
CREATE TABLE bundles (
|
|
6
|
+
id uuid PRIMARY KEY,
|
|
7
|
+
platform platforms NOT NULL,
|
|
8
|
+
should_force_update boolean NOT NULL,
|
|
9
|
+
enabled boolean NOT NULL,
|
|
10
|
+
file_hash text NOT NULL,
|
|
11
|
+
git_commit_hash text,
|
|
12
|
+
message text,
|
|
13
|
+
channel text NOT NULL DEFAULT 'production',
|
|
14
|
+
storage_uri text NOT NULL,
|
|
15
|
+
target_app_version text,
|
|
16
|
+
fingerprint_hash text,
|
|
17
|
+
CONSTRAINT check_version_or_fingerprint CHECK (
|
|
18
|
+
(target_app_version IS NOT NULL) OR (fingerprint_hash IS NOT NULL)
|
|
19
|
+
),
|
|
20
|
+
metadata jsonb DEFAULT '{}'::jsonb,
|
|
21
|
+
manifest_storage_uri text,
|
|
22
|
+
manifest_file_hash text,
|
|
23
|
+
asset_base_storage_uri text,
|
|
24
|
+
rollout_cohort_count INTEGER DEFAULT 1000
|
|
25
|
+
CHECK (rollout_cohort_count >= 0 AND rollout_cohort_count <= 1000),
|
|
26
|
+
target_cohorts TEXT[]
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE TABLE bundle_patches (
|
|
30
|
+
id text PRIMARY KEY,
|
|
31
|
+
bundle_id uuid NOT NULL REFERENCES bundles(id) ON DELETE CASCADE,
|
|
32
|
+
base_bundle_id uuid NOT NULL REFERENCES bundles(id) ON DELETE CASCADE,
|
|
33
|
+
base_file_hash text NOT NULL,
|
|
34
|
+
patch_file_hash text NOT NULL,
|
|
35
|
+
patch_storage_uri text NOT NULL,
|
|
36
|
+
order_index integer NOT NULL DEFAULT 0
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
CREATE INDEX bundles_target_app_version_idx ON bundles(target_app_version);
|
|
40
|
+
CREATE INDEX bundles_fingerprint_hash_idx ON bundles(fingerprint_hash);
|
|
41
|
+
CREATE INDEX bundles_channel_idx ON bundles(channel);
|
|
42
|
+
CREATE INDEX bundles_rollout_idx ON bundles(rollout_cohort_count);
|
|
43
|
+
CREATE INDEX bundles_target_cohorts_idx ON bundles USING GIN (target_cohorts);
|
|
44
|
+
CREATE INDEX bundle_patches_bundle_id_idx ON bundle_patches(bundle_id);
|
|
45
|
+
CREATE INDEX bundle_patches_base_bundle_id_idx ON bundle_patches(base_bundle_id);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- HotUpdater.get_target_app_version_list
|
|
2
|
+
|
|
3
|
+
CREATE OR REPLACE FUNCTION get_target_app_version_list (
|
|
4
|
+
app_platform platforms,
|
|
5
|
+
min_bundle_id uuid
|
|
6
|
+
)
|
|
7
|
+
RETURNS TABLE (
|
|
8
|
+
target_app_version text
|
|
9
|
+
)
|
|
10
|
+
LANGUAGE plpgsql
|
|
11
|
+
AS
|
|
12
|
+
$$
|
|
13
|
+
BEGIN
|
|
14
|
+
RETURN QUERY
|
|
15
|
+
SELECT b.target_app_version
|
|
16
|
+
FROM bundles b
|
|
17
|
+
WHERE b.platform = app_platform
|
|
18
|
+
AND b.id >= min_bundle_id
|
|
19
|
+
GROUP BY b.target_app_version;
|
|
20
|
+
END;
|
|
21
|
+
$$;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
-- HotUpdater.get_update_info_by_app_version
|
|
2
|
+
|
|
3
|
+
DROP FUNCTION IF EXISTS get_update_info_by_app_version;
|
|
4
|
+
|
|
5
|
+
CREATE OR REPLACE FUNCTION get_update_info_by_app_version (
|
|
6
|
+
app_platform platforms,
|
|
7
|
+
app_version text,
|
|
8
|
+
bundle_id uuid,
|
|
9
|
+
min_bundle_id uuid,
|
|
10
|
+
target_channel text,
|
|
11
|
+
target_app_version_list text[],
|
|
12
|
+
cohort TEXT DEFAULT NULL
|
|
13
|
+
)
|
|
14
|
+
RETURNS TABLE (
|
|
15
|
+
id uuid,
|
|
16
|
+
should_force_update boolean,
|
|
17
|
+
message text,
|
|
18
|
+
status text,
|
|
19
|
+
storage_uri text,
|
|
20
|
+
file_hash text
|
|
21
|
+
)
|
|
22
|
+
LANGUAGE plpgsql
|
|
23
|
+
AS
|
|
24
|
+
$$
|
|
25
|
+
DECLARE
|
|
26
|
+
NIL_UUID CONSTANT uuid := '00000000-0000-0000-0000-000000000000';
|
|
27
|
+
BEGIN
|
|
28
|
+
RETURN QUERY
|
|
29
|
+
WITH candidate_bundles AS (
|
|
30
|
+
SELECT
|
|
31
|
+
b.id,
|
|
32
|
+
b.should_force_update,
|
|
33
|
+
b.message,
|
|
34
|
+
b.storage_uri,
|
|
35
|
+
b.file_hash,
|
|
36
|
+
b.rollout_cohort_count,
|
|
37
|
+
b.target_cohorts
|
|
38
|
+
FROM bundles b
|
|
39
|
+
WHERE b.enabled = TRUE
|
|
40
|
+
AND b.platform = app_platform
|
|
41
|
+
AND b.id >= min_bundle_id
|
|
42
|
+
AND b.target_app_version IN (SELECT unnest(target_app_version_list))
|
|
43
|
+
AND b.channel = target_channel
|
|
44
|
+
),
|
|
45
|
+
current_candidate AS (
|
|
46
|
+
SELECT
|
|
47
|
+
cb.id,
|
|
48
|
+
is_cohort_eligible(
|
|
49
|
+
cb.id,
|
|
50
|
+
cohort,
|
|
51
|
+
cb.rollout_cohort_count,
|
|
52
|
+
cb.target_cohorts
|
|
53
|
+
) AS is_eligible
|
|
54
|
+
FROM candidate_bundles cb
|
|
55
|
+
WHERE cb.id = bundle_id
|
|
56
|
+
LIMIT 1
|
|
57
|
+
),
|
|
58
|
+
eligible_update_candidate AS (
|
|
59
|
+
SELECT
|
|
60
|
+
cb.id,
|
|
61
|
+
cb.should_force_update,
|
|
62
|
+
cb.message,
|
|
63
|
+
'UPDATE' AS status,
|
|
64
|
+
cb.storage_uri,
|
|
65
|
+
cb.file_hash
|
|
66
|
+
FROM candidate_bundles cb
|
|
67
|
+
WHERE cb.id > bundle_id
|
|
68
|
+
AND is_cohort_eligible(
|
|
69
|
+
cb.id,
|
|
70
|
+
cohort,
|
|
71
|
+
cb.rollout_cohort_count,
|
|
72
|
+
cb.target_cohorts
|
|
73
|
+
)
|
|
74
|
+
ORDER BY cb.id DESC
|
|
75
|
+
LIMIT 1
|
|
76
|
+
),
|
|
77
|
+
rollback_candidate AS (
|
|
78
|
+
SELECT
|
|
79
|
+
cb.id,
|
|
80
|
+
TRUE AS should_force_update,
|
|
81
|
+
cb.message,
|
|
82
|
+
'ROLLBACK' AS status,
|
|
83
|
+
cb.storage_uri,
|
|
84
|
+
cb.file_hash
|
|
85
|
+
FROM candidate_bundles cb
|
|
86
|
+
WHERE cb.id < bundle_id
|
|
87
|
+
AND NOT EXISTS (
|
|
88
|
+
SELECT 1
|
|
89
|
+
FROM current_candidate
|
|
90
|
+
WHERE current_candidate.is_eligible = TRUE
|
|
91
|
+
)
|
|
92
|
+
AND NOT EXISTS (SELECT 1 FROM eligible_update_candidate)
|
|
93
|
+
ORDER BY cb.id DESC
|
|
94
|
+
LIMIT 1
|
|
95
|
+
),
|
|
96
|
+
final_result AS (
|
|
97
|
+
SELECT * FROM eligible_update_candidate
|
|
98
|
+
UNION ALL
|
|
99
|
+
SELECT * FROM rollback_candidate
|
|
100
|
+
WHERE NOT EXISTS (SELECT 1 FROM eligible_update_candidate)
|
|
101
|
+
)
|
|
102
|
+
SELECT *
|
|
103
|
+
FROM final_result
|
|
104
|
+
WHERE final_result.id != bundle_id
|
|
105
|
+
|
|
106
|
+
UNION ALL
|
|
107
|
+
|
|
108
|
+
SELECT
|
|
109
|
+
NIL_UUID AS id,
|
|
110
|
+
TRUE AS should_force_update,
|
|
111
|
+
NULL AS message,
|
|
112
|
+
'ROLLBACK' AS status,
|
|
113
|
+
NULL AS storage_uri,
|
|
114
|
+
NULL AS file_hash
|
|
115
|
+
WHERE (SELECT COUNT(*) FROM final_result) = 0
|
|
116
|
+
AND bundle_id != NIL_UUID
|
|
117
|
+
AND bundle_id > min_bundle_id
|
|
118
|
+
AND NOT EXISTS (
|
|
119
|
+
SELECT 1
|
|
120
|
+
FROM current_candidate
|
|
121
|
+
WHERE current_candidate.is_eligible = TRUE
|
|
122
|
+
)
|
|
123
|
+
AND NOT EXISTS (SELECT 1 FROM eligible_update_candidate)
|
|
124
|
+
AND NOT EXISTS (SELECT 1 FROM rollback_candidate);
|
|
125
|
+
END;
|
|
126
|
+
$$;
|