@_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.
- package/bin/flutter-patcher.js +48 -39
- package/dart-src/packages/cli-tools/.gitkeep +0 -0
- package/dart-src/packages/cli-tools/bin/flutter_patcher.dart +9 -0
- package/dart-src/packages/cli-tools/lib/flutter_patcher_cli.dart +73 -0
- package/dart-src/packages/cli-tools/lib/src/backend.dart +162 -0
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +56 -0
- package/dart-src/packages/cli-tools/lib/src/commands/build.dart +99 -0
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +151 -0
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +140 -0
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +113 -0
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +55 -0
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +95 -0
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +48 -0
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +29 -0
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +82 -0
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +55 -0
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +132 -0
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +41 -0
- package/dart-src/packages/cli-tools/lib/src/config.dart +715 -0
- package/dart-src/packages/cli-tools/lib/src/operations.dart +206 -0
- package/dart-src/packages/cli-tools/lib/src/pack.dart +417 -0
- package/dart-src/packages/cli-tools/lib/src/sign.dart +60 -0
- package/dart-src/packages/cli-tools/lib/src/util.dart +86 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0001_hot-updater_init.sql +16 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0002_hot-updater_0.13.0.sql +8 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0003_hot-updater_0.18.0.sql +38 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0004_hot-updater_0.29.0.sql +11 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0005_hot-updater_0.31.0.sql +24 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/bundles.sql +42 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20250103114225_init.sql +228 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20250314000000_hot-updater_0.13.0.sql +134 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20250516000000_hot-updater_0.18.0.sql +194 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20251014000000_hot-updater_0.21.0.sql +184 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260401000000_hot-updater_0.29.0.sql +503 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260414000000_hot-updater_0.30.0.sql +50 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260422000000_hot-updater_0.31.0.sql +20 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260520014100_hot-updater_rls.sql +48 -0
- package/dart-src/packages/cli-tools/pubspec.yaml +37 -0
- package/dart-src/packages/core/lib/flutter_patcher_core.dart +29 -0
- package/dart-src/packages/core/lib/src/app_update_info.dart +111 -0
- package/dart-src/packages/core/lib/src/bundle.dart +191 -0
- package/dart-src/packages/core/lib/src/bundle_artifacts.dart +39 -0
- package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +47 -0
- package/dart-src/packages/core/lib/src/changed_asset.dart +77 -0
- package/dart-src/packages/core/lib/src/get_bundles_args.dart +78 -0
- package/dart-src/packages/core/lib/src/metadata.dart +31 -0
- package/dart-src/packages/core/lib/src/patch_info.dart +284 -0
- package/dart-src/packages/core/lib/src/platform.dart +15 -0
- package/dart-src/packages/core/lib/src/rollout.dart +209 -0
- package/dart-src/packages/core/lib/src/semver.dart +70 -0
- package/dart-src/packages/core/lib/src/semver_range.dart +358 -0
- package/dart-src/packages/core/lib/src/semver_version.dart +94 -0
- package/dart-src/packages/core/lib/src/status.dart +24 -0
- package/dart-src/packages/core/lib/src/strategy.dart +10 -0
- package/dart-src/packages/core/lib/src/update_bundle_params.dart +30 -0
- package/dart-src/packages/core/lib/src/uuid.dart +45 -0
- package/dart-src/packages/core/pubspec.yaml +9 -0
- package/dart-src/packages/core/tool/dbg.dart +6 -0
- package/dart-src/plugins/aws/.gitkeep +0 -0
- package/dart-src/plugins/aws/lib/flutter_patcher_aws.dart +12 -0
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +207 -0
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +130 -0
- package/dart-src/plugins/aws/lib/src/aws_config.dart +59 -0
- package/dart-src/plugins/aws/lib/src/aws_database.dart +198 -0
- package/dart-src/plugins/aws/lib/src/aws_lambda_edge_storage.dart +72 -0
- package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +312 -0
- package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +125 -0
- package/dart-src/plugins/aws/lib/src/aws_storage.dart +20 -0
- package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +123 -0
- package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +136 -0
- package/dart-src/plugins/aws/pubspec.yaml +22 -0
- package/dart-src/plugins/cloudflare/.gitkeep +0 -0
- package/dart-src/plugins/cloudflare/lib/flutter_patcher_cloudflare.dart +18 -0
- package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +29 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +93 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +131 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_client.dart +66 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_config.dart +30 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_database.dart +16 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +314 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +50 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +287 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +23 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +110 -0
- package/dart-src/plugins/cloudflare/migrations/0001_hot-updater_init.sql +16 -0
- package/dart-src/plugins/cloudflare/migrations/0002_hot-updater_0.13.0.sql +8 -0
- package/dart-src/plugins/cloudflare/migrations/0003_hot-updater_0.18.0.sql +38 -0
- package/dart-src/plugins/cloudflare/migrations/0004_hot-updater_0.29.0.sql +11 -0
- package/dart-src/plugins/cloudflare/migrations/0005_hot-updater_0.31.0.sql +24 -0
- package/dart-src/plugins/cloudflare/pubspec.yaml +20 -0
- package/dart-src/plugins/cloudflare/sql/bundles.sql +42 -0
- package/dart-src/plugins/plugin-core/lib/flutter_patcher_plugin_core.dart +42 -0
- package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +94 -0
- package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +44 -0
- package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +259 -0
- package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work_store.dart +20 -0
- package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +29 -0
- package/dart-src/plugins/plugin-core/lib/src/compression_format.dart +63 -0
- package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +22 -0
- package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +853 -0
- package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +525 -0
- package/dart-src/plugins/plugin-core/lib/src/create_storage_key_builder.dart +15 -0
- package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +130 -0
- package/dart-src/plugins/plugin-core/lib/src/filter_compatible_app_versions.dart +18 -0
- package/dart-src/plugins/plugin-core/lib/src/generate_min_bundle_id.dart +19 -0
- package/dart-src/plugins/plugin-core/lib/src/get_update_info.dart +241 -0
- package/dart-src/plugins/plugin-core/lib/src/is_object.dart +5 -0
- package/dart-src/plugins/plugin-core/lib/src/legacy_asset_storage_layout.dart +9 -0
- package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +112 -0
- package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +44 -0
- package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +70 -0
- package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +62 -0
- package/dart-src/plugins/plugin-core/lib/src/resolve_update_info_from_bundles.dart +43 -0
- package/dart-src/plugins/plugin-core/lib/src/storage_profile.dart +27 -0
- package/dart-src/plugins/plugin-core/lib/src/types.dart +454 -0
- package/dart-src/plugins/plugin-core/lib/src/uuidv7.dart +41 -0
- package/dart-src/plugins/plugin-core/pubspec.yaml +15 -0
- package/dart-src/plugins/postgres/.gitkeep +0 -0
- package/dart-src/plugins/postgres/lib/flutter_patcher_postgres.dart +17 -0
- package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +130 -0
- package/dart-src/plugins/postgres/lib/src/postgres_client.dart +90 -0
- package/dart-src/plugins/postgres/lib/src/postgres_config.dart +35 -0
- package/dart-src/plugins/postgres/lib/src/postgres_database.dart +302 -0
- package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +120 -0
- package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +205 -0
- package/dart-src/plugins/postgres/lib/src/postgres_types.dart +132 -0
- package/dart-src/plugins/postgres/pubspec.yaml +18 -0
- package/dart-src/plugins/postgres/sql/bundles.sql +45 -0
- package/dart-src/plugins/postgres/sql/get_target_app_version_list.sql +21 -0
- package/dart-src/plugins/postgres/sql/get_update_info.spec.ts +171 -0
- package/dart-src/plugins/postgres/sql/get_update_info_by_app_version.sql +126 -0
- package/dart-src/plugins/postgres/sql/get_update_info_by_fingerprint_hash.sql +125 -0
- package/dart-src/plugins/postgres/sql/hash_user_id.sql +30 -0
- package/dart-src/plugins/postgres/sql/is_device_eligible.sql +237 -0
- package/dart-src/plugins/postgres/sql/prepareSql.ts +11 -0
- package/dart-src/plugins/standalone/.gitkeep +0 -0
- package/dart-src/plugins/standalone/lib/flutter_patcher_standalone.dart +12 -0
- package/dart-src/plugins/standalone/lib/src/standalone_config.dart +107 -0
- package/dart-src/plugins/standalone/lib/src/standalone_database.dart +233 -0
- package/dart-src/plugins/standalone/lib/src/standalone_storage.dart +175 -0
- package/dart-src/plugins/standalone/pubspec.yaml +18 -0
- package/dart-src/plugins/supabase/lib/edge.dart +10 -0
- package/dart-src/plugins/supabase/lib/flutter_patcher_supabase.dart +21 -0
- package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +129 -0
- package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +154 -0
- package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +516 -0
- package/dart-src/plugins/supabase/lib/src/supabase_config.dart +34 -0
- package/dart-src/plugins/supabase/lib/src/supabase_database.dart +422 -0
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_database.dart +32 -0
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +99 -0
- package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +145 -0
- package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +298 -0
- package/dart-src/plugins/supabase/lib/src/types.dart +130 -0
- package/dart-src/plugins/supabase/pubspec.yaml +19 -0
- package/dart-src/plugins/supabase/supabase/edge-functions/index.ts +44 -0
- package/dart-src/plugins/supabase/supabase/edge-functions/runtime.docker.integration.spec.ts +968 -0
- package/dart-src/plugins/supabase/supabase/migrations/20250103114225_init.sql +228 -0
- package/dart-src/plugins/supabase/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +134 -0
- package/dart-src/plugins/supabase/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +194 -0
- package/dart-src/plugins/supabase/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +184 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +503 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260414000000_hot-updater_0.30.0.sql +50 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260422000000_hot-updater_0.31.0.sql +20 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260520014100_hot-updater_rls.sql +48 -0
- package/package.json +2 -1
- package/scripts/postinstall.js +53 -25
- /package/bin/{flutter-patcher-linux → flutter-patcher-linux-x64} +0 -0
|
@@ -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
|
+
$$;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
-- HotUpdater.get_update_info_by_fingerprint_hash
|
|
2
|
+
|
|
3
|
+
DROP FUNCTION IF EXISTS get_update_info_by_fingerprint_hash;
|
|
4
|
+
|
|
5
|
+
CREATE OR REPLACE FUNCTION get_update_info_by_fingerprint_hash (
|
|
6
|
+
app_platform platforms,
|
|
7
|
+
bundle_id uuid,
|
|
8
|
+
min_bundle_id uuid,
|
|
9
|
+
target_channel text,
|
|
10
|
+
target_fingerprint_hash text,
|
|
11
|
+
cohort TEXT DEFAULT NULL
|
|
12
|
+
)
|
|
13
|
+
RETURNS TABLE (
|
|
14
|
+
id uuid,
|
|
15
|
+
should_force_update boolean,
|
|
16
|
+
message text,
|
|
17
|
+
status text,
|
|
18
|
+
storage_uri text,
|
|
19
|
+
file_hash text
|
|
20
|
+
)
|
|
21
|
+
LANGUAGE plpgsql
|
|
22
|
+
AS
|
|
23
|
+
$$
|
|
24
|
+
DECLARE
|
|
25
|
+
NIL_UUID CONSTANT uuid := '00000000-0000-0000-0000-000000000000';
|
|
26
|
+
BEGIN
|
|
27
|
+
RETURN QUERY
|
|
28
|
+
WITH candidate_bundles AS (
|
|
29
|
+
SELECT
|
|
30
|
+
b.id,
|
|
31
|
+
b.should_force_update,
|
|
32
|
+
b.message,
|
|
33
|
+
b.storage_uri,
|
|
34
|
+
b.file_hash,
|
|
35
|
+
b.rollout_cohort_count,
|
|
36
|
+
b.target_cohorts
|
|
37
|
+
FROM bundles b
|
|
38
|
+
WHERE b.enabled = TRUE
|
|
39
|
+
AND b.platform = app_platform
|
|
40
|
+
AND b.id >= min_bundle_id
|
|
41
|
+
AND b.channel = target_channel
|
|
42
|
+
AND b.fingerprint_hash = target_fingerprint_hash
|
|
43
|
+
),
|
|
44
|
+
current_candidate AS (
|
|
45
|
+
SELECT
|
|
46
|
+
cb.id,
|
|
47
|
+
is_cohort_eligible(
|
|
48
|
+
cb.id,
|
|
49
|
+
cohort,
|
|
50
|
+
cb.rollout_cohort_count,
|
|
51
|
+
cb.target_cohorts
|
|
52
|
+
) AS is_eligible
|
|
53
|
+
FROM candidate_bundles cb
|
|
54
|
+
WHERE cb.id = bundle_id
|
|
55
|
+
LIMIT 1
|
|
56
|
+
),
|
|
57
|
+
eligible_update_candidate AS (
|
|
58
|
+
SELECT
|
|
59
|
+
cb.id,
|
|
60
|
+
cb.should_force_update,
|
|
61
|
+
cb.message,
|
|
62
|
+
'UPDATE' AS status,
|
|
63
|
+
cb.storage_uri,
|
|
64
|
+
cb.file_hash
|
|
65
|
+
FROM candidate_bundles cb
|
|
66
|
+
WHERE cb.id > bundle_id
|
|
67
|
+
AND is_cohort_eligible(
|
|
68
|
+
cb.id,
|
|
69
|
+
cohort,
|
|
70
|
+
cb.rollout_cohort_count,
|
|
71
|
+
cb.target_cohorts
|
|
72
|
+
)
|
|
73
|
+
ORDER BY cb.id DESC
|
|
74
|
+
LIMIT 1
|
|
75
|
+
),
|
|
76
|
+
rollback_candidate AS (
|
|
77
|
+
SELECT
|
|
78
|
+
cb.id,
|
|
79
|
+
TRUE AS should_force_update,
|
|
80
|
+
cb.message,
|
|
81
|
+
'ROLLBACK' AS status,
|
|
82
|
+
cb.storage_uri,
|
|
83
|
+
cb.file_hash
|
|
84
|
+
FROM candidate_bundles cb
|
|
85
|
+
WHERE cb.id < bundle_id
|
|
86
|
+
AND NOT EXISTS (
|
|
87
|
+
SELECT 1
|
|
88
|
+
FROM current_candidate
|
|
89
|
+
WHERE current_candidate.is_eligible = TRUE
|
|
90
|
+
)
|
|
91
|
+
AND NOT EXISTS (SELECT 1 FROM eligible_update_candidate)
|
|
92
|
+
ORDER BY cb.id DESC
|
|
93
|
+
LIMIT 1
|
|
94
|
+
),
|
|
95
|
+
final_result AS (
|
|
96
|
+
SELECT * FROM eligible_update_candidate
|
|
97
|
+
UNION ALL
|
|
98
|
+
SELECT * FROM rollback_candidate
|
|
99
|
+
WHERE NOT EXISTS (SELECT 1 FROM eligible_update_candidate)
|
|
100
|
+
)
|
|
101
|
+
SELECT *
|
|
102
|
+
FROM final_result
|
|
103
|
+
WHERE final_result.id != bundle_id
|
|
104
|
+
|
|
105
|
+
UNION ALL
|
|
106
|
+
|
|
107
|
+
SELECT
|
|
108
|
+
NIL_UUID AS id,
|
|
109
|
+
TRUE AS should_force_update,
|
|
110
|
+
NULL AS message,
|
|
111
|
+
'ROLLBACK' AS status,
|
|
112
|
+
NULL AS storage_uri,
|
|
113
|
+
NULL AS file_hash
|
|
114
|
+
WHERE (SELECT COUNT(*) FROM final_result) = 0
|
|
115
|
+
AND bundle_id != NIL_UUID
|
|
116
|
+
AND bundle_id > min_bundle_id
|
|
117
|
+
AND NOT EXISTS (
|
|
118
|
+
SELECT 1
|
|
119
|
+
FROM current_candidate
|
|
120
|
+
WHERE current_candidate.is_eligible = TRUE
|
|
121
|
+
)
|
|
122
|
+
AND NOT EXISTS (SELECT 1 FROM eligible_update_candidate)
|
|
123
|
+
AND NOT EXISTS (SELECT 1 FROM rollback_candidate);
|
|
124
|
+
END;
|
|
125
|
+
$$;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- Deterministic hash function matching JavaScript implementation
|
|
2
|
+
-- Returns hash value in range [0, 99]
|
|
3
|
+
CREATE OR REPLACE FUNCTION hash_user_id(user_id TEXT)
|
|
4
|
+
RETURNS INTEGER
|
|
5
|
+
LANGUAGE plpgsql
|
|
6
|
+
IMMUTABLE
|
|
7
|
+
AS $$
|
|
8
|
+
DECLARE
|
|
9
|
+
hash BIGINT := 0;
|
|
10
|
+
char_code INTEGER;
|
|
11
|
+
i INTEGER;
|
|
12
|
+
normalized BIGINT;
|
|
13
|
+
BEGIN
|
|
14
|
+
-- Replicate JavaScript hash algorithm
|
|
15
|
+
FOR i IN 1..length(user_id) LOOP
|
|
16
|
+
char_code := ascii(substring(user_id from i for 1));
|
|
17
|
+
hash := (hash * 31) + char_code;
|
|
18
|
+
|
|
19
|
+
-- Simulate JavaScript's |= 0 by wrapping into a signed 32-bit range.
|
|
20
|
+
normalized := mod(hash + 2147483648, 4294967296);
|
|
21
|
+
IF normalized < 0 THEN
|
|
22
|
+
normalized := normalized + 4294967296;
|
|
23
|
+
END IF;
|
|
24
|
+
hash := normalized - 2147483648;
|
|
25
|
+
END LOOP;
|
|
26
|
+
|
|
27
|
+
-- Return absolute value modulo 100
|
|
28
|
+
RETURN abs(hash % 100);
|
|
29
|
+
END;
|
|
30
|
+
$$;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
-- HotUpdater.is_cohort_eligible
|
|
2
|
+
-- Cohort eligibility helpers matching @hot-updater/core rollout.ts
|
|
3
|
+
|
|
4
|
+
CREATE OR REPLACE FUNCTION positive_mod(
|
|
5
|
+
value INTEGER,
|
|
6
|
+
modulus INTEGER
|
|
7
|
+
)
|
|
8
|
+
RETURNS INTEGER
|
|
9
|
+
LANGUAGE plpgsql
|
|
10
|
+
IMMUTABLE
|
|
11
|
+
AS $$
|
|
12
|
+
BEGIN
|
|
13
|
+
RETURN ((value % modulus) + modulus) % modulus;
|
|
14
|
+
END;
|
|
15
|
+
$$;
|
|
16
|
+
|
|
17
|
+
CREATE OR REPLACE FUNCTION hash_rollout_value(input TEXT)
|
|
18
|
+
RETURNS INTEGER
|
|
19
|
+
LANGUAGE plpgsql
|
|
20
|
+
IMMUTABLE
|
|
21
|
+
AS $$
|
|
22
|
+
DECLARE
|
|
23
|
+
hash_value NUMERIC := 0;
|
|
24
|
+
char_code INTEGER;
|
|
25
|
+
i INTEGER;
|
|
26
|
+
BEGIN
|
|
27
|
+
FOR i IN 1..length(input) LOOP
|
|
28
|
+
char_code := ascii(substring(input from i for 1));
|
|
29
|
+
hash_value := mod((hash_value * 31) + char_code, 4294967296);
|
|
30
|
+
END LOOP;
|
|
31
|
+
|
|
32
|
+
IF hash_value >= 2147483648 THEN
|
|
33
|
+
hash_value := hash_value - 4294967296;
|
|
34
|
+
END IF;
|
|
35
|
+
|
|
36
|
+
RETURN hash_value::INTEGER;
|
|
37
|
+
END;
|
|
38
|
+
$$;
|
|
39
|
+
|
|
40
|
+
CREATE OR REPLACE FUNCTION normalize_cohort_value(cohort TEXT)
|
|
41
|
+
RETURNS TEXT
|
|
42
|
+
LANGUAGE plpgsql
|
|
43
|
+
IMMUTABLE
|
|
44
|
+
AS $$
|
|
45
|
+
DECLARE
|
|
46
|
+
normalized TEXT;
|
|
47
|
+
cohort_value INTEGER;
|
|
48
|
+
BEGIN
|
|
49
|
+
IF cohort IS NULL THEN
|
|
50
|
+
RETURN NULL;
|
|
51
|
+
END IF;
|
|
52
|
+
|
|
53
|
+
normalized := lower(btrim(cohort));
|
|
54
|
+
|
|
55
|
+
IF normalized ~ '^[0-9]+$' THEN
|
|
56
|
+
cohort_value := normalized::INTEGER;
|
|
57
|
+
IF cohort_value BETWEEN 1 AND 1000 THEN
|
|
58
|
+
RETURN cohort_value::TEXT;
|
|
59
|
+
END IF;
|
|
60
|
+
END IF;
|
|
61
|
+
|
|
62
|
+
RETURN normalized;
|
|
63
|
+
END;
|
|
64
|
+
$$;
|
|
65
|
+
|
|
66
|
+
CREATE OR REPLACE FUNCTION gcd_int(a INTEGER, b INTEGER)
|
|
67
|
+
RETURNS INTEGER
|
|
68
|
+
LANGUAGE plpgsql
|
|
69
|
+
IMMUTABLE
|
|
70
|
+
AS $$
|
|
71
|
+
DECLARE
|
|
72
|
+
x INTEGER := abs(a);
|
|
73
|
+
y INTEGER := abs(b);
|
|
74
|
+
next_value INTEGER;
|
|
75
|
+
BEGIN
|
|
76
|
+
WHILE y <> 0 LOOP
|
|
77
|
+
next_value := x % y;
|
|
78
|
+
x := y;
|
|
79
|
+
y := next_value;
|
|
80
|
+
END LOOP;
|
|
81
|
+
|
|
82
|
+
RETURN x;
|
|
83
|
+
END;
|
|
84
|
+
$$;
|
|
85
|
+
|
|
86
|
+
CREATE OR REPLACE FUNCTION get_rollout_multiplier(bundle_id UUID)
|
|
87
|
+
RETURNS INTEGER
|
|
88
|
+
LANGUAGE plpgsql
|
|
89
|
+
IMMUTABLE
|
|
90
|
+
AS $$
|
|
91
|
+
DECLARE
|
|
92
|
+
candidate INTEGER := positive_mod(
|
|
93
|
+
hash_rollout_value(bundle_id::TEXT || ':multiplier'),
|
|
94
|
+
997
|
|
95
|
+
);
|
|
96
|
+
BEGIN
|
|
97
|
+
IF candidate = 0 THEN
|
|
98
|
+
candidate := 1;
|
|
99
|
+
END IF;
|
|
100
|
+
|
|
101
|
+
WHILE gcd_int(candidate, 1000) <> 1 LOOP
|
|
102
|
+
candidate := positive_mod(candidate + 1, 1000);
|
|
103
|
+
IF candidate = 0 THEN
|
|
104
|
+
candidate := 1;
|
|
105
|
+
END IF;
|
|
106
|
+
END LOOP;
|
|
107
|
+
|
|
108
|
+
RETURN candidate;
|
|
109
|
+
END;
|
|
110
|
+
$$;
|
|
111
|
+
|
|
112
|
+
CREATE OR REPLACE FUNCTION get_rollout_offset(bundle_id UUID)
|
|
113
|
+
RETURNS INTEGER
|
|
114
|
+
LANGUAGE plpgsql
|
|
115
|
+
IMMUTABLE
|
|
116
|
+
AS $$
|
|
117
|
+
BEGIN
|
|
118
|
+
RETURN positive_mod(hash_rollout_value(bundle_id::TEXT || ':offset'), 1000);
|
|
119
|
+
END;
|
|
120
|
+
$$;
|
|
121
|
+
|
|
122
|
+
CREATE OR REPLACE FUNCTION get_modular_inverse(value INTEGER, modulus INTEGER)
|
|
123
|
+
RETURNS INTEGER
|
|
124
|
+
LANGUAGE plpgsql
|
|
125
|
+
IMMUTABLE
|
|
126
|
+
AS $$
|
|
127
|
+
DECLARE
|
|
128
|
+
candidate INTEGER;
|
|
129
|
+
BEGIN
|
|
130
|
+
FOR candidate IN 1..(modulus - 1) LOOP
|
|
131
|
+
IF positive_mod(value * candidate, modulus) = 1 THEN
|
|
132
|
+
RETURN candidate;
|
|
133
|
+
END IF;
|
|
134
|
+
END LOOP;
|
|
135
|
+
|
|
136
|
+
RAISE EXCEPTION 'No modular inverse for % mod %', value, modulus;
|
|
137
|
+
END;
|
|
138
|
+
$$;
|
|
139
|
+
|
|
140
|
+
CREATE OR REPLACE FUNCTION is_numeric_cohort(cohort TEXT)
|
|
141
|
+
RETURNS BOOLEAN
|
|
142
|
+
LANGUAGE plpgsql
|
|
143
|
+
IMMUTABLE
|
|
144
|
+
AS $$
|
|
145
|
+
DECLARE
|
|
146
|
+
normalized_cohort TEXT := normalize_cohort_value(cohort);
|
|
147
|
+
cohort_value INTEGER;
|
|
148
|
+
BEGIN
|
|
149
|
+
IF normalized_cohort IS NULL OR normalized_cohort !~ '^[0-9]+$' THEN
|
|
150
|
+
RETURN FALSE;
|
|
151
|
+
END IF;
|
|
152
|
+
|
|
153
|
+
cohort_value := normalized_cohort::INTEGER;
|
|
154
|
+
RETURN cohort_value BETWEEN 1 AND 1000;
|
|
155
|
+
END;
|
|
156
|
+
$$;
|
|
157
|
+
|
|
158
|
+
CREATE OR REPLACE FUNCTION get_numeric_cohort_rollout_position(
|
|
159
|
+
bundle_id UUID,
|
|
160
|
+
cohort TEXT
|
|
161
|
+
)
|
|
162
|
+
RETURNS INTEGER
|
|
163
|
+
LANGUAGE plpgsql
|
|
164
|
+
IMMUTABLE
|
|
165
|
+
AS $$
|
|
166
|
+
DECLARE
|
|
167
|
+
normalized_cohort TEXT := normalize_cohort_value(cohort);
|
|
168
|
+
cohort_value INTEGER;
|
|
169
|
+
multiplier INTEGER;
|
|
170
|
+
offset_value INTEGER;
|
|
171
|
+
inverse_multiplier INTEGER;
|
|
172
|
+
BEGIN
|
|
173
|
+
IF NOT is_numeric_cohort(normalized_cohort) THEN
|
|
174
|
+
RAISE EXCEPTION 'Invalid numeric cohort: %', cohort;
|
|
175
|
+
END IF;
|
|
176
|
+
|
|
177
|
+
cohort_value := normalized_cohort::INTEGER - 1;
|
|
178
|
+
multiplier := get_rollout_multiplier(bundle_id);
|
|
179
|
+
offset_value := get_rollout_offset(bundle_id);
|
|
180
|
+
inverse_multiplier := get_modular_inverse(multiplier, 1000);
|
|
181
|
+
|
|
182
|
+
RETURN positive_mod(
|
|
183
|
+
inverse_multiplier * (cohort_value - offset_value),
|
|
184
|
+
1000
|
|
185
|
+
);
|
|
186
|
+
END;
|
|
187
|
+
$$;
|
|
188
|
+
|
|
189
|
+
CREATE OR REPLACE FUNCTION is_cohort_eligible(
|
|
190
|
+
bundle_id UUID,
|
|
191
|
+
cohort TEXT,
|
|
192
|
+
rollout_cohort_count INTEGER,
|
|
193
|
+
target_cohorts TEXT[]
|
|
194
|
+
)
|
|
195
|
+
RETURNS BOOLEAN
|
|
196
|
+
LANGUAGE plpgsql
|
|
197
|
+
IMMUTABLE
|
|
198
|
+
AS $$
|
|
199
|
+
DECLARE
|
|
200
|
+
normalized_cohort TEXT := normalize_cohort_value(cohort);
|
|
201
|
+
normalized_rollout_count INTEGER := COALESCE(rollout_cohort_count, 1000);
|
|
202
|
+
normalized_target_cohorts TEXT[];
|
|
203
|
+
BEGIN
|
|
204
|
+
IF target_cohorts IS NOT NULL THEN
|
|
205
|
+
normalized_target_cohorts := ARRAY(
|
|
206
|
+
SELECT normalize_cohort_value(value)
|
|
207
|
+
FROM unnest(target_cohorts) AS value
|
|
208
|
+
);
|
|
209
|
+
END IF;
|
|
210
|
+
|
|
211
|
+
IF normalized_target_cohorts IS NOT NULL
|
|
212
|
+
AND array_length(normalized_target_cohorts, 1) > 0
|
|
213
|
+
AND normalized_cohort IS NOT NULL
|
|
214
|
+
AND normalized_cohort = ANY(normalized_target_cohorts) THEN
|
|
215
|
+
RETURN TRUE;
|
|
216
|
+
END IF;
|
|
217
|
+
|
|
218
|
+
IF normalized_rollout_count <= 0 THEN
|
|
219
|
+
RETURN FALSE;
|
|
220
|
+
END IF;
|
|
221
|
+
|
|
222
|
+
IF normalized_cohort IS NULL THEN
|
|
223
|
+
RETURN normalized_rollout_count >= 1000;
|
|
224
|
+
END IF;
|
|
225
|
+
|
|
226
|
+
IF NOT is_numeric_cohort(normalized_cohort) THEN
|
|
227
|
+
RETURN FALSE;
|
|
228
|
+
END IF;
|
|
229
|
+
|
|
230
|
+
IF normalized_rollout_count >= 1000 THEN
|
|
231
|
+
RETURN TRUE;
|
|
232
|
+
END IF;
|
|
233
|
+
|
|
234
|
+
RETURN get_numeric_cohort_rollout_position(bundle_id, normalized_cohort)
|
|
235
|
+
< normalized_rollout_count;
|
|
236
|
+
END;
|
|
237
|
+
$$;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export const prepareSql = async () => {
|
|
5
|
+
const files = await fs.readdir(__dirname);
|
|
6
|
+
const sqlFiles = files.filter((file) => file.endsWith(".sql"));
|
|
7
|
+
const contents = await Promise.all(
|
|
8
|
+
sqlFiles.map((file) => fs.readFile(path.join(__dirname, file), "utf-8")),
|
|
9
|
+
);
|
|
10
|
+
return contents.join("\n");
|
|
11
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export 'src/standalone_config.dart'
|
|
2
|
+
show
|
|
3
|
+
StandaloneClientFactory,
|
|
4
|
+
StandaloneRepositoryConfig,
|
|
5
|
+
StandaloneRepositoryRoutes,
|
|
6
|
+
StandaloneStorageConfig,
|
|
7
|
+
StandaloneStorageRoutes,
|
|
8
|
+
buildPath,
|
|
9
|
+
joinUrl,
|
|
10
|
+
mergeHeaders;
|
|
11
|
+
export 'src/standalone_database.dart' show standaloneRepository;
|
|
12
|
+
export 'src/standalone_storage.dart' show standaloneStorage;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import 'dart:convert' show jsonEncode;
|
|
2
|
+
|
|
3
|
+
import 'package:http/http.dart' as http;
|
|
4
|
+
import 'package:http/http.dart';
|
|
5
|
+
|
|
6
|
+
/// A factory that returns the [http.Client] used by standalone plugins.
|
|
7
|
+
///
|
|
8
|
+
/// Injectable so tests can supply a mock client.
|
|
9
|
+
typedef StandaloneClientFactory = Client Function();
|
|
10
|
+
|
|
11
|
+
/// Configuration for the standalone repository (the database half of the
|
|
12
|
+
/// self-hosted backend).
|
|
13
|
+
class StandaloneRepositoryConfig {
|
|
14
|
+
const StandaloneRepositoryConfig({
|
|
15
|
+
required this.baseUrl,
|
|
16
|
+
this.commonHeaders,
|
|
17
|
+
this.routes,
|
|
18
|
+
this.clientFactory,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
final String baseUrl;
|
|
22
|
+
final Map<String, String>? commonHeaders;
|
|
23
|
+
final StandaloneRepositoryRoutes? routes;
|
|
24
|
+
final StandaloneClientFactory? clientFactory;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Overridable route templates for the repository REST contract.
|
|
28
|
+
class StandaloneRepositoryRoutes {
|
|
29
|
+
const StandaloneRepositoryRoutes({
|
|
30
|
+
this.create = '/api/bundles',
|
|
31
|
+
this.list = '/api/bundles',
|
|
32
|
+
this.channels = '/api/bundles/channels',
|
|
33
|
+
this.retrieve = '/api/bundles/:id',
|
|
34
|
+
this.update = '/api/bundles/:id',
|
|
35
|
+
this.delete = '/api/bundles/:id',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
final String create;
|
|
39
|
+
final String list;
|
|
40
|
+
final String channels;
|
|
41
|
+
final String retrieve;
|
|
42
|
+
final String update;
|
|
43
|
+
final String delete;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Configuration for the standalone storage (the storage half of the
|
|
47
|
+
/// self-hosted backend).
|
|
48
|
+
class StandaloneStorageConfig {
|
|
49
|
+
const StandaloneStorageConfig({
|
|
50
|
+
required this.baseUrl,
|
|
51
|
+
this.commonHeaders,
|
|
52
|
+
this.routes,
|
|
53
|
+
this.clientFactory,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
final String baseUrl;
|
|
57
|
+
final Map<String, String>? commonHeaders;
|
|
58
|
+
final StandaloneStorageRoutes? routes;
|
|
59
|
+
final StandaloneClientFactory? clientFactory;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Overridable route templates for the storage REST contract.
|
|
63
|
+
class StandaloneStorageRoutes {
|
|
64
|
+
const StandaloneStorageRoutes({
|
|
65
|
+
this.upload = '/api/upload',
|
|
66
|
+
this.getDownloadUrl = '/api/getDownloadUrl',
|
|
67
|
+
this.delete = '/api/delete',
|
|
68
|
+
this.readText = '/api/readText',
|
|
69
|
+
this.list = '/api/list',
|
|
70
|
+
this.serveFile = '/api/_file',
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
final String upload;
|
|
74
|
+
final String getDownloadUrl;
|
|
75
|
+
final String delete;
|
|
76
|
+
final String readText;
|
|
77
|
+
final String list;
|
|
78
|
+
final String serveFile;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Build a request path from a route template that may contain `:id`.
|
|
82
|
+
String buildPath(String template, {String? id}) {
|
|
83
|
+
var path = template;
|
|
84
|
+
if (id != null) path = path.replaceAll(':id', id);
|
|
85
|
+
return path;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Join [baseUrl] and [path], ensuring exactly one `/` between them.
|
|
89
|
+
String joinUrl(String baseUrl, String path) {
|
|
90
|
+
final base = baseUrl.endsWith('/') ? baseUrl.substring(0, baseUrl.length - 1) : baseUrl;
|
|
91
|
+
final p = path.startsWith('/') ? path : '/$path';
|
|
92
|
+
return '$base$p';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Merge [commonHeaders] with extra per-request headers.
|
|
96
|
+
Map<String, String> mergeHeaders(
|
|
97
|
+
Map<String, String>? common,
|
|
98
|
+
Map<String, String>? extra,
|
|
99
|
+
) {
|
|
100
|
+
final result = <String, String>{};
|
|
101
|
+
if (common != null) result.addAll(common);
|
|
102
|
+
if (extra != null) result.addAll(extra);
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Envelope helper: standalone create expects `[bundle]`.
|
|
107
|
+
String encodeBundleList(Map<String, dynamic> bundle) => jsonEncode([bundle]);
|