@hot-updater/supabase 0.36.6 → 1.0.0-rc.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.
Files changed (37) hide show
  1. package/dist/edge.cjs +6 -3
  2. package/dist/edge.d.cts +14 -2
  3. package/dist/edge.d.mts +14 -2
  4. package/dist/edge.mjs +5 -2
  5. package/dist/iac/index.cjs +607 -50
  6. package/dist/iac/index.d.cts +26 -8
  7. package/dist/iac/index.d.mts +26 -8
  8. package/dist/iac/index.mjs +604 -52
  9. package/dist/index.cjs +3 -145
  10. package/dist/index.d.cts +7 -24
  11. package/dist/index.d.mts +7 -24
  12. package/dist/index.mjs +2 -117
  13. package/dist/init/index.cjs +5 -2
  14. package/dist/init/index.d.cts +2 -2
  15. package/dist/init/index.d.mts +2 -2
  16. package/dist/init/index.mjs +5 -2
  17. package/dist/supabaseDatabase-BDIBsXpz.d.cts +13 -0
  18. package/dist/supabaseDatabase-CP9WalBi.d.mts +13 -0
  19. package/dist/supabaseStorage-Cw294tR4.mjs +557 -0
  20. package/dist/supabaseStorage-yP7u4m7E.cjs +568 -0
  21. package/package.json +9 -9
  22. package/supabase/edge-functions/index.ts +9 -13
  23. package/supabase/edge-functions/runtime.docker.integration.spec.ts +417 -202
  24. package/supabase/migrations/20260818000000_hot-updater_1.0.0.sql +583 -0
  25. package/supabase/migrations.spec.ts +127 -46
  26. package/dist/supabaseEdgeFunctionStorage-BRxGvt-r.d.mts +0 -19
  27. package/dist/supabaseEdgeFunctionStorage-BSLlUeqi.cjs +0 -386
  28. package/dist/supabaseEdgeFunctionStorage-CU396KO3.d.cts +0 -19
  29. package/dist/supabaseEdgeFunctionStorage-pV0s_mTF.mjs +0 -356
  30. package/supabase/migrations/20250103114225_init.sql +0 -228
  31. package/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +0 -134
  32. package/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +0 -194
  33. package/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +0 -184
  34. package/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +0 -503
  35. package/supabase/migrations/20260414000000_hot-updater_0.30.0.sql +0 -50
  36. package/supabase/migrations/20260422000000_hot-updater_0.31.0.sql +0 -20
  37. package/supabase/migrations/20260520014100_hot-updater_rls.sql +0 -48
@@ -1,356 +0,0 @@
1
- import { DEFAULT_ROLLOUT_COHORT_COUNT, NIL_UUID, getAssetBaseStorageUri, getBundlePatches, getManifestFileHash, getManifestStorageUri, stripBundleArtifactMetadata } from "@hot-updater/core";
2
- import { calculatePagination, createDatabasePlugin, createRuntimeStoragePlugin, filterCompatibleAppVersions, parseStorageUri } from "@hot-updater/plugin-core";
3
- import { createClient } from "@supabase/supabase-js";
4
- //#region src/supabaseBundleMapper.ts
5
- const BUNDLE_SELECT_COLUMNS = "id, channel, enabled, platform, should_force_update, file_hash, git_commit_hash, message, fingerprint_hash, target_app_version, storage_uri, metadata, manifest_storage_uri, manifest_file_hash, asset_base_storage_uri, rollout_cohort_count, target_cohorts";
6
- const normalizeMetadata = (value) => {
7
- if (!value) return {};
8
- if (typeof value === "string") try {
9
- return normalizeMetadata(JSON.parse(value));
10
- } catch {
11
- return {};
12
- }
13
- if (typeof value === "object" && !Array.isArray(value)) return value;
14
- return {};
15
- };
16
- const buildBundlePatchId = (bundleId, baseBundleId) => `${bundleId}:${baseBundleId}`;
17
- const mapRowToBundle = (row, patchRows = []) => {
18
- const rawMetadata = normalizeMetadata(row.metadata);
19
- const patches = patchRows.slice().sort((left, right) => left.order_index - right.order_index || left.base_bundle_id.localeCompare(right.base_bundle_id)).map((patch) => ({
20
- baseBundleId: patch.base_bundle_id,
21
- baseFileHash: patch.base_file_hash,
22
- patchFileHash: patch.patch_file_hash,
23
- patchStorageUri: patch.patch_storage_uri
24
- }));
25
- const primaryPatch = patches[0] ?? null;
26
- return {
27
- channel: row.channel,
28
- enabled: Boolean(row.enabled),
29
- shouldForceUpdate: Boolean(row.should_force_update),
30
- fileHash: row.file_hash,
31
- gitCommitHash: row.git_commit_hash,
32
- id: row.id,
33
- message: row.message,
34
- platform: row.platform,
35
- targetAppVersion: row.target_app_version,
36
- fingerprintHash: row.fingerprint_hash,
37
- storageUri: row.storage_uri,
38
- metadata: stripBundleArtifactMetadata(rawMetadata),
39
- manifestStorageUri: row.manifest_storage_uri ?? null,
40
- manifestFileHash: row.manifest_file_hash ?? null,
41
- assetBaseStorageUri: row.asset_base_storage_uri ?? null,
42
- patches,
43
- patchBaseBundleId: primaryPatch?.baseBundleId ?? null,
44
- patchBaseFileHash: primaryPatch?.baseFileHash ?? null,
45
- patchFileHash: primaryPatch?.patchFileHash ?? null,
46
- patchStorageUri: primaryPatch?.patchStorageUri ?? null,
47
- rolloutCohortCount: row.rollout_cohort_count ?? DEFAULT_ROLLOUT_COHORT_COUNT,
48
- targetCohorts: row.target_cohorts ?? null
49
- };
50
- };
51
- const bundleToRow = (bundle) => ({
52
- id: bundle.id,
53
- channel: bundle.channel,
54
- enabled: bundle.enabled,
55
- should_force_update: bundle.shouldForceUpdate,
56
- file_hash: bundle.fileHash,
57
- git_commit_hash: bundle.gitCommitHash,
58
- message: bundle.message,
59
- platform: bundle.platform,
60
- target_app_version: bundle.targetAppVersion,
61
- fingerprint_hash: bundle.fingerprintHash,
62
- storage_uri: bundle.storageUri,
63
- metadata: stripBundleArtifactMetadata(bundle.metadata) ?? {},
64
- manifest_storage_uri: getManifestStorageUri(bundle),
65
- manifest_file_hash: getManifestFileHash(bundle),
66
- asset_base_storage_uri: getAssetBaseStorageUri(bundle),
67
- rollout_cohort_count: bundle.rolloutCohortCount ?? DEFAULT_ROLLOUT_COHORT_COUNT,
68
- target_cohorts: bundle.targetCohorts ?? null
69
- });
70
- const bundleToPatchRows = (bundle) => getBundlePatches(bundle).map((patch, index) => ({
71
- id: buildBundlePatchId(bundle.id, patch.baseBundleId),
72
- bundle_id: bundle.id,
73
- base_bundle_id: patch.baseBundleId,
74
- base_file_hash: patch.baseFileHash,
75
- patch_file_hash: patch.patchFileHash,
76
- patch_storage_uri: patch.patchStorageUri,
77
- order_index: index
78
- }));
79
- //#endregion
80
- //#region src/supabaseConfig.ts
81
- const resolveSupabaseServiceRoleKey = (config) => {
82
- const key = config.supabaseServiceRoleKey ?? config.supabaseAnonKey;
83
- if (!key) throw new Error("Supabase service role key is required. Set supabaseServiceRoleKey.");
84
- return key;
85
- };
86
- //#endregion
87
- //#region src/supabaseDatabase.ts
88
- const createSupabaseError = (error) => {
89
- if (error instanceof Error) return error;
90
- if (error && typeof error === "object") {
91
- const properties = {};
92
- let target = error;
93
- while (target && target !== Object.prototype) {
94
- for (const key of Object.getOwnPropertyNames(target)) properties[key] = error[key];
95
- target = Object.getPrototypeOf(target);
96
- }
97
- return new Error(JSON.stringify({
98
- name: error.constructor.name,
99
- ...properties
100
- }));
101
- }
102
- try {
103
- return new Error(JSON.stringify(error));
104
- } catch {
105
- return new Error(String(error));
106
- }
107
- };
108
- const mapUpdateInfoRow = (row) => ({
109
- id: row.id,
110
- shouldForceUpdate: row.should_force_update,
111
- message: row.message,
112
- status: row.status,
113
- storageUri: row.storage_uri,
114
- fileHash: row.file_hash
115
- });
116
- const supabaseDatabase = createDatabasePlugin({
117
- name: "supabaseDatabase",
118
- factory: (config) => {
119
- const supabase = createClient(config.supabaseUrl, resolveSupabaseServiceRoleKey(config));
120
- const fetchPatchMap = async (bundleIds) => {
121
- const patchMap = /* @__PURE__ */ new Map();
122
- if (bundleIds.length === 0) return patchMap;
123
- const { data, error } = await supabase.from("bundle_patches").select("*").in("bundle_id", bundleIds).order("order_index", { ascending: true });
124
- if (error) throw createSupabaseError(error);
125
- for (const row of data ?? []) {
126
- const current = patchMap.get(row.bundle_id) ?? [];
127
- current.push(row);
128
- patchMap.set(row.bundle_id, current);
129
- }
130
- return patchMap;
131
- };
132
- return {
133
- async getUpdateInfo(args) {
134
- const channel = args.channel ?? "production";
135
- const minBundleId = args.minBundleId ?? NIL_UUID;
136
- if (args._updateStrategy === "appVersion") {
137
- const { data: targetAppVersionRows, error: targetAppVersionError } = await supabase.rpc("get_target_app_version_list", {
138
- app_platform: args.platform,
139
- min_bundle_id: minBundleId
140
- });
141
- if (targetAppVersionError) throw createSupabaseError(targetAppVersionError);
142
- const targetAppVersionList = filterCompatibleAppVersions((targetAppVersionRows ?? []).map((row) => row.target_app_version).filter((version) => Boolean(version)), args.appVersion);
143
- const { data, error } = await supabase.rpc("get_update_info_by_app_version", {
144
- app_platform: args.platform,
145
- app_version: args.appVersion,
146
- bundle_id: args.bundleId,
147
- min_bundle_id: minBundleId,
148
- target_channel: channel,
149
- target_app_version_list: targetAppVersionList,
150
- cohort: args.cohort ?? null
151
- });
152
- if (error) throw createSupabaseError(error);
153
- const updateInfo = data?.[0] ?? null;
154
- return updateInfo ? mapUpdateInfoRow(updateInfo) : null;
155
- }
156
- const { data, error } = await supabase.rpc("get_update_info_by_fingerprint_hash", {
157
- app_platform: args.platform,
158
- bundle_id: args.bundleId,
159
- min_bundle_id: minBundleId,
160
- target_channel: channel,
161
- target_fingerprint_hash: args.fingerprintHash,
162
- cohort: args.cohort ?? null
163
- });
164
- if (error) throw createSupabaseError(error);
165
- const updateInfo = data?.[0] ?? null;
166
- return updateInfo ? mapUpdateInfoRow(updateInfo) : null;
167
- },
168
- async getBundleById(bundleId) {
169
- const [{ data, error }, patchMap] = await Promise.all([supabase.from("bundles").select(BUNDLE_SELECT_COLUMNS).eq("id", bundleId).single(), fetchPatchMap([bundleId])]);
170
- if (!data || error) return null;
171
- return mapRowToBundle(data, patchMap.get(bundleId) ?? []);
172
- },
173
- async getBundles(options) {
174
- const { where, limit, orderBy } = options ?? {};
175
- const offset = (options && "offset" in options ? options.offset : void 0) ?? 0;
176
- if (where?.targetAppVersionIn && where.targetAppVersionIn.length === 0 || where?.id?.in && where.id.in.length === 0) return {
177
- data: [],
178
- pagination: calculatePagination(0, {
179
- limit,
180
- offset
181
- })
182
- };
183
- let countQuery = supabase.from("bundles").select("*", {
184
- count: "exact",
185
- head: true
186
- });
187
- if (where?.channel) countQuery = countQuery.eq("channel", where.channel);
188
- if (where?.platform) countQuery = countQuery.eq("platform", where.platform);
189
- if (where?.enabled !== void 0) countQuery = countQuery.eq("enabled", where.enabled);
190
- if (where?.fingerprintHash !== void 0) countQuery = where.fingerprintHash === null ? countQuery.is("fingerprint_hash", null) : countQuery.eq("fingerprint_hash", where.fingerprintHash);
191
- if (where?.targetAppVersion !== void 0) countQuery = where.targetAppVersion === null ? countQuery.is("target_app_version", null) : countQuery.eq("target_app_version", where.targetAppVersion);
192
- if (where?.targetAppVersionIn) countQuery = countQuery.in("target_app_version", where.targetAppVersionIn);
193
- if (where?.targetAppVersionNotNull) countQuery = countQuery.not("target_app_version", "is", null);
194
- if (where?.id?.eq) countQuery = countQuery.eq("id", where.id.eq);
195
- if (where?.id?.gt) countQuery = countQuery.gt("id", where.id.gt);
196
- if (where?.id?.gte) countQuery = countQuery.gte("id", where.id.gte);
197
- if (where?.id?.lt) countQuery = countQuery.lt("id", where.id.lt);
198
- if (where?.id?.lte) countQuery = countQuery.lte("id", where.id.lte);
199
- if (where?.id?.in) countQuery = countQuery.in("id", where.id.in);
200
- const { count: total = 0 } = await countQuery;
201
- let query = supabase.from("bundles").select(BUNDLE_SELECT_COLUMNS).order("id", { ascending: orderBy?.direction === "asc" });
202
- if (where?.channel) query = query.eq("channel", where.channel);
203
- if (where?.platform) query = query.eq("platform", where.platform);
204
- if (where?.enabled !== void 0) query = query.eq("enabled", where.enabled);
205
- if (where?.fingerprintHash !== void 0) query = where.fingerprintHash === null ? query.is("fingerprint_hash", null) : query.eq("fingerprint_hash", where.fingerprintHash);
206
- if (where?.targetAppVersion !== void 0) query = where.targetAppVersion === null ? query.is("target_app_version", null) : query.eq("target_app_version", where.targetAppVersion);
207
- if (where?.targetAppVersionIn) query = query.in("target_app_version", where.targetAppVersionIn);
208
- if (where?.targetAppVersionNotNull) query = query.not("target_app_version", "is", null);
209
- if (where?.id?.eq) query = query.eq("id", where.id.eq);
210
- if (where?.id?.gt) query = query.gt("id", where.id.gt);
211
- if (where?.id?.gte) query = query.gte("id", where.id.gte);
212
- if (where?.id?.lt) query = query.lt("id", where.id.lt);
213
- if (where?.id?.lte) query = query.lte("id", where.id.lte);
214
- if (where?.id?.in) query = query.in("id", where.id.in);
215
- if (limit) query = query.limit(limit);
216
- if (offset) query = query.range(offset, offset + (limit || 20) - 1);
217
- const { data } = await query;
218
- const patchMap = await fetchPatchMap((data ?? []).map((bundle) => bundle.id));
219
- return {
220
- data: (data ?? []).map((bundle) => mapRowToBundle(bundle, patchMap.get(bundle.id) ?? [])),
221
- pagination: calculatePagination(total ?? 0, {
222
- limit,
223
- offset
224
- })
225
- };
226
- },
227
- async getChannels() {
228
- const { data, error } = await supabase.rpc("get_channels");
229
- if (error) throw error;
230
- return data.map((bundle) => bundle.channel);
231
- },
232
- async commitBundle({ changedSets }) {
233
- if (changedSets.length === 0) return;
234
- for (const op of changedSets) if (op.operation === "delete") {
235
- const { error: patchDeleteError } = await supabase.from("bundle_patches").delete().eq("bundle_id", op.data.id);
236
- if (patchDeleteError) throw new Error(`Failed to delete bundle patches: ${patchDeleteError.message}`);
237
- const { error: basePatchDeleteError } = await supabase.from("bundle_patches").delete().eq("base_bundle_id", op.data.id);
238
- if (basePatchDeleteError) throw new Error(`Failed to delete base bundle patches: ${basePatchDeleteError.message}`);
239
- const { error } = await supabase.from("bundles").delete().eq("id", op.data.id);
240
- if (error) throw new Error(`Failed to delete bundle: ${error.message}`);
241
- } else if (op.operation === "insert" || op.operation === "update") {
242
- const bundle = op.data;
243
- const patchRows = bundleToPatchRows(bundle);
244
- const { error } = await supabase.from("bundles").upsert(bundleToRow(bundle), { onConflict: "id" });
245
- if (error) throw error;
246
- const { error: patchDeleteError } = await supabase.from("bundle_patches").delete().eq("bundle_id", bundle.id);
247
- if (patchDeleteError) throw patchDeleteError;
248
- if (patchRows.length > 0) {
249
- const { error: patchInsertError } = await supabase.from("bundle_patches").upsert(patchRows, { onConflict: "id" });
250
- if (patchInsertError) throw patchInsertError;
251
- }
252
- }
253
- }
254
- };
255
- }
256
- });
257
- //#endregion
258
- //#region src/supabaseEdgeFunctionDatabase.ts
259
- const supabaseEdgeFunctionDatabase = (config, hooks) => {
260
- return supabaseDatabase({
261
- supabaseUrl: config.supabaseUrl,
262
- supabaseServiceRoleKey: config.supabaseServiceRoleKey
263
- }, hooks);
264
- };
265
- //#endregion
266
- //#region src/supabaseSignedUrlBatcher.ts
267
- function getErrorMessage(error) {
268
- if (error instanceof Error) return error.message;
269
- if (error && typeof error === "object" && "message" in error && typeof error.message === "string") return error.message;
270
- return String(error);
271
- }
272
- const createSupabaseSignedUrlBatcher = ({ createSignedUrls, expiresIn, formatObjectPath }) => {
273
- let pendingByBucket = /* @__PURE__ */ new Map();
274
- let flushScheduled = false;
275
- const createSignedUrlError = (bucketName, key, error) => /* @__PURE__ */ new Error(`Failed to generate download URL for "${formatObjectPath(bucketName, key)}": ${getErrorMessage(error)}`);
276
- const flush = async () => {
277
- const batches = pendingByBucket;
278
- pendingByBucket = /* @__PURE__ */ new Map();
279
- flushScheduled = false;
280
- await Promise.all([...batches.entries()].map(async ([bucketName, pending]) => {
281
- try {
282
- const { data, error } = await createSignedUrls(bucketName, pending.map(({ key }) => key), expiresIn);
283
- if (error || !data) {
284
- const batchError = error ?? /* @__PURE__ */ new Error("missing signed URL response");
285
- for (const request of pending) request.reject(createSignedUrlError(bucketName, request.key, batchError));
286
- return;
287
- }
288
- pending.forEach((request, index) => {
289
- const result = data[index];
290
- if (!result?.error && result?.signedUrl) {
291
- request.resolve(result.signedUrl);
292
- return;
293
- }
294
- request.reject(createSignedUrlError(bucketName, request.key, result?.error ?? /* @__PURE__ */ new Error("missing signed URL")));
295
- });
296
- } catch (error) {
297
- for (const request of pending) request.reject(createSignedUrlError(bucketName, request.key, error));
298
- }
299
- }));
300
- };
301
- return (bucketName, key) => new Promise((resolve, reject) => {
302
- const pending = pendingByBucket.get(bucketName) ?? [];
303
- pending.push({
304
- key,
305
- reject,
306
- resolve
307
- });
308
- pendingByBucket.set(bucketName, pending);
309
- if (!flushScheduled) {
310
- flushScheduled = true;
311
- queueMicrotask(() => {
312
- flush();
313
- });
314
- }
315
- });
316
- };
317
- //#endregion
318
- //#region src/supabaseEdgeFunctionStorage.ts
319
- const parseSupabaseStorageUri = (storageUri) => {
320
- const { bucket: bucketName, key } = parseStorageUri(storageUri, "supabase-storage");
321
- if (!bucketName || !key) throw new Error("Invalid Supabase storage URI");
322
- return {
323
- bucketName,
324
- key
325
- };
326
- };
327
- const supabaseEdgeFunctionStorage = createRuntimeStoragePlugin({
328
- name: "supabaseEdgeFunctionStorage",
329
- supportedProtocol: "supabase-storage",
330
- factory: (config) => {
331
- const supabase = createClient(config.supabaseUrl, config.supabaseServiceRoleKey);
332
- const resolveSignedUrl = createSupabaseSignedUrlBatcher({
333
- createSignedUrls: (bucketName, keys, expiresIn) => supabase.storage.from(bucketName).createSignedUrls(keys, expiresIn),
334
- expiresIn: config.signedUrlExpiresIn ?? 3600,
335
- formatObjectPath: (bucketName, key) => `${bucketName}/${key}`
336
- });
337
- return {
338
- async readText(storageUri) {
339
- const { bucketName, key } = parseSupabaseStorageUri(storageUri);
340
- const { data, error } = await supabase.storage.from(bucketName).download(key);
341
- if (error) {
342
- if (error.message?.includes("not found")) return null;
343
- throw new Error(`Failed to read storage text: ${error.message}`);
344
- }
345
- if (!data) return null;
346
- return data.text();
347
- },
348
- async getDownloadUrl(storageUri) {
349
- const { bucketName, key } = parseSupabaseStorageUri(storageUri);
350
- return { fileUrl: await resolveSignedUrl(bucketName, key) };
351
- }
352
- };
353
- }
354
- });
355
- //#endregion
356
- export { resolveSupabaseServiceRoleKey as a, supabaseDatabase as i, createSupabaseSignedUrlBatcher as n, supabaseEdgeFunctionDatabase as r, supabaseEdgeFunctionStorage as t };
@@ -1,228 +0,0 @@
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
- target_app_version text NOT NULL,
9
- should_force_update boolean NOT NULL,
10
- enabled boolean NOT NULL,
11
- file_url text NOT NULL,
12
- file_hash text NOT NULL,
13
- git_commit_hash text,
14
- message text
15
- );
16
-
17
- CREATE INDEX bundles_target_app_version_idx ON bundles(target_app_version);
18
-
19
-
20
- -- HotUpdater.get_update_info
21
-
22
- CREATE OR REPLACE FUNCTION get_update_info (
23
- app_platform platforms,
24
- app_version text,
25
- bundle_id uuid
26
- )
27
- RETURNS TABLE (
28
- id uuid,
29
- should_force_update boolean,
30
- file_url text,
31
- file_hash text,
32
- status text
33
- )
34
- LANGUAGE plpgsql
35
- AS
36
- $$
37
- DECLARE
38
- NIL_UUID CONSTANT uuid := '00000000-0000-0000-0000-000000000000';
39
- BEGIN
40
- RETURN QUERY
41
- WITH rollback_candidate AS (
42
- SELECT
43
- b.id,
44
- -- If status is 'ROLLBACK', should_force_update is always TRUE
45
- TRUE AS should_force_update,
46
- b.file_url,
47
- b.file_hash,
48
- 'ROLLBACK' AS status
49
- FROM bundles b
50
- WHERE b.enabled = TRUE
51
- AND b.platform = app_platform
52
- AND b.id < bundle_id
53
- ORDER BY b.id DESC
54
- LIMIT 1
55
- ),
56
- update_candidate AS (
57
- SELECT
58
- b.id,
59
- b.should_force_update,
60
- b.file_url,
61
- b.file_hash,
62
- 'UPDATE' AS status
63
- FROM bundles b
64
- WHERE b.enabled = TRUE
65
- AND b.platform = app_platform
66
- AND b.id >= bundle_id
67
- AND semver_satisfies(b.target_app_version, app_version)
68
- ORDER BY b.id DESC
69
- LIMIT 1
70
- ),
71
- final_result AS (
72
- SELECT *
73
- FROM update_candidate
74
-
75
- UNION ALL
76
-
77
- SELECT *
78
- FROM rollback_candidate
79
- WHERE NOT EXISTS (SELECT 1 FROM update_candidate)
80
- )
81
- SELECT *
82
- FROM final_result WHERE final_result.id != bundle_id
83
-
84
- UNION ALL
85
- /*
86
- When there are no final results and bundle_id != NIL_UUID,
87
- add one fallback row.
88
- This fallback row is also ROLLBACK so shouldForceUpdate = TRUE.
89
- */
90
- SELECT
91
- NIL_UUID AS id,
92
- TRUE AS should_force_update, -- Always TRUE
93
- NULL AS file_url,
94
- NULL AS file_hash,
95
- 'ROLLBACK' AS status
96
- WHERE (SELECT COUNT(*) FROM final_result) = 0
97
- AND bundle_id != NIL_UUID;
98
-
99
- END;
100
- $$;
101
-
102
- -- HotUpdater.semver_satisfies
103
-
104
- CREATE OR REPLACE FUNCTION semver_satisfies(range_expression TEXT, version TEXT)
105
- RETURNS BOOLEAN AS $$
106
- DECLARE
107
- version_parts TEXT[];
108
- version_major INT;
109
- version_minor INT;
110
- version_patch INT;
111
- satisfies BOOLEAN := FALSE;
112
- BEGIN
113
- -- Split the version into major, minor, and patch
114
- version_parts := string_to_array(version, '.');
115
- version_major := version_parts[1]::INT;
116
- version_minor := version_parts[2]::INT;
117
- version_patch := version_parts[3]::INT;
118
-
119
- -- Parse range expression and evaluate
120
- IF range_expression ~ '^\d+\.\d+\.\d+$' THEN
121
- -- Exact match
122
- satisfies := (range_expression = version);
123
-
124
- ELSIF range_expression = '*' THEN
125
- -- Matches any version
126
- satisfies := TRUE;
127
-
128
- ELSIF range_expression ~ '^\d+\.x\.x$' THEN
129
- -- Matches major.x.x
130
- DECLARE
131
- major_range INT := split_part(range_expression, '.', 1)::INT;
132
- BEGIN
133
- satisfies := (version_major = major_range);
134
- END;
135
-
136
- ELSIF range_expression ~ '^\d+\.\d+\.x$' THEN
137
- -- Matches major.minor.x
138
- DECLARE
139
- major_range INT := split_part(range_expression, '.', 1)::INT;
140
- minor_range INT := split_part(range_expression, '.', 2)::INT;
141
- BEGIN
142
- satisfies := (version_major = major_range AND version_minor = minor_range);
143
- END;
144
-
145
- ELSIF range_expression ~ '^\d+\.\d+$' THEN
146
- -- Matches major.minor
147
- DECLARE
148
- major_range INT := split_part(range_expression, '.', 1)::INT;
149
- minor_range INT := split_part(range_expression, '.', 2)::INT;
150
- BEGIN
151
- satisfies := (version_major = major_range AND version_minor = minor_range);
152
- END;
153
-
154
- ELSIF range_expression ~ '^\d+\.\d+\.\d+ - \d+\.\d+\.\d+$' THEN
155
- -- Matches range e.g., 1.2.3 - 1.2.7
156
- DECLARE
157
- lower_bound TEXT := split_part(range_expression, ' - ', 1);
158
- upper_bound TEXT := split_part(range_expression, ' - ', 2);
159
- BEGIN
160
- satisfies := (version >= lower_bound AND version <= upper_bound);
161
- END;
162
-
163
- ELSIF range_expression ~ '^>=\d+\.\d+\.\d+ <\d+\.\d+\.\d+$' THEN
164
- -- Matches range with inequalities
165
- DECLARE
166
- lower_bound TEXT := regexp_replace(range_expression, '>=([\d\.]+) <.*', '\1');
167
- upper_bound TEXT := regexp_replace(range_expression, '.*<([\d\.]+)', '\1');
168
- BEGIN
169
- satisfies := (version >= lower_bound AND version < upper_bound);
170
- END;
171
-
172
- ELSIF range_expression ~ '^~\d+\.\d+\.\d+$' THEN
173
- -- Matches ~1.2.3 (>=1.2.3 <1.3.0)
174
- DECLARE
175
- lower_bound TEXT := regexp_replace(range_expression, '~', '');
176
- upper_bound_major INT := split_part(lower_bound, '.', 1)::INT;
177
- upper_bound_minor INT := split_part(lower_bound, '.', 2)::INT + 1;
178
- upper_bound TEXT := upper_bound_major || '.' || upper_bound_minor || '.0';
179
- BEGIN
180
- satisfies := (version >= lower_bound AND version < upper_bound);
181
- END;
182
-
183
- ELSIF range_expression ~ '^\^\d+\.\d+\.\d+$' THEN
184
- -- Matches ^1.2.3 (>=1.2.3 <2.0.0)
185
- DECLARE
186
- lower_bound TEXT := regexp_replace(range_expression, '\^', '');
187
- upper_bound_major INT := split_part(lower_bound, '.', 1)::INT + 1;
188
- upper_bound TEXT := upper_bound_major || '.0.0';
189
- BEGIN
190
- satisfies := (version >= lower_bound AND version < upper_bound);
191
- END;
192
-
193
- -- [Added] 1) Single major version pattern '^(\d+)$'
194
- ELSIF range_expression ~ '^\d+$' THEN
195
- /*
196
- e.g.) "1" is interpreted as (>=1.0.0 <2.0.0) in semver range
197
- "2" would be interpreted as (>=2.0.0 <3.0.0)
198
- */
199
- DECLARE
200
- major_range INT := range_expression::INT;
201
- lower_bound TEXT := major_range || '.0.0';
202
- upper_bound TEXT := (major_range + 1) || '.0.0';
203
- BEGIN
204
- satisfies := (version >= lower_bound AND version < upper_bound);
205
- END;
206
-
207
- -- [Added] 2) major.x pattern '^(\d+)\.x$'
208
- ELSIF range_expression ~ '^\d+\.x$' THEN
209
- /*
210
- e.g.) "2.x" => as long as major=2 matches, any minor and patch is OK
211
- effectively works like (>=2.0.0 <3.0.0)
212
- */
213
- DECLARE
214
- major_range INT := split_part(range_expression, '.', 1)::INT;
215
- lower_bound TEXT := major_range || '.0.0';
216
- upper_bound TEXT := (major_range + 1) || '.0.0';
217
- BEGIN
218
- satisfies := (version >= lower_bound AND version < upper_bound);
219
- END;
220
-
221
- ELSE
222
- RAISE EXCEPTION 'Unsupported range expression: %', range_expression;
223
- END IF;
224
-
225
- RETURN satisfies;
226
- END;
227
- $$ LANGUAGE plpgsql;
228
-