@hot-updater/supabase 0.30.12 → 0.31.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 +1 -1
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.mts +1 -1
- package/dist/edge.mjs +1 -1
- package/dist/iac/index.cjs +5 -4
- package/dist/iac/index.mjs +5 -4
- package/dist/index.cjs +55 -31
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +56 -32
- package/dist/supabaseEdgeFunctionStorage-BKU_mLzA.mjs +274 -0
- package/dist/{supabaseEdgeFunctionStorage-ByPGforO.d.mts → supabaseEdgeFunctionStorage-BRxGvt-r.d.mts} +2 -2
- package/dist/supabaseEdgeFunctionStorage-BZC0Z0XP.cjs +292 -0
- package/dist/{supabaseEdgeFunctionStorage-CSPi2UB8.d.cts → supabaseEdgeFunctionStorage-CU396KO3.d.cts} +2 -2
- package/package.json +9 -9
- package/supabase/edge-functions/runtime.docker.integration.spec.ts +194 -14
- package/supabase/migrations/20260422000000_hot-updater_0.31.0.sql +20 -0
- package/dist/supabaseEdgeFunctionStorage-Dd8ytWP1.cjs +0 -545
- package/dist/supabaseEdgeFunctionStorage-DnnViEfo.mjs +0 -527
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
require("./index.cjs");
|
|
2
|
+
let _hot_updater_core = require("@hot-updater/core");
|
|
3
|
+
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
4
|
+
let _supabase_supabase_js = require("@supabase/supabase-js");
|
|
5
|
+
//#region src/supabaseDatabase.ts
|
|
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 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";
|
|
17
|
+
const createSupabaseError = (error) => {
|
|
18
|
+
if (error instanceof Error) return error;
|
|
19
|
+
if (error && typeof error === "object") {
|
|
20
|
+
const properties = {};
|
|
21
|
+
let target = error;
|
|
22
|
+
while (target && target !== Object.prototype) {
|
|
23
|
+
for (const key of Object.getOwnPropertyNames(target)) properties[key] = error[key];
|
|
24
|
+
target = Object.getPrototypeOf(target);
|
|
25
|
+
}
|
|
26
|
+
return new Error(JSON.stringify({
|
|
27
|
+
name: error.constructor.name,
|
|
28
|
+
...properties
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
return new Error(JSON.stringify(error));
|
|
33
|
+
} catch {
|
|
34
|
+
return new Error(String(error));
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const buildBundlePatchId = (bundleId, baseBundleId) => `${bundleId}:${baseBundleId}`;
|
|
38
|
+
const mapRowToBundle = (row, patchRows = []) => {
|
|
39
|
+
const rawMetadata = normalizeMetadata(row.metadata);
|
|
40
|
+
const patches = patchRows.slice().sort((left, right) => left.order_index - right.order_index || left.base_bundle_id.localeCompare(right.base_bundle_id)).map((patch) => ({
|
|
41
|
+
baseBundleId: patch.base_bundle_id,
|
|
42
|
+
baseFileHash: patch.base_file_hash,
|
|
43
|
+
patchFileHash: patch.patch_file_hash,
|
|
44
|
+
patchStorageUri: patch.patch_storage_uri
|
|
45
|
+
}));
|
|
46
|
+
const primaryPatch = patches[0] ?? null;
|
|
47
|
+
return {
|
|
48
|
+
channel: row.channel,
|
|
49
|
+
enabled: Boolean(row.enabled),
|
|
50
|
+
shouldForceUpdate: Boolean(row.should_force_update),
|
|
51
|
+
fileHash: row.file_hash,
|
|
52
|
+
gitCommitHash: row.git_commit_hash,
|
|
53
|
+
id: row.id,
|
|
54
|
+
message: row.message,
|
|
55
|
+
platform: row.platform,
|
|
56
|
+
targetAppVersion: row.target_app_version,
|
|
57
|
+
fingerprintHash: row.fingerprint_hash,
|
|
58
|
+
storageUri: row.storage_uri,
|
|
59
|
+
metadata: (0, _hot_updater_core.stripBundleArtifactMetadata)(rawMetadata),
|
|
60
|
+
manifestStorageUri: row.manifest_storage_uri ?? null,
|
|
61
|
+
manifestFileHash: row.manifest_file_hash ?? null,
|
|
62
|
+
assetBaseStorageUri: row.asset_base_storage_uri ?? null,
|
|
63
|
+
patches,
|
|
64
|
+
patchBaseBundleId: primaryPatch?.baseBundleId ?? null,
|
|
65
|
+
patchBaseFileHash: primaryPatch?.baseFileHash ?? null,
|
|
66
|
+
patchFileHash: primaryPatch?.patchFileHash ?? null,
|
|
67
|
+
patchStorageUri: primaryPatch?.patchStorageUri ?? null,
|
|
68
|
+
rolloutCohortCount: row.rollout_cohort_count ?? _hot_updater_core.DEFAULT_ROLLOUT_COHORT_COUNT,
|
|
69
|
+
targetCohorts: row.target_cohorts ?? null
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
const bundleToRow = (bundle) => ({
|
|
73
|
+
id: bundle.id,
|
|
74
|
+
channel: bundle.channel,
|
|
75
|
+
enabled: bundle.enabled,
|
|
76
|
+
should_force_update: bundle.shouldForceUpdate,
|
|
77
|
+
file_hash: bundle.fileHash,
|
|
78
|
+
git_commit_hash: bundle.gitCommitHash,
|
|
79
|
+
message: bundle.message,
|
|
80
|
+
platform: bundle.platform,
|
|
81
|
+
target_app_version: bundle.targetAppVersion,
|
|
82
|
+
fingerprint_hash: bundle.fingerprintHash,
|
|
83
|
+
storage_uri: bundle.storageUri,
|
|
84
|
+
metadata: (0, _hot_updater_core.stripBundleArtifactMetadata)(bundle.metadata) ?? {},
|
|
85
|
+
manifest_storage_uri: (0, _hot_updater_core.getManifestStorageUri)(bundle),
|
|
86
|
+
manifest_file_hash: (0, _hot_updater_core.getManifestFileHash)(bundle),
|
|
87
|
+
asset_base_storage_uri: (0, _hot_updater_core.getAssetBaseStorageUri)(bundle),
|
|
88
|
+
rollout_cohort_count: bundle.rolloutCohortCount ?? _hot_updater_core.DEFAULT_ROLLOUT_COHORT_COUNT,
|
|
89
|
+
target_cohorts: bundle.targetCohorts ?? null
|
|
90
|
+
});
|
|
91
|
+
const bundleToPatchRows = (bundle) => (0, _hot_updater_core.getBundlePatches)(bundle).map((patch, index) => ({
|
|
92
|
+
id: buildBundlePatchId(bundle.id, patch.baseBundleId),
|
|
93
|
+
bundle_id: bundle.id,
|
|
94
|
+
base_bundle_id: patch.baseBundleId,
|
|
95
|
+
base_file_hash: patch.baseFileHash,
|
|
96
|
+
patch_file_hash: patch.patchFileHash,
|
|
97
|
+
patch_storage_uri: patch.patchStorageUri,
|
|
98
|
+
order_index: index
|
|
99
|
+
}));
|
|
100
|
+
const supabaseDatabase = (0, _hot_updater_plugin_core.createDatabasePlugin)({
|
|
101
|
+
name: "supabaseDatabase",
|
|
102
|
+
factory: (config) => {
|
|
103
|
+
const supabase = (0, _supabase_supabase_js.createClient)(config.supabaseUrl, config.supabaseAnonKey);
|
|
104
|
+
const fetchPatchMap = async (bundleIds) => {
|
|
105
|
+
const patchMap = /* @__PURE__ */ new Map();
|
|
106
|
+
if (bundleIds.length === 0) return patchMap;
|
|
107
|
+
const { data, error } = await supabase.from("bundle_patches").select("*").in("bundle_id", bundleIds).order("order_index", { ascending: true });
|
|
108
|
+
if (error) throw createSupabaseError(error);
|
|
109
|
+
for (const row of data ?? []) {
|
|
110
|
+
const current = patchMap.get(row.bundle_id) ?? [];
|
|
111
|
+
current.push(row);
|
|
112
|
+
patchMap.set(row.bundle_id, current);
|
|
113
|
+
}
|
|
114
|
+
return patchMap;
|
|
115
|
+
};
|
|
116
|
+
const mapRowsToBundles = async (rows) => {
|
|
117
|
+
const patchMap = await fetchPatchMap(rows.map((row) => row.id));
|
|
118
|
+
return rows.map((row) => mapRowToBundle(row, patchMap.get(row.id)));
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
getUpdateInfo: (0, _hot_updater_plugin_core.createDatabasePluginGetUpdateInfo)({
|
|
122
|
+
async listTargetAppVersions({ platform, channel, minBundleId }) {
|
|
123
|
+
const { data, error } = await supabase.from("bundles").select("target_app_version").eq("platform", platform).eq("channel", channel).eq("enabled", true).gte("id", minBundleId).not("target_app_version", "is", null);
|
|
124
|
+
if (error) throw createSupabaseError(error);
|
|
125
|
+
return Array.from(new Set((data ?? []).map((row) => row.target_app_version).filter((version) => Boolean(version))));
|
|
126
|
+
},
|
|
127
|
+
async getBundlesByTargetAppVersions({ platform, channel, minBundleId }, targetAppVersions) {
|
|
128
|
+
const { data, error } = await supabase.from("bundles").select(BUNDLE_SELECT_COLUMNS).eq("platform", platform).eq("channel", channel).eq("enabled", true).gte("id", minBundleId).in("target_app_version", targetAppVersions);
|
|
129
|
+
if (error) throw createSupabaseError(error);
|
|
130
|
+
return mapRowsToBundles(data ?? []);
|
|
131
|
+
},
|
|
132
|
+
async getBundlesByFingerprint({ platform, channel, minBundleId, fingerprintHash }) {
|
|
133
|
+
const { data, error } = await supabase.from("bundles").select(BUNDLE_SELECT_COLUMNS).eq("platform", platform).eq("channel", channel).eq("enabled", true).gte("id", minBundleId).eq("fingerprint_hash", fingerprintHash);
|
|
134
|
+
if (error) throw createSupabaseError(error);
|
|
135
|
+
return mapRowsToBundles(data ?? []);
|
|
136
|
+
}
|
|
137
|
+
}),
|
|
138
|
+
async getBundleById(bundleId) {
|
|
139
|
+
const [{ data, error }, patchMap] = await Promise.all([supabase.from("bundles").select(BUNDLE_SELECT_COLUMNS).eq("id", bundleId).single(), fetchPatchMap([bundleId])]);
|
|
140
|
+
if (!data || error) return null;
|
|
141
|
+
return mapRowToBundle(data, patchMap.get(bundleId) ?? []);
|
|
142
|
+
},
|
|
143
|
+
async getBundles(options) {
|
|
144
|
+
const { where, limit, orderBy } = options ?? {};
|
|
145
|
+
const offset = (options && "offset" in options ? options.offset : void 0) ?? 0;
|
|
146
|
+
if (where?.targetAppVersionIn && where.targetAppVersionIn.length === 0 || where?.id?.in && where.id.in.length === 0) return {
|
|
147
|
+
data: [],
|
|
148
|
+
pagination: (0, _hot_updater_plugin_core.calculatePagination)(0, {
|
|
149
|
+
limit,
|
|
150
|
+
offset
|
|
151
|
+
})
|
|
152
|
+
};
|
|
153
|
+
let countQuery = supabase.from("bundles").select("*", {
|
|
154
|
+
count: "exact",
|
|
155
|
+
head: true
|
|
156
|
+
});
|
|
157
|
+
if (where?.channel) countQuery = countQuery.eq("channel", where.channel);
|
|
158
|
+
if (where?.platform) countQuery = countQuery.eq("platform", where.platform);
|
|
159
|
+
if (where?.enabled !== void 0) countQuery = countQuery.eq("enabled", where.enabled);
|
|
160
|
+
if (where?.fingerprintHash !== void 0) countQuery = where.fingerprintHash === null ? countQuery.is("fingerprint_hash", null) : countQuery.eq("fingerprint_hash", where.fingerprintHash);
|
|
161
|
+
if (where?.targetAppVersion !== void 0) countQuery = where.targetAppVersion === null ? countQuery.is("target_app_version", null) : countQuery.eq("target_app_version", where.targetAppVersion);
|
|
162
|
+
if (where?.targetAppVersionIn) countQuery = countQuery.in("target_app_version", where.targetAppVersionIn);
|
|
163
|
+
if (where?.targetAppVersionNotNull) countQuery = countQuery.not("target_app_version", "is", null);
|
|
164
|
+
if (where?.id?.eq) countQuery = countQuery.eq("id", where.id.eq);
|
|
165
|
+
if (where?.id?.gt) countQuery = countQuery.gt("id", where.id.gt);
|
|
166
|
+
if (where?.id?.gte) countQuery = countQuery.gte("id", where.id.gte);
|
|
167
|
+
if (where?.id?.lt) countQuery = countQuery.lt("id", where.id.lt);
|
|
168
|
+
if (where?.id?.lte) countQuery = countQuery.lte("id", where.id.lte);
|
|
169
|
+
if (where?.id?.in) countQuery = countQuery.in("id", where.id.in);
|
|
170
|
+
const { count: total = 0 } = await countQuery;
|
|
171
|
+
let query = supabase.from("bundles").select(BUNDLE_SELECT_COLUMNS).order("id", { ascending: orderBy?.direction === "asc" });
|
|
172
|
+
if (where?.channel) query = query.eq("channel", where.channel);
|
|
173
|
+
if (where?.platform) query = query.eq("platform", where.platform);
|
|
174
|
+
if (where?.enabled !== void 0) query = query.eq("enabled", where.enabled);
|
|
175
|
+
if (where?.fingerprintHash !== void 0) query = where.fingerprintHash === null ? query.is("fingerprint_hash", null) : query.eq("fingerprint_hash", where.fingerprintHash);
|
|
176
|
+
if (where?.targetAppVersion !== void 0) query = where.targetAppVersion === null ? query.is("target_app_version", null) : query.eq("target_app_version", where.targetAppVersion);
|
|
177
|
+
if (where?.targetAppVersionIn) query = query.in("target_app_version", where.targetAppVersionIn);
|
|
178
|
+
if (where?.targetAppVersionNotNull) query = query.not("target_app_version", "is", null);
|
|
179
|
+
if (where?.id?.eq) query = query.eq("id", where.id.eq);
|
|
180
|
+
if (where?.id?.gt) query = query.gt("id", where.id.gt);
|
|
181
|
+
if (where?.id?.gte) query = query.gte("id", where.id.gte);
|
|
182
|
+
if (where?.id?.lt) query = query.lt("id", where.id.lt);
|
|
183
|
+
if (where?.id?.lte) query = query.lte("id", where.id.lte);
|
|
184
|
+
if (where?.id?.in) query = query.in("id", where.id.in);
|
|
185
|
+
if (limit) query = query.limit(limit);
|
|
186
|
+
if (offset) query = query.range(offset, offset + (limit || 20) - 1);
|
|
187
|
+
const { data } = await query;
|
|
188
|
+
const patchMap = await fetchPatchMap((data ?? []).map((bundle) => bundle.id));
|
|
189
|
+
return {
|
|
190
|
+
data: (data ?? []).map((bundle) => mapRowToBundle(bundle, patchMap.get(bundle.id) ?? [])),
|
|
191
|
+
pagination: (0, _hot_updater_plugin_core.calculatePagination)(total ?? 0, {
|
|
192
|
+
limit,
|
|
193
|
+
offset
|
|
194
|
+
})
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
async getChannels() {
|
|
198
|
+
const { data, error } = await supabase.rpc("get_channels");
|
|
199
|
+
if (error) throw error;
|
|
200
|
+
return data.map((bundle) => bundle.channel);
|
|
201
|
+
},
|
|
202
|
+
async commitBundle({ changedSets }) {
|
|
203
|
+
if (changedSets.length === 0) return;
|
|
204
|
+
for (const op of changedSets) if (op.operation === "delete") {
|
|
205
|
+
const { error: patchDeleteError } = await supabase.from("bundle_patches").delete().eq("bundle_id", op.data.id);
|
|
206
|
+
if (patchDeleteError) throw new Error(`Failed to delete bundle patches: ${patchDeleteError.message}`);
|
|
207
|
+
const { error: basePatchDeleteError } = await supabase.from("bundle_patches").delete().eq("base_bundle_id", op.data.id);
|
|
208
|
+
if (basePatchDeleteError) throw new Error(`Failed to delete base bundle patches: ${basePatchDeleteError.message}`);
|
|
209
|
+
const { error } = await supabase.from("bundles").delete().eq("id", op.data.id);
|
|
210
|
+
if (error) throw new Error(`Failed to delete bundle: ${error.message}`);
|
|
211
|
+
} else if (op.operation === "insert" || op.operation === "update") {
|
|
212
|
+
const bundle = op.data;
|
|
213
|
+
const patchRows = bundleToPatchRows(bundle);
|
|
214
|
+
const { error } = await supabase.from("bundles").upsert(bundleToRow(bundle), { onConflict: "id" });
|
|
215
|
+
if (error) throw error;
|
|
216
|
+
const { error: patchDeleteError } = await supabase.from("bundle_patches").delete().eq("bundle_id", bundle.id);
|
|
217
|
+
if (patchDeleteError) throw patchDeleteError;
|
|
218
|
+
if (patchRows.length > 0) {
|
|
219
|
+
const { error: patchInsertError } = await supabase.from("bundle_patches").upsert(patchRows, { onConflict: "id" });
|
|
220
|
+
if (patchInsertError) throw patchInsertError;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/supabaseEdgeFunctionDatabase.ts
|
|
229
|
+
const supabaseEdgeFunctionDatabase = (config, hooks) => {
|
|
230
|
+
return supabaseDatabase({
|
|
231
|
+
supabaseUrl: config.supabaseUrl,
|
|
232
|
+
supabaseAnonKey: config.supabaseServiceRoleKey
|
|
233
|
+
}, hooks);
|
|
234
|
+
};
|
|
235
|
+
//#endregion
|
|
236
|
+
//#region src/supabaseEdgeFunctionStorage.ts
|
|
237
|
+
const parseSupabaseStorageUri = (storageUri) => {
|
|
238
|
+
const storageUrl = new URL(storageUri);
|
|
239
|
+
if (storageUrl.protocol !== "supabase-storage:") throw new Error("Invalid Supabase storage URI protocol");
|
|
240
|
+
const bucketName = storageUrl.host;
|
|
241
|
+
const key = storageUrl.pathname.replace(/^\/+/, "");
|
|
242
|
+
if (!bucketName || !key) throw new Error("Invalid Supabase storage URI");
|
|
243
|
+
return {
|
|
244
|
+
bucketName,
|
|
245
|
+
key
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
const supabaseEdgeFunctionStorage = (0, _hot_updater_plugin_core.createRuntimeStoragePlugin)({
|
|
249
|
+
name: "supabaseEdgeFunctionStorage",
|
|
250
|
+
supportedProtocol: "supabase-storage",
|
|
251
|
+
factory: (config) => {
|
|
252
|
+
const supabase = (0, _supabase_supabase_js.createClient)(config.supabaseUrl, config.supabaseServiceRoleKey);
|
|
253
|
+
return {
|
|
254
|
+
async readText(storageUri) {
|
|
255
|
+
const { bucketName, key } = parseSupabaseStorageUri(storageUri);
|
|
256
|
+
const { data, error } = await supabase.storage.from(bucketName).download(key);
|
|
257
|
+
if (error) {
|
|
258
|
+
if (error.message?.includes("not found")) return null;
|
|
259
|
+
throw new Error(`Failed to read storage text: ${error.message}`);
|
|
260
|
+
}
|
|
261
|
+
if (!data) return null;
|
|
262
|
+
return data.text();
|
|
263
|
+
},
|
|
264
|
+
async getDownloadUrl(storageUri) {
|
|
265
|
+
const { bucketName, key } = parseSupabaseStorageUri(storageUri);
|
|
266
|
+
const { data, error } = await supabase.storage.from(bucketName).createSignedUrl(key, config.signedUrlExpiresIn ?? 3600);
|
|
267
|
+
if (error) throw new Error(`Failed to generate download URL: ${error.message}`);
|
|
268
|
+
if (!data?.signedUrl) throw new Error("Failed to generate download URL");
|
|
269
|
+
return { fileUrl: data.signedUrl };
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
//#endregion
|
|
275
|
+
Object.defineProperty(exports, "supabaseDatabase", {
|
|
276
|
+
enumerable: true,
|
|
277
|
+
get: function() {
|
|
278
|
+
return supabaseDatabase;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
Object.defineProperty(exports, "supabaseEdgeFunctionDatabase", {
|
|
282
|
+
enumerable: true,
|
|
283
|
+
get: function() {
|
|
284
|
+
return supabaseEdgeFunctionDatabase;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
Object.defineProperty(exports, "supabaseEdgeFunctionStorage", {
|
|
288
|
+
enumerable: true,
|
|
289
|
+
get: function() {
|
|
290
|
+
return supabaseEdgeFunctionStorage;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _$_hot_updater_plugin_core0 from "@hot-updater/plugin-core";
|
|
2
|
-
import { DatabasePluginHooks
|
|
2
|
+
import { DatabasePluginHooks } from "@hot-updater/plugin-core";
|
|
3
3
|
|
|
4
4
|
//#region src/supabaseEdgeFunctionDatabase.d.ts
|
|
5
5
|
interface SupabaseEdgeFunctionDatabaseConfig {
|
|
@@ -14,6 +14,6 @@ interface SupabaseEdgeFunctionStorageConfig {
|
|
|
14
14
|
supabaseServiceRoleKey: string;
|
|
15
15
|
signedUrlExpiresIn?: number;
|
|
16
16
|
}
|
|
17
|
-
declare const supabaseEdgeFunctionStorage: (config: SupabaseEdgeFunctionStorageConfig) => () =>
|
|
17
|
+
declare const supabaseEdgeFunctionStorage: (config: SupabaseEdgeFunctionStorageConfig, hooks?: _$_hot_updater_plugin_core0.StoragePluginHooks) => () => _$_hot_updater_plugin_core0.RuntimeStoragePlugin<unknown>;
|
|
18
18
|
//#endregion
|
|
19
19
|
export { supabaseEdgeFunctionDatabase as i, supabaseEdgeFunctionStorage as n, SupabaseEdgeFunctionDatabaseConfig as r, SupabaseEdgeFunctionStorageConfig as t };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/supabase",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.31.1",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
7
7
|
"module": "dist/index.mjs",
|
|
@@ -47,10 +47,10 @@
|
|
|
47
47
|
"@supabase/supabase-js": "2.76.1",
|
|
48
48
|
"hono": "4.12.9",
|
|
49
49
|
"uuidv7": "^1.0.2",
|
|
50
|
-
"@hot-updater/core": "0.
|
|
51
|
-
"@hot-updater/
|
|
52
|
-
"@hot-updater/
|
|
53
|
-
"@hot-updater/
|
|
50
|
+
"@hot-updater/core": "0.31.1",
|
|
51
|
+
"@hot-updater/cli-tools": "0.31.1",
|
|
52
|
+
"@hot-updater/server": "0.31.1",
|
|
53
|
+
"@hot-updater/plugin-core": "0.31.1"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@electric-sql/pglite": "0.2.17",
|
|
@@ -60,10 +60,10 @@
|
|
|
60
60
|
"execa": "9.5.2",
|
|
61
61
|
"@types/node": "^20",
|
|
62
62
|
"mime": "^4.0.4",
|
|
63
|
-
"@hot-updater/js": "0.
|
|
64
|
-
"@hot-updater/mock": "0.
|
|
65
|
-
"@hot-updater/
|
|
66
|
-
"@hot-updater/
|
|
63
|
+
"@hot-updater/js": "0.31.1",
|
|
64
|
+
"@hot-updater/mock": "0.31.1",
|
|
65
|
+
"@hot-updater/test-utils": "0.31.1",
|
|
66
|
+
"@hot-updater/postgres": "0.31.1"
|
|
67
67
|
},
|
|
68
68
|
"scripts": {
|
|
69
69
|
"build": "tsdown",
|
|
@@ -16,7 +16,10 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
16
16
|
import { transformEnv } from "@hot-updater/cli-tools";
|
|
17
17
|
import { type Bundle, type GetBundlesArgs, NIL_UUID } from "@hot-updater/core";
|
|
18
18
|
import { createHotUpdater } from "@hot-updater/server/runtime";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
setupBsdiffManifestUpdateInfoTestSuite,
|
|
21
|
+
setupGetUpdateInfoTestSuite,
|
|
22
|
+
} from "@hot-updater/test-utils";
|
|
20
23
|
import { createClient } from "@supabase/supabase-js";
|
|
21
24
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
22
25
|
|
|
@@ -26,6 +29,7 @@ import {
|
|
|
26
29
|
runCheckedCommand,
|
|
27
30
|
spawnRuntime,
|
|
28
31
|
stopRuntime,
|
|
32
|
+
formatRuntimeLogs,
|
|
29
33
|
waitForHttpOk,
|
|
30
34
|
} from "../../../../packages/test-utils/src/runtimeProcess";
|
|
31
35
|
import { supabaseDatabase } from "../../src/supabaseDatabase";
|
|
@@ -211,7 +215,8 @@ describe.sequential("supabase edge runtime acceptance", () => {
|
|
|
211
215
|
);
|
|
212
216
|
}
|
|
213
217
|
|
|
214
|
-
await
|
|
218
|
+
await waitForRestApiReady(gatewayBaseUrl, 180_000);
|
|
219
|
+
await waitForUrlOk(`${gatewayBaseUrl}/storage/v1/status`, 180_000);
|
|
215
220
|
|
|
216
221
|
supabaseAdmin = createClient(gatewayBaseUrl, SERVICE_ROLE_KEY);
|
|
217
222
|
await ensureBucketExists(supabaseAdmin);
|
|
@@ -242,10 +247,12 @@ describe.sequential("supabase edge runtime acceptance", () => {
|
|
|
242
247
|
"--rm",
|
|
243
248
|
"--network",
|
|
244
249
|
`${composeProjectName}_default`,
|
|
250
|
+
"--add-host",
|
|
251
|
+
"host.docker.internal:host-gateway",
|
|
245
252
|
"-p",
|
|
246
253
|
`127.0.0.1:${edgePort}:8000`,
|
|
247
254
|
"-e",
|
|
248
|
-
`SUPABASE_URL=http://
|
|
255
|
+
`SUPABASE_URL=http://host.docker.internal:${gatewayPort}`,
|
|
249
256
|
"-e",
|
|
250
257
|
`SUPABASE_SERVICE_ROLE_KEY=${SERVICE_ROLE_KEY}`,
|
|
251
258
|
"-e",
|
|
@@ -280,7 +287,7 @@ describe.sequential("supabase edge runtime acceptance", () => {
|
|
|
280
287
|
logs: edgeRuntime.logs,
|
|
281
288
|
timeoutMs: 90_000,
|
|
282
289
|
});
|
|
283
|
-
},
|
|
290
|
+
}, 300_000);
|
|
284
291
|
|
|
285
292
|
beforeEach(async () => {
|
|
286
293
|
if (!supabaseAdmin) {
|
|
@@ -328,24 +335,153 @@ describe.sequential("supabase edge runtime acceptance", () => {
|
|
|
328
335
|
}
|
|
329
336
|
}, 60_000);
|
|
330
337
|
|
|
331
|
-
const
|
|
332
|
-
if (!supabaseAdmin) {
|
|
333
|
-
throw new Error("Supabase admin client was not initialized.");
|
|
334
|
-
}
|
|
335
|
-
|
|
338
|
+
const seedRuntimeBundles = async (bundles: Bundle[]) => {
|
|
336
339
|
for (const bundle of bundles.map(toRuntimeBundle)) {
|
|
337
|
-
await uploadBundleObject(supabaseAdmin, bundle.id);
|
|
338
340
|
await seedHotUpdater.insertBundle(bundle);
|
|
339
341
|
}
|
|
342
|
+
};
|
|
340
343
|
|
|
344
|
+
const requestUpdateInfo = async (args: GetBundlesArgs) => {
|
|
341
345
|
const response = await fetch(
|
|
342
346
|
`http://127.0.0.1:${edgePort}${FUNCTION_BASE_PATH}${createCanonicalPath(args)}`,
|
|
343
347
|
);
|
|
344
348
|
|
|
349
|
+
if (!response.ok) {
|
|
350
|
+
throw new Error(
|
|
351
|
+
[
|
|
352
|
+
`Edge runtime returned ${response.status} ${response.statusText}`,
|
|
353
|
+
await response.text(),
|
|
354
|
+
edgeRuntime ? formatRuntimeLogs(edgeRuntime.logs) : "",
|
|
355
|
+
].join("\n\n"),
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
345
359
|
return (await response.json()) as any;
|
|
346
360
|
};
|
|
347
361
|
|
|
348
|
-
|
|
362
|
+
const getUpdateInfo = async (bundles: Bundle[], args: GetBundlesArgs) => {
|
|
363
|
+
if (!supabaseAdmin) {
|
|
364
|
+
throw new Error("Supabase admin client was not initialized.");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
for (const bundle of bundles) {
|
|
368
|
+
await uploadBundleObject(supabaseAdmin, bundle.id);
|
|
369
|
+
}
|
|
370
|
+
await seedRuntimeBundles(bundles);
|
|
371
|
+
return requestUpdateInfo(args);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
setupGetUpdateInfoTestSuite({
|
|
375
|
+
getUpdateInfo,
|
|
376
|
+
manifestArtifacts: {
|
|
377
|
+
prepareArtifacts: async (fixture) => {
|
|
378
|
+
await Promise.all([
|
|
379
|
+
uploadStorageObject(
|
|
380
|
+
supabaseAdmin,
|
|
381
|
+
`${fixture.currentBundleId}/manifest.json`,
|
|
382
|
+
JSON.stringify(fixture.currentManifest),
|
|
383
|
+
"application/json",
|
|
384
|
+
),
|
|
385
|
+
uploadStorageObject(
|
|
386
|
+
supabaseAdmin,
|
|
387
|
+
`${fixture.nextBundleId}/manifest.json`,
|
|
388
|
+
JSON.stringify(fixture.nextManifest),
|
|
389
|
+
"application/json",
|
|
390
|
+
),
|
|
391
|
+
uploadStorageObject(
|
|
392
|
+
supabaseAdmin,
|
|
393
|
+
`${fixture.nextBundleId}/files/${fixture.changedAssetPath}.br`,
|
|
394
|
+
"next-bundle-bytes",
|
|
395
|
+
"application/javascript",
|
|
396
|
+
),
|
|
397
|
+
]);
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
currentArtifacts: {
|
|
401
|
+
assetBaseStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.currentBundleId}/files`,
|
|
402
|
+
manifestFileHash: "sig:manifest-current",
|
|
403
|
+
manifestStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.currentBundleId}/manifest.json`,
|
|
404
|
+
},
|
|
405
|
+
nextArtifacts: {
|
|
406
|
+
assetBaseStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.nextBundleId}/files`,
|
|
407
|
+
manifestFileHash: "sig:manifest-next",
|
|
408
|
+
manifestStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.nextBundleId}/manifest.json`,
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
},
|
|
412
|
+
expectFileUrl: (fileUrl, fixture) => {
|
|
413
|
+
expect(fileUrl).toContain(
|
|
414
|
+
`/storage/v1/object/sign/${BUCKET_NAME}/${fixture.nextBundleId}/files/${fixture.changedAssetPath}.br`,
|
|
415
|
+
);
|
|
416
|
+
},
|
|
417
|
+
expectManifestUrl: (manifestUrl, fixture) => {
|
|
418
|
+
expect(manifestUrl).toContain(
|
|
419
|
+
`/storage/v1/object/sign/${BUCKET_NAME}/${fixture.nextBundleId}/manifest.json`,
|
|
420
|
+
);
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
setupBsdiffManifestUpdateInfoTestSuite({
|
|
426
|
+
seedBundles: seedRuntimeBundles,
|
|
427
|
+
getUpdateInfo: requestUpdateInfo,
|
|
428
|
+
prepareArtifacts: async (fixture) => {
|
|
429
|
+
await Promise.all([
|
|
430
|
+
uploadStorageObject(
|
|
431
|
+
supabaseAdmin,
|
|
432
|
+
`${fixture.currentBundleId}/manifest.json`,
|
|
433
|
+
JSON.stringify(fixture.currentManifest),
|
|
434
|
+
"application/json",
|
|
435
|
+
),
|
|
436
|
+
uploadStorageObject(
|
|
437
|
+
supabaseAdmin,
|
|
438
|
+
`${fixture.nextBundleId}/manifest.json`,
|
|
439
|
+
JSON.stringify(fixture.nextManifest),
|
|
440
|
+
"application/json",
|
|
441
|
+
),
|
|
442
|
+
uploadStorageObject(
|
|
443
|
+
supabaseAdmin,
|
|
444
|
+
`${fixture.nextBundleId}/files/${fixture.assetPath}`,
|
|
445
|
+
"next-bundle-bytes",
|
|
446
|
+
"application/javascript",
|
|
447
|
+
),
|
|
448
|
+
uploadStorageObject(
|
|
449
|
+
supabaseAdmin,
|
|
450
|
+
fixture.patchPath,
|
|
451
|
+
"patch-bytes",
|
|
452
|
+
"application/octet-stream",
|
|
453
|
+
),
|
|
454
|
+
uploadBundleObject(supabaseAdmin, fixture.currentBundleId),
|
|
455
|
+
uploadBundleObject(supabaseAdmin, fixture.nextBundleId),
|
|
456
|
+
]);
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
currentArtifacts: {
|
|
460
|
+
assetBaseStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.currentBundleId}/files`,
|
|
461
|
+
manifestFileHash: "sig:manifest-current",
|
|
462
|
+
manifestStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.currentBundleId}/manifest.json`,
|
|
463
|
+
},
|
|
464
|
+
nextArtifacts: {
|
|
465
|
+
assetBaseStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.nextBundleId}/files`,
|
|
466
|
+
manifestFileHash: "sig:manifest-next",
|
|
467
|
+
manifestStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.nextBundleId}/manifest.json`,
|
|
468
|
+
patches: [
|
|
469
|
+
{
|
|
470
|
+
baseBundleId: fixture.currentBundleId,
|
|
471
|
+
baseFileHash: "hash-old-bundle",
|
|
472
|
+
patchFileHash: "hash-bsdiff",
|
|
473
|
+
patchStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.patchPath}`,
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
},
|
|
479
|
+
expectPatchUrl: (patchUrl, fixture) => {
|
|
480
|
+
expect(patchUrl).toContain(
|
|
481
|
+
`/storage/v1/object/sign/${BUCKET_NAME}/${fixture.patchPath}`,
|
|
482
|
+
);
|
|
483
|
+
},
|
|
484
|
+
});
|
|
349
485
|
|
|
350
486
|
it("serves canonical routes from the edge function entrypoint", async () => {
|
|
351
487
|
const bundle = toRuntimeBundle({
|
|
@@ -451,6 +587,36 @@ const waitForUrlOk = async (url: string, timeoutMs = 90_000) => {
|
|
|
451
587
|
throw new Error(`Timed out waiting for ${url}: ${lastError}`);
|
|
452
588
|
};
|
|
453
589
|
|
|
590
|
+
const waitForRestApiReady = async (baseUrl: string, timeoutMs = 90_000) => {
|
|
591
|
+
const deadline = Date.now() + timeoutMs;
|
|
592
|
+
let lastError = "no response";
|
|
593
|
+
|
|
594
|
+
while (Date.now() < deadline) {
|
|
595
|
+
try {
|
|
596
|
+
const response = await fetch(
|
|
597
|
+
`${baseUrl}/rest/v1/bundles?select=id&limit=1`,
|
|
598
|
+
{
|
|
599
|
+
headers: {
|
|
600
|
+
apikey: SERVICE_ROLE_KEY,
|
|
601
|
+
Authorization: `Bearer ${SERVICE_ROLE_KEY}`,
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
);
|
|
605
|
+
if (response.ok) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
lastError = `${response.status} ${response.statusText}: ${await response.text()}`;
|
|
610
|
+
} catch (error) {
|
|
611
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
await sleep(500);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
throw new Error(`Timed out waiting for PostgREST: ${lastError}`);
|
|
618
|
+
};
|
|
619
|
+
|
|
454
620
|
const sleep = async (ms: number) => {
|
|
455
621
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
456
622
|
};
|
|
@@ -479,11 +645,25 @@ const ensureBucketExists = async (
|
|
|
479
645
|
const uploadBundleObject = async (
|
|
480
646
|
supabaseAdmin: ReturnType<typeof createClient>,
|
|
481
647
|
bundleId: string,
|
|
648
|
+
) => {
|
|
649
|
+
await uploadStorageObject(
|
|
650
|
+
supabaseAdmin,
|
|
651
|
+
`${bundleId}/bundle.zip`,
|
|
652
|
+
Buffer.from("zip"),
|
|
653
|
+
"application/zip",
|
|
654
|
+
);
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
const uploadStorageObject = async (
|
|
658
|
+
supabaseAdmin: ReturnType<typeof createClient>,
|
|
659
|
+
key: string,
|
|
660
|
+
body: string | Buffer,
|
|
661
|
+
contentType: string,
|
|
482
662
|
) => {
|
|
483
663
|
const { error } = await supabaseAdmin.storage
|
|
484
664
|
.from(BUCKET_NAME)
|
|
485
|
-
.upload(
|
|
486
|
-
contentType
|
|
665
|
+
.upload(key, body, {
|
|
666
|
+
contentType,
|
|
487
667
|
cacheControl: "31536000",
|
|
488
668
|
upsert: true,
|
|
489
669
|
});
|
|
@@ -664,7 +844,7 @@ services:
|
|
|
664
844
|
rest:
|
|
665
845
|
condition: service_started
|
|
666
846
|
ports:
|
|
667
|
-
- "
|
|
847
|
+
- "0.0.0.0:${gatewayPort}:8000"
|
|
668
848
|
volumes:
|
|
669
849
|
- ${path.join(runtimeRoot, "nginx.conf")}:/etc/nginx/nginx.conf:ro
|
|
670
850
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- HotUpdater.bundle_artifact_columns
|
|
2
|
+
|
|
3
|
+
ALTER TABLE bundles ADD COLUMN IF NOT EXISTS manifest_storage_uri text;
|
|
4
|
+
ALTER TABLE bundles ADD COLUMN IF NOT EXISTS manifest_file_hash text;
|
|
5
|
+
ALTER TABLE bundles ADD COLUMN IF NOT EXISTS asset_base_storage_uri text;
|
|
6
|
+
|
|
7
|
+
CREATE TABLE IF NOT EXISTS bundle_patches (
|
|
8
|
+
id text PRIMARY KEY,
|
|
9
|
+
bundle_id uuid NOT NULL REFERENCES bundles(id) ON DELETE CASCADE,
|
|
10
|
+
base_bundle_id uuid NOT NULL REFERENCES bundles(id) ON DELETE CASCADE,
|
|
11
|
+
base_file_hash text NOT NULL,
|
|
12
|
+
patch_file_hash text NOT NULL,
|
|
13
|
+
patch_storage_uri text NOT NULL,
|
|
14
|
+
order_index integer NOT NULL DEFAULT 0
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE INDEX IF NOT EXISTS bundle_patches_bundle_id_idx
|
|
18
|
+
ON bundle_patches(bundle_id);
|
|
19
|
+
CREATE INDEX IF NOT EXISTS bundle_patches_base_bundle_id_idx
|
|
20
|
+
ON bundle_patches(base_bundle_id);
|