@hot-updater/supabase 0.12.7 → 0.13.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/supabase",
3
3
  "type": "module",
4
- "version": "0.12.7",
4
+ "version": "0.13.1",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.js",
@@ -10,6 +10,15 @@
10
10
  ".": {
11
11
  "import": "./dist/index.js",
12
12
  "require": "./dist/index.cjs"
13
+ },
14
+ "./edge-functions": {
15
+ "import": "./supabase/index.ts",
16
+ "require": "./supabase/index.ts"
17
+ },
18
+ "./iac": {
19
+ "types": "./dist/iac/index.d.ts",
20
+ "import": "./dist/iac/index.js",
21
+ "require": "./dist/iac/index.cjs"
13
22
  }
14
23
  },
15
24
  "license": "MIT",
@@ -30,18 +39,29 @@
30
39
  "package.json"
31
40
  ],
32
41
  "dependencies": {
33
- "@hot-updater/core": "0.12.7",
34
- "@hot-updater/plugin-core": "0.12.7",
42
+ "@hot-updater/core": "0.13.1",
43
+ "@hot-updater/plugin-core": "0.13.1",
35
44
  "@supabase/supabase-js": "^2.47.10"
36
45
  },
37
46
  "devDependencies": {
38
- "picocolors": "^1.0.0",
39
- "@hot-updater/postgres": "0.12.7",
47
+ "@hot-updater/postgres": "0.13.1",
40
48
  "dayjs": "^1.11.13",
41
- "mime": "^4.0.4"
49
+ "es-toolkit": "^1.32.0",
50
+ "execa": "^9.5.2",
51
+ "mime": "^4.0.4",
52
+ "picocolors": "^1.0.0",
53
+ "@clack/prompts": "^0.10.0"
54
+ },
55
+ "peerDependencies": {
56
+ "@clack/prompts": "*"
57
+ },
58
+ "peerDependenciesMeta": {
59
+ "@clack/prompts": {
60
+ "optional": true
61
+ }
42
62
  },
43
63
  "scripts": {
44
- "build": "rslib build",
64
+ "build": "tsup",
45
65
  "test:type": "tsc --noEmit",
46
66
  "make-migrations": "node --experimental-strip-types ./scripts/make-migrations.ts"
47
67
  }
@@ -1,6 +1,37 @@
1
1
  import "jsr:@supabase/functions-js/edge-runtime.d.ts";
2
2
  import camelcaseKeys from "npm:camelcase-keys@9.1.3";
3
- import { createClient } from "jsr:@supabase/supabase-js@2.47.10";
3
+ import semver from "npm:semver@7.7.1";
4
+ import { createClient } from "jsr:@supabase/supabase-js@2.49.1";
5
+
6
+ const NIL_UUID = "00000000-0000-0000-0000-000000000000";
7
+
8
+ const semverSatisfies = (targetAppVersion: string, currentVersion: string) => {
9
+ const currentCoerce = semver.coerce(currentVersion);
10
+ if (!currentCoerce) {
11
+ return false;
12
+ }
13
+
14
+ return semver.satisfies(currentCoerce.version, targetAppVersion);
15
+ };
16
+
17
+ /**
18
+ * Filters target app versions that are compatible with the current app version.
19
+ * Returns only versions that are compatible with the current version according to semver rules.
20
+ *
21
+ * @param targetAppVersionList - List of target app versions to filter
22
+ * @param currentVersion - Current app version
23
+ * @returns Array of target app versions compatible with the current version
24
+ */
25
+ export const filterCompatibleAppVersions = (
26
+ targetAppVersionList: string[],
27
+ currentVersion: string,
28
+ ) => {
29
+ const compatibleAppVersionList = targetAppVersionList.filter((version) =>
30
+ semverSatisfies(version, currentVersion),
31
+ );
32
+
33
+ return compatibleAppVersionList.sort((a, b) => b.localeCompare(a));
34
+ };
4
35
 
5
36
  const createErrorResponse = (message: string, statusCode: number) => {
6
37
  return new Response(JSON.stringify({ code: statusCode, message }), {
@@ -9,21 +40,29 @@ const createErrorResponse = (message: string, statusCode: number) => {
9
40
  });
10
41
  };
11
42
 
43
+ declare global {
44
+ var HotUpdater: {
45
+ BUCKET_NAME: string;
46
+ };
47
+ }
48
+
12
49
  Deno.serve(async (req) => {
13
50
  try {
14
51
  const supabase = createClient(
15
52
  Deno.env.get("SUPABASE_URL") ?? "",
16
- Deno.env.get("SUPABASE_ANON_KEY") ?? "",
53
+ Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "",
17
54
  {
18
- global: {
19
- headers: { Authorization: req.headers.get("Authorization")! },
20
- },
55
+ auth: { autoRefreshToken: false, persistSession: false },
21
56
  },
22
57
  );
23
58
 
24
59
  const bundleId = req.headers.get("x-bundle-id") as string;
25
60
  const appPlatform = req.headers.get("x-app-platform") as "ios" | "android";
26
61
  const appVersion = req.headers.get("x-app-version") as string;
62
+ const minBundleId = req.headers.get("x-min-bundle-id") as
63
+ | string
64
+ | undefined;
65
+ const channel = req.headers.get("x-channel") as string | undefined;
27
66
 
28
67
  if (!bundleId || !appPlatform || !appVersion) {
29
68
  return createErrorResponse(
@@ -32,10 +71,25 @@ Deno.serve(async (req) => {
32
71
  );
33
72
  }
34
73
 
74
+ const { data: appVersionList } = await supabase.rpc(
75
+ "get_target_app_version_list",
76
+ {
77
+ app_platform: appPlatform,
78
+ min_bundle_id: minBundleId || NIL_UUID,
79
+ },
80
+ );
81
+ const compatibleAppVersionList = filterCompatibleAppVersions(
82
+ appVersionList?.map((group) => group.target_app_version) ?? [],
83
+ appVersion,
84
+ );
85
+
35
86
  const { data, error } = await supabase.rpc("get_update_info", {
36
87
  app_platform: appPlatform,
37
88
  app_version: appVersion,
38
89
  bundle_id: bundleId,
90
+ min_bundle_id: minBundleId || NIL_UUID,
91
+ target_channel: channel || "production",
92
+ target_app_version_list: compatibleAppVersionList,
39
93
  });
40
94
 
41
95
  if (error) {
@@ -43,11 +97,44 @@ Deno.serve(async (req) => {
43
97
  }
44
98
 
45
99
  const response = data[0] ? camelcaseKeys(data[0]) : null;
46
- return new Response(JSON.stringify(response), {
47
- headers: { "Content-Type": "application/json" },
48
- status: 200,
49
- });
100
+ if (!response) {
101
+ return new Response(JSON.stringify(null), {
102
+ headers: { "Content-Type": "application/json" },
103
+ status: 200,
104
+ });
105
+ }
106
+
107
+ if (response.id === NIL_UUID) {
108
+ return new Response(
109
+ JSON.stringify({
110
+ ...response,
111
+ fileUrl: null,
112
+ }),
113
+ {
114
+ headers: { "Content-Type": "application/json" },
115
+ status: 200,
116
+ },
117
+ );
118
+ }
119
+
120
+ const { data: signedUrlData } = await supabase.storage
121
+ .from(HotUpdater.BUCKET_NAME)
122
+ .createSignedUrl([response.id, "bundle.zip"].join("/"), 60);
123
+
124
+ return new Response(
125
+ JSON.stringify({
126
+ ...response,
127
+ fileUrl: signedUrlData?.signedUrl ?? null,
128
+ }),
129
+ {
130
+ headers: { "Content-Type": "application/json" },
131
+ status: 200,
132
+ },
133
+ );
50
134
  } catch (err: unknown) {
51
- return createErrorResponse(JSON.stringify(err), 500);
135
+ return createErrorResponse(
136
+ err instanceof Error ? err.message : "Unknown error",
137
+ 500,
138
+ );
52
139
  }
53
140
  });
@@ -0,0 +1,134 @@
1
+ -- HotUpdater.semver_satisfies
2
+ DROP FUNCTION IF EXISTS semver_satisfies;
3
+
4
+ -- HotUpdater.get_update_info
5
+ DROP FUNCTION IF EXISTS get_update_info;
6
+
7
+ -- HotUpdater.get_update_info
8
+ CREATE OR REPLACE FUNCTION get_update_info (
9
+ app_platform platforms,
10
+ app_version text,
11
+ bundle_id uuid,
12
+ min_bundle_id uuid,
13
+ target_channel text,
14
+ target_app_version_list text[]
15
+ )
16
+ RETURNS TABLE (
17
+ id uuid,
18
+ should_force_update boolean,
19
+ message text,
20
+ status text
21
+ )
22
+ LANGUAGE plpgsql
23
+ AS
24
+ $$
25
+ DECLARE
26
+ NIL_UUID CONSTANT uuid := '00000000-0000-0000-0000-000000000000';
27
+ BEGIN
28
+ RETURN QUERY
29
+ WITH update_candidate AS (
30
+ SELECT
31
+ b.id,
32
+ b.should_force_update,
33
+ b.message,
34
+ 'UPDATE' AS status
35
+ FROM bundles b
36
+ WHERE b.enabled = TRUE
37
+ AND b.platform = app_platform
38
+ AND b.id >= bundle_id
39
+ AND b.id > min_bundle_id
40
+ AND b.target_app_version IN (SELECT unnest(target_app_version_list))
41
+ AND b.channel = target_channel
42
+ ORDER BY b.id DESC
43
+ LIMIT 1
44
+ ),
45
+ rollback_candidate AS (
46
+ SELECT
47
+ b.id,
48
+ TRUE AS should_force_update,
49
+ b.message,
50
+ 'ROLLBACK' AS status
51
+ FROM bundles b
52
+ WHERE b.enabled = TRUE
53
+ AND b.platform = app_platform
54
+ AND b.id < bundle_id
55
+ AND b.id > min_bundle_id
56
+ ORDER BY b.id DESC
57
+ LIMIT 1
58
+ ),
59
+ final_result AS (
60
+ SELECT * FROM update_candidate
61
+ UNION ALL
62
+ SELECT * FROM rollback_candidate
63
+ WHERE NOT EXISTS (SELECT 1 FROM update_candidate)
64
+ )
65
+ SELECT *
66
+ FROM final_result
67
+ WHERE final_result.id != bundle_id
68
+
69
+ UNION ALL
70
+
71
+ SELECT
72
+ NIL_UUID AS id,
73
+ TRUE AS should_force_update,
74
+ NULL AS message,
75
+ 'ROLLBACK' AS status
76
+ WHERE (SELECT COUNT(*) FROM final_result) = 0
77
+ AND bundle_id != NIL_UUID
78
+ AND bundle_id > min_bundle_id
79
+ AND NOT EXISTS (
80
+ SELECT 1
81
+ FROM bundles b
82
+ WHERE b.id = bundle_id
83
+ AND b.enabled = TRUE
84
+ AND b.platform = app_platform
85
+ );
86
+ END;
87
+ $$;
88
+
89
+ -- HotUpdater.bundles
90
+ ALTER TABLE bundles
91
+ ADD COLUMN channel text NOT NULL DEFAULT 'production';
92
+
93
+ ALTER TABLE bundles
94
+ DROP COLUMN file_url;
95
+
96
+ -- HotUpdater.get_target_app_version_list
97
+
98
+ CREATE OR REPLACE FUNCTION get_target_app_version_list (
99
+ app_platform platforms,
100
+ min_bundle_id uuid
101
+ )
102
+ RETURNS TABLE (
103
+ target_app_version text
104
+ )
105
+ LANGUAGE plpgsql
106
+ AS
107
+ $$
108
+ BEGIN
109
+ RETURN QUERY
110
+ SELECT b.target_app_version
111
+ FROM bundles b
112
+ WHERE b.platform = app_platform
113
+ AND b.id >= min_bundle_id
114
+ GROUP BY b.target_app_version;
115
+ END;
116
+ $$;
117
+
118
+ -- HotUpdater.get_channels
119
+ CREATE OR REPLACE FUNCTION get_channels ()
120
+ RETURNS TABLE (
121
+ channel text
122
+ )
123
+ LANGUAGE plpgsql
124
+ AS
125
+ $$
126
+ BEGIN
127
+ RETURN QUERY
128
+ SELECT b.channel
129
+ FROM bundles b
130
+ GROUP BY b.channel;
131
+ END;
132
+ $$;
133
+
134
+ CREATE INDEX bundles_channel_idx ON bundles(channel);
@@ -1,6 +0,0 @@
1
- import type { BasePluginArgs, DatabasePlugin, DatabasePluginHooks } from "@hot-updater/plugin-core";
2
- export interface SupabaseDatabaseConfig {
3
- supabaseUrl: string;
4
- supabaseAnonKey: string;
5
- }
6
- export declare const supabaseDatabase: (config: SupabaseDatabaseConfig, hooks?: DatabasePluginHooks) => (_: BasePluginArgs) => DatabasePlugin;
@@ -1,7 +0,0 @@
1
- import type { BasePluginArgs, StoragePlugin, StoragePluginHooks } from "@hot-updater/plugin-core";
2
- export interface SupabaseStorageConfig {
3
- supabaseUrl: string;
4
- supabaseAnonKey: string;
5
- bucketName: string;
6
- }
7
- export declare const supabaseStorage: (config: SupabaseStorageConfig, hooks?: StoragePluginHooks) => (_: BasePluginArgs) => StoragePlugin;
package/dist/types.d.ts DELETED
@@ -1,17 +0,0 @@
1
- import type { SnakeCaseBundle } from "@hot-updater/core";
2
- export type Database = {
3
- public: {
4
- Tables: {
5
- bundles: {
6
- Row: SnakeCaseBundle;
7
- Insert: SnakeCaseBundle;
8
- Update: SnakeCaseBundle;
9
- Relationships: [];
10
- };
11
- };
12
- Views: {
13
- [_ in never]: never;
14
- };
15
- Functions: any;
16
- };
17
- };
@@ -1,2 +0,0 @@
1
- export * from "./supabaseApi";
2
- export * from "./templates";
@@ -1,14 +0,0 @@
1
- export interface SupabaseApi {
2
- listBuckets: () => Promise<{
3
- id: string;
4
- name: string;
5
- isPublic: boolean;
6
- createdAt: string;
7
- }[]>;
8
- createBucket: (bucketName: string, options: {
9
- public: boolean;
10
- }) => Promise<{
11
- name: string;
12
- }>;
13
- }
14
- export declare const supabaseApi: (supabaseUrl: string, supabaseAnonKey: string) => SupabaseApi;
@@ -1 +0,0 @@
1
- export declare const supabaseConfigTomlTemplate = "\nproject_id = \"%%projectId%%\"\n\n[db.seed]\nenabled = false\n";