@hot-updater/supabase 0.28.0 → 0.29.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.
@@ -0,0 +1,503 @@
1
+ -- HotUpdater.bundles
2
+
3
+ ALTER TABLE bundles
4
+ ADD COLUMN IF NOT EXISTS rollout_cohort_count INTEGER DEFAULT 1000
5
+ CHECK (rollout_cohort_count >= 0 AND rollout_cohort_count <= 1000);
6
+
7
+ CREATE INDEX IF NOT EXISTS bundles_rollout_idx ON bundles(rollout_cohort_count);
8
+
9
+ ALTER TABLE bundles
10
+ ADD COLUMN IF NOT EXISTS target_cohorts TEXT[];
11
+
12
+ CREATE INDEX IF NOT EXISTS bundles_target_cohorts_idx ON bundles
13
+ USING GIN (target_cohorts);
14
+
15
+ -- HotUpdater.is_cohort_eligible
16
+ -- Cohort eligibility helpers matching @hot-updater/core rollout.ts
17
+
18
+ CREATE OR REPLACE FUNCTION positive_mod(
19
+ value INTEGER,
20
+ modulus INTEGER
21
+ )
22
+ RETURNS INTEGER
23
+ LANGUAGE plpgsql
24
+ IMMUTABLE
25
+ AS $$
26
+ BEGIN
27
+ RETURN ((value % modulus) + modulus) % modulus;
28
+ END;
29
+ $$;
30
+
31
+ CREATE OR REPLACE FUNCTION hash_rollout_value(input TEXT)
32
+ RETURNS INTEGER
33
+ LANGUAGE plpgsql
34
+ IMMUTABLE
35
+ AS $$
36
+ DECLARE
37
+ hash_value NUMERIC := 0;
38
+ char_code INTEGER;
39
+ i INTEGER;
40
+ BEGIN
41
+ FOR i IN 1..length(input) LOOP
42
+ char_code := ascii(substring(input from i for 1));
43
+ hash_value := mod((hash_value * 31) + char_code, 4294967296);
44
+ END LOOP;
45
+
46
+ IF hash_value >= 2147483648 THEN
47
+ hash_value := hash_value - 4294967296;
48
+ END IF;
49
+
50
+ RETURN hash_value::INTEGER;
51
+ END;
52
+ $$;
53
+
54
+ CREATE OR REPLACE FUNCTION normalize_cohort_value(cohort TEXT)
55
+ RETURNS TEXT
56
+ LANGUAGE plpgsql
57
+ IMMUTABLE
58
+ AS $$
59
+ DECLARE
60
+ normalized TEXT;
61
+ cohort_value INTEGER;
62
+ BEGIN
63
+ IF cohort IS NULL THEN
64
+ RETURN NULL;
65
+ END IF;
66
+
67
+ normalized := lower(btrim(cohort));
68
+
69
+ IF normalized ~ '^[0-9]+$' THEN
70
+ cohort_value := normalized::INTEGER;
71
+ IF cohort_value BETWEEN 1 AND 1000 THEN
72
+ RETURN cohort_value::TEXT;
73
+ END IF;
74
+ END IF;
75
+
76
+ RETURN normalized;
77
+ END;
78
+ $$;
79
+
80
+ CREATE OR REPLACE FUNCTION gcd_int(a INTEGER, b INTEGER)
81
+ RETURNS INTEGER
82
+ LANGUAGE plpgsql
83
+ IMMUTABLE
84
+ AS $$
85
+ DECLARE
86
+ x INTEGER := abs(a);
87
+ y INTEGER := abs(b);
88
+ next_value INTEGER;
89
+ BEGIN
90
+ WHILE y <> 0 LOOP
91
+ next_value := x % y;
92
+ x := y;
93
+ y := next_value;
94
+ END LOOP;
95
+
96
+ RETURN x;
97
+ END;
98
+ $$;
99
+
100
+ CREATE OR REPLACE FUNCTION get_rollout_multiplier(bundle_id UUID)
101
+ RETURNS INTEGER
102
+ LANGUAGE plpgsql
103
+ IMMUTABLE
104
+ AS $$
105
+ DECLARE
106
+ candidate INTEGER := positive_mod(
107
+ hash_rollout_value(bundle_id::TEXT || ':multiplier'),
108
+ 997
109
+ );
110
+ BEGIN
111
+ IF candidate = 0 THEN
112
+ candidate := 1;
113
+ END IF;
114
+
115
+ WHILE gcd_int(candidate, 1000) <> 1 LOOP
116
+ candidate := positive_mod(candidate + 1, 1000);
117
+ IF candidate = 0 THEN
118
+ candidate := 1;
119
+ END IF;
120
+ END LOOP;
121
+
122
+ RETURN candidate;
123
+ END;
124
+ $$;
125
+
126
+ CREATE OR REPLACE FUNCTION get_rollout_offset(bundle_id UUID)
127
+ RETURNS INTEGER
128
+ LANGUAGE plpgsql
129
+ IMMUTABLE
130
+ AS $$
131
+ BEGIN
132
+ RETURN positive_mod(hash_rollout_value(bundle_id::TEXT || ':offset'), 1000);
133
+ END;
134
+ $$;
135
+
136
+ CREATE OR REPLACE FUNCTION get_modular_inverse(value INTEGER, modulus INTEGER)
137
+ RETURNS INTEGER
138
+ LANGUAGE plpgsql
139
+ IMMUTABLE
140
+ AS $$
141
+ DECLARE
142
+ candidate INTEGER;
143
+ BEGIN
144
+ FOR candidate IN 1..(modulus - 1) LOOP
145
+ IF positive_mod(value * candidate, modulus) = 1 THEN
146
+ RETURN candidate;
147
+ END IF;
148
+ END LOOP;
149
+
150
+ RAISE EXCEPTION 'No modular inverse for % mod %', value, modulus;
151
+ END;
152
+ $$;
153
+
154
+ CREATE OR REPLACE FUNCTION is_numeric_cohort(cohort TEXT)
155
+ RETURNS BOOLEAN
156
+ LANGUAGE plpgsql
157
+ IMMUTABLE
158
+ AS $$
159
+ DECLARE
160
+ normalized_cohort TEXT := normalize_cohort_value(cohort);
161
+ cohort_value INTEGER;
162
+ BEGIN
163
+ IF normalized_cohort IS NULL OR normalized_cohort !~ '^[0-9]+$' THEN
164
+ RETURN FALSE;
165
+ END IF;
166
+
167
+ cohort_value := normalized_cohort::INTEGER;
168
+ RETURN cohort_value BETWEEN 1 AND 1000;
169
+ END;
170
+ $$;
171
+
172
+ CREATE OR REPLACE FUNCTION get_numeric_cohort_rollout_position(
173
+ bundle_id UUID,
174
+ cohort TEXT
175
+ )
176
+ RETURNS INTEGER
177
+ LANGUAGE plpgsql
178
+ IMMUTABLE
179
+ AS $$
180
+ DECLARE
181
+ normalized_cohort TEXT := normalize_cohort_value(cohort);
182
+ cohort_value INTEGER;
183
+ multiplier INTEGER;
184
+ offset_value INTEGER;
185
+ inverse_multiplier INTEGER;
186
+ BEGIN
187
+ IF NOT is_numeric_cohort(normalized_cohort) THEN
188
+ RAISE EXCEPTION 'Invalid numeric cohort: %', cohort;
189
+ END IF;
190
+
191
+ cohort_value := normalized_cohort::INTEGER - 1;
192
+ multiplier := get_rollout_multiplier(bundle_id);
193
+ offset_value := get_rollout_offset(bundle_id);
194
+ inverse_multiplier := get_modular_inverse(multiplier, 1000);
195
+
196
+ RETURN positive_mod(
197
+ inverse_multiplier * (cohort_value - offset_value),
198
+ 1000
199
+ );
200
+ END;
201
+ $$;
202
+
203
+ CREATE OR REPLACE FUNCTION is_cohort_eligible(
204
+ bundle_id UUID,
205
+ cohort TEXT,
206
+ rollout_cohort_count INTEGER,
207
+ target_cohorts TEXT[]
208
+ )
209
+ RETURNS BOOLEAN
210
+ LANGUAGE plpgsql
211
+ IMMUTABLE
212
+ AS $$
213
+ DECLARE
214
+ normalized_cohort TEXT := normalize_cohort_value(cohort);
215
+ normalized_rollout_count INTEGER := COALESCE(rollout_cohort_count, 1000);
216
+ normalized_target_cohorts TEXT[];
217
+ BEGIN
218
+ IF target_cohorts IS NOT NULL THEN
219
+ normalized_target_cohorts := ARRAY(
220
+ SELECT normalize_cohort_value(value)
221
+ FROM unnest(target_cohorts) AS value
222
+ );
223
+ END IF;
224
+
225
+ IF normalized_target_cohorts IS NOT NULL
226
+ AND array_length(normalized_target_cohorts, 1) > 0 THEN
227
+ RETURN normalized_cohort IS NOT NULL
228
+ AND normalized_cohort = ANY(normalized_target_cohorts);
229
+ END IF;
230
+
231
+ IF normalized_rollout_count <= 0 THEN
232
+ RETURN FALSE;
233
+ END IF;
234
+
235
+ IF normalized_cohort IS NULL THEN
236
+ RETURN normalized_rollout_count >= 1000;
237
+ END IF;
238
+
239
+ IF NOT is_numeric_cohort(normalized_cohort) THEN
240
+ RETURN FALSE;
241
+ END IF;
242
+
243
+ IF normalized_rollout_count >= 1000 THEN
244
+ RETURN TRUE;
245
+ END IF;
246
+
247
+ RETURN get_numeric_cohort_rollout_position(bundle_id, normalized_cohort)
248
+ < normalized_rollout_count;
249
+ END;
250
+ $$;
251
+
252
+ -- HotUpdater.get_update_info_by_fingerprint_hash
253
+
254
+ DROP FUNCTION IF EXISTS get_update_info_by_fingerprint_hash;
255
+
256
+ CREATE OR REPLACE FUNCTION get_update_info_by_fingerprint_hash (
257
+ app_platform platforms,
258
+ bundle_id uuid,
259
+ min_bundle_id uuid,
260
+ target_channel text,
261
+ target_fingerprint_hash text,
262
+ cohort TEXT DEFAULT NULL
263
+ )
264
+ RETURNS TABLE (
265
+ id uuid,
266
+ should_force_update boolean,
267
+ message text,
268
+ status text,
269
+ storage_uri text,
270
+ file_hash text
271
+ )
272
+ LANGUAGE plpgsql
273
+ AS
274
+ $$
275
+ DECLARE
276
+ NIL_UUID CONSTANT uuid := '00000000-0000-0000-0000-000000000000';
277
+ BEGIN
278
+ RETURN QUERY
279
+ WITH candidate_bundles AS (
280
+ SELECT
281
+ b.id,
282
+ b.should_force_update,
283
+ b.message,
284
+ b.storage_uri,
285
+ b.file_hash,
286
+ b.rollout_cohort_count,
287
+ b.target_cohorts
288
+ FROM bundles b
289
+ WHERE b.enabled = TRUE
290
+ AND b.platform = app_platform
291
+ AND b.id >= min_bundle_id
292
+ AND b.channel = target_channel
293
+ AND b.fingerprint_hash = target_fingerprint_hash
294
+ ),
295
+ current_candidate AS (
296
+ SELECT
297
+ cb.id,
298
+ is_cohort_eligible(
299
+ cb.id,
300
+ cohort,
301
+ cb.rollout_cohort_count,
302
+ cb.target_cohorts
303
+ ) AS is_eligible
304
+ FROM candidate_bundles cb
305
+ WHERE cb.id = bundle_id
306
+ LIMIT 1
307
+ ),
308
+ eligible_update_candidate AS (
309
+ SELECT
310
+ cb.id,
311
+ cb.should_force_update,
312
+ cb.message,
313
+ 'UPDATE' AS status,
314
+ cb.storage_uri,
315
+ cb.file_hash
316
+ FROM candidate_bundles cb
317
+ WHERE cb.id > bundle_id
318
+ AND is_cohort_eligible(
319
+ cb.id,
320
+ cohort,
321
+ cb.rollout_cohort_count,
322
+ cb.target_cohorts
323
+ )
324
+ ORDER BY cb.id DESC
325
+ LIMIT 1
326
+ ),
327
+ rollback_candidate AS (
328
+ SELECT
329
+ cb.id,
330
+ TRUE AS should_force_update,
331
+ cb.message,
332
+ 'ROLLBACK' AS status,
333
+ cb.storage_uri,
334
+ cb.file_hash
335
+ FROM candidate_bundles cb
336
+ WHERE cb.id < bundle_id
337
+ AND NOT EXISTS (
338
+ SELECT 1
339
+ FROM current_candidate
340
+ WHERE current_candidate.is_eligible = TRUE
341
+ )
342
+ AND NOT EXISTS (SELECT 1 FROM eligible_update_candidate)
343
+ ORDER BY cb.id DESC
344
+ LIMIT 1
345
+ ),
346
+ final_result AS (
347
+ SELECT * FROM eligible_update_candidate
348
+ UNION ALL
349
+ SELECT * FROM rollback_candidate
350
+ WHERE NOT EXISTS (SELECT 1 FROM eligible_update_candidate)
351
+ )
352
+ SELECT *
353
+ FROM final_result
354
+ WHERE final_result.id != bundle_id
355
+
356
+ UNION ALL
357
+
358
+ SELECT
359
+ NIL_UUID AS id,
360
+ TRUE AS should_force_update,
361
+ NULL AS message,
362
+ 'ROLLBACK' AS status,
363
+ NULL AS storage_uri,
364
+ NULL AS file_hash
365
+ WHERE (SELECT COUNT(*) FROM final_result) = 0
366
+ AND bundle_id != NIL_UUID
367
+ AND bundle_id > min_bundle_id
368
+ AND NOT EXISTS (
369
+ SELECT 1
370
+ FROM current_candidate
371
+ WHERE current_candidate.is_eligible = TRUE
372
+ )
373
+ AND NOT EXISTS (SELECT 1 FROM eligible_update_candidate)
374
+ AND NOT EXISTS (SELECT 1 FROM rollback_candidate);
375
+ END;
376
+ $$;
377
+
378
+ -- HotUpdater.get_update_info_by_app_version
379
+
380
+ DROP FUNCTION IF EXISTS get_update_info_by_app_version;
381
+
382
+ CREATE OR REPLACE FUNCTION get_update_info_by_app_version (
383
+ app_platform platforms,
384
+ app_version text,
385
+ bundle_id uuid,
386
+ min_bundle_id uuid,
387
+ target_channel text,
388
+ target_app_version_list text[],
389
+ cohort TEXT DEFAULT NULL
390
+ )
391
+ RETURNS TABLE (
392
+ id uuid,
393
+ should_force_update boolean,
394
+ message text,
395
+ status text,
396
+ storage_uri text,
397
+ file_hash text
398
+ )
399
+ LANGUAGE plpgsql
400
+ AS
401
+ $$
402
+ DECLARE
403
+ NIL_UUID CONSTANT uuid := '00000000-0000-0000-0000-000000000000';
404
+ BEGIN
405
+ RETURN QUERY
406
+ WITH candidate_bundles AS (
407
+ SELECT
408
+ b.id,
409
+ b.should_force_update,
410
+ b.message,
411
+ b.storage_uri,
412
+ b.file_hash,
413
+ b.rollout_cohort_count,
414
+ b.target_cohorts
415
+ FROM bundles b
416
+ WHERE b.enabled = TRUE
417
+ AND b.platform = app_platform
418
+ AND b.id >= min_bundle_id
419
+ AND b.target_app_version IN (SELECT unnest(target_app_version_list))
420
+ AND b.channel = target_channel
421
+ ),
422
+ current_candidate AS (
423
+ SELECT
424
+ cb.id,
425
+ is_cohort_eligible(
426
+ cb.id,
427
+ cohort,
428
+ cb.rollout_cohort_count,
429
+ cb.target_cohorts
430
+ ) AS is_eligible
431
+ FROM candidate_bundles cb
432
+ WHERE cb.id = bundle_id
433
+ LIMIT 1
434
+ ),
435
+ eligible_update_candidate AS (
436
+ SELECT
437
+ cb.id,
438
+ cb.should_force_update,
439
+ cb.message,
440
+ 'UPDATE' AS status,
441
+ cb.storage_uri,
442
+ cb.file_hash
443
+ FROM candidate_bundles cb
444
+ WHERE cb.id > bundle_id
445
+ AND is_cohort_eligible(
446
+ cb.id,
447
+ cohort,
448
+ cb.rollout_cohort_count,
449
+ cb.target_cohorts
450
+ )
451
+ ORDER BY cb.id DESC
452
+ LIMIT 1
453
+ ),
454
+ rollback_candidate AS (
455
+ SELECT
456
+ cb.id,
457
+ TRUE AS should_force_update,
458
+ cb.message,
459
+ 'ROLLBACK' AS status,
460
+ cb.storage_uri,
461
+ cb.file_hash
462
+ FROM candidate_bundles cb
463
+ WHERE cb.id < bundle_id
464
+ AND NOT EXISTS (
465
+ SELECT 1
466
+ FROM current_candidate
467
+ WHERE current_candidate.is_eligible = TRUE
468
+ )
469
+ AND NOT EXISTS (SELECT 1 FROM eligible_update_candidate)
470
+ ORDER BY cb.id DESC
471
+ LIMIT 1
472
+ ),
473
+ final_result AS (
474
+ SELECT * FROM eligible_update_candidate
475
+ UNION ALL
476
+ SELECT * FROM rollback_candidate
477
+ WHERE NOT EXISTS (SELECT 1 FROM eligible_update_candidate)
478
+ )
479
+ SELECT *
480
+ FROM final_result
481
+ WHERE final_result.id != bundle_id
482
+
483
+ UNION ALL
484
+
485
+ SELECT
486
+ NIL_UUID AS id,
487
+ TRUE AS should_force_update,
488
+ NULL AS message,
489
+ 'ROLLBACK' AS status,
490
+ NULL AS storage_uri,
491
+ NULL AS file_hash
492
+ WHERE (SELECT COUNT(*) FROM final_result) = 0
493
+ AND bundle_id != NIL_UUID
494
+ AND bundle_id > min_bundle_id
495
+ AND NOT EXISTS (
496
+ SELECT 1
497
+ FROM current_candidate
498
+ WHERE current_candidate.is_eligible = TRUE
499
+ )
500
+ AND NOT EXISTS (SELECT 1 FROM eligible_update_candidate)
501
+ AND NOT EXISTS (SELECT 1 FROM rollback_candidate);
502
+ END;
503
+ $$;
package/dist/index.d.ts DELETED
@@ -1,22 +0,0 @@
1
- import * as _hot_updater_plugin_core1 from "@hot-updater/plugin-core";
2
-
3
- //#region src/supabaseDatabase.d.ts
4
- interface SupabaseDatabaseConfig {
5
- supabaseUrl: string;
6
- supabaseAnonKey: string;
7
- }
8
- declare const supabaseDatabase: (config: SupabaseDatabaseConfig, hooks?: _hot_updater_plugin_core1.DatabasePluginHooks) => (() => _hot_updater_plugin_core1.DatabasePlugin);
9
- //#endregion
10
- //#region src/supabaseStorage.d.ts
11
- interface SupabaseStorageConfig {
12
- supabaseUrl: string;
13
- supabaseAnonKey: string;
14
- bucketName: string;
15
- /**
16
- * Base path where bundles will be stored in the bucket
17
- */
18
- basePath?: string;
19
- }
20
- declare const supabaseStorage: (config: SupabaseStorageConfig, hooks?: _hot_updater_plugin_core1.StoragePluginHooks) => () => _hot_updater_plugin_core1.StoragePlugin;
21
- //#endregion
22
- export { SupabaseDatabaseConfig, SupabaseStorageConfig, supabaseDatabase, supabaseStorage };
package/dist/index.js DELETED
@@ -1,145 +0,0 @@
1
- import { calculatePagination, createDatabasePlugin, createStorageKeyBuilder, createStoragePlugin, getContentType, parseStorageUri } from "@hot-updater/plugin-core";
2
- import { createClient } from "@supabase/supabase-js";
3
- import fs from "fs/promises";
4
- import path from "path";
5
-
6
- //#region src/supabaseDatabase.ts
7
- const supabaseDatabase = createDatabasePlugin({
8
- name: "supabaseDatabase",
9
- factory: (config) => {
10
- const supabase = createClient(config.supabaseUrl, config.supabaseAnonKey);
11
- return {
12
- async getBundleById(bundleId) {
13
- const { data, error } = await supabase.from("bundles").select("channel, enabled, should_force_update, file_hash, git_commit_hash, id, message, platform, target_app_version, fingerprint_hash, storage_uri, metadata").eq("id", bundleId).single();
14
- if (!data || error) return null;
15
- return {
16
- channel: data.channel,
17
- enabled: data.enabled,
18
- shouldForceUpdate: data.should_force_update,
19
- fileHash: data.file_hash,
20
- gitCommitHash: data.git_commit_hash,
21
- id: data.id,
22
- message: data.message,
23
- platform: data.platform,
24
- targetAppVersion: data.target_app_version,
25
- fingerprintHash: data.fingerprint_hash,
26
- storageUri: data.storage_uri,
27
- metadata: data.metadata ?? {}
28
- };
29
- },
30
- async getBundles(options) {
31
- const { where, limit, offset } = options ?? {};
32
- let countQuery = supabase.from("bundles").select("*", {
33
- count: "exact",
34
- head: true
35
- });
36
- if (where?.channel) countQuery = countQuery.eq("channel", where.channel);
37
- if (where?.platform) countQuery = countQuery.eq("platform", where.platform);
38
- const { count: total = 0 } = await countQuery;
39
- let query = supabase.from("bundles").select("id, channel, enabled, platform, should_force_update, file_hash, git_commit_hash, message, fingerprint_hash, target_app_version, storage_uri, metadata").order("id", { ascending: false });
40
- if (where?.channel) query = query.eq("channel", where.channel);
41
- if (where?.platform) query = query.eq("platform", where.platform);
42
- if (limit) query = query.limit(limit);
43
- if (offset) query = query.range(offset, offset + (limit || 20) - 1);
44
- const { data } = await query;
45
- return {
46
- data: data ? data.map((bundle) => ({
47
- channel: bundle.channel,
48
- enabled: bundle.enabled,
49
- shouldForceUpdate: bundle.should_force_update,
50
- fileHash: bundle.file_hash,
51
- gitCommitHash: bundle.git_commit_hash,
52
- id: bundle.id,
53
- message: bundle.message,
54
- platform: bundle.platform,
55
- targetAppVersion: bundle.target_app_version,
56
- fingerprintHash: bundle.fingerprint_hash,
57
- storageUri: bundle.storage_uri,
58
- metadata: bundle.metadata ?? {}
59
- })) : [],
60
- pagination: calculatePagination(total ?? 0, {
61
- limit,
62
- offset
63
- })
64
- };
65
- },
66
- async getChannels() {
67
- const { data, error } = await supabase.rpc("get_channels");
68
- if (error) throw error;
69
- return data.map((bundle) => bundle.channel);
70
- },
71
- async commitBundle({ changedSets }) {
72
- if (changedSets.length === 0) return;
73
- for (const op of changedSets) if (op.operation === "delete") {
74
- const { error } = await supabase.from("bundles").delete().eq("id", op.data.id);
75
- if (error) throw new Error(`Failed to delete bundle: ${error.message}`);
76
- } else if (op.operation === "insert" || op.operation === "update") {
77
- const bundle = op.data;
78
- const { error } = await supabase.from("bundles").upsert({
79
- id: bundle.id,
80
- channel: bundle.channel,
81
- enabled: bundle.enabled,
82
- should_force_update: bundle.shouldForceUpdate,
83
- file_hash: bundle.fileHash,
84
- git_commit_hash: bundle.gitCommitHash,
85
- message: bundle.message,
86
- platform: bundle.platform,
87
- target_app_version: bundle.targetAppVersion,
88
- fingerprint_hash: bundle.fingerprintHash,
89
- storage_uri: bundle.storageUri,
90
- metadata: bundle.metadata
91
- }, { onConflict: "id" });
92
- if (error) throw error;
93
- }
94
- }
95
- };
96
- }
97
- });
98
-
99
- //#endregion
100
- //#region src/supabaseStorage.ts
101
- const supabaseStorage = createStoragePlugin({
102
- name: "supabaseStorage",
103
- supportedProtocol: "supabase-storage",
104
- factory: (config) => {
105
- const bucket = createClient(config.supabaseUrl, config.supabaseAnonKey).storage.from(config.bucketName);
106
- const getStorageKey = createStorageKeyBuilder(config.basePath);
107
- return {
108
- async delete(storageUri) {
109
- const { key, bucket: bucketName } = parseStorageUri(storageUri, "supabase-storage");
110
- if (bucketName !== config.bucketName) throw new Error(`Bucket name mismatch: expected "${config.bucketName}", but found "${bucketName}".`);
111
- const { error } = await bucket.remove([key]);
112
- if (error) {
113
- if (error.message?.includes("not found")) throw new Error(`Bundle not found`);
114
- throw new Error(`Failed to delete bundle: ${error.message}`);
115
- }
116
- },
117
- async upload(key, filePath) {
118
- const Body = await fs.readFile(filePath);
119
- const ContentType = getContentType(filePath);
120
- const Key = getStorageKey(key, path.basename(filePath));
121
- const upload = await bucket.upload(Key, Body, {
122
- contentType: ContentType,
123
- cacheControl: "max-age=31536000",
124
- headers: {}
125
- });
126
- if (upload.error) throw upload.error;
127
- return { storageUri: `supabase-storage://${upload.data.fullPath}` };
128
- },
129
- async getDownloadUrl(storageUri) {
130
- const u = new URL(storageUri);
131
- if (u.protocol.replace(":", "") !== "supabase-storage") throw new Error("Invalid Supabase storage URI protocol");
132
- let key = `${u.host}${u.pathname}`.replace(/^\//, "");
133
- if (!key) throw new Error("Invalid Supabase storage URI: missing key");
134
- if (key.startsWith(`${config.bucketName}/`)) key = key.substring(`${config.bucketName}/`.length);
135
- const { data, error } = await bucket.createSignedUrl(key, 3600);
136
- if (error) throw new Error(`Failed to generate download URL: ${error.message}`);
137
- if (!data?.signedUrl) throw new Error("Failed to generate download URL");
138
- return { fileUrl: data.signedUrl };
139
- }
140
- };
141
- }
142
- });
143
-
144
- //#endregion
145
- export { supabaseDatabase, supabaseStorage };