@_nazmiforreal/flutter-ota 0.1.0
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 +38 -0
- package/bin/flutter-patcher-linux +0 -0
- package/bin/flutter-patcher.js +59 -0
- package/migrations/supabase/20250103114225_init.sql +228 -0
- package/migrations/supabase/20250314000000_hot-updater_0.13.0.sql +134 -0
- package/migrations/supabase/20250516000000_hot-updater_0.18.0.sql +194 -0
- package/migrations/supabase/20251014000000_hot-updater_0.21.0.sql +184 -0
- package/migrations/supabase/20260401000000_hot-updater_0.29.0.sql +503 -0
- package/migrations/supabase/20260414000000_hot-updater_0.30.0.sql +50 -0
- package/migrations/supabase/20260422000000_hot-updater_0.31.0.sql +20 -0
- package/migrations/supabase/20260520014100_hot-updater_rls.sql +48 -0
- package/package.json +25 -0
- package/scripts/postinstall.js +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# flutter-patcher (flutter-ota CLI)
|
|
2
|
+
|
|
3
|
+
Command-line interface for [flutter_patcher](https://github.com/HYPER12755/flutter_patcher) — a self-hosted OTA ("code push") platform for Flutter Android, hot-updater compatible.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i -g @_nazmiforreal/flutter-ota
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This installs the `flutter-patcher` command. A prebuilt Linux binary ships with the package; on other platforms it falls back to `dart compile exe` (requires the [Dart SDK](https://dart.dev)).
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
flutter-patcher --help
|
|
17
|
+
flutter-patcher keys # generate an Ed25519 keypair
|
|
18
|
+
flutter-patcher build -a app-release.apk \
|
|
19
|
+
--version 1.0.0 --target-version-code 1 # -> dist/patch.zip (all ABIs)
|
|
20
|
+
flutter-patcher deploy --source dist \
|
|
21
|
+
--channel production --backend standalone \
|
|
22
|
+
--key <PRIVATE_KEY_BASE64> # upload + sign a bundle
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Backends
|
|
26
|
+
|
|
27
|
+
`--backend supabase|postgres|cloudflare|aws|standalone` (or `FLUTTER_PATCHER_BACKEND`).
|
|
28
|
+
Each resolves its own env / `.flutter_patcher.json` config.
|
|
29
|
+
|
|
30
|
+
### Commands
|
|
31
|
+
|
|
32
|
+
`init`, `build`, `deploy`, `bundle` (list/delete/promote), `rollback`, `channel`
|
|
33
|
+
(get/set/list), `fingerprint`, `doctor`, `migrate`, `config`, `keys`, `console`,
|
|
34
|
+
`patch`, `mock_server`.
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
MIT
|
|
Binary file
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Launcher for the flutter_patcher CLI.
|
|
5
|
+
//
|
|
6
|
+
// Resolves the prebuilt Dart binary shipped next to this script (platform
|
|
7
|
+
// specific), falling back to `dart run` when Dart is available and no binary
|
|
8
|
+
// is present. This lets `npm i -g flutter-patcher` work on any machine that
|
|
9
|
+
// either ships a prebuilt binary or has the Dart SDK installed.
|
|
10
|
+
|
|
11
|
+
const { spawnSync } = require('child_process');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const platform =
|
|
16
|
+
process.platform === 'win32'
|
|
17
|
+
? 'windows'
|
|
18
|
+
: process.platform === 'darwin'
|
|
19
|
+
? 'macos'
|
|
20
|
+
: 'linux';
|
|
21
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
22
|
+
const binDir = __dirname;
|
|
23
|
+
const binName = `flutter-patcher-${platform}${ext}`;
|
|
24
|
+
const binPath = path.join(binDir, binName);
|
|
25
|
+
|
|
26
|
+
function run(bin, args) {
|
|
27
|
+
const res = spawnSync(bin, args, { stdio: 'inherit', windowsHide: false });
|
|
28
|
+
process.exit(res.status == null ? 1 : res.status);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (fs.existsSync(binPath)) {
|
|
32
|
+
run(binPath, process.argv.slice(2));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Fallback: run from source via the Dart SDK (works when this npm package is
|
|
37
|
+
// installed from the monorepo, since the Dart package lives at
|
|
38
|
+
// ../../packages/cli-tools/bin/flutter_patcher.dart).
|
|
39
|
+
const repoEntry = path.join(
|
|
40
|
+
binDir,
|
|
41
|
+
'..',
|
|
42
|
+
'..',
|
|
43
|
+
'..',
|
|
44
|
+
'packages',
|
|
45
|
+
'cli-tools',
|
|
46
|
+
'bin',
|
|
47
|
+
'flutter-patcher.dart',
|
|
48
|
+
);
|
|
49
|
+
const dartCheck = spawnSync('dart', ['--version'], { stdio: 'ignore' });
|
|
50
|
+
if (dartCheck.status === 0 && fs.existsSync(repoEntry)) {
|
|
51
|
+
run('dart', [repoEntry, ...process.argv.slice(2)]);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.error(
|
|
56
|
+
'flutter-patcher: no prebuilt binary for this platform and Dart SDK not ' +
|
|
57
|
+
'found. Install Dart (https://dart.dev) or ship a prebuilt binary.',
|
|
58
|
+
);
|
|
59
|
+
process.exit(1);
|
|
@@ -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);
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
|
|
2
|
+
ALTER TABLE bundles ADD COLUMN IF NOT EXISTS fingerprint_hash text;
|
|
3
|
+
ALTER TABLE bundles ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT '{}'::jsonb;
|
|
4
|
+
|
|
5
|
+
ALTER TABLE bundles ADD COLUMN IF NOT EXISTS storage_uri TEXT;
|
|
6
|
+
|
|
7
|
+
UPDATE bundles
|
|
8
|
+
SET storage_uri = 'supabase-storage://%%BUCKET_NAME%%/' || id || '/bundle.zip'
|
|
9
|
+
WHERE storage_uri IS NULL;
|
|
10
|
+
|
|
11
|
+
ALTER TABLE bundles ALTER COLUMN storage_uri SET NOT NULL;
|
|
12
|
+
ALTER TABLE bundles ALTER COLUMN target_app_version DROP NOT NULL;
|
|
13
|
+
|
|
14
|
+
ALTER TABLE bundles ADD CONSTRAINT check_version_or_fingerprint CHECK (
|
|
15
|
+
(target_app_version IS NOT NULL) OR (fingerprint_hash IS NOT NULL)
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE INDEX bundles_fingerprint_hash_idx ON bundles(fingerprint_hash);
|
|
19
|
+
|
|
20
|
+
DROP FUNCTION IF EXISTS get_update_info;
|
|
21
|
+
|
|
22
|
+
-- HotUpdater.get_update_info
|
|
23
|
+
CREATE OR REPLACE FUNCTION get_update_info_by_fingerprint_hash (
|
|
24
|
+
app_platform platforms,
|
|
25
|
+
bundle_id uuid,
|
|
26
|
+
min_bundle_id uuid,
|
|
27
|
+
target_channel text,
|
|
28
|
+
target_fingerprint_hash text
|
|
29
|
+
)
|
|
30
|
+
RETURNS TABLE (
|
|
31
|
+
id uuid,
|
|
32
|
+
should_force_update boolean,
|
|
33
|
+
message text,
|
|
34
|
+
status text,
|
|
35
|
+
storage_uri text
|
|
36
|
+
)
|
|
37
|
+
LANGUAGE plpgsql
|
|
38
|
+
AS
|
|
39
|
+
$$
|
|
40
|
+
DECLARE
|
|
41
|
+
NIL_UUID CONSTANT uuid := '00000000-0000-0000-0000-000000000000';
|
|
42
|
+
BEGIN
|
|
43
|
+
RETURN QUERY
|
|
44
|
+
WITH update_candidate AS (
|
|
45
|
+
SELECT
|
|
46
|
+
b.id,
|
|
47
|
+
b.should_force_update,
|
|
48
|
+
b.message,
|
|
49
|
+
'UPDATE' AS status,
|
|
50
|
+
b.storage_uri
|
|
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
|
+
AND b.channel = target_channel
|
|
57
|
+
AND b.fingerprint_hash = target_fingerprint_hash
|
|
58
|
+
ORDER BY b.id DESC
|
|
59
|
+
LIMIT 1
|
|
60
|
+
),
|
|
61
|
+
rollback_candidate AS (
|
|
62
|
+
SELECT
|
|
63
|
+
b.id,
|
|
64
|
+
TRUE AS should_force_update,
|
|
65
|
+
b.message,
|
|
66
|
+
'ROLLBACK' AS status,
|
|
67
|
+
b.storage_uri
|
|
68
|
+
FROM bundles b
|
|
69
|
+
WHERE b.enabled = TRUE
|
|
70
|
+
AND b.platform = app_platform
|
|
71
|
+
AND b.id < bundle_id
|
|
72
|
+
AND b.id > min_bundle_id
|
|
73
|
+
AND b.channel = target_channel
|
|
74
|
+
AND b.fingerprint_hash = target_fingerprint_hash
|
|
75
|
+
ORDER BY b.id DESC
|
|
76
|
+
LIMIT 1
|
|
77
|
+
),
|
|
78
|
+
final_result AS (
|
|
79
|
+
SELECT * FROM update_candidate
|
|
80
|
+
UNION ALL
|
|
81
|
+
SELECT * FROM rollback_candidate
|
|
82
|
+
WHERE NOT EXISTS (SELECT 1 FROM update_candidate)
|
|
83
|
+
)
|
|
84
|
+
SELECT *
|
|
85
|
+
FROM final_result
|
|
86
|
+
WHERE final_result.id != bundle_id
|
|
87
|
+
|
|
88
|
+
UNION ALL
|
|
89
|
+
|
|
90
|
+
SELECT
|
|
91
|
+
NIL_UUID AS id,
|
|
92
|
+
TRUE AS should_force_update,
|
|
93
|
+
NULL AS message,
|
|
94
|
+
'ROLLBACK' AS status,
|
|
95
|
+
NULL AS storage_uri
|
|
96
|
+
WHERE (SELECT COUNT(*) FROM final_result) = 0
|
|
97
|
+
AND bundle_id != NIL_UUID
|
|
98
|
+
AND bundle_id > min_bundle_id
|
|
99
|
+
AND NOT EXISTS (
|
|
100
|
+
SELECT 1
|
|
101
|
+
FROM bundles b
|
|
102
|
+
WHERE b.id = bundle_id
|
|
103
|
+
AND b.enabled = TRUE
|
|
104
|
+
AND b.platform = app_platform
|
|
105
|
+
);
|
|
106
|
+
END;
|
|
107
|
+
$$;
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
-- HotUpdater.get_update_info
|
|
111
|
+
CREATE OR REPLACE FUNCTION get_update_info_by_app_version (
|
|
112
|
+
app_platform platforms,
|
|
113
|
+
app_version text,
|
|
114
|
+
bundle_id uuid,
|
|
115
|
+
min_bundle_id uuid,
|
|
116
|
+
target_channel text,
|
|
117
|
+
target_app_version_list text[]
|
|
118
|
+
)
|
|
119
|
+
RETURNS TABLE (
|
|
120
|
+
id uuid,
|
|
121
|
+
should_force_update boolean,
|
|
122
|
+
message text,
|
|
123
|
+
status text,
|
|
124
|
+
storage_uri text
|
|
125
|
+
)
|
|
126
|
+
LANGUAGE plpgsql
|
|
127
|
+
AS
|
|
128
|
+
$$
|
|
129
|
+
DECLARE
|
|
130
|
+
NIL_UUID CONSTANT uuid := '00000000-0000-0000-0000-000000000000';
|
|
131
|
+
BEGIN
|
|
132
|
+
RETURN QUERY
|
|
133
|
+
WITH update_candidate AS (
|
|
134
|
+
SELECT
|
|
135
|
+
b.id,
|
|
136
|
+
b.should_force_update,
|
|
137
|
+
b.message,
|
|
138
|
+
'UPDATE' AS status,
|
|
139
|
+
b.storage_uri
|
|
140
|
+
FROM bundles b
|
|
141
|
+
WHERE b.enabled = TRUE
|
|
142
|
+
AND b.platform = app_platform
|
|
143
|
+
AND b.id >= bundle_id
|
|
144
|
+
AND b.id > min_bundle_id
|
|
145
|
+
AND b.target_app_version IN (SELECT unnest(target_app_version_list))
|
|
146
|
+
AND b.channel = target_channel
|
|
147
|
+
ORDER BY b.id DESC
|
|
148
|
+
LIMIT 1
|
|
149
|
+
),
|
|
150
|
+
rollback_candidate AS (
|
|
151
|
+
SELECT
|
|
152
|
+
b.id,
|
|
153
|
+
TRUE AS should_force_update,
|
|
154
|
+
b.message,
|
|
155
|
+
'ROLLBACK' AS status,
|
|
156
|
+
b.storage_uri
|
|
157
|
+
FROM bundles b
|
|
158
|
+
WHERE b.enabled = TRUE
|
|
159
|
+
AND b.platform = app_platform
|
|
160
|
+
AND b.id < bundle_id
|
|
161
|
+
AND b.id > min_bundle_id
|
|
162
|
+
ORDER BY b.id DESC
|
|
163
|
+
LIMIT 1
|
|
164
|
+
),
|
|
165
|
+
final_result AS (
|
|
166
|
+
SELECT * FROM update_candidate
|
|
167
|
+
UNION ALL
|
|
168
|
+
SELECT * FROM rollback_candidate
|
|
169
|
+
WHERE NOT EXISTS (SELECT 1 FROM update_candidate)
|
|
170
|
+
)
|
|
171
|
+
SELECT *
|
|
172
|
+
FROM final_result
|
|
173
|
+
WHERE final_result.id != bundle_id
|
|
174
|
+
|
|
175
|
+
UNION ALL
|
|
176
|
+
|
|
177
|
+
SELECT
|
|
178
|
+
NIL_UUID AS id,
|
|
179
|
+
TRUE AS should_force_update,
|
|
180
|
+
NULL AS message,
|
|
181
|
+
'ROLLBACK' AS status,
|
|
182
|
+
NULL AS storage_uri
|
|
183
|
+
WHERE (SELECT COUNT(*) FROM final_result) = 0
|
|
184
|
+
AND bundle_id != NIL_UUID
|
|
185
|
+
AND bundle_id > min_bundle_id
|
|
186
|
+
AND NOT EXISTS (
|
|
187
|
+
SELECT 1
|
|
188
|
+
FROM bundles b
|
|
189
|
+
WHERE b.id = bundle_id
|
|
190
|
+
AND b.enabled = TRUE
|
|
191
|
+
AND b.platform = app_platform
|
|
192
|
+
);
|
|
193
|
+
END;
|
|
194
|
+
$$;
|