@_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,86 @@
1
+ import 'dart:convert';
2
+ import 'dart:io';
3
+ import 'dart:math';
4
+
5
+ import 'package:archive/archive.dart';
6
+ import 'package:crypto/crypto.dart';
7
+ import 'package:path/path.dart' as p;
8
+
9
+ /// Hex-encoded SHA-256 of [bytes].
10
+ String sha256Hex(List<int> bytes) => sha256.convert(bytes).toString();
11
+
12
+ /// Zip [directory] into a temp file and return its path.
13
+ ///
14
+ /// File paths inside the archive are relative to [directory], matching
15
+ /// hot-updater's bundle layout.
16
+ Future<String> zipDirectory(String directory) async {
17
+ final dir = Directory(directory);
18
+ if (!dir.existsSync()) {
19
+ throw StateError('Source directory does not exist: $directory');
20
+ }
21
+ final archive = Archive();
22
+ await for (final entity in dir.list(recursive: true)) {
23
+ if (entity is! File) continue;
24
+ final relative = p.relative(entity.path, from: dir.path);
25
+ final bytes = await entity.readAsBytes();
26
+ archive.addFile(ArchiveFile(relative, bytes.length, bytes));
27
+ }
28
+ if (archive.files.isEmpty) {
29
+ throw StateError('Source directory is empty: $directory');
30
+ }
31
+ final encoder = ZipEncoder();
32
+ final out = encoder.encode(archive);
33
+ final tmp = File('${Directory.systemTemp.path}/fp_${Random().nextInt(1 << 30)}.zip');
34
+ await tmp.writeAsBytes(out);
35
+ return tmp.path;
36
+ }
37
+
38
+ /// Compute a deterministic fingerprint hash for a directory (used at build time
39
+ /// to stamp the app's runtime fingerprint).
40
+ ///
41
+ /// The hash covers every file's relative path + content hash, sorted for
42
+ /// stability, so identical trees always yield the same fingerprint.
43
+ String generateFingerprint(String directory) {
44
+ final dir = Directory(directory);
45
+ if (!dir.existsSync()) {
46
+ throw StateError('Directory does not exist: $directory');
47
+ }
48
+ final entries = <String>[];
49
+ for (final entity in dir.listSync(recursive: true)) {
50
+ if (entity is! File) continue;
51
+ final relative = p.relative(entity.path, from: dir.path);
52
+ final content = File(entity.path).readAsBytesSync();
53
+ final hash = sha256Hex(content);
54
+ entries.add('$relative:$hash');
55
+ }
56
+ entries.sort();
57
+ return sha256Hex(utf8.encode(entries.join('\n')));
58
+ }
59
+
60
+ /// Best-effort git commit hash for a directory.
61
+ Future<String?> gitCommitHash(String directory) async {
62
+ try {
63
+ final result = await Process.run(
64
+ 'git',
65
+ ['rev-parse', 'HEAD'],
66
+ workingDirectory: directory,
67
+ runInShell: true,
68
+ );
69
+ if (result.exitCode == 0) {
70
+ return (result.stdout as String).trim();
71
+ }
72
+ } catch (_) {
73
+ // git unavailable — ignore.
74
+ }
75
+ return null;
76
+ }
77
+
78
+ /// Human readable byte size.
79
+ String humanSize(int bytes) {
80
+ if (bytes < 1024) return '$bytes B';
81
+ final kb = bytes / 1024;
82
+ if (kb < 1024) return '${kb.toStringAsFixed(1)} KB';
83
+ final mb = kb / 1024;
84
+ if (mb < 1024) return '${mb.toStringAsFixed(1)} MB';
85
+ return '${(mb / 1024).toStringAsFixed(2)} GB';
86
+ }
@@ -0,0 +1,16 @@
1
+ -- Migration number: 0001 2025-01-27T08:37:50.374Z
2
+ -- HotUpdater.bundles
3
+
4
+ CREATE TABLE bundles (
5
+ id TEXT PRIMARY KEY,
6
+ platform TEXT NOT NULL CHECK (platform IN ('ios', 'android')),
7
+ target_app_version TEXT NOT NULL,
8
+ should_force_update INTEGER NOT NULL CHECK (should_force_update IN (0, 1)),
9
+ enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
10
+ file_url TEXT NOT NULL,
11
+ file_hash TEXT NOT NULL,
12
+ git_commit_hash TEXT,
13
+ message TEXT
14
+ );
15
+
16
+ CREATE INDEX bundles_target_app_version_idx ON bundles(target_app_version);
@@ -0,0 +1,8 @@
1
+ -- Migration number: 0002 2025-03-07T16:25:12.486Z
2
+ -- HotUpdater.bundles
3
+
4
+ ALTER TABLE bundles
5
+ ADD COLUMN channel TEXT NOT NULL DEFAULT 'production';
6
+
7
+ ALTER TABLE bundles
8
+ DROP COLUMN file_url;
@@ -0,0 +1,38 @@
1
+ -- Migration number: 0003 2025-05-18T16:25:12.486Z
2
+ -- HotUpdater.bundles
3
+
4
+ ALTER TABLE bundles ADD COLUMN fingerprint_hash TEXT;
5
+ ALTER TABLE bundles ADD COLUMN storage_uri TEXT;
6
+ ALTER TABLE bundles ADD COLUMN metadata JSONB DEFAULT '{}';
7
+
8
+ UPDATE bundles
9
+ SET storage_uri = 'r2://%%BUCKET_NAME%%/' || id || '/bundle.zip'
10
+ WHERE storage_uri IS NULL;
11
+
12
+ CREATE TABLE bundles_temp (
13
+ id TEXT PRIMARY KEY,
14
+ platform TEXT NOT NULL,
15
+ should_force_update INTEGER NOT NULL,
16
+ enabled INTEGER NOT NULL,
17
+ file_hash TEXT NOT NULL,
18
+ git_commit_hash TEXT,
19
+ message TEXT,
20
+ channel TEXT NOT NULL DEFAULT 'production',
21
+ storage_uri TEXT NOT NULL,
22
+ target_app_version TEXT,
23
+ fingerprint_hash TEXT,
24
+ metadata JSONB DEFAULT '{}',
25
+ CHECK ((target_app_version IS NOT NULL) OR (fingerprint_hash IS NOT NULL))
26
+ );
27
+
28
+ INSERT INTO bundles_temp
29
+ SELECT id, platform, should_force_update, enabled, file_hash, git_commit_hash, message, channel, storage_uri, target_app_version, fingerprint_hash, metadata
30
+ FROM bundles;
31
+
32
+ DROP TABLE bundles;
33
+
34
+ ALTER TABLE bundles_temp RENAME TO bundles;
35
+
36
+ CREATE INDEX bundles_target_app_version_idx ON bundles(target_app_version);
37
+ CREATE INDEX bundles_fingerprint_hash_idx ON bundles(fingerprint_hash);
38
+ CREATE INDEX bundles_channel_idx ON bundles(channel);
@@ -0,0 +1,11 @@
1
+ -- Migration number: 0004 2025-12-21T00:00:00.000Z
2
+
3
+ -- HotUpdater.bundles
4
+
5
+ ALTER TABLE bundles ADD COLUMN rollout_cohort_count INTEGER DEFAULT 1000
6
+ CHECK (rollout_cohort_count >= 0 AND rollout_cohort_count <= 1000);
7
+
8
+ CREATE INDEX IF NOT EXISTS bundles_rollout_idx ON bundles(rollout_cohort_count);
9
+
10
+ -- SQLite doesn't have array type, store as JSON text
11
+ ALTER TABLE bundles ADD COLUMN target_cohorts TEXT;
@@ -0,0 +1,24 @@
1
+ -- Migration number: 0005 2026-04-22T00:00:00.000Z
2
+
3
+ -- HotUpdater.bundle_artifact_columns
4
+
5
+ ALTER TABLE bundles ADD COLUMN manifest_storage_uri TEXT;
6
+ ALTER TABLE bundles ADD COLUMN manifest_file_hash TEXT;
7
+ ALTER TABLE bundles ADD COLUMN asset_base_storage_uri TEXT;
8
+
9
+ CREATE TABLE bundle_patches (
10
+ id TEXT PRIMARY KEY,
11
+ bundle_id TEXT NOT NULL,
12
+ base_bundle_id TEXT NOT NULL,
13
+ base_file_hash TEXT NOT NULL,
14
+ patch_file_hash TEXT NOT NULL,
15
+ patch_storage_uri TEXT NOT NULL,
16
+ order_index INTEGER NOT NULL DEFAULT 0,
17
+ FOREIGN KEY (bundle_id) REFERENCES bundles(id) ON DELETE CASCADE,
18
+ FOREIGN KEY (base_bundle_id) REFERENCES bundles(id) ON DELETE CASCADE
19
+ );
20
+
21
+ CREATE INDEX IF NOT EXISTS bundle_patches_bundle_id_idx
22
+ ON bundle_patches(bundle_id);
23
+ CREATE INDEX IF NOT EXISTS bundle_patches_base_bundle_id_idx
24
+ ON bundle_patches(base_bundle_id);
@@ -0,0 +1,42 @@
1
+ -- HotUpdater.bundles
2
+
3
+ CREATE TABLE bundles (
4
+ id TEXT PRIMARY KEY,
5
+ platform TEXT NOT NULL CHECK (platform IN ('ios', 'android')),
6
+ target_app_version TEXT,
7
+ should_force_update INTEGER NOT NULL CHECK (should_force_update IN (0, 1)),
8
+ enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
9
+ file_hash TEXT NOT NULL,
10
+ git_commit_hash TEXT,
11
+ message TEXT,
12
+ channel TEXT NOT NULL DEFAULT 'production',
13
+ storage_uri TEXT NOT NULL,
14
+ fingerprint_hash TEXT,
15
+ metadata JSONB DEFAULT '{}',
16
+ manifest_storage_uri TEXT,
17
+ manifest_file_hash TEXT,
18
+ asset_base_storage_uri TEXT,
19
+ rollout_cohort_count INTEGER DEFAULT 1000
20
+ CHECK (rollout_cohort_count >= 0 AND rollout_cohort_count <= 1000),
21
+ target_cohorts TEXT,
22
+ CHECK ((target_app_version IS NOT NULL) OR (fingerprint_hash IS NOT NULL))
23
+ );
24
+
25
+ CREATE TABLE bundle_patches (
26
+ id TEXT PRIMARY KEY,
27
+ bundle_id TEXT NOT NULL,
28
+ base_bundle_id TEXT NOT NULL,
29
+ base_file_hash TEXT NOT NULL,
30
+ patch_file_hash TEXT NOT NULL,
31
+ patch_storage_uri TEXT NOT NULL,
32
+ order_index INTEGER NOT NULL DEFAULT 0,
33
+ FOREIGN KEY (bundle_id) REFERENCES bundles(id) ON DELETE CASCADE,
34
+ FOREIGN KEY (base_bundle_id) REFERENCES bundles(id) ON DELETE CASCADE
35
+ );
36
+
37
+ CREATE INDEX bundles_target_app_version_idx ON bundles(target_app_version);
38
+ CREATE INDEX bundles_fingerprint_hash_idx ON bundles(fingerprint_hash);
39
+ CREATE INDEX bundles_channel_idx ON bundles(channel);
40
+ CREATE INDEX bundles_rollout_idx ON bundles(rollout_cohort_count);
41
+ CREATE INDEX bundle_patches_bundle_id_idx ON bundle_patches(bundle_id);
42
+ CREATE INDEX bundle_patches_base_bundle_id_idx ON bundle_patches(base_bundle_id);
@@ -0,0 +1,228 @@
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
+ target_app_version text NOT NULL,
9
+ should_force_update boolean NOT NULL,
10
+ enabled boolean NOT NULL,
11
+ file_url text NOT NULL,
12
+ file_hash text NOT NULL,
13
+ git_commit_hash text,
14
+ message text
15
+ );
16
+
17
+ CREATE INDEX bundles_target_app_version_idx ON bundles(target_app_version);
18
+
19
+
20
+ -- HotUpdater.get_update_info
21
+
22
+ CREATE OR REPLACE FUNCTION get_update_info (
23
+ app_platform platforms,
24
+ app_version text,
25
+ bundle_id uuid
26
+ )
27
+ RETURNS TABLE (
28
+ id uuid,
29
+ should_force_update boolean,
30
+ file_url text,
31
+ file_hash text,
32
+ status text
33
+ )
34
+ LANGUAGE plpgsql
35
+ AS
36
+ $$
37
+ DECLARE
38
+ NIL_UUID CONSTANT uuid := '00000000-0000-0000-0000-000000000000';
39
+ BEGIN
40
+ RETURN QUERY
41
+ WITH rollback_candidate AS (
42
+ SELECT
43
+ b.id,
44
+ -- If status is 'ROLLBACK', should_force_update is always TRUE
45
+ TRUE AS should_force_update,
46
+ b.file_url,
47
+ b.file_hash,
48
+ 'ROLLBACK' AS status
49
+ FROM bundles b
50
+ WHERE b.enabled = TRUE
51
+ AND b.platform = app_platform
52
+ AND b.id < bundle_id
53
+ ORDER BY b.id DESC
54
+ LIMIT 1
55
+ ),
56
+ update_candidate AS (
57
+ SELECT
58
+ b.id,
59
+ b.should_force_update,
60
+ b.file_url,
61
+ b.file_hash,
62
+ 'UPDATE' AS status
63
+ FROM bundles b
64
+ WHERE b.enabled = TRUE
65
+ AND b.platform = app_platform
66
+ AND b.id >= bundle_id
67
+ AND semver_satisfies(b.target_app_version, app_version)
68
+ ORDER BY b.id DESC
69
+ LIMIT 1
70
+ ),
71
+ final_result AS (
72
+ SELECT *
73
+ FROM update_candidate
74
+
75
+ UNION ALL
76
+
77
+ SELECT *
78
+ FROM rollback_candidate
79
+ WHERE NOT EXISTS (SELECT 1 FROM update_candidate)
80
+ )
81
+ SELECT *
82
+ FROM final_result WHERE final_result.id != bundle_id
83
+
84
+ UNION ALL
85
+ /*
86
+ When there are no final results and bundle_id != NIL_UUID,
87
+ add one fallback row.
88
+ This fallback row is also ROLLBACK so shouldForceUpdate = TRUE.
89
+ */
90
+ SELECT
91
+ NIL_UUID AS id,
92
+ TRUE AS should_force_update, -- Always TRUE
93
+ NULL AS file_url,
94
+ NULL AS file_hash,
95
+ 'ROLLBACK' AS status
96
+ WHERE (SELECT COUNT(*) FROM final_result) = 0
97
+ AND bundle_id != NIL_UUID;
98
+
99
+ END;
100
+ $$;
101
+
102
+ -- HotUpdater.semver_satisfies
103
+
104
+ CREATE OR REPLACE FUNCTION semver_satisfies(range_expression TEXT, version TEXT)
105
+ RETURNS BOOLEAN AS $$
106
+ DECLARE
107
+ version_parts TEXT[];
108
+ version_major INT;
109
+ version_minor INT;
110
+ version_patch INT;
111
+ satisfies BOOLEAN := FALSE;
112
+ BEGIN
113
+ -- Split the version into major, minor, and patch
114
+ version_parts := string_to_array(version, '.');
115
+ version_major := version_parts[1]::INT;
116
+ version_minor := version_parts[2]::INT;
117
+ version_patch := version_parts[3]::INT;
118
+
119
+ -- Parse range expression and evaluate
120
+ IF range_expression ~ '^\d+\.\d+\.\d+$' THEN
121
+ -- Exact match
122
+ satisfies := (range_expression = version);
123
+
124
+ ELSIF range_expression = '*' THEN
125
+ -- Matches any version
126
+ satisfies := TRUE;
127
+
128
+ ELSIF range_expression ~ '^\d+\.x\.x$' THEN
129
+ -- Matches major.x.x
130
+ DECLARE
131
+ major_range INT := split_part(range_expression, '.', 1)::INT;
132
+ BEGIN
133
+ satisfies := (version_major = major_range);
134
+ END;
135
+
136
+ ELSIF range_expression ~ '^\d+\.\d+\.x$' THEN
137
+ -- Matches major.minor.x
138
+ DECLARE
139
+ major_range INT := split_part(range_expression, '.', 1)::INT;
140
+ minor_range INT := split_part(range_expression, '.', 2)::INT;
141
+ BEGIN
142
+ satisfies := (version_major = major_range AND version_minor = minor_range);
143
+ END;
144
+
145
+ ELSIF range_expression ~ '^\d+\.\d+$' THEN
146
+ -- Matches major.minor
147
+ DECLARE
148
+ major_range INT := split_part(range_expression, '.', 1)::INT;
149
+ minor_range INT := split_part(range_expression, '.', 2)::INT;
150
+ BEGIN
151
+ satisfies := (version_major = major_range AND version_minor = minor_range);
152
+ END;
153
+
154
+ ELSIF range_expression ~ '^\d+\.\d+\.\d+ - \d+\.\d+\.\d+$' THEN
155
+ -- Matches range e.g., 1.2.3 - 1.2.7
156
+ DECLARE
157
+ lower_bound TEXT := split_part(range_expression, ' - ', 1);
158
+ upper_bound TEXT := split_part(range_expression, ' - ', 2);
159
+ BEGIN
160
+ satisfies := (version >= lower_bound AND version <= upper_bound);
161
+ END;
162
+
163
+ ELSIF range_expression ~ '^>=\d+\.\d+\.\d+ <\d+\.\d+\.\d+$' THEN
164
+ -- Matches range with inequalities
165
+ DECLARE
166
+ lower_bound TEXT := regexp_replace(range_expression, '>=([\d\.]+) <.*', '\1');
167
+ upper_bound TEXT := regexp_replace(range_expression, '.*<([\d\.]+)', '\1');
168
+ BEGIN
169
+ satisfies := (version >= lower_bound AND version < upper_bound);
170
+ END;
171
+
172
+ ELSIF range_expression ~ '^~\d+\.\d+\.\d+$' THEN
173
+ -- Matches ~1.2.3 (>=1.2.3 <1.3.0)
174
+ DECLARE
175
+ lower_bound TEXT := regexp_replace(range_expression, '~', '');
176
+ upper_bound_major INT := split_part(lower_bound, '.', 1)::INT;
177
+ upper_bound_minor INT := split_part(lower_bound, '.', 2)::INT + 1;
178
+ upper_bound TEXT := upper_bound_major || '.' || upper_bound_minor || '.0';
179
+ BEGIN
180
+ satisfies := (version >= lower_bound AND version < upper_bound);
181
+ END;
182
+
183
+ ELSIF range_expression ~ '^\^\d+\.\d+\.\d+$' THEN
184
+ -- Matches ^1.2.3 (>=1.2.3 <2.0.0)
185
+ DECLARE
186
+ lower_bound TEXT := regexp_replace(range_expression, '\^', '');
187
+ upper_bound_major INT := split_part(lower_bound, '.', 1)::INT + 1;
188
+ upper_bound TEXT := upper_bound_major || '.0.0';
189
+ BEGIN
190
+ satisfies := (version >= lower_bound AND version < upper_bound);
191
+ END;
192
+
193
+ -- [Added] 1) Single major version pattern '^(\d+)$'
194
+ ELSIF range_expression ~ '^\d+$' THEN
195
+ /*
196
+ e.g.) "1" is interpreted as (>=1.0.0 <2.0.0) in semver range
197
+ "2" would be interpreted as (>=2.0.0 <3.0.0)
198
+ */
199
+ DECLARE
200
+ major_range INT := range_expression::INT;
201
+ lower_bound TEXT := major_range || '.0.0';
202
+ upper_bound TEXT := (major_range + 1) || '.0.0';
203
+ BEGIN
204
+ satisfies := (version >= lower_bound AND version < upper_bound);
205
+ END;
206
+
207
+ -- [Added] 2) major.x pattern '^(\d+)\.x$'
208
+ ELSIF range_expression ~ '^\d+\.x$' THEN
209
+ /*
210
+ e.g.) "2.x" => as long as major=2 matches, any minor and patch is OK
211
+ effectively works like (>=2.0.0 <3.0.0)
212
+ */
213
+ DECLARE
214
+ major_range INT := split_part(range_expression, '.', 1)::INT;
215
+ lower_bound TEXT := major_range || '.0.0';
216
+ upper_bound TEXT := (major_range + 1) || '.0.0';
217
+ BEGIN
218
+ satisfies := (version >= lower_bound AND version < upper_bound);
219
+ END;
220
+
221
+ ELSE
222
+ RAISE EXCEPTION 'Unsupported range expression: %', range_expression;
223
+ END IF;
224
+
225
+ RETURN satisfies;
226
+ END;
227
+ $$ LANGUAGE plpgsql;
228
+
@@ -0,0 +1,134 @@
1
+ -- HotUpdater.semver_satisfies
2
+ DROP FUNCTION IF EXISTS semver_satisfies;
3
+
4
+ -- HotUpdater.get_update_info
5
+ DROP FUNCTION IF EXISTS get_update_info;
6
+
7
+ -- HotUpdater.get_update_info
8
+ CREATE OR REPLACE FUNCTION get_update_info (
9
+ app_platform platforms,
10
+ app_version text,
11
+ bundle_id uuid,
12
+ min_bundle_id uuid,
13
+ target_channel text,
14
+ target_app_version_list text[]
15
+ )
16
+ RETURNS TABLE (
17
+ id uuid,
18
+ should_force_update boolean,
19
+ message text,
20
+ status 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 update_candidate AS (
30
+ SELECT
31
+ b.id,
32
+ b.should_force_update,
33
+ b.message,
34
+ 'UPDATE' AS status
35
+ FROM bundles b
36
+ WHERE b.enabled = TRUE
37
+ AND b.platform = app_platform
38
+ AND b.id >= bundle_id
39
+ AND b.id > min_bundle_id
40
+ AND b.target_app_version IN (SELECT unnest(target_app_version_list))
41
+ AND b.channel = target_channel
42
+ ORDER BY b.id DESC
43
+ LIMIT 1
44
+ ),
45
+ rollback_candidate AS (
46
+ SELECT
47
+ b.id,
48
+ TRUE AS should_force_update,
49
+ b.message,
50
+ 'ROLLBACK' AS status
51
+ FROM bundles b
52
+ WHERE b.enabled = TRUE
53
+ AND b.platform = app_platform
54
+ AND b.id < bundle_id
55
+ AND b.id > min_bundle_id
56
+ ORDER BY b.id DESC
57
+ LIMIT 1
58
+ ),
59
+ final_result AS (
60
+ SELECT * FROM update_candidate
61
+ UNION ALL
62
+ SELECT * FROM rollback_candidate
63
+ WHERE NOT EXISTS (SELECT 1 FROM update_candidate)
64
+ )
65
+ SELECT *
66
+ FROM final_result
67
+ WHERE final_result.id != bundle_id
68
+
69
+ UNION ALL
70
+
71
+ SELECT
72
+ NIL_UUID AS id,
73
+ TRUE AS should_force_update,
74
+ NULL AS message,
75
+ 'ROLLBACK' AS status
76
+ WHERE (SELECT COUNT(*) FROM final_result) = 0
77
+ AND bundle_id != NIL_UUID
78
+ AND bundle_id > min_bundle_id
79
+ AND NOT EXISTS (
80
+ SELECT 1
81
+ FROM bundles b
82
+ WHERE b.id = bundle_id
83
+ AND b.enabled = TRUE
84
+ AND b.platform = app_platform
85
+ );
86
+ END;
87
+ $$;
88
+
89
+ -- HotUpdater.bundles
90
+ ALTER TABLE bundles
91
+ ADD COLUMN channel text NOT NULL DEFAULT 'production';
92
+
93
+ ALTER TABLE bundles
94
+ DROP COLUMN file_url;
95
+
96
+ -- HotUpdater.get_target_app_version_list
97
+
98
+ CREATE OR REPLACE FUNCTION get_target_app_version_list (
99
+ app_platform platforms,
100
+ min_bundle_id uuid
101
+ )
102
+ RETURNS TABLE (
103
+ target_app_version text
104
+ )
105
+ LANGUAGE plpgsql
106
+ AS
107
+ $$
108
+ BEGIN
109
+ RETURN QUERY
110
+ SELECT b.target_app_version
111
+ FROM bundles b
112
+ WHERE b.platform = app_platform
113
+ AND b.id >= min_bundle_id
114
+ GROUP BY b.target_app_version;
115
+ END;
116
+ $$;
117
+
118
+ -- HotUpdater.get_channels
119
+ CREATE OR REPLACE FUNCTION get_channels ()
120
+ RETURNS TABLE (
121
+ channel text
122
+ )
123
+ LANGUAGE plpgsql
124
+ AS
125
+ $$
126
+ BEGIN
127
+ RETURN QUERY
128
+ SELECT b.channel
129
+ FROM bundles b
130
+ GROUP BY b.channel;
131
+ END;
132
+ $$;
133
+
134
+ CREATE INDEX bundles_channel_idx ON bundles(channel);