@hot-updater/postgres 0.1.5 → 0.1.6-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/dist/index.cjs +12 -8
- package/dist/index.js +12 -8
- package/package.json +7 -3
- package/sql/bundles.sql +13 -0
- package/sql/get_update_info.sql +79 -0
- package/sql/get_update_info.test.ts +74 -0
- package/sql/prepareSql.ts +11 -0
- package/sql/semver.sql +97 -0
- package/sql/semver_satisfies.test.ts +26 -0
package/dist/index.cjs
CHANGED
|
@@ -46,18 +46,20 @@ var postgres = (config, hooks) => (_) => {
|
|
|
46
46
|
tx.insertInto("bundles").values({
|
|
47
47
|
id: bundle.id,
|
|
48
48
|
enabled: bundle.enabled,
|
|
49
|
-
|
|
49
|
+
file_url: bundle.fileUrl,
|
|
50
50
|
force_update: bundle.forceUpdate,
|
|
51
|
-
|
|
51
|
+
file_hash: bundle.fileHash,
|
|
52
|
+
git_commit_hash: bundle.gitCommitHash,
|
|
52
53
|
message: bundle.message,
|
|
53
54
|
platform: bundle.platform,
|
|
54
55
|
target_version: bundle.targetVersion
|
|
55
56
|
}).onConflict(
|
|
56
57
|
(oc) => oc.column("id").doUpdateSet({
|
|
57
58
|
enabled: bundle.enabled,
|
|
58
|
-
|
|
59
|
+
file_url: bundle.fileUrl,
|
|
59
60
|
force_update: bundle.forceUpdate,
|
|
60
|
-
|
|
61
|
+
file_hash: bundle.fileHash,
|
|
62
|
+
git_commit_hash: bundle.gitCommitHash,
|
|
61
63
|
message: bundle.message,
|
|
62
64
|
platform: bundle.platform,
|
|
63
65
|
target_version: bundle.targetVersion
|
|
@@ -89,9 +91,10 @@ var postgres = (config, hooks) => (_) => {
|
|
|
89
91
|
}
|
|
90
92
|
return {
|
|
91
93
|
enabled: data.enabled,
|
|
92
|
-
|
|
94
|
+
fileUrl: data.file_url,
|
|
93
95
|
forceUpdate: data.force_update,
|
|
94
|
-
|
|
96
|
+
fileHash: data.file_hash,
|
|
97
|
+
gitCommitHash: data.git_commit_hash,
|
|
95
98
|
id: data.id,
|
|
96
99
|
message: data.message,
|
|
97
100
|
platform: data.platform,
|
|
@@ -105,9 +108,10 @@ var postgres = (config, hooks) => (_) => {
|
|
|
105
108
|
const data = await db.selectFrom("bundles").selectAll().execute();
|
|
106
109
|
return data.map((bundle) => ({
|
|
107
110
|
enabled: bundle.enabled,
|
|
108
|
-
|
|
111
|
+
fileUrl: bundle.file_url,
|
|
109
112
|
forceUpdate: bundle.force_update,
|
|
110
|
-
|
|
113
|
+
fileHash: bundle.file_hash,
|
|
114
|
+
gitCommitHash: bundle.git_commit_hash,
|
|
111
115
|
id: bundle.id,
|
|
112
116
|
message: bundle.message,
|
|
113
117
|
platform: bundle.platform,
|
package/dist/index.js
CHANGED
|
@@ -20,18 +20,20 @@ var postgres = (config, hooks) => (_) => {
|
|
|
20
20
|
tx.insertInto("bundles").values({
|
|
21
21
|
id: bundle.id,
|
|
22
22
|
enabled: bundle.enabled,
|
|
23
|
-
|
|
23
|
+
file_url: bundle.fileUrl,
|
|
24
24
|
force_update: bundle.forceUpdate,
|
|
25
|
-
|
|
25
|
+
file_hash: bundle.fileHash,
|
|
26
|
+
git_commit_hash: bundle.gitCommitHash,
|
|
26
27
|
message: bundle.message,
|
|
27
28
|
platform: bundle.platform,
|
|
28
29
|
target_version: bundle.targetVersion
|
|
29
30
|
}).onConflict(
|
|
30
31
|
(oc) => oc.column("id").doUpdateSet({
|
|
31
32
|
enabled: bundle.enabled,
|
|
32
|
-
|
|
33
|
+
file_url: bundle.fileUrl,
|
|
33
34
|
force_update: bundle.forceUpdate,
|
|
34
|
-
|
|
35
|
+
file_hash: bundle.fileHash,
|
|
36
|
+
git_commit_hash: bundle.gitCommitHash,
|
|
35
37
|
message: bundle.message,
|
|
36
38
|
platform: bundle.platform,
|
|
37
39
|
target_version: bundle.targetVersion
|
|
@@ -63,9 +65,10 @@ var postgres = (config, hooks) => (_) => {
|
|
|
63
65
|
}
|
|
64
66
|
return {
|
|
65
67
|
enabled: data.enabled,
|
|
66
|
-
|
|
68
|
+
fileUrl: data.file_url,
|
|
67
69
|
forceUpdate: data.force_update,
|
|
68
|
-
|
|
70
|
+
fileHash: data.file_hash,
|
|
71
|
+
gitCommitHash: data.git_commit_hash,
|
|
69
72
|
id: data.id,
|
|
70
73
|
message: data.message,
|
|
71
74
|
platform: data.platform,
|
|
@@ -79,9 +82,10 @@ var postgres = (config, hooks) => (_) => {
|
|
|
79
82
|
const data = await db.selectFrom("bundles").selectAll().execute();
|
|
80
83
|
return data.map((bundle) => ({
|
|
81
84
|
enabled: bundle.enabled,
|
|
82
|
-
|
|
85
|
+
fileUrl: bundle.file_url,
|
|
83
86
|
forceUpdate: bundle.force_update,
|
|
84
|
-
|
|
87
|
+
fileHash: bundle.file_hash,
|
|
88
|
+
gitCommitHash: bundle.git_commit_hash,
|
|
85
89
|
id: bundle.id,
|
|
86
90
|
message: bundle.message,
|
|
87
91
|
platform: bundle.platform,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/postgres",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.6-0",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
7
7
|
"module": "dist/index.js",
|
|
@@ -18,15 +18,19 @@
|
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
20
|
"dist",
|
|
21
|
+
"sql",
|
|
21
22
|
"package.json"
|
|
22
23
|
],
|
|
23
24
|
"dependencies": {
|
|
24
|
-
"@hot-updater/
|
|
25
|
+
"@hot-updater/core": "0.1.6-0",
|
|
26
|
+
"@hot-updater/plugin-core": "0.1.6-0",
|
|
25
27
|
"kysely": "^0.27.4",
|
|
26
28
|
"pg": "^8.13.1"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|
|
29
|
-
"@
|
|
31
|
+
"@electric-sql/pglite": "^0.2.15",
|
|
32
|
+
"@types/pg": "^8.11.10",
|
|
33
|
+
"camelcase-keys": "^9.1.3"
|
|
30
34
|
},
|
|
31
35
|
"scripts": {
|
|
32
36
|
"build": "tsup src/index.ts --format cjs,esm --dts",
|
package/sql/bundles.sql
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
CREATE TYPE platforms AS ENUM ('ios', 'android');
|
|
2
|
+
|
|
3
|
+
CREATE TABLE bundles (
|
|
4
|
+
id uuid PRIMARY KEY,
|
|
5
|
+
platform platforms NOT NULL,
|
|
6
|
+
target_version text NOT NULL,
|
|
7
|
+
force_update boolean NOT NULL,
|
|
8
|
+
enabled boolean NOT NULL,
|
|
9
|
+
file_url text NOT NULL,
|
|
10
|
+
file_hash text NOT NULL,
|
|
11
|
+
git_commit_hash text,
|
|
12
|
+
message text
|
|
13
|
+
);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION get_update_info (
|
|
2
|
+
current_platform platforms,
|
|
3
|
+
current_app_version text,
|
|
4
|
+
current_bundle_id uuid
|
|
5
|
+
)
|
|
6
|
+
RETURNS TABLE (
|
|
7
|
+
id uuid,
|
|
8
|
+
force_update boolean,
|
|
9
|
+
file_url text,
|
|
10
|
+
file_hash text,
|
|
11
|
+
status text
|
|
12
|
+
)
|
|
13
|
+
LANGUAGE plpgsql
|
|
14
|
+
AS
|
|
15
|
+
$$
|
|
16
|
+
DECLARE
|
|
17
|
+
NIL_UUID CONSTANT uuid := '00000000-0000-0000-0000-000000000000';
|
|
18
|
+
BEGIN
|
|
19
|
+
RETURN QUERY
|
|
20
|
+
WITH rollback_candidate AS (
|
|
21
|
+
SELECT
|
|
22
|
+
b.id,
|
|
23
|
+
-- If status is 'ROLLBACK', force_update is always TRUE
|
|
24
|
+
TRUE AS force_update,
|
|
25
|
+
b.file_url,
|
|
26
|
+
b.file_hash,
|
|
27
|
+
'ROLLBACK' AS status
|
|
28
|
+
FROM bundles b
|
|
29
|
+
WHERE b.enabled = TRUE
|
|
30
|
+
AND b.platform = current_platform
|
|
31
|
+
AND b.id < current_bundle_id
|
|
32
|
+
ORDER BY b.id DESC
|
|
33
|
+
LIMIT 1
|
|
34
|
+
),
|
|
35
|
+
update_candidate AS (
|
|
36
|
+
SELECT
|
|
37
|
+
b.id,
|
|
38
|
+
b.force_update,
|
|
39
|
+
b.file_url,
|
|
40
|
+
b.file_hash,
|
|
41
|
+
'UPDATE' AS status
|
|
42
|
+
FROM bundles b
|
|
43
|
+
WHERE b.enabled = TRUE
|
|
44
|
+
AND b.platform = current_platform
|
|
45
|
+
AND b.id >= current_bundle_id
|
|
46
|
+
AND semver_satisfies(b.target_version, current_app_version)
|
|
47
|
+
ORDER BY b.id DESC
|
|
48
|
+
LIMIT 1
|
|
49
|
+
),
|
|
50
|
+
final_result AS (
|
|
51
|
+
SELECT *
|
|
52
|
+
FROM update_candidate
|
|
53
|
+
|
|
54
|
+
UNION ALL
|
|
55
|
+
|
|
56
|
+
SELECT *
|
|
57
|
+
FROM rollback_candidate
|
|
58
|
+
WHERE NOT EXISTS (SELECT 1 FROM update_candidate)
|
|
59
|
+
)
|
|
60
|
+
SELECT *
|
|
61
|
+
FROM final_result WHERE final_result.id != current_bundle_id
|
|
62
|
+
|
|
63
|
+
UNION ALL
|
|
64
|
+
/*
|
|
65
|
+
When there are no final results and current_bundle_id != NIL_UUID,
|
|
66
|
+
add one fallback row.
|
|
67
|
+
This fallback row is also ROLLBACK so forceUpdate = TRUE.
|
|
68
|
+
*/
|
|
69
|
+
SELECT
|
|
70
|
+
NIL_UUID AS id,
|
|
71
|
+
TRUE AS force_update, -- Always TRUE
|
|
72
|
+
NULL AS file_url,
|
|
73
|
+
NULL AS file_hash,
|
|
74
|
+
'ROLLBACK' AS status
|
|
75
|
+
WHERE (SELECT COUNT(*) FROM final_result) = 0
|
|
76
|
+
AND current_bundle_id != NIL_UUID;
|
|
77
|
+
|
|
78
|
+
END;
|
|
79
|
+
$$;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
2
|
+
import type { Bundle, GetBundlesArgs, UpdateInfo } from "@hot-updater/core";
|
|
3
|
+
import { setupGetUpdateInfoTestSuite } from "@hot-updater/core/test-utils";
|
|
4
|
+
import camelcaseKeys from "camelcase-keys";
|
|
5
|
+
import { afterAll, beforeEach, describe } from "vitest";
|
|
6
|
+
import { prepareSql } from "./prepareSql";
|
|
7
|
+
|
|
8
|
+
const createInsertBundleQuery = (bundle: Bundle) => {
|
|
9
|
+
return `
|
|
10
|
+
INSERT INTO bundles (
|
|
11
|
+
id, file_url, file_hash, platform, target_version,
|
|
12
|
+
force_update, enabled, git_commit_hash, message
|
|
13
|
+
) VALUES (
|
|
14
|
+
'${bundle.id}',
|
|
15
|
+
'${bundle.fileUrl}',
|
|
16
|
+
'${bundle.fileHash}',
|
|
17
|
+
'${bundle.platform}',
|
|
18
|
+
'${bundle.targetVersion}',
|
|
19
|
+
${bundle.forceUpdate},
|
|
20
|
+
${bundle.enabled},
|
|
21
|
+
${bundle.gitCommitHash ? `'${bundle.gitCommitHash}'` : "null"},
|
|
22
|
+
${bundle.message ? `'${bundle.message}'` : "null"}
|
|
23
|
+
);
|
|
24
|
+
`;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const createGetUpdateInfo =
|
|
28
|
+
(db: PGlite) =>
|
|
29
|
+
async (
|
|
30
|
+
bundles: Bundle[],
|
|
31
|
+
{ appVersion, bundleId, platform }: GetBundlesArgs,
|
|
32
|
+
): Promise<UpdateInfo | null> => {
|
|
33
|
+
await db.exec(createInsertBundleQuerys(bundles));
|
|
34
|
+
|
|
35
|
+
const result = await db.query<{
|
|
36
|
+
id: string;
|
|
37
|
+
force_update: boolean;
|
|
38
|
+
file_url: string;
|
|
39
|
+
file_hash: string;
|
|
40
|
+
status: string;
|
|
41
|
+
}>(
|
|
42
|
+
`
|
|
43
|
+
SELECT * FROM get_update_info('${platform}', '${appVersion}', '${bundleId}')
|
|
44
|
+
`,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return result.rows[0]
|
|
48
|
+
? (camelcaseKeys(result.rows[0]) as UpdateInfo)
|
|
49
|
+
: null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const createInsertBundleQuerys = (bundles: Bundle[]) => {
|
|
53
|
+
return bundles.map(createInsertBundleQuery).join("\n");
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const db = new PGlite();
|
|
57
|
+
|
|
58
|
+
const sql = await prepareSql();
|
|
59
|
+
await db.exec(sql);
|
|
60
|
+
const getUpdateInfo = createGetUpdateInfo(db);
|
|
61
|
+
|
|
62
|
+
describe("getUpdateInfo", () => {
|
|
63
|
+
beforeEach(async () => {
|
|
64
|
+
await db.exec("DELETE FROM bundles");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterAll(async () => {
|
|
68
|
+
await db.close();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
setupGetUpdateInfoTestSuite({
|
|
72
|
+
getUpdateInfo: getUpdateInfo,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
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
|
+
};
|
package/sql/semver.sql
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION semver_satisfies(range_expression TEXT, version TEXT)
|
|
2
|
+
RETURNS BOOLEAN AS $$
|
|
3
|
+
DECLARE
|
|
4
|
+
version_parts TEXT[];
|
|
5
|
+
version_major INT;
|
|
6
|
+
version_minor INT;
|
|
7
|
+
version_patch INT;
|
|
8
|
+
satisfies BOOLEAN := FALSE;
|
|
9
|
+
BEGIN
|
|
10
|
+
-- Split the version into major, minor, and patch
|
|
11
|
+
version_parts := string_to_array(version, '.');
|
|
12
|
+
version_major := version_parts[1]::INT;
|
|
13
|
+
version_minor := version_parts[2]::INT;
|
|
14
|
+
version_patch := version_parts[3]::INT;
|
|
15
|
+
|
|
16
|
+
-- Parse range expression and evaluate
|
|
17
|
+
IF range_expression ~ '^\d+\.\d+\.\d+$' THEN
|
|
18
|
+
-- Exact match
|
|
19
|
+
satisfies := (range_expression = version);
|
|
20
|
+
|
|
21
|
+
ELSIF range_expression = '*' THEN
|
|
22
|
+
-- Matches any version
|
|
23
|
+
satisfies := TRUE;
|
|
24
|
+
|
|
25
|
+
ELSIF range_expression ~ '^\d+\.x\.x$' THEN
|
|
26
|
+
-- Matches major.x.x
|
|
27
|
+
DECLARE
|
|
28
|
+
major_range INT := split_part(range_expression, '.', 1)::INT;
|
|
29
|
+
BEGIN
|
|
30
|
+
satisfies := (version_major = major_range);
|
|
31
|
+
END;
|
|
32
|
+
|
|
33
|
+
ELSIF range_expression ~ '^\d+\.\d+\.x$' THEN
|
|
34
|
+
-- Matches major.minor.x
|
|
35
|
+
DECLARE
|
|
36
|
+
major_range INT := split_part(range_expression, '.', 1)::INT;
|
|
37
|
+
minor_range INT := split_part(range_expression, '.', 2)::INT;
|
|
38
|
+
BEGIN
|
|
39
|
+
satisfies := (version_major = major_range AND version_minor = minor_range);
|
|
40
|
+
END;
|
|
41
|
+
|
|
42
|
+
ELSIF range_expression ~ '^\d+\.\d+$' THEN
|
|
43
|
+
-- Matches major.minor
|
|
44
|
+
DECLARE
|
|
45
|
+
major_range INT := split_part(range_expression, '.', 1)::INT;
|
|
46
|
+
minor_range INT := split_part(range_expression, '.', 2)::INT;
|
|
47
|
+
BEGIN
|
|
48
|
+
satisfies := (version_major = major_range AND version_minor = minor_range);
|
|
49
|
+
END;
|
|
50
|
+
|
|
51
|
+
ELSIF range_expression ~ '^\d+\.\d+\.\d+ - \d+\.\d+\.\d+$' THEN
|
|
52
|
+
-- Matches range e.g., 1.2.3 - 1.2.7
|
|
53
|
+
DECLARE
|
|
54
|
+
lower_bound TEXT := split_part(range_expression, ' - ', 1);
|
|
55
|
+
upper_bound TEXT := split_part(range_expression, ' - ', 2);
|
|
56
|
+
BEGIN
|
|
57
|
+
satisfies := (version >= lower_bound AND version <= upper_bound);
|
|
58
|
+
END;
|
|
59
|
+
|
|
60
|
+
ELSIF range_expression ~ '^>=\d+\.\d+\.\d+ <\d+\.\d+\.\d+$' THEN
|
|
61
|
+
-- Matches range with inequalities
|
|
62
|
+
DECLARE
|
|
63
|
+
lower_bound TEXT := regexp_replace(range_expression, '>=([\d\.]+) <.*', '\1');
|
|
64
|
+
upper_bound TEXT := regexp_replace(range_expression, '.*<([\d\.]+)', '\1');
|
|
65
|
+
BEGIN
|
|
66
|
+
satisfies := (version >= lower_bound AND version < upper_bound);
|
|
67
|
+
END;
|
|
68
|
+
|
|
69
|
+
ELSIF range_expression ~ '^~\d+\.\d+\.\d+$' THEN
|
|
70
|
+
-- Matches ~1.2.3 (>=1.2.3 <1.3.0)
|
|
71
|
+
DECLARE
|
|
72
|
+
lower_bound TEXT := regexp_replace(range_expression, '~', '');
|
|
73
|
+
upper_bound_major INT := split_part(lower_bound, '.', 1)::INT;
|
|
74
|
+
upper_bound_minor INT := split_part(lower_bound, '.', 2)::INT + 1;
|
|
75
|
+
upper_bound TEXT := upper_bound_major || '.' || upper_bound_minor || '.0';
|
|
76
|
+
BEGIN
|
|
77
|
+
satisfies := (version >= lower_bound AND version < upper_bound);
|
|
78
|
+
END;
|
|
79
|
+
|
|
80
|
+
ELSIF range_expression ~ '^\^\d+\.\d+\.\d+$' THEN
|
|
81
|
+
-- Matches ^1.2.3 (>=1.2.3 <2.0.0)
|
|
82
|
+
DECLARE
|
|
83
|
+
lower_bound TEXT := regexp_replace(range_expression, '\^', '');
|
|
84
|
+
upper_bound_major INT := split_part(lower_bound, '.', 1)::INT + 1;
|
|
85
|
+
upper_bound TEXT := upper_bound_major || '.0.0';
|
|
86
|
+
BEGIN
|
|
87
|
+
satisfies := (version >= lower_bound AND version < upper_bound);
|
|
88
|
+
END;
|
|
89
|
+
|
|
90
|
+
ELSE
|
|
91
|
+
RAISE EXCEPTION 'Unsupported range expression: %', range_expression;
|
|
92
|
+
END IF;
|
|
93
|
+
|
|
94
|
+
RETURN satisfies;
|
|
95
|
+
END;
|
|
96
|
+
$$ LANGUAGE plpgsql;
|
|
97
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
2
|
+
import { setupSemverSatisfiesTestSuite } from "@hot-updater/core/test-utils";
|
|
3
|
+
import { afterAll, describe } from "vitest";
|
|
4
|
+
import { prepareSql } from "./prepareSql";
|
|
5
|
+
|
|
6
|
+
const db = new PGlite();
|
|
7
|
+
const sql = await prepareSql();
|
|
8
|
+
await db.exec(sql);
|
|
9
|
+
|
|
10
|
+
const createSemverSatisfies =
|
|
11
|
+
(db: PGlite) => async (targetVersion: string, currentVersion: string) => {
|
|
12
|
+
const result = await db.query<{ actual: boolean }>(`
|
|
13
|
+
SELECT semver_satisfies('${targetVersion}', '${currentVersion}') AS actual;
|
|
14
|
+
`);
|
|
15
|
+
return result.rows[0].actual;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const semverSatisfies = createSemverSatisfies(db);
|
|
19
|
+
|
|
20
|
+
describe("semverSatisfies", () => {
|
|
21
|
+
afterAll(async () => {
|
|
22
|
+
await db.close();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
setupSemverSatisfiesTestSuite({ semverSatisfies });
|
|
26
|
+
});
|