@hot-updater/supabase 0.36.7 → 1.0.0-rc.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/dist/edge.cjs +6 -3
- package/dist/edge.d.cts +14 -2
- package/dist/edge.d.mts +14 -2
- package/dist/edge.mjs +5 -2
- package/dist/iac/index.cjs +607 -50
- package/dist/iac/index.d.cts +26 -8
- package/dist/iac/index.d.mts +26 -8
- package/dist/iac/index.mjs +604 -52
- package/dist/index.cjs +3 -145
- package/dist/index.d.cts +7 -24
- package/dist/index.d.mts +7 -24
- package/dist/index.mjs +2 -117
- package/dist/init/index.cjs +5 -2
- package/dist/init/index.d.cts +2 -2
- package/dist/init/index.d.mts +2 -2
- package/dist/init/index.mjs +5 -2
- package/dist/supabaseDatabase-BDIBsXpz.d.cts +13 -0
- package/dist/supabaseDatabase-CP9WalBi.d.mts +13 -0
- package/dist/supabaseStorage-Cw294tR4.mjs +557 -0
- package/dist/supabaseStorage-yP7u4m7E.cjs +568 -0
- package/package.json +9 -9
- package/supabase/edge-functions/index.ts +9 -13
- package/supabase/edge-functions/runtime.docker.integration.spec.ts +417 -202
- package/supabase/migrations/20260818000000_hot-updater_1.0.0.sql +583 -0
- package/supabase/migrations.spec.ts +127 -46
- package/dist/supabaseEdgeFunctionStorage-BRxGvt-r.d.mts +0 -19
- package/dist/supabaseEdgeFunctionStorage-BSLlUeqi.cjs +0 -386
- package/dist/supabaseEdgeFunctionStorage-CU396KO3.d.cts +0 -19
- package/dist/supabaseEdgeFunctionStorage-pV0s_mTF.mjs +0 -356
- package/supabase/migrations/20250103114225_init.sql +0 -228
- package/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +0 -134
- package/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +0 -194
- package/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +0 -184
- package/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +0 -503
- package/supabase/migrations/20260414000000_hot-updater_0.30.0.sql +0 -50
- package/supabase/migrations/20260422000000_hot-updater_0.31.0.sql +0 -20
- 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
|
-
|