@hot-updater/supabase 0.36.7 → 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.
- 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
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
2
|
+
let _hot_updater_plugin_core_internal = require("@hot-updater/plugin-core/internal");
|
|
3
|
+
let _supabase_supabase_js = require("@supabase/supabase-js");
|
|
4
|
+
//#region src/supabaseConfig.ts
|
|
5
|
+
const resolveSupabaseServiceRoleKey = (config) => {
|
|
6
|
+
if (!config.supabaseServiceRoleKey) throw new Error("Supabase service role key is required. Set supabaseServiceRoleKey.");
|
|
7
|
+
return config.supabaseServiceRoleKey;
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/supabaseFilter.ts
|
|
11
|
+
var InvalidSupabasePredicateError = class extends Error {
|
|
12
|
+
name = "InvalidSupabasePredicateError";
|
|
13
|
+
};
|
|
14
|
+
const encodeValue = (value) => {
|
|
15
|
+
if (typeof value !== "string") return String(value);
|
|
16
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
|
|
17
|
+
};
|
|
18
|
+
const encodePattern = (value) => encodeValue(value.replaceAll("*", "\\*").replaceAll("%", "\\%").replaceAll("_", "\\_"));
|
|
19
|
+
const predicate = (condition) => {
|
|
20
|
+
const operator = condition.operator ?? "eq";
|
|
21
|
+
const field = condition.field;
|
|
22
|
+
switch (operator) {
|
|
23
|
+
case "eq":
|
|
24
|
+
case "ne":
|
|
25
|
+
if (condition.value === null) return `${field}.${operator === "eq" ? "is" : "not.is"}.null`;
|
|
26
|
+
if (typeof condition.value !== "boolean" && typeof condition.value !== "number" && typeof condition.value !== "string") throw new InvalidSupabasePredicateError();
|
|
27
|
+
if ("mode" in condition && condition.mode === "insensitive") {
|
|
28
|
+
const comparison = encodePattern(condition.value);
|
|
29
|
+
return `${field}.${operator === "eq" ? "ilike" : "not.ilike"}.${comparison}`;
|
|
30
|
+
}
|
|
31
|
+
return `${field}.${operator === "eq" ? "eq" : "neq"}.${encodeValue(condition.value)}`;
|
|
32
|
+
case "gt":
|
|
33
|
+
case "gte":
|
|
34
|
+
case "lt":
|
|
35
|
+
case "lte":
|
|
36
|
+
if (typeof condition.value !== "number" && typeof condition.value !== "string") throw new InvalidSupabasePredicateError();
|
|
37
|
+
return `${field}.${operator}.${encodeValue(condition.value)}`;
|
|
38
|
+
case "in":
|
|
39
|
+
case "not_in": {
|
|
40
|
+
if (!Array.isArray(condition.value)) throw new InvalidSupabasePredicateError();
|
|
41
|
+
if (condition.value.length === 0) return operator === "in" ? `and(${field}.is.null,${field}.not.is.null)` : `or(${field}.is.null,${field}.not.is.null)`;
|
|
42
|
+
const values = condition.value.map(encodeValue).join(",");
|
|
43
|
+
return `${field}.${operator === "in" ? "in" : "not.in"}.(${values})`;
|
|
44
|
+
}
|
|
45
|
+
case "contains":
|
|
46
|
+
case "starts_with":
|
|
47
|
+
case "ends_with": {
|
|
48
|
+
if (typeof condition.value !== "string") throw new InvalidSupabasePredicateError();
|
|
49
|
+
const value = encodePattern(condition.value).slice(1, -1);
|
|
50
|
+
const pattern = operator === "contains" ? `"*${value}*"` : operator === "starts_with" ? `"${value}*"` : `"*${value}"`;
|
|
51
|
+
return `${field}.${"mode" in condition && condition.mode === "insensitive" ? "ilike" : "like"}.${pattern}`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const buildSupabaseFilter = (where) => {
|
|
56
|
+
const [first, ...rest] = where ?? [];
|
|
57
|
+
if (first === void 0) return;
|
|
58
|
+
let expression = predicate(first);
|
|
59
|
+
for (const condition of rest) {
|
|
60
|
+
const next = predicate(condition);
|
|
61
|
+
expression = condition.connector === "OR" ? `or(${expression},${next})` : `and(${expression},${next})`;
|
|
62
|
+
}
|
|
63
|
+
return expression;
|
|
64
|
+
};
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/supabaseInfrastructureNames.ts
|
|
67
|
+
const SUPABASE_V1_TABLE_NAMES = {
|
|
68
|
+
apiKeys: "hot_updater_v1_api_keys",
|
|
69
|
+
bundleEvents: "hot_updater_v1_bundle_events",
|
|
70
|
+
bundlePatches: "hot_updater_v1_bundle_patches",
|
|
71
|
+
bundles: "hot_updater_v1_bundles",
|
|
72
|
+
channels: "hot_updater_v1_channels",
|
|
73
|
+
releaseCatalogs: "hot_updater_v1_release_catalogs",
|
|
74
|
+
releases: "hot_updater_v1_releases",
|
|
75
|
+
settings: "hot_updater_v1_private_settings"
|
|
76
|
+
};
|
|
77
|
+
const SUPABASE_V1_FUNCTION_NAMES = {
|
|
78
|
+
commit: "hot_updater_v1_commit",
|
|
79
|
+
deleteChannel: "hot_updater_v1_delete_channel"
|
|
80
|
+
};
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/supabaseResult.ts
|
|
83
|
+
var SupabaseDatabaseError = class extends Error {
|
|
84
|
+
name = "SupabaseDatabaseError";
|
|
85
|
+
constructor(operation, cause) {
|
|
86
|
+
super(`Supabase database operation failed: ${operation}`, { cause });
|
|
87
|
+
this.operation = operation;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var SupabaseMissingDataError = class extends Error {
|
|
91
|
+
name = "SupabaseMissingDataError";
|
|
92
|
+
constructor(operation) {
|
|
93
|
+
super(`Supabase database operation returned no row: ${operation}`);
|
|
94
|
+
this.operation = operation;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const throwSupabaseError = (operation, error) => {
|
|
98
|
+
if (error !== null) throw new SupabaseDatabaseError(operation, error);
|
|
99
|
+
};
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/supabaseDatabase.ts
|
|
102
|
+
const isForeignKeyViolation = (error) => typeof error === "object" && error !== null && Reflect.get(error, "code") === "23503";
|
|
103
|
+
const createSupabaseImplementation = (supabase) => {
|
|
104
|
+
const implementation = {
|
|
105
|
+
async create(input) {
|
|
106
|
+
switch (input.model) {
|
|
107
|
+
case "bundles": {
|
|
108
|
+
const { data, error } = await supabase.from(SUPABASE_V1_TABLE_NAMES.bundles).insert(input.data).select("*").single();
|
|
109
|
+
throwSupabaseError("create bundles", error);
|
|
110
|
+
if (data === null) throw new SupabaseMissingDataError("create bundles");
|
|
111
|
+
return data;
|
|
112
|
+
}
|
|
113
|
+
case "bundle_patches": {
|
|
114
|
+
const { data, error } = await supabase.from(SUPABASE_V1_TABLE_NAMES.bundlePatches).insert(input.data).select("*").single();
|
|
115
|
+
throwSupabaseError("create bundle_patches", error);
|
|
116
|
+
if (data === null) throw new SupabaseMissingDataError("create bundle_patches");
|
|
117
|
+
return data;
|
|
118
|
+
}
|
|
119
|
+
case "channels": {
|
|
120
|
+
if (input.onConflict === "ignore") {
|
|
121
|
+
const { data, error } = await supabase.from(SUPABASE_V1_TABLE_NAMES.channels).upsert(input.data, {
|
|
122
|
+
onConflict: "name",
|
|
123
|
+
ignoreDuplicates: true
|
|
124
|
+
}).select("*").maybeSingle();
|
|
125
|
+
throwSupabaseError("create channels", error);
|
|
126
|
+
return data ?? input.data;
|
|
127
|
+
}
|
|
128
|
+
const { data, error } = await supabase.from(SUPABASE_V1_TABLE_NAMES.channels).insert(input.data).select("*").single();
|
|
129
|
+
throwSupabaseError("create channels", error);
|
|
130
|
+
if (data === null) throw new SupabaseMissingDataError("create channels");
|
|
131
|
+
return data;
|
|
132
|
+
}
|
|
133
|
+
case "bundle_events": {
|
|
134
|
+
const { data, error } = await supabase.from(SUPABASE_V1_TABLE_NAMES.bundleEvents).insert(input.data).select("*").single();
|
|
135
|
+
throwSupabaseError("create bundle_events", error);
|
|
136
|
+
if (data === null) throw new SupabaseMissingDataError("create bundle_events");
|
|
137
|
+
return data;
|
|
138
|
+
}
|
|
139
|
+
case "releases": {
|
|
140
|
+
const { data, error } = await supabase.from(SUPABASE_V1_TABLE_NAMES.releases).insert(input.data).select("*").single();
|
|
141
|
+
throwSupabaseError("create releases", error);
|
|
142
|
+
if (data === null) throw new SupabaseMissingDataError("create releases");
|
|
143
|
+
return data;
|
|
144
|
+
}
|
|
145
|
+
case "release_catalogs": {
|
|
146
|
+
const { data, error } = await supabase.from(SUPABASE_V1_TABLE_NAMES.releaseCatalogs).insert(input.data).select("*").single();
|
|
147
|
+
throwSupabaseError("create release_catalogs", error);
|
|
148
|
+
if (data === null) throw new SupabaseMissingDataError("create release_catalogs");
|
|
149
|
+
return data;
|
|
150
|
+
}
|
|
151
|
+
case "api_keys": {
|
|
152
|
+
const { data, error } = await (input.onConflict === "ignore" ? supabase.from(SUPABASE_V1_TABLE_NAMES.apiKeys).upsert(input.data, {
|
|
153
|
+
onConflict: "hash",
|
|
154
|
+
ignoreDuplicates: true
|
|
155
|
+
}) : supabase.from(SUPABASE_V1_TABLE_NAMES.apiKeys).insert(input.data)).select("*").maybeSingle();
|
|
156
|
+
throwSupabaseError("create api_keys", error);
|
|
157
|
+
if (data === null && input.onConflict !== "ignore") throw new SupabaseMissingDataError("create api_keys");
|
|
158
|
+
return data ?? input.data;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
async update(input) {
|
|
163
|
+
const filter = buildSupabaseFilter(input.where);
|
|
164
|
+
if (input.model === "api_keys") {
|
|
165
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.apiKeys).update(input.update);
|
|
166
|
+
if (filter !== void 0) query = query.or(filter);
|
|
167
|
+
const { data, error } = await query.select("*").maybeSingle();
|
|
168
|
+
throwSupabaseError("update api_keys", error);
|
|
169
|
+
return data;
|
|
170
|
+
}
|
|
171
|
+
if (input.model === "releases") {
|
|
172
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.releases).update(input.update);
|
|
173
|
+
if (filter !== void 0) query = query.or(filter);
|
|
174
|
+
const { data, error } = await query.select("*").maybeSingle();
|
|
175
|
+
throwSupabaseError("update releases", error);
|
|
176
|
+
return data;
|
|
177
|
+
}
|
|
178
|
+
if (input.model === "release_catalogs") {
|
|
179
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.releaseCatalogs).update(input.update);
|
|
180
|
+
if (filter !== void 0) query = query.or(filter);
|
|
181
|
+
const { data, error } = await query.select("*").maybeSingle();
|
|
182
|
+
throwSupabaseError("update release_catalogs", error);
|
|
183
|
+
return data;
|
|
184
|
+
}
|
|
185
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.bundles).update(input.update);
|
|
186
|
+
if (filter !== void 0) query = query.or(filter);
|
|
187
|
+
const { data, error } = await query.select("*").maybeSingle();
|
|
188
|
+
throwSupabaseError("update bundles", error);
|
|
189
|
+
return data;
|
|
190
|
+
},
|
|
191
|
+
async delete(input) {
|
|
192
|
+
const filter = buildSupabaseFilter(input.where);
|
|
193
|
+
switch (input.model) {
|
|
194
|
+
case "bundles": {
|
|
195
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.bundles).delete();
|
|
196
|
+
if (filter !== void 0) query = query.or(filter);
|
|
197
|
+
const { error } = await query;
|
|
198
|
+
throwSupabaseError("delete bundles", error);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
case "bundle_patches": {
|
|
202
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.bundlePatches).delete();
|
|
203
|
+
if (filter !== void 0) query = query.or(filter);
|
|
204
|
+
const { error } = await query;
|
|
205
|
+
throwSupabaseError("delete bundle_patches", error);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
case "channels": {
|
|
209
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.channels).delete();
|
|
210
|
+
if (filter !== void 0) query = query.or(filter);
|
|
211
|
+
const { error } = await query;
|
|
212
|
+
if (isForeignKeyViolation(error)) throw new _hot_updater_plugin_core_internal.DatabaseRowReferencedError();
|
|
213
|
+
throwSupabaseError("delete channels", error);
|
|
214
|
+
}
|
|
215
|
+
case "releases": {
|
|
216
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.releases).delete();
|
|
217
|
+
if (filter !== void 0) query = query.or(filter);
|
|
218
|
+
const { error } = await query;
|
|
219
|
+
throwSupabaseError("delete releases", error);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
async count(input) {
|
|
224
|
+
if (input.distinct !== void 0) throw new _hot_updater_plugin_core.DatabasePluginInputError("invalid-operation");
|
|
225
|
+
const filter = buildSupabaseFilter(input.where);
|
|
226
|
+
switch (input.model) {
|
|
227
|
+
case "bundles": {
|
|
228
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.bundles).select("*", {
|
|
229
|
+
count: "exact",
|
|
230
|
+
head: true
|
|
231
|
+
});
|
|
232
|
+
if (filter !== void 0) query = query.or(filter);
|
|
233
|
+
const { count, error } = await query;
|
|
234
|
+
throwSupabaseError("count bundles", error);
|
|
235
|
+
return count ?? 0;
|
|
236
|
+
}
|
|
237
|
+
case "bundle_patches": {
|
|
238
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.bundlePatches).select("*", {
|
|
239
|
+
count: "exact",
|
|
240
|
+
head: true
|
|
241
|
+
});
|
|
242
|
+
if (filter !== void 0) query = query.or(filter);
|
|
243
|
+
const { count, error } = await query;
|
|
244
|
+
throwSupabaseError("count bundle_patches", error);
|
|
245
|
+
return count ?? 0;
|
|
246
|
+
}
|
|
247
|
+
case "releases": {
|
|
248
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.releases).select("*", {
|
|
249
|
+
count: "exact",
|
|
250
|
+
head: true
|
|
251
|
+
});
|
|
252
|
+
if (filter !== void 0) query = query.or(filter);
|
|
253
|
+
const { count, error } = await query;
|
|
254
|
+
throwSupabaseError("count releases", error);
|
|
255
|
+
return count ?? 0;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
async findOne(input) {
|
|
260
|
+
const filter = buildSupabaseFilter(input.where);
|
|
261
|
+
switch (input.model) {
|
|
262
|
+
case "bundles": {
|
|
263
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.bundles).select("*");
|
|
264
|
+
if (filter !== void 0) query = query.or(filter);
|
|
265
|
+
const { data, error } = await query.limit(1).maybeSingle();
|
|
266
|
+
throwSupabaseError("findOne bundles", error);
|
|
267
|
+
return data;
|
|
268
|
+
}
|
|
269
|
+
case "api_keys": {
|
|
270
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.apiKeys).select("*");
|
|
271
|
+
if (filter !== void 0) query = query.or(filter);
|
|
272
|
+
const { data, error } = await query.limit(1).maybeSingle();
|
|
273
|
+
throwSupabaseError("findOne api_keys", error);
|
|
274
|
+
return data;
|
|
275
|
+
}
|
|
276
|
+
case "channels": {
|
|
277
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.channels).select("*");
|
|
278
|
+
if (filter !== void 0) query = query.or(filter);
|
|
279
|
+
const { data, error } = await query.limit(1).maybeSingle();
|
|
280
|
+
throwSupabaseError("findOne channels", error);
|
|
281
|
+
return data;
|
|
282
|
+
}
|
|
283
|
+
case "bundle_patches": {
|
|
284
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.bundlePatches).select("*");
|
|
285
|
+
if (filter !== void 0) query = query.or(filter);
|
|
286
|
+
const { data, error } = await query.limit(1).maybeSingle();
|
|
287
|
+
throwSupabaseError("findOne bundle_patches", error);
|
|
288
|
+
return data;
|
|
289
|
+
}
|
|
290
|
+
case "releases": {
|
|
291
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.releases).select("*");
|
|
292
|
+
if (filter !== void 0) query = query.or(filter);
|
|
293
|
+
const { data, error } = await query.limit(1).maybeSingle();
|
|
294
|
+
throwSupabaseError("findOne releases", error);
|
|
295
|
+
return data;
|
|
296
|
+
}
|
|
297
|
+
case "release_catalogs": {
|
|
298
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.releaseCatalogs).select("*");
|
|
299
|
+
if (filter !== void 0) query = query.or(filter);
|
|
300
|
+
const { data, error } = await query.limit(1).maybeSingle();
|
|
301
|
+
throwSupabaseError("findOne release_catalogs", error);
|
|
302
|
+
return data;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
async findMany(input) {
|
|
307
|
+
if (input.distinctOn !== void 0) throw new _hot_updater_plugin_core.DatabasePluginInputError("invalid-operation");
|
|
308
|
+
if (input.limit === 0) return [];
|
|
309
|
+
const filter = buildSupabaseFilter(input.where);
|
|
310
|
+
const rangeEnd = input.offset + input.limit - 1;
|
|
311
|
+
const orderBy = input.orderBy ?? [];
|
|
312
|
+
switch (input.model) {
|
|
313
|
+
case "bundles": {
|
|
314
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.bundles).select("*");
|
|
315
|
+
if (filter !== void 0) query = query.or(filter);
|
|
316
|
+
for (const clause of orderBy) query = query.order(clause.field, {
|
|
317
|
+
ascending: clause.direction === "asc",
|
|
318
|
+
...clause.nulls ? { nullsFirst: clause.nulls === "first" } : {}
|
|
319
|
+
});
|
|
320
|
+
const { data, error } = await query.range(input.offset, rangeEnd);
|
|
321
|
+
throwSupabaseError("findMany bundles", error);
|
|
322
|
+
return data ?? [];
|
|
323
|
+
}
|
|
324
|
+
case "bundle_events": {
|
|
325
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.bundleEvents).select("*");
|
|
326
|
+
if (filter !== void 0) query = query.or(filter);
|
|
327
|
+
for (const clause of orderBy) query = query.order(clause.field, {
|
|
328
|
+
ascending: clause.direction === "asc",
|
|
329
|
+
...clause.nulls ? { nullsFirst: clause.nulls === "first" } : {}
|
|
330
|
+
});
|
|
331
|
+
const { data, error } = await query.range(input.offset, rangeEnd);
|
|
332
|
+
throwSupabaseError("findMany bundle_events", error);
|
|
333
|
+
return data ?? [];
|
|
334
|
+
}
|
|
335
|
+
case "api_keys": {
|
|
336
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.apiKeys).select("*");
|
|
337
|
+
if (filter !== void 0) query = query.or(filter);
|
|
338
|
+
for (const clause of orderBy) query = query.order(clause.field, {
|
|
339
|
+
ascending: clause.direction === "asc",
|
|
340
|
+
...clause.nulls ? { nullsFirst: clause.nulls === "first" } : {}
|
|
341
|
+
});
|
|
342
|
+
const { data, error } = await query.range(input.offset, rangeEnd);
|
|
343
|
+
throwSupabaseError("findMany api_keys", error);
|
|
344
|
+
return data ?? [];
|
|
345
|
+
}
|
|
346
|
+
case "bundle_patches": {
|
|
347
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.bundlePatches).select("*");
|
|
348
|
+
if (filter !== void 0) query = query.or(filter);
|
|
349
|
+
for (const clause of orderBy) query = query.order(clause.field, {
|
|
350
|
+
ascending: clause.direction === "asc",
|
|
351
|
+
...clause.nulls ? { nullsFirst: clause.nulls === "first" } : {}
|
|
352
|
+
});
|
|
353
|
+
const { data, error } = await query.range(input.offset, rangeEnd);
|
|
354
|
+
throwSupabaseError("findMany bundle_patches", error);
|
|
355
|
+
return data ?? [];
|
|
356
|
+
}
|
|
357
|
+
case "channels": {
|
|
358
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.channels).select("*");
|
|
359
|
+
if (filter !== void 0) query = query.or(filter);
|
|
360
|
+
for (const clause of orderBy) query = query.order(clause.field, {
|
|
361
|
+
ascending: clause.direction === "asc",
|
|
362
|
+
...clause.nulls ? { nullsFirst: clause.nulls === "first" } : {}
|
|
363
|
+
});
|
|
364
|
+
const { data, error } = await query.range(input.offset, rangeEnd);
|
|
365
|
+
throwSupabaseError("findMany channels", error);
|
|
366
|
+
return data ?? [];
|
|
367
|
+
}
|
|
368
|
+
case "releases": {
|
|
369
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.releases).select("*");
|
|
370
|
+
if (filter !== void 0) query = query.or(filter);
|
|
371
|
+
for (const clause of orderBy) query = query.order(clause.field, {
|
|
372
|
+
ascending: clause.direction === "asc",
|
|
373
|
+
...clause.nulls ? { nullsFirst: clause.nulls === "first" } : {}
|
|
374
|
+
});
|
|
375
|
+
const { data, error } = await query.range(input.offset, rangeEnd);
|
|
376
|
+
throwSupabaseError("findMany releases", error);
|
|
377
|
+
return data ?? [];
|
|
378
|
+
}
|
|
379
|
+
case "release_catalogs": {
|
|
380
|
+
let query = supabase.from(SUPABASE_V1_TABLE_NAMES.releaseCatalogs).select("*");
|
|
381
|
+
if (filter !== void 0) query = query.or(filter);
|
|
382
|
+
for (const clause of orderBy) query = query.order(clause.field, {
|
|
383
|
+
ascending: clause.direction === "asc",
|
|
384
|
+
...clause.nulls ? { nullsFirst: clause.nulls === "first" } : {}
|
|
385
|
+
});
|
|
386
|
+
const { data, error } = await query.range(input.offset, rangeEnd);
|
|
387
|
+
throwSupabaseError("findMany release_catalogs", error);
|
|
388
|
+
return data ?? [];
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
async insertChannel(input) {
|
|
393
|
+
const { data: inserted, error: insertError } = await supabase.from(SUPABASE_V1_TABLE_NAMES.channels).upsert(input.row, {
|
|
394
|
+
onConflict: "name",
|
|
395
|
+
ignoreDuplicates: true
|
|
396
|
+
}).select("*").maybeSingle();
|
|
397
|
+
throwSupabaseError("insert channel", insertError);
|
|
398
|
+
if (inserted !== null) return {
|
|
399
|
+
row: inserted,
|
|
400
|
+
inserted: true
|
|
401
|
+
};
|
|
402
|
+
const { data: existing, error: findError } = await supabase.from(SUPABASE_V1_TABLE_NAMES.channels).select("*").eq("name", input.row.name).single();
|
|
403
|
+
throwSupabaseError("find canonical channel", findError);
|
|
404
|
+
if (existing === null) throw new SupabaseMissingDataError("find canonical channel");
|
|
405
|
+
return {
|
|
406
|
+
row: existing,
|
|
407
|
+
inserted: false
|
|
408
|
+
};
|
|
409
|
+
},
|
|
410
|
+
async deleteChannel(input) {
|
|
411
|
+
const { data, error } = await supabase.rpc(SUPABASE_V1_FUNCTION_NAMES.deleteChannel, { p_id: input.id });
|
|
412
|
+
throwSupabaseError("delete channel", error);
|
|
413
|
+
if (data === null || typeof data !== "object" || !("deleted" in data) || typeof data.deleted !== "boolean") throw new SupabaseMissingDataError("delete channel");
|
|
414
|
+
return data;
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
implementation.commit = async (input) => {
|
|
418
|
+
const { data, error } = await supabase.rpc(SUPABASE_V1_FUNCTION_NAMES.commit, { p_commit: input });
|
|
419
|
+
throwSupabaseError("commit database changes", error);
|
|
420
|
+
if (data === null || typeof data !== "object" || !("committed" in data) || typeof data.committed !== "boolean") throw new SupabaseMissingDataError("commit database changes");
|
|
421
|
+
return data;
|
|
422
|
+
};
|
|
423
|
+
return implementation;
|
|
424
|
+
};
|
|
425
|
+
const supabaseDatabase = (config) => {
|
|
426
|
+
const adapter = (0, _hot_updater_plugin_core_internal.createDatabasePluginAdapter)("supabaseDatabase", createSupabaseImplementation((0, _supabase_supabase_js.createClient)(config.supabaseUrl, resolveSupabaseServiceRoleKey(config))));
|
|
427
|
+
return (0, _hot_updater_plugin_core.createDatabasePlugin)({
|
|
428
|
+
name: "supabaseDatabase",
|
|
429
|
+
models: adapter.models,
|
|
430
|
+
commit: adapter.commit
|
|
431
|
+
});
|
|
432
|
+
};
|
|
433
|
+
//#endregion
|
|
434
|
+
//#region src/supabaseSignedUrlBatcher.ts
|
|
435
|
+
function getErrorMessage(error) {
|
|
436
|
+
if (error instanceof Error) return error.message;
|
|
437
|
+
if (error && typeof error === "object" && "message" in error && typeof error.message === "string") return error.message;
|
|
438
|
+
return String(error);
|
|
439
|
+
}
|
|
440
|
+
const createSupabaseSignedUrlBatcher = ({ createSignedUrls, expiresIn, formatObjectPath }) => {
|
|
441
|
+
let pendingByBucket = /* @__PURE__ */ new Map();
|
|
442
|
+
let flushScheduled = false;
|
|
443
|
+
const createSignedUrlError = (bucketName, key, error) => /* @__PURE__ */ new Error(`Failed to generate download URL for "${formatObjectPath(bucketName, key)}": ${getErrorMessage(error)}`);
|
|
444
|
+
const flush = async () => {
|
|
445
|
+
const batches = pendingByBucket;
|
|
446
|
+
pendingByBucket = /* @__PURE__ */ new Map();
|
|
447
|
+
flushScheduled = false;
|
|
448
|
+
await Promise.all([...batches.entries()].map(async ([bucketName, pending]) => {
|
|
449
|
+
try {
|
|
450
|
+
const { data, error } = await createSignedUrls(bucketName, pending.map(({ key }) => key), expiresIn);
|
|
451
|
+
if (error || !data) {
|
|
452
|
+
const batchError = error ?? /* @__PURE__ */ new Error("missing signed URL response");
|
|
453
|
+
for (const request of pending) request.reject(createSignedUrlError(bucketName, request.key, batchError));
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
pending.forEach((request, index) => {
|
|
457
|
+
const result = data[index];
|
|
458
|
+
if (!result?.error && result?.signedUrl) {
|
|
459
|
+
request.resolve(result.signedUrl);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
request.reject(createSignedUrlError(bucketName, request.key, result?.error ?? /* @__PURE__ */ new Error("missing signed URL")));
|
|
463
|
+
});
|
|
464
|
+
} catch (error) {
|
|
465
|
+
for (const request of pending) request.reject(createSignedUrlError(bucketName, request.key, error));
|
|
466
|
+
}
|
|
467
|
+
}));
|
|
468
|
+
};
|
|
469
|
+
return (bucketName, key) => new Promise((resolve, reject) => {
|
|
470
|
+
const pending = pendingByBucket.get(bucketName) ?? [];
|
|
471
|
+
pending.push({
|
|
472
|
+
key,
|
|
473
|
+
reject,
|
|
474
|
+
resolve
|
|
475
|
+
});
|
|
476
|
+
pendingByBucket.set(bucketName, pending);
|
|
477
|
+
if (!flushScheduled) {
|
|
478
|
+
flushScheduled = true;
|
|
479
|
+
queueMicrotask(() => {
|
|
480
|
+
flush();
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
};
|
|
485
|
+
//#endregion
|
|
486
|
+
//#region src/supabaseStorage.ts
|
|
487
|
+
const isNotFoundError = (error) => error?.message?.toLowerCase().includes("not found") === true;
|
|
488
|
+
const isMissingExistsError = (error) => {
|
|
489
|
+
if (typeof error !== "object" || error === null || Reflect.get(error, "name") !== "StorageUnknownError") return false;
|
|
490
|
+
const originalError = Reflect.get(error, "originalError");
|
|
491
|
+
if (typeof originalError !== "object" || originalError === null) return false;
|
|
492
|
+
const status = Reflect.get(originalError, "status");
|
|
493
|
+
return status === 400 || status === 404;
|
|
494
|
+
};
|
|
495
|
+
const supabaseStorage = (config) => {
|
|
496
|
+
const bucket = (0, _supabase_supabase_js.createClient)(config.supabaseUrl, resolveSupabaseServiceRoleKey(config)).storage.from(config.bucketName);
|
|
497
|
+
const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(config.basePath);
|
|
498
|
+
const resolveSignedUrl = createSupabaseSignedUrlBatcher({
|
|
499
|
+
createSignedUrls: (_bucketName, keys, expiresIn) => bucket.createSignedUrls(keys, expiresIn),
|
|
500
|
+
expiresIn: config.signedUrlExpiresIn ?? 3600,
|
|
501
|
+
formatObjectPath: (_bucketName, key) => key
|
|
502
|
+
});
|
|
503
|
+
const parseAndValidate = (storageUri) => {
|
|
504
|
+
const parsed = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "supabase-storage");
|
|
505
|
+
if (parsed.bucket !== config.bucketName) throw new Error(`Bucket name mismatch: expected "${config.bucketName}", but found "${parsed.bucket}".`);
|
|
506
|
+
return parsed;
|
|
507
|
+
};
|
|
508
|
+
return (0, _hot_updater_plugin_core.createStoragePlugin)({
|
|
509
|
+
name: "supabaseStorage",
|
|
510
|
+
protocol: "supabase-storage",
|
|
511
|
+
async put({ key, body, contentLength, contentType }) {
|
|
512
|
+
const storageKey = getStorageKey(key);
|
|
513
|
+
const { error } = await bucket.upload(storageKey, body, {
|
|
514
|
+
contentType,
|
|
515
|
+
cacheControl: "max-age=31536000",
|
|
516
|
+
duplex: "half",
|
|
517
|
+
...contentLength === void 0 ? {} : { headers: { "content-length": String(contentLength) } }
|
|
518
|
+
});
|
|
519
|
+
if (error) throw error;
|
|
520
|
+
return { storageUri: (0, _hot_updater_plugin_core.createStorageUri)({
|
|
521
|
+
protocol: "supabase-storage",
|
|
522
|
+
bucket: config.bucketName,
|
|
523
|
+
key: storageKey
|
|
524
|
+
}) };
|
|
525
|
+
},
|
|
526
|
+
async get({ storageUri }) {
|
|
527
|
+
const { key } = parseAndValidate(storageUri);
|
|
528
|
+
const { data, error } = await bucket.download(key);
|
|
529
|
+
if (error) {
|
|
530
|
+
if (isNotFoundError(error)) return { response: null };
|
|
531
|
+
throw new Error(`Failed to download storage object: ${error.message}`);
|
|
532
|
+
}
|
|
533
|
+
return { response: data ? new Response(data) : null };
|
|
534
|
+
},
|
|
535
|
+
async getDownloadUrl({ storageUri }) {
|
|
536
|
+
const { key } = parseAndValidate(storageUri);
|
|
537
|
+
return { url: await resolveSignedUrl(config.bucketName, key) };
|
|
538
|
+
},
|
|
539
|
+
async exists({ storageUri }) {
|
|
540
|
+
const { key } = parseAndValidate(storageUri);
|
|
541
|
+
const { data, error } = await bucket.exists(key);
|
|
542
|
+
if (error) {
|
|
543
|
+
if (isMissingExistsError(error)) return { exists: false };
|
|
544
|
+
throw error;
|
|
545
|
+
}
|
|
546
|
+
return { exists: data };
|
|
547
|
+
},
|
|
548
|
+
async delete({ storageUri }) {
|
|
549
|
+
const { key } = parseAndValidate(storageUri);
|
|
550
|
+
const { error } = await bucket.remove([key]);
|
|
551
|
+
if (error && !isNotFoundError(error)) throw new Error(`Failed to delete storage object: ${error.message}`);
|
|
552
|
+
return { deleted: true };
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
};
|
|
556
|
+
//#endregion
|
|
557
|
+
Object.defineProperty(exports, "supabaseDatabase", {
|
|
558
|
+
enumerable: true,
|
|
559
|
+
get: function() {
|
|
560
|
+
return supabaseDatabase;
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
Object.defineProperty(exports, "supabaseStorage", {
|
|
564
|
+
enumerable: true,
|
|
565
|
+
get: function() {
|
|
566
|
+
return supabaseStorage;
|
|
567
|
+
}
|
|
568
|
+
});
|
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": "1.0.0-rc.0",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
7
7
|
"module": "dist/index.mjs",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"require": "./dist/init/index.cjs"
|
|
18
18
|
},
|
|
19
19
|
"./edge": {
|
|
20
|
+
"types": "./dist/edge.d.cts",
|
|
20
21
|
"import": "./dist/edge.mjs",
|
|
21
22
|
"require": "./dist/edge.cjs"
|
|
22
23
|
},
|
|
@@ -55,10 +56,10 @@
|
|
|
55
56
|
"@supabase/supabase-js": "2.76.1",
|
|
56
57
|
"hono": "4.12.34",
|
|
57
58
|
"uuidv7": "^1.0.2",
|
|
58
|
-
"@hot-updater/core": "0.
|
|
59
|
-
"@hot-updater/
|
|
60
|
-
"@hot-updater/
|
|
61
|
-
"@hot-updater/
|
|
59
|
+
"@hot-updater/core": "1.0.0-rc.0",
|
|
60
|
+
"@hot-updater/server": "1.0.0-rc.0",
|
|
61
|
+
"@hot-updater/cli-tools": "1.0.0-rc.0",
|
|
62
|
+
"@hot-updater/plugin-core": "1.0.0-rc.0"
|
|
62
63
|
},
|
|
63
64
|
"devDependencies": {
|
|
64
65
|
"@electric-sql/pglite": "0.4.1",
|
|
@@ -68,10 +69,9 @@
|
|
|
68
69
|
"execa": "9.5.2",
|
|
69
70
|
"@types/node": "^20",
|
|
70
71
|
"mime": "^4.0.4",
|
|
71
|
-
"@hot-updater/
|
|
72
|
-
"@hot-updater/
|
|
73
|
-
"@hot-updater/test-utils": "0.
|
|
74
|
-
"@hot-updater/postgres": "0.36.7"
|
|
72
|
+
"@hot-updater/mock": "1.0.0-rc.0",
|
|
73
|
+
"@hot-updater/postgres": "1.0.0-rc.0",
|
|
74
|
+
"@hot-updater/test-utils": "1.0.0-rc.0"
|
|
75
75
|
},
|
|
76
76
|
"scripts": {
|
|
77
77
|
"build": "tsdown",
|
|
@@ -1,44 +1,40 @@
|
|
|
1
1
|
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
|
2
2
|
import { createHotUpdater } from "@hot-updater/server";
|
|
3
|
-
import {
|
|
4
|
-
supabaseEdgeFunctionDatabase,
|
|
5
|
-
supabaseEdgeFunctionStorage,
|
|
6
|
-
} from "@hot-updater/supabase";
|
|
3
|
+
import { supabaseDatabase, supabaseStorage } from "@hot-updater/supabase/edge";
|
|
7
4
|
import { Hono } from "hono";
|
|
8
5
|
|
|
9
6
|
declare global {
|
|
10
7
|
var HotUpdater: {
|
|
8
|
+
BUCKET_NAME: string;
|
|
11
9
|
FUNCTION_NAME: string;
|
|
12
10
|
};
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
const functionName = HotUpdater.FUNCTION_NAME;
|
|
14
|
+
const bucketName = HotUpdater.BUCKET_NAME;
|
|
16
15
|
const supabaseUrl = Deno.env.get("SUPABASE_URL") ?? "";
|
|
17
16
|
const supabaseServiceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";
|
|
18
17
|
const functionBasePath = `/${functionName}`;
|
|
19
18
|
const hotUpdaterBasePath = "/";
|
|
20
19
|
|
|
21
20
|
const hotUpdater = createHotUpdater({
|
|
22
|
-
database:
|
|
21
|
+
database: supabaseDatabase({
|
|
23
22
|
supabaseUrl,
|
|
24
23
|
supabaseServiceRoleKey,
|
|
25
24
|
}),
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
clientAccess: { type: "api-key" },
|
|
26
|
+
storage: [
|
|
27
|
+
supabaseStorage({
|
|
28
28
|
supabaseUrl,
|
|
29
29
|
supabaseServiceRoleKey,
|
|
30
|
+
bucketName,
|
|
30
31
|
}),
|
|
31
32
|
],
|
|
32
|
-
basePath: hotUpdaterBasePath,
|
|
33
|
-
routes: {
|
|
34
|
-
updateCheck: true,
|
|
35
|
-
bundles: false,
|
|
36
|
-
},
|
|
37
33
|
});
|
|
38
34
|
|
|
39
35
|
const app = new Hono().basePath(functionBasePath);
|
|
40
36
|
|
|
41
37
|
app.get("/ping", (c) => c.text("pong"));
|
|
42
|
-
app.mount(hotUpdaterBasePath, hotUpdater.
|
|
38
|
+
app.mount(hotUpdaterBasePath, hotUpdater.handlers.client);
|
|
43
39
|
|
|
44
40
|
Deno.serve(app.fetch);
|