@hot-updater/postgres 0.12.6 → 0.13.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 CHANGED
@@ -26,103 +26,89 @@ __webpack_require__.r(__webpack_exports__);
26
26
  __webpack_require__.d(__webpack_exports__, {
27
27
  postgres: ()=>postgres
28
28
  });
29
+ const plugin_core_namespaceObject = require("@hot-updater/plugin-core");
29
30
  const external_kysely_namespaceObject = require("kysely");
30
31
  const external_pg_namespaceObject = require("pg");
31
- const postgres = (config, hooks)=>(_)=>{
32
- const pool = new external_pg_namespaceObject.Pool(config);
33
- const dialect = new external_kysely_namespaceObject.PostgresDialect({
34
- pool
35
- });
36
- const db = new external_kysely_namespaceObject.Kysely({
37
- dialect
38
- });
39
- let bundles = [];
40
- const changedIds = new Set();
41
- function markChanged(id) {
42
- changedIds.add(id);
43
- }
44
- let isUnmount = false;
45
- return {
46
- name: "postgres",
47
- async onUnmount () {
48
- if (isUnmount) return;
49
- isUnmount = true;
50
- await pool.end();
51
- changedIds.clear();
52
- },
53
- async commitBundle () {
54
- if (0 === changedIds.size) return;
55
- const changedBundles = bundles.filter((b)=>changedIds.has(b.id));
56
- if (0 === changedBundles.length) return;
57
- await db.transaction().execute(async (tx)=>{
58
- for (const bundle of changedBundles)await tx.insertInto("bundles").values({
59
- id: bundle.id,
32
+ const postgres = (config, hooks)=>{
33
+ const pool = new external_pg_namespaceObject.Pool(config);
34
+ const dialect = new external_kysely_namespaceObject.PostgresDialect({
35
+ pool
36
+ });
37
+ const db = new external_kysely_namespaceObject.Kysely({
38
+ dialect
39
+ });
40
+ return (0, plugin_core_namespaceObject.createDatabasePlugin)("postgres", {
41
+ async onUnmount () {
42
+ await db.destroy();
43
+ await pool.end();
44
+ },
45
+ async getBundleById (bundleId) {
46
+ const data = await db.selectFrom("bundles").selectAll().where("id", "=", bundleId).executeTakeFirst();
47
+ if (!data) return null;
48
+ return {
49
+ enabled: data.enabled,
50
+ shouldForceUpdate: data.should_force_update,
51
+ fileHash: data.file_hash,
52
+ gitCommitHash: data.git_commit_hash,
53
+ id: data.id,
54
+ message: data.message,
55
+ platform: data.platform,
56
+ targetAppVersion: data.target_app_version,
57
+ channel: data.channel
58
+ };
59
+ },
60
+ async getBundles (options) {
61
+ const { where, limit, offset = 0 } = options ?? {};
62
+ let query = db.selectFrom("bundles").orderBy("id", "desc");
63
+ if (where?.channel) query = query.where("channel", "=", where.channel);
64
+ if (where?.platform) query = query.where("platform", "=", where.platform);
65
+ if (limit) query = query.limit(limit);
66
+ if (offset) query = query.offset(offset);
67
+ const data = await query.selectAll().execute();
68
+ return data.map((bundle)=>({
69
+ enabled: bundle.enabled,
70
+ shouldForceUpdate: bundle.should_force_update,
71
+ fileHash: bundle.file_hash,
72
+ gitCommitHash: bundle.git_commit_hash,
73
+ id: bundle.id,
74
+ message: bundle.message,
75
+ platform: bundle.platform,
76
+ targetAppVersion: bundle.target_app_version,
77
+ channel: bundle.channel
78
+ }));
79
+ },
80
+ async getChannels () {
81
+ const data = await db.selectFrom("bundles").select("channel").groupBy("channel").execute();
82
+ return data.map((bundle)=>bundle.channel);
83
+ },
84
+ async commitBundle ({ changedSets }) {
85
+ if (0 === changedSets.length) return;
86
+ const bundles = changedSets.map((op)=>op.data);
87
+ await db.transaction().execute(async (tx)=>{
88
+ for (const bundle of bundles)await tx.insertInto("bundles").values({
89
+ id: bundle.id,
90
+ enabled: bundle.enabled,
91
+ should_force_update: bundle.shouldForceUpdate,
92
+ file_hash: bundle.fileHash,
93
+ git_commit_hash: bundle.gitCommitHash,
94
+ message: bundle.message,
95
+ platform: bundle.platform,
96
+ target_app_version: bundle.targetAppVersion,
97
+ channel: bundle.channel
98
+ }).onConflict((oc)=>oc.column("id").doUpdateSet({
60
99
  enabled: bundle.enabled,
61
- file_url: bundle.fileUrl,
62
100
  should_force_update: bundle.shouldForceUpdate,
63
101
  file_hash: bundle.fileHash,
64
102
  git_commit_hash: bundle.gitCommitHash,
65
103
  message: bundle.message,
66
104
  platform: bundle.platform,
67
- target_app_version: bundle.targetAppVersion
68
- }).onConflict((oc)=>oc.column("id").doUpdateSet({
69
- enabled: bundle.enabled,
70
- file_url: bundle.fileUrl,
71
- should_force_update: bundle.shouldForceUpdate,
72
- file_hash: bundle.fileHash,
73
- git_commit_hash: bundle.gitCommitHash,
74
- message: bundle.message,
75
- platform: bundle.platform,
76
- target_app_version: bundle.targetAppVersion
77
- })).execute();
78
- });
79
- changedIds.clear();
80
- hooks?.onDatabaseUpdated?.();
81
- },
82
- async updateBundle (targetBundleId, newBundle) {
83
- bundles = await this.getBundles();
84
- const targetIndex = bundles.findIndex((u)=>u.id === targetBundleId);
85
- if (-1 === targetIndex) throw new Error("target bundle version not found");
86
- Object.assign(bundles[targetIndex], newBundle);
87
- markChanged(targetBundleId);
88
- },
89
- async appendBundle (inputBundle) {
90
- bundles = await this.getBundles();
91
- bundles.unshift(inputBundle);
92
- markChanged(inputBundle.id);
93
- },
94
- async getBundleById (bundleId) {
95
- const data = await db.selectFrom("bundles").selectAll().where("id", "=", bundleId).executeTakeFirst();
96
- if (!data) return null;
97
- return {
98
- enabled: data.enabled,
99
- fileUrl: data.file_url,
100
- shouldForceUpdate: data.should_force_update,
101
- fileHash: data.file_hash,
102
- gitCommitHash: data.git_commit_hash,
103
- id: data.id,
104
- message: data.message,
105
- platform: data.platform,
106
- targetAppVersion: data.target_app_version
107
- };
108
- },
109
- async getBundles (refresh = false) {
110
- if (bundles.length > 0 && !refresh) return bundles;
111
- const data = await db.selectFrom("bundles").orderBy("id", "desc").selectAll().execute();
112
- return data.map((bundle)=>({
113
- enabled: bundle.enabled,
114
- fileUrl: bundle.file_url,
115
- shouldForceUpdate: bundle.should_force_update,
116
- fileHash: bundle.file_hash,
117
- gitCommitHash: bundle.git_commit_hash,
118
- id: bundle.id,
119
- message: bundle.message,
120
- platform: bundle.platform,
121
- targetAppVersion: bundle.target_app_version
122
- }));
123
- }
124
- };
125
- };
105
+ target_app_version: bundle.targetAppVersion,
106
+ channel: bundle.channel
107
+ })).execute();
108
+ });
109
+ }
110
+ }, hooks);
111
+ };
126
112
  var __webpack_export_target__ = exports;
127
113
  for(var __webpack_i__ in __webpack_exports__)__webpack_export_target__[__webpack_i__] = __webpack_exports__[__webpack_i__];
128
114
  if (__webpack_exports__.__esModule) Object.defineProperty(__webpack_export_target__, '__esModule', {
package/dist/index.js CHANGED
@@ -1,98 +1,84 @@
1
+ import * as __WEBPACK_EXTERNAL_MODULE__hot_updater_plugin_core_40c1c502__ from "@hot-updater/plugin-core";
1
2
  import * as __WEBPACK_EXTERNAL_MODULE_kysely__ from "kysely";
2
3
  import * as __WEBPACK_EXTERNAL_MODULE_pg__ from "pg";
3
- const postgres = (config, hooks)=>(_)=>{
4
- const pool = new __WEBPACK_EXTERNAL_MODULE_pg__.Pool(config);
5
- const dialect = new __WEBPACK_EXTERNAL_MODULE_kysely__.PostgresDialect({
6
- pool
7
- });
8
- const db = new __WEBPACK_EXTERNAL_MODULE_kysely__.Kysely({
9
- dialect
10
- });
11
- let bundles = [];
12
- const changedIds = new Set();
13
- function markChanged(id) {
14
- changedIds.add(id);
15
- }
16
- let isUnmount = false;
17
- return {
18
- name: "postgres",
19
- async onUnmount () {
20
- if (isUnmount) return;
21
- isUnmount = true;
22
- await pool.end();
23
- changedIds.clear();
24
- },
25
- async commitBundle () {
26
- if (0 === changedIds.size) return;
27
- const changedBundles = bundles.filter((b)=>changedIds.has(b.id));
28
- if (0 === changedBundles.length) return;
29
- await db.transaction().execute(async (tx)=>{
30
- for (const bundle of changedBundles)await tx.insertInto("bundles").values({
31
- id: bundle.id,
4
+ const postgres = (config, hooks)=>{
5
+ const pool = new __WEBPACK_EXTERNAL_MODULE_pg__.Pool(config);
6
+ const dialect = new __WEBPACK_EXTERNAL_MODULE_kysely__.PostgresDialect({
7
+ pool
8
+ });
9
+ const db = new __WEBPACK_EXTERNAL_MODULE_kysely__.Kysely({
10
+ dialect
11
+ });
12
+ return (0, __WEBPACK_EXTERNAL_MODULE__hot_updater_plugin_core_40c1c502__.createDatabasePlugin)("postgres", {
13
+ async onUnmount () {
14
+ await db.destroy();
15
+ await pool.end();
16
+ },
17
+ async getBundleById (bundleId) {
18
+ const data = await db.selectFrom("bundles").selectAll().where("id", "=", bundleId).executeTakeFirst();
19
+ if (!data) return null;
20
+ return {
21
+ enabled: data.enabled,
22
+ shouldForceUpdate: data.should_force_update,
23
+ fileHash: data.file_hash,
24
+ gitCommitHash: data.git_commit_hash,
25
+ id: data.id,
26
+ message: data.message,
27
+ platform: data.platform,
28
+ targetAppVersion: data.target_app_version,
29
+ channel: data.channel
30
+ };
31
+ },
32
+ async getBundles (options) {
33
+ const { where, limit, offset = 0 } = options ?? {};
34
+ let query = db.selectFrom("bundles").orderBy("id", "desc");
35
+ if (where?.channel) query = query.where("channel", "=", where.channel);
36
+ if (where?.platform) query = query.where("platform", "=", where.platform);
37
+ if (limit) query = query.limit(limit);
38
+ if (offset) query = query.offset(offset);
39
+ const data = await query.selectAll().execute();
40
+ return data.map((bundle)=>({
41
+ enabled: bundle.enabled,
42
+ shouldForceUpdate: bundle.should_force_update,
43
+ fileHash: bundle.file_hash,
44
+ gitCommitHash: bundle.git_commit_hash,
45
+ id: bundle.id,
46
+ message: bundle.message,
47
+ platform: bundle.platform,
48
+ targetAppVersion: bundle.target_app_version,
49
+ channel: bundle.channel
50
+ }));
51
+ },
52
+ async getChannels () {
53
+ const data = await db.selectFrom("bundles").select("channel").groupBy("channel").execute();
54
+ return data.map((bundle)=>bundle.channel);
55
+ },
56
+ async commitBundle ({ changedSets }) {
57
+ if (0 === changedSets.length) return;
58
+ const bundles = changedSets.map((op)=>op.data);
59
+ await db.transaction().execute(async (tx)=>{
60
+ for (const bundle of bundles)await tx.insertInto("bundles").values({
61
+ id: bundle.id,
62
+ enabled: bundle.enabled,
63
+ should_force_update: bundle.shouldForceUpdate,
64
+ file_hash: bundle.fileHash,
65
+ git_commit_hash: bundle.gitCommitHash,
66
+ message: bundle.message,
67
+ platform: bundle.platform,
68
+ target_app_version: bundle.targetAppVersion,
69
+ channel: bundle.channel
70
+ }).onConflict((oc)=>oc.column("id").doUpdateSet({
32
71
  enabled: bundle.enabled,
33
- file_url: bundle.fileUrl,
34
72
  should_force_update: bundle.shouldForceUpdate,
35
73
  file_hash: bundle.fileHash,
36
74
  git_commit_hash: bundle.gitCommitHash,
37
75
  message: bundle.message,
38
76
  platform: bundle.platform,
39
- target_app_version: bundle.targetAppVersion
40
- }).onConflict((oc)=>oc.column("id").doUpdateSet({
41
- enabled: bundle.enabled,
42
- file_url: bundle.fileUrl,
43
- should_force_update: bundle.shouldForceUpdate,
44
- file_hash: bundle.fileHash,
45
- git_commit_hash: bundle.gitCommitHash,
46
- message: bundle.message,
47
- platform: bundle.platform,
48
- target_app_version: bundle.targetAppVersion
49
- })).execute();
50
- });
51
- changedIds.clear();
52
- hooks?.onDatabaseUpdated?.();
53
- },
54
- async updateBundle (targetBundleId, newBundle) {
55
- bundles = await this.getBundles();
56
- const targetIndex = bundles.findIndex((u)=>u.id === targetBundleId);
57
- if (-1 === targetIndex) throw new Error("target bundle version not found");
58
- Object.assign(bundles[targetIndex], newBundle);
59
- markChanged(targetBundleId);
60
- },
61
- async appendBundle (inputBundle) {
62
- bundles = await this.getBundles();
63
- bundles.unshift(inputBundle);
64
- markChanged(inputBundle.id);
65
- },
66
- async getBundleById (bundleId) {
67
- const data = await db.selectFrom("bundles").selectAll().where("id", "=", bundleId).executeTakeFirst();
68
- if (!data) return null;
69
- return {
70
- enabled: data.enabled,
71
- fileUrl: data.file_url,
72
- shouldForceUpdate: data.should_force_update,
73
- fileHash: data.file_hash,
74
- gitCommitHash: data.git_commit_hash,
75
- id: data.id,
76
- message: data.message,
77
- platform: data.platform,
78
- targetAppVersion: data.target_app_version
79
- };
80
- },
81
- async getBundles (refresh = false) {
82
- if (bundles.length > 0 && !refresh) return bundles;
83
- const data = await db.selectFrom("bundles").orderBy("id", "desc").selectAll().execute();
84
- return data.map((bundle)=>({
85
- enabled: bundle.enabled,
86
- fileUrl: bundle.file_url,
87
- shouldForceUpdate: bundle.should_force_update,
88
- fileHash: bundle.file_hash,
89
- gitCommitHash: bundle.git_commit_hash,
90
- id: bundle.id,
91
- message: bundle.message,
92
- platform: bundle.platform,
93
- targetAppVersion: bundle.target_app_version
94
- }));
95
- }
96
- };
97
- };
77
+ target_app_version: bundle.targetAppVersion,
78
+ channel: bundle.channel
79
+ })).execute();
80
+ });
81
+ }
82
+ }, hooks);
83
+ };
98
84
  export { postgres };
@@ -1,5 +1,5 @@
1
- import type { BasePluginArgs, DatabasePlugin, DatabasePluginHooks } from "@hot-updater/plugin-core";
1
+ import type { DatabasePluginHooks } from "@hot-updater/plugin-core";
2
2
  import { type PoolConfig } from "pg";
3
3
  export interface PostgresConfig extends PoolConfig {
4
4
  }
5
- export declare const postgres: (config: PostgresConfig, hooks?: DatabasePluginHooks) => (_: BasePluginArgs) => DatabasePlugin;
5
+ export declare const postgres: (config: PostgresConfig, hooks?: DatabasePluginHooks) => (options: import("@hot-updater/plugin-core").BasePluginArgs) => import("@hot-updater/plugin-core").DatabasePlugin;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/postgres",
3
3
  "type": "module",
4
- "version": "0.12.6",
4
+ "version": "0.13.0",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.js",
@@ -29,13 +29,14 @@
29
29
  "package.json"
30
30
  ],
31
31
  "dependencies": {
32
- "@hot-updater/core": "0.12.6",
33
- "@hot-updater/plugin-core": "0.12.6",
32
+ "@hot-updater/core": "0.13.0",
33
+ "@hot-updater/plugin-core": "0.13.0",
34
34
  "kysely": "^0.27.5",
35
35
  "pg": "^8.13.1"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@electric-sql/pglite": "^0.2.15",
39
+ "@hot-updater/js": "0.13.0",
39
40
  "@types/pg": "^8.11.10",
40
41
  "camelcase-keys": "^9.1.3"
41
42
  },
package/sql/bundles.sql CHANGED
@@ -8,10 +8,10 @@ CREATE TABLE bundles (
8
8
  target_app_version text NOT NULL,
9
9
  should_force_update boolean NOT NULL,
10
10
  enabled boolean NOT NULL,
11
- file_url text NOT NULL,
12
11
  file_hash text NOT NULL,
13
12
  git_commit_hash text,
14
- message text
13
+ message text,
14
+ channel text NOT NULL DEFAULT 'production'
15
15
  );
16
16
 
17
17
  CREATE INDEX bundles_target_app_version_idx ON bundles(target_app_version);
@@ -0,0 +1,21 @@
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,6 +1,12 @@
1
1
  import { PGlite } from "@electric-sql/pglite";
2
- import type { Bundle, GetBundlesArgs, UpdateInfo } from "@hot-updater/core";
2
+ import {
3
+ type Bundle,
4
+ type GetBundlesArgs,
5
+ NIL_UUID,
6
+ type UpdateInfo,
7
+ } from "@hot-updater/core";
3
8
  import { setupGetUpdateInfoTestSuite } from "@hot-updater/core/test-utils";
9
+ import { filterCompatibleAppVersions } from "@hot-updater/js";
4
10
  import camelcaseKeys from "camelcase-keys";
5
11
  import { afterAll, beforeEach, describe } from "vitest";
6
12
  import { prepareSql } from "./prepareSql";
@@ -8,18 +14,18 @@ import { prepareSql } from "./prepareSql";
8
14
  const createInsertBundleQuery = (bundle: Bundle) => {
9
15
  return `
10
16
  INSERT INTO bundles (
11
- id, file_url, file_hash, platform, target_app_version,
12
- should_force_update, enabled, git_commit_hash, message
17
+ id, file_hash, platform, target_app_version,
18
+ should_force_update, enabled, git_commit_hash, message, channel
13
19
  ) VALUES (
14
20
  '${bundle.id}',
15
- '${bundle.fileUrl}',
16
21
  '${bundle.fileHash}',
17
22
  '${bundle.platform}',
18
23
  '${bundle.targetAppVersion}',
19
24
  ${bundle.shouldForceUpdate},
20
25
  ${bundle.enabled},
21
26
  ${bundle.gitCommitHash ? `'${bundle.gitCommitHash}'` : "null"},
22
- ${bundle.message ? `'${bundle.message}'` : "null"}
27
+ ${bundle.message ? `'${bundle.message}'` : "null"},
28
+ '${bundle.channel}'
23
29
  );
24
30
  `;
25
31
  };
@@ -28,20 +34,45 @@ const createGetUpdateInfo =
28
34
  (db: PGlite) =>
29
35
  async (
30
36
  bundles: Bundle[],
31
- { appVersion, bundleId, platform }: GetBundlesArgs,
37
+ {
38
+ appVersion,
39
+ bundleId,
40
+ platform,
41
+ minBundleId = NIL_UUID,
42
+ channel = "production",
43
+ }: GetBundlesArgs,
32
44
  ): Promise<UpdateInfo | null> => {
33
45
  await db.exec(createInsertBundleQuerys(bundles));
34
46
 
47
+ const { rows: appVersionList } = await db.query<{
48
+ target_app_version: string;
49
+ }>(
50
+ `
51
+ SELECT target_app_version FROM get_target_app_version_list('${platform}', '${minBundleId}');
52
+ `,
53
+ );
54
+
55
+ const targetAppVersionList = filterCompatibleAppVersions(
56
+ appVersionList?.map((group) => group.target_app_version) ?? [],
57
+ appVersion,
58
+ );
59
+
35
60
  const result = await db.query<{
36
61
  id: string;
37
62
  should_force_update: boolean;
38
- file_url: string;
39
- file_hash: string;
63
+ message: string;
40
64
  status: string;
41
65
  }>(
42
66
  `
43
- SELECT * FROM get_update_info('${platform}', '${appVersion}', '${bundleId}')
44
- `,
67
+ SELECT * FROM get_update_info(
68
+ '${platform}',
69
+ '${appVersion}',
70
+ '${bundleId}',
71
+ '${minBundleId ?? NIL_UUID}',
72
+ '${channel}',
73
+ ARRAY[${targetAppVersionList.map((v) => `'${v}'`).join(",")}]::text[]
74
+ );
75
+ `,
45
76
  );
46
77
 
47
78
  return result.rows[0]
@@ -3,13 +3,15 @@
3
3
  CREATE OR REPLACE FUNCTION get_update_info (
4
4
  app_platform platforms,
5
5
  app_version text,
6
- bundle_id uuid
6
+ bundle_id uuid,
7
+ min_bundle_id uuid,
8
+ target_channel text,
9
+ target_app_version_list text[]
7
10
  )
8
11
  RETURNS TABLE (
9
12
  id uuid,
10
13
  should_force_update boolean,
11
- file_url text,
12
- file_hash text,
14
+ message text,
13
15
  status text
14
16
  )
15
17
  LANGUAGE plpgsql
@@ -19,63 +21,62 @@ DECLARE
19
21
  NIL_UUID CONSTANT uuid := '00000000-0000-0000-0000-000000000000';
20
22
  BEGIN
21
23
  RETURN QUERY
22
- WITH rollback_candidate AS (
24
+ WITH update_candidate AS (
23
25
  SELECT
24
26
  b.id,
25
- -- If status is 'ROLLBACK', should_force_update is always TRUE
26
- TRUE AS should_force_update,
27
- b.file_url,
28
- b.file_hash,
29
- 'ROLLBACK' AS status
27
+ b.should_force_update,
28
+ b.message,
29
+ 'UPDATE' AS status
30
30
  FROM bundles b
31
31
  WHERE b.enabled = TRUE
32
32
  AND b.platform = app_platform
33
- AND b.id < bundle_id
33
+ AND b.id >= bundle_id
34
+ AND b.id > min_bundle_id
35
+ AND b.target_app_version IN (SELECT unnest(target_app_version_list))
36
+ AND b.channel = target_channel
34
37
  ORDER BY b.id DESC
35
38
  LIMIT 1
36
39
  ),
37
- update_candidate AS (
40
+ rollback_candidate AS (
38
41
  SELECT
39
42
  b.id,
40
- b.should_force_update,
41
- b.file_url,
42
- b.file_hash,
43
- 'UPDATE' AS status
43
+ TRUE AS should_force_update,
44
+ b.message,
45
+ 'ROLLBACK' AS status
44
46
  FROM bundles b
45
47
  WHERE b.enabled = TRUE
46
48
  AND b.platform = app_platform
47
- AND b.id >= bundle_id
48
- AND semver_satisfies(b.target_app_version, app_version)
49
+ AND b.id < bundle_id
50
+ AND b.id > min_bundle_id
49
51
  ORDER BY b.id DESC
50
52
  LIMIT 1
51
53
  ),
52
54
  final_result AS (
53
- SELECT *
54
- FROM update_candidate
55
-
55
+ SELECT * FROM update_candidate
56
56
  UNION ALL
57
-
58
- SELECT *
59
- FROM rollback_candidate
57
+ SELECT * FROM rollback_candidate
60
58
  WHERE NOT EXISTS (SELECT 1 FROM update_candidate)
61
59
  )
62
60
  SELECT *
63
- FROM final_result WHERE final_result.id != bundle_id
61
+ FROM final_result
62
+ WHERE final_result.id != bundle_id
64
63
 
65
64
  UNION ALL
66
- /*
67
- When there are no final results and bundle_id != NIL_UUID,
68
- add one fallback row.
69
- This fallback row is also ROLLBACK so shouldForceUpdate = TRUE.
70
- */
65
+
71
66
  SELECT
72
67
  NIL_UUID AS id,
73
- TRUE AS should_force_update, -- Always TRUE
74
- NULL AS file_url,
75
- NULL AS file_hash,
68
+ TRUE AS should_force_update,
69
+ NULL AS message,
76
70
  'ROLLBACK' AS status
77
71
  WHERE (SELECT COUNT(*) FROM final_result) = 0
78
- AND bundle_id != NIL_UUID;
79
-
72
+ AND bundle_id != NIL_UUID
73
+ AND bundle_id > min_bundle_id
74
+ AND NOT EXISTS (
75
+ SELECT 1
76
+ FROM bundles b
77
+ WHERE b.id = bundle_id
78
+ AND b.enabled = TRUE
79
+ AND b.platform = app_platform
80
+ );
80
81
  END;
81
82
  $$;
@@ -1,26 +0,0 @@
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 (targetAppVersion: string, currentVersion: string) => {
12
- const result = await db.query<{ actual: boolean }>(`
13
- SELECT semver_satisfies('${targetAppVersion}', '${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
- });
@@ -1,126 +0,0 @@
1
- -- HotUpdater.semver_satisfies
2
-
3
- CREATE OR REPLACE FUNCTION semver_satisfies(range_expression TEXT, version TEXT)
4
- RETURNS BOOLEAN AS $$
5
- DECLARE
6
- version_parts TEXT[];
7
- version_major INT;
8
- version_minor INT;
9
- version_patch INT;
10
- satisfies BOOLEAN := FALSE;
11
- BEGIN
12
- -- Split the version into major, minor, and patch
13
- version_parts := string_to_array(version, '.');
14
- version_major := version_parts[1]::INT;
15
- version_minor := version_parts[2]::INT;
16
- version_patch := version_parts[3]::INT;
17
-
18
- -- Parse range expression and evaluate
19
- IF range_expression ~ '^\d+\.\d+\.\d+$' THEN
20
- -- Exact match
21
- satisfies := (range_expression = version);
22
-
23
- ELSIF range_expression = '*' THEN
24
- -- Matches any version
25
- satisfies := TRUE;
26
-
27
- ELSIF range_expression ~ '^\d+\.x\.x$' THEN
28
- -- Matches major.x.x
29
- DECLARE
30
- major_range INT := split_part(range_expression, '.', 1)::INT;
31
- BEGIN
32
- satisfies := (version_major = major_range);
33
- END;
34
-
35
- ELSIF range_expression ~ '^\d+\.\d+\.x$' THEN
36
- -- Matches major.minor.x
37
- DECLARE
38
- major_range INT := split_part(range_expression, '.', 1)::INT;
39
- minor_range INT := split_part(range_expression, '.', 2)::INT;
40
- BEGIN
41
- satisfies := (version_major = major_range AND version_minor = minor_range);
42
- END;
43
-
44
- ELSIF range_expression ~ '^\d+\.\d+$' THEN
45
- -- Matches major.minor
46
- DECLARE
47
- major_range INT := split_part(range_expression, '.', 1)::INT;
48
- minor_range INT := split_part(range_expression, '.', 2)::INT;
49
- BEGIN
50
- satisfies := (version_major = major_range AND version_minor = minor_range);
51
- END;
52
-
53
- ELSIF range_expression ~ '^\d+\.\d+\.\d+ - \d+\.\d+\.\d+$' THEN
54
- -- Matches range e.g., 1.2.3 - 1.2.7
55
- DECLARE
56
- lower_bound TEXT := split_part(range_expression, ' - ', 1);
57
- upper_bound TEXT := split_part(range_expression, ' - ', 2);
58
- BEGIN
59
- satisfies := (version >= lower_bound AND version <= upper_bound);
60
- END;
61
-
62
- ELSIF range_expression ~ '^>=\d+\.\d+\.\d+ <\d+\.\d+\.\d+$' THEN
63
- -- Matches range with inequalities
64
- DECLARE
65
- lower_bound TEXT := regexp_replace(range_expression, '>=([\d\.]+) <.*', '\1');
66
- upper_bound TEXT := regexp_replace(range_expression, '.*<([\d\.]+)', '\1');
67
- BEGIN
68
- satisfies := (version >= lower_bound AND version < upper_bound);
69
- END;
70
-
71
- ELSIF range_expression ~ '^~\d+\.\d+\.\d+$' THEN
72
- -- Matches ~1.2.3 (>=1.2.3 <1.3.0)
73
- DECLARE
74
- lower_bound TEXT := regexp_replace(range_expression, '~', '');
75
- upper_bound_major INT := split_part(lower_bound, '.', 1)::INT;
76
- upper_bound_minor INT := split_part(lower_bound, '.', 2)::INT + 1;
77
- upper_bound TEXT := upper_bound_major || '.' || upper_bound_minor || '.0';
78
- BEGIN
79
- satisfies := (version >= lower_bound AND version < upper_bound);
80
- END;
81
-
82
- ELSIF range_expression ~ '^\^\d+\.\d+\.\d+$' THEN
83
- -- Matches ^1.2.3 (>=1.2.3 <2.0.0)
84
- DECLARE
85
- lower_bound TEXT := regexp_replace(range_expression, '\^', '');
86
- upper_bound_major INT := split_part(lower_bound, '.', 1)::INT + 1;
87
- upper_bound TEXT := upper_bound_major || '.0.0';
88
- BEGIN
89
- satisfies := (version >= lower_bound AND version < upper_bound);
90
- END;
91
-
92
- -- [Added] 1) Single major version pattern '^(\d+)$'
93
- ELSIF range_expression ~ '^\d+$' THEN
94
- /*
95
- e.g.) "1" is interpreted as (>=1.0.0 <2.0.0) in semver range
96
- "2" would be interpreted as (>=2.0.0 <3.0.0)
97
- */
98
- DECLARE
99
- major_range INT := range_expression::INT;
100
- lower_bound TEXT := major_range || '.0.0';
101
- upper_bound TEXT := (major_range + 1) || '.0.0';
102
- BEGIN
103
- satisfies := (version >= lower_bound AND version < upper_bound);
104
- END;
105
-
106
- -- [Added] 2) major.x pattern '^(\d+)\.x$'
107
- ELSIF range_expression ~ '^\d+\.x$' THEN
108
- /*
109
- e.g.) "2.x" => as long as major=2 matches, any minor and patch is OK
110
- effectively works like (>=2.0.0 <3.0.0)
111
- */
112
- DECLARE
113
- major_range INT := split_part(range_expression, '.', 1)::INT;
114
- lower_bound TEXT := major_range || '.0.0';
115
- upper_bound TEXT := (major_range + 1) || '.0.0';
116
- BEGIN
117
- satisfies := (version >= lower_bound AND version < upper_bound);
118
- END;
119
-
120
- ELSE
121
- RAISE EXCEPTION 'Unsupported range expression: %', range_expression;
122
- END IF;
123
-
124
- RETURN satisfies;
125
- END;
126
- $$ LANGUAGE plpgsql;