@hot-updater/postgres 0.36.7 → 1.0.0-rc.1

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/postgres",
3
3
  "type": "module",
4
- "version": "0.36.7",
4
+ "version": "1.0.0-rc.1",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.mjs",
@@ -34,24 +34,17 @@
34
34
  "dependencies": {
35
35
  "kysely": "0.28.17",
36
36
  "pg": "8.13.1",
37
- "@hot-updater/core": "0.36.7",
38
- "@hot-updater/plugin-core": "0.36.7"
37
+ "@hot-updater/core": "1.0.0-rc.0",
38
+ "@hot-updater/plugin-core": "1.0.0-rc.1"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@electric-sql/pglite": "0.4.1",
42
42
  "@types/node": "^20",
43
43
  "@types/pg": "^8.11.10",
44
44
  "camelcase-keys": "^9.1.3",
45
+ "kysely-pglite-dialect": "1.2.0",
45
46
  "pg-minify": "^1.6.5",
46
- "@hot-updater/js": "0.36.7",
47
- "@hot-updater/test-utils": "0.36.7"
48
- },
49
- "inlinedDependencies": {
50
- "camelcase": "8.0.0",
51
- "camelcase-keys": "9.1.3",
52
- "map-obj": "5.0.0",
53
- "pg-minify": "1.8.0",
54
- "quick-lru": "6.1.2"
47
+ "@hot-updater/test-utils": "1.0.0-rc.1"
55
48
  },
56
49
  "scripts": {
57
50
  "build": "tsdown",
package/sql/bundles.sql CHANGED
@@ -1,45 +1,179 @@
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);
1
+ -- HotUpdater.schema
2
+
3
+ create table channels (
4
+ id varchar(255) primary key not null,
5
+ name varchar(255) not null
6
+ );
7
+
8
+ create table bundles (
9
+ id uuid primary key not null,
10
+ platform text not null,
11
+ file_hash text not null,
12
+ git_commit_hash text,
13
+ storage_uri text not null,
14
+ archive_byte_size double precision not null,
15
+ metadata json not null default '{}'::json,
16
+ manifest_storage_uri text,
17
+ manifest_file_hash text,
18
+ asset_base_storage_uri text
19
+ );
20
+
21
+ create table bundle_patches (
22
+ id varchar(255) primary key not null,
23
+ bundle_id uuid not null,
24
+ base_bundle_id uuid not null,
25
+ base_file_hash text not null,
26
+ patch_file_hash text not null,
27
+ patch_storage_uri text not null,
28
+ byte_size double precision not null,
29
+ order_index integer not null default 0
30
+ );
31
+
32
+ create table releases (
33
+ id uuid primary key not null,
34
+ revision integer not null,
35
+ scope_key varchar(2048) not null,
36
+ channel_id varchar(255) not null,
37
+ platform text not null,
38
+ kind text not null,
39
+ bundle_id uuid,
40
+ strategy text not null,
41
+ target_app_version text,
42
+ fingerprint_hash text,
43
+ enabled boolean not null,
44
+ should_force_update boolean not null,
45
+ message text,
46
+ rollout_cohort_count integer not null default 1000,
47
+ target_cohorts json not null default '[]'::json,
48
+ operation text not null,
49
+ source_release_id uuid,
50
+ created_at_ms double precision not null,
51
+ updated_at_ms double precision not null
52
+ );
53
+
54
+ create table release_catalogs (
55
+ scope_key varchar(2048) primary key not null,
56
+ catalog_id varchar(255) not null,
57
+ strategy text not null,
58
+ channel_id varchar(255) not null,
59
+ channel_key varchar(1400) not null,
60
+ platform text not null,
61
+ fingerprint_hash text,
62
+ generation double precision not null,
63
+ payload text not null,
64
+ catalog_hash varchar(71) not null,
65
+ byte_size integer not null,
66
+ is_tombstone boolean not null,
67
+ updated_at_ms double precision not null
68
+ );
69
+
70
+ create table bundle_events (
71
+ id uuid primary key not null,
72
+ type text not null,
73
+ install_id text not null,
74
+ user_id text,
75
+ username text,
76
+ from_release_id uuid,
77
+ from_bundle_id uuid,
78
+ to_release_id uuid,
79
+ to_bundle_id uuid not null,
80
+ platform text not null,
81
+ app_version text not null,
82
+ channel text not null,
83
+ cohort text not null,
84
+ update_strategy text,
85
+ fingerprint_hash text,
86
+ sdk_version text,
87
+ received_at_ms double precision not null
88
+ );
89
+
90
+ create table api_keys (
91
+ id varchar(255) primary key not null,
92
+ hash text not null,
93
+ name text not null,
94
+ prefix text not null,
95
+ role text not null,
96
+ created_at_ms double precision not null,
97
+ revoked_at_ms double precision
98
+ );
99
+
100
+ create table private_hot_updater_settings (
101
+ key varchar(255) primary key not null,
102
+ value text not null default '1.0.0'
103
+ );
104
+
105
+ create unique index channels_name_key on channels(name);
106
+ create index bundle_patches_bundle_id_idx on bundle_patches(bundle_id);
107
+ create index bundle_patches_base_bundle_id_idx on bundle_patches(base_bundle_id);
108
+ create index releases_scope_order_idx on releases(scope_key, id);
109
+ create index releases_channel_platform_order_idx on releases(channel_id, platform, id);
110
+ create index releases_bundle_id_idx on releases(bundle_id);
111
+ create index releases_fingerprint_hash_idx on releases(fingerprint_hash);
112
+ create index releases_enabled_idx on releases(enabled);
113
+ create index release_catalogs_channel_idx on release_catalogs(channel_id);
114
+ create index bundle_events_received_at_idx on bundle_events(received_at_ms, id);
115
+ create index bundle_events_install_idx on bundle_events(install_id, received_at_ms, id);
116
+ create index bundle_events_user_id_idx on bundle_events(user_id, received_at_ms, id);
117
+ create index bundle_events_username_idx on bundle_events(username, received_at_ms, id);
118
+ create index bundle_events_to_bundle_idx on bundle_events(type, to_bundle_id, received_at_ms, id);
119
+ create index bundle_events_from_bundle_idx on bundle_events(type, from_bundle_id, received_at_ms, id);
120
+ create index bundle_events_to_release_idx on bundle_events(type, to_release_id, received_at_ms, id);
121
+ create index bundle_events_from_release_idx on bundle_events(type, from_release_id, received_at_ms, id);
122
+ create unique index api_keys_hash_key on api_keys(hash);
123
+ create index api_keys_created_at_idx on api_keys(created_at_ms, id);
124
+
125
+ alter table channels add constraint channels_id_length_check
126
+ check (char_length(id) between 1 and 255);
127
+ alter table channels add constraint channels_name_length_check
128
+ check (char_length(name) between 1 and 255);
129
+ alter table bundles add constraint bundles_archive_byte_size_check
130
+ check (archive_byte_size >= 0 and archive_byte_size <= 9007199254740991);
131
+ alter table bundle_patches add constraint bundle_patches_byte_size_check
132
+ check (byte_size >= 0 and byte_size <= 9007199254740991);
133
+ alter table releases add constraint releases_revision_check
134
+ check (revision >= 1);
135
+ alter table releases add constraint releases_kind_bundle_check
136
+ check ((kind = 'BUNDLE' and bundle_id is not null) or (kind = 'EMBEDDED' and bundle_id is null));
137
+ alter table releases add constraint releases_strategy_target_check
138
+ check ((strategy = 'APP_VERSION' and target_app_version is not null and fingerprint_hash is null) or (strategy = 'FINGERPRINT' and target_app_version is null and fingerprint_hash is not null));
139
+ alter table releases add constraint releases_rollout_cohort_count_check
140
+ check (rollout_cohort_count >= 0 and rollout_cohort_count <= 1000);
141
+ alter table releases add constraint releases_operation_check
142
+ check (operation in ('DEPLOY', 'PROMOTE', 'ROLLBACK'));
143
+ alter table release_catalogs add constraint release_catalogs_strategy_target_check
144
+ check ((strategy = 'APP_VERSION' and fingerprint_hash is null) or (strategy = 'FINGERPRINT' and fingerprint_hash is not null));
145
+ alter table release_catalogs add constraint release_catalogs_generation_check
146
+ check (generation >= 1 and generation <= 9007199254740991);
147
+ alter table release_catalogs add constraint release_catalogs_byte_size_check
148
+ check (byte_size >= 0 and byte_size <= 262144);
149
+ alter table bundle_events add constraint bundle_events_type_check
150
+ check (type in ('UPDATE_APPLIED', 'RECOVERED', 'RELEASE_ADOPTED', 'UNCHANGED'));
151
+ alter table bundle_events add constraint bundle_events_platform_check
152
+ check (platform in ('ios', 'android'));
153
+ alter table bundle_events add constraint bundle_events_shape_check
154
+ check (((type in ('UPDATE_APPLIED', 'RECOVERED', 'RELEASE_ADOPTED')) and from_bundle_id is not null and update_strategy is not null and update_strategy in ('fingerprint', 'appVersion')) or (type = 'UNCHANGED' and from_bundle_id is null and update_strategy is null));
155
+ alter table bundle_events add constraint bundle_events_received_at_check
156
+ check (received_at_ms >= 0);
157
+ alter table api_keys add constraint api_keys_role_check
158
+ check (role = 'client');
159
+ alter table api_keys add constraint api_keys_created_at_check
160
+ check (created_at_ms >= 0);
161
+ alter table api_keys add constraint api_keys_revoked_at_check
162
+ check (revoked_at_ms is null or revoked_at_ms >= 0);
163
+
164
+ alter table bundle_patches add constraint bundle_patches_bundle_id_fk
165
+ foreign key (bundle_id) references bundles(id) on update restrict on delete cascade;
166
+ alter table bundle_patches add constraint bundle_patches_base_bundle_id_fk
167
+ foreign key (base_bundle_id) references bundles(id) on update restrict on delete cascade;
168
+ alter table releases add constraint releases_channel_id_fk
169
+ foreign key (channel_id) references channels(id) on update restrict on delete restrict;
170
+ alter table releases add constraint releases_bundle_id_fk
171
+ foreign key (bundle_id) references bundles(id) on update restrict on delete restrict;
172
+ alter table releases add constraint releases_source_release_id_fk
173
+ foreign key (source_release_id) references releases(id) on update restrict on delete set null;
174
+ alter table release_catalogs add constraint release_catalogs_channel_id_fk
175
+ foreign key (channel_id) references channels(id) on update restrict on delete restrict;
176
+
177
+ insert into private_hot_updater_settings (key, value)
178
+ values ('schema.core', '1.0.0')
179
+ on conflict (key) do update set value = excluded.value;
@@ -1,21 +0,0 @@
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
- $$;
@@ -1,171 +0,0 @@
1
- import { PGlite } from "@electric-sql/pglite";
2
- import {
3
- type Bundle,
4
- type GetBundlesArgs,
5
- NIL_UUID,
6
- type UpdateInfo,
7
- } from "@hot-updater/core";
8
- import { filterCompatibleAppVersions } from "@hot-updater/js";
9
- import { setupGetUpdateInfoTestSuite } from "@hot-updater/test-utils";
10
- import camelcaseKeys from "camelcase-keys";
11
- import { afterAll, beforeEach, describe } from "vitest";
12
-
13
- import { prepareSql } from "./prepareSql";
14
-
15
- const createInsertBundleQuery = (bundle: Bundle) => {
16
- const rolloutCohortCount = bundle.rolloutCohortCount ?? 1000;
17
- const targetCohorts = bundle.targetCohorts
18
- ? `ARRAY[${bundle.targetCohorts.map((id) => `'${id}'`).join(",")}]::TEXT[]`
19
- : "NULL";
20
-
21
- return `
22
- INSERT INTO bundles (
23
- id, file_hash, platform, target_app_version,
24
- should_force_update, enabled, git_commit_hash, message, channel, storage_uri, fingerprint_hash,
25
- rollout_cohort_count, target_cohorts
26
- ) VALUES (
27
- '${bundle.id}',
28
- '${bundle.fileHash}',
29
- '${bundle.platform}',
30
- ${bundle.targetAppVersion ? `'${bundle.targetAppVersion}'` : "null"},
31
- ${bundle.shouldForceUpdate},
32
- ${bundle.enabled},
33
- ${bundle.gitCommitHash ? `'${bundle.gitCommitHash}'` : "null"},
34
- ${bundle.message ? `'${bundle.message}'` : "null"},
35
- '${bundle.channel}',
36
- '${bundle.storageUri}',
37
- ${bundle.fingerprintHash ? `'${bundle.fingerprintHash}'` : "null"},
38
- ${rolloutCohortCount},
39
- ${targetCohorts}
40
- ) ON CONFLICT (id) DO UPDATE SET
41
- file_hash = EXCLUDED.file_hash,
42
- platform = EXCLUDED.platform,
43
- target_app_version = EXCLUDED.target_app_version,
44
- should_force_update = EXCLUDED.should_force_update,
45
- enabled = EXCLUDED.enabled,
46
- git_commit_hash = EXCLUDED.git_commit_hash,
47
- message = EXCLUDED.message,
48
- channel = EXCLUDED.channel,
49
- storage_uri = EXCLUDED.storage_uri,
50
- fingerprint_hash = EXCLUDED.fingerprint_hash,
51
- rollout_cohort_count = EXCLUDED.rollout_cohort_count,
52
- target_cohorts = EXCLUDED.target_cohorts;
53
- `;
54
- };
55
-
56
- const createGetUpdateInfo =
57
- (db: PGlite) =>
58
- async (
59
- bundles: Bundle[],
60
- args: GetBundlesArgs,
61
- ): Promise<UpdateInfo | null> => {
62
- const {
63
- bundleId,
64
- platform,
65
- minBundleId = NIL_UUID,
66
- channel = "production",
67
- _updateStrategy,
68
- } = args;
69
- await db.exec(createInsertBundleQuerys(bundles));
70
-
71
- if (_updateStrategy === "fingerprint") {
72
- const fingerprintHash = args.fingerprintHash;
73
- const cohort = args.cohort;
74
- const cohortSql = cohort ? `'${cohort}'` : "NULL";
75
- const result = await db.query<{
76
- id: string;
77
- should_force_update: boolean;
78
- message: string;
79
- status: string;
80
- storage_uri: string | null;
81
- file_hash: string | null;
82
- }>(
83
- `
84
- SELECT * FROM get_update_info_by_fingerprint_hash(
85
- '${platform}',
86
- '${bundleId}',
87
- '${minBundleId}',
88
- '${channel}',
89
- '${fingerprintHash}',
90
- ${cohortSql}
91
- );
92
- `,
93
- );
94
-
95
- if (!result.rows[0]) {
96
- return null;
97
- }
98
-
99
- const row = result.rows[0];
100
- return camelcaseKeys(row) as UpdateInfo;
101
- }
102
-
103
- const appVersion = args.appVersion;
104
- const { rows: appVersionList } = await db.query<{
105
- target_app_version: string;
106
- }>(
107
- `
108
- SELECT target_app_version FROM get_target_app_version_list('${platform}', '${minBundleId}');
109
- `,
110
- );
111
-
112
- const targetAppVersionList = filterCompatibleAppVersions(
113
- appVersionList?.map((group) => group.target_app_version) ?? [],
114
- appVersion,
115
- );
116
-
117
- const cohort = args.cohort;
118
- const cohortSql = cohort ? `'${cohort}'` : "NULL";
119
- const result = await db.query<{
120
- id: string;
121
- should_force_update: boolean;
122
- message: string;
123
- status: string;
124
- storage_uri: string | null;
125
- file_hash: string | null;
126
- }>(
127
- `
128
- SELECT * FROM get_update_info_by_app_version(
129
- '${platform}',
130
- '${appVersion}',
131
- '${bundleId}',
132
- '${minBundleId ?? NIL_UUID}',
133
- '${channel}',
134
- ARRAY[${targetAppVersionList.map((v) => `'${v}'`).join(",")}]::text[],
135
- ${cohortSql}
136
- );
137
- `,
138
- );
139
-
140
- if (!result.rows[0]) {
141
- return null;
142
- }
143
-
144
- const row = result.rows[0];
145
- return camelcaseKeys(row) as UpdateInfo;
146
- };
147
-
148
- const createInsertBundleQuerys = (bundles: Bundle[]) => {
149
- return bundles.map(createInsertBundleQuery).join("\n");
150
- };
151
-
152
- const db = new PGlite();
153
-
154
- const sql = await prepareSql();
155
- await db.exec(sql);
156
- const getUpdateInfo = createGetUpdateInfo(db);
157
-
158
- describe("getUpdateInfo", () => {
159
- beforeEach(async () => {
160
- await db.exec("DELETE FROM bundle_patches");
161
- await db.exec("DELETE FROM bundles");
162
- });
163
-
164
- afterAll(async () => {
165
- await db.close();
166
- });
167
-
168
- setupGetUpdateInfoTestSuite({
169
- getUpdateInfo: getUpdateInfo,
170
- });
171
- });
@@ -1,126 +0,0 @@
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
- $$;