@hot-updater/supabase 1.2.0-rc.7 → 1.2.0-rc.9
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/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/init/index.cjs +9 -9
- package/dist/init/index.mjs +10 -10
- package/dist/{supabaseStorage-8VAuNYHy.mjs → supabaseStorage-B6IM8UX2.mjs} +82 -0
- package/dist/{supabaseStorage-B131_auF.d.cts → supabaseStorage-C9W-69OP.d.cts} +5 -5
- package/dist/{supabaseStorage-B131_auF.d.mts → supabaseStorage-C9W-69OP.d.mts} +5 -5
- package/dist/{supabaseStorage-BV6GuUz_.cjs → supabaseStorage-DUa49vGh.cjs} +82 -0
- package/package.json +8 -8
package/dist/edge.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_supabaseStorage = require("./supabaseStorage-
|
|
2
|
+
const require_supabaseStorage = require("./supabaseStorage-DUa49vGh.cjs");
|
|
3
3
|
exports.supabaseDatabase = require_supabaseStorage.supabaseDatabase;
|
|
4
4
|
exports.supabaseStorage = require_supabaseStorage.supabaseStorage;
|
package/dist/edge.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as supabaseDatabase, n as supabaseStorage, r as SupabaseDatabaseConfig, t as SupabaseStorageConfig } from "./supabaseStorage-
|
|
1
|
+
import { i as supabaseDatabase, n as supabaseStorage, r as SupabaseDatabaseConfig, t as SupabaseStorageConfig } from "./supabaseStorage-C9W-69OP.cjs";
|
|
2
2
|
export { type SupabaseDatabaseConfig, type SupabaseStorageConfig, supabaseDatabase, supabaseStorage };
|
package/dist/edge.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as supabaseDatabase, n as supabaseStorage, r as SupabaseDatabaseConfig, t as SupabaseStorageConfig } from "./supabaseStorage-
|
|
1
|
+
import { i as supabaseDatabase, n as supabaseStorage, r as SupabaseDatabaseConfig, t as SupabaseStorageConfig } from "./supabaseStorage-C9W-69OP.mjs";
|
|
2
2
|
export { type SupabaseDatabaseConfig, type SupabaseStorageConfig, supabaseDatabase, supabaseStorage };
|
package/dist/edge.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as supabaseDatabase, t as supabaseStorage } from "./supabaseStorage-
|
|
1
|
+
import { n as supabaseDatabase, t as supabaseStorage } from "./supabaseStorage-B6IM8UX2.mjs";
|
|
2
2
|
export { supabaseDatabase, supabaseStorage };
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_supabaseStorage = require("./supabaseStorage-
|
|
2
|
+
const require_supabaseStorage = require("./supabaseStorage-DUa49vGh.cjs");
|
|
3
3
|
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
4
4
|
//#region src/supabaseSchema.ts
|
|
5
5
|
/** Every table the apply RPC may name: models, index tables, settings, and the write guard. */
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as supabaseStorage, r as SupabaseDatabaseConfig, t as SupabaseStorageConfig } from "./supabaseStorage-
|
|
1
|
+
import { n as supabaseStorage, r as SupabaseDatabaseConfig, t as SupabaseStorageConfig } from "./supabaseStorage-C9W-69OP.cjs";
|
|
2
2
|
import { ToolingDatabase } from "@hot-updater/plugin-core";
|
|
3
3
|
//#region src/supabaseMigration.d.ts
|
|
4
4
|
/**
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as supabaseStorage, r as SupabaseDatabaseConfig, t as SupabaseStorageConfig } from "./supabaseStorage-
|
|
1
|
+
import { n as supabaseStorage, r as SupabaseDatabaseConfig, t as SupabaseStorageConfig } from "./supabaseStorage-C9W-69OP.mjs";
|
|
2
2
|
import { ToolingDatabase } from "@hot-updater/plugin-core";
|
|
3
3
|
//#region src/supabaseMigration.d.ts
|
|
4
4
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as SUPABASE_TABLE_PREFIX, n as supabaseDatabase$1, r as SUPABASE_APPLY_FUNCTION, t as supabaseStorage } from "./supabaseStorage-
|
|
1
|
+
import { i as SUPABASE_TABLE_PREFIX, n as supabaseDatabase$1, r as SUPABASE_APPLY_FUNCTION, t as supabaseStorage } from "./supabaseStorage-B6IM8UX2.mjs";
|
|
2
2
|
import { SETTINGS_TABLE, WRITE_GUARD_TABLE, coreSettings, coreTarget, createTableStatements, generateEngineSql, isMultiIndex } from "@hot-updater/plugin-core";
|
|
3
3
|
//#region src/supabaseSchema.ts
|
|
4
4
|
/** Every table the apply RPC may name: models, index tables, settings, and the write guard. */
|
package/dist/init/index.cjs
CHANGED
|
@@ -6421,14 +6421,15 @@ const supabaseDatabase = (config) => {
|
|
|
6421
6421
|
//#region iac/configTemplate.ts
|
|
6422
6422
|
/**
|
|
6423
6423
|
* The config init writes. With the `resources` init set up, it names them,
|
|
6424
|
-
* and reads the service role key with `
|
|
6424
|
+
* and reads the service role key with `secret()`, from the environment and
|
|
6425
6425
|
* `.env.hotupdater`, then the config's `secrets` adapter. Without them, as
|
|
6426
|
-
* the agent scaffold writes it
|
|
6426
|
+
* the agent scaffold writes it before the agent creates the project, it
|
|
6427
|
+
* reads the URL and bucket from the environment, where the agent records
|
|
6428
|
+
* them, and the key with `secret()` alike.
|
|
6427
6429
|
*/
|
|
6428
6430
|
const getConfigScaffold = (build, resources) => {
|
|
6429
6431
|
const supabaseUrl = resources === void 0 ? "process.env.HOT_UPDATER_SUPABASE_URL!" : JSON.stringify(resources.supabaseUrl);
|
|
6430
|
-
const serviceRoleKey =
|
|
6431
|
-
(await deploySecrets()).HOT_UPDATER_SUPABASE_SERVICE_ROLE_KEY`;
|
|
6432
|
+
const serviceRoleKey = "secret(\"HOT_UPDATER_SUPABASE_SERVICE_ROLE_KEY\")";
|
|
6432
6433
|
const storageConfig = {
|
|
6433
6434
|
imports: [{
|
|
6434
6435
|
pkg: "@hot-updater/supabase",
|
|
@@ -6450,7 +6451,6 @@ const getConfigScaffold = (build, resources) => {
|
|
|
6450
6451
|
supabaseServiceRoleKey: ${serviceRoleKey},
|
|
6451
6452
|
})`
|
|
6452
6453
|
};
|
|
6453
|
-
const helperStatements = resources === void 0 ? [] : [(0, _hot_updater_cli_tools.secretEnvHelper)("deploySecrets", ["HOT_UPDATER_SUPABASE_SERVICE_ROLE_KEY"])];
|
|
6454
6454
|
const builder = new _hot_updater_cli_tools.ConfigBuilder().setBuildType(build).setStorage(storageConfig).setDatabase(databaseConfig).setPlugins({
|
|
6455
6455
|
imports: [{
|
|
6456
6456
|
pkg: "hot-updater/plugins",
|
|
@@ -6462,11 +6462,11 @@ const getConfigScaffold = (build, resources) => {
|
|
|
6462
6462
|
}],
|
|
6463
6463
|
configString: "[apiKeys(), insights(), remoteConfig()]"
|
|
6464
6464
|
});
|
|
6465
|
-
|
|
6465
|
+
builder.addImport({
|
|
6466
6466
|
pkg: "hot-updater",
|
|
6467
|
-
named: ["
|
|
6468
|
-
})
|
|
6469
|
-
return (0, _hot_updater_cli_tools.createHotUpdaterConfigScaffoldFromBuilder)(builder
|
|
6467
|
+
named: ["secret"]
|
|
6468
|
+
});
|
|
6469
|
+
return (0, _hot_updater_cli_tools.createHotUpdaterConfigScaffoldFromBuilder)(builder);
|
|
6470
6470
|
};
|
|
6471
6471
|
//#endregion
|
|
6472
6472
|
//#region iac/init/index.ts
|
package/dist/init/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import fs from "fs/promises";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import { ConfigBuilder, InitError, MissingInitInputsError, assembleServer, assertInfrastructureGenerationAtUrl, assertInitInputs, assertInitProviderInputs, confirmInitInputPersistence, copyDirToTmp, createHotUpdaterConfigScaffoldFromBuilder, getHotUpdaterEnvValue, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, link, makeEnv, moduleSpecifiersOf, p, printAppSetup, provisionClientCredential, readHotUpdaterInitEnv, resolveInitProviderInput, resolvePackageVersion,
|
|
4
|
+
import { ConfigBuilder, InitError, MissingInitInputsError, assembleServer, assertInfrastructureGenerationAtUrl, assertInitInputs, assertInitProviderInputs, confirmInitInputPersistence, copyDirToTmp, createHotUpdaterConfigScaffoldFromBuilder, getHotUpdaterEnvValue, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, link, makeEnv, moduleSpecifiersOf, p, printAppSetup, provisionClientCredential, readHotUpdaterInitEnv, resolveInitProviderInput, resolvePackageVersion, transformEnv, transformTemplate, writeHotUpdaterFiles } from "@hot-updater/cli-tools";
|
|
5
5
|
import { apiKeys, insights, remoteConfig } from "@hot-updater/server/plugins";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
|
|
@@ -6416,14 +6416,15 @@ const supabaseDatabase = (config) => {
|
|
|
6416
6416
|
//#region iac/configTemplate.ts
|
|
6417
6417
|
/**
|
|
6418
6418
|
* The config init writes. With the `resources` init set up, it names them,
|
|
6419
|
-
* and reads the service role key with `
|
|
6419
|
+
* and reads the service role key with `secret()`, from the environment and
|
|
6420
6420
|
* `.env.hotupdater`, then the config's `secrets` adapter. Without them, as
|
|
6421
|
-
* the agent scaffold writes it
|
|
6421
|
+
* the agent scaffold writes it before the agent creates the project, it
|
|
6422
|
+
* reads the URL and bucket from the environment, where the agent records
|
|
6423
|
+
* them, and the key with `secret()` alike.
|
|
6422
6424
|
*/
|
|
6423
6425
|
const getConfigScaffold = (build, resources) => {
|
|
6424
6426
|
const supabaseUrl = resources === void 0 ? "process.env.HOT_UPDATER_SUPABASE_URL!" : JSON.stringify(resources.supabaseUrl);
|
|
6425
|
-
const serviceRoleKey =
|
|
6426
|
-
(await deploySecrets()).HOT_UPDATER_SUPABASE_SERVICE_ROLE_KEY`;
|
|
6427
|
+
const serviceRoleKey = "secret(\"HOT_UPDATER_SUPABASE_SERVICE_ROLE_KEY\")";
|
|
6427
6428
|
const storageConfig = {
|
|
6428
6429
|
imports: [{
|
|
6429
6430
|
pkg: "@hot-updater/supabase",
|
|
@@ -6445,7 +6446,6 @@ const getConfigScaffold = (build, resources) => {
|
|
|
6445
6446
|
supabaseServiceRoleKey: ${serviceRoleKey},
|
|
6446
6447
|
})`
|
|
6447
6448
|
};
|
|
6448
|
-
const helperStatements = resources === void 0 ? [] : [secretEnvHelper("deploySecrets", ["HOT_UPDATER_SUPABASE_SERVICE_ROLE_KEY"])];
|
|
6449
6449
|
const builder = new ConfigBuilder().setBuildType(build).setStorage(storageConfig).setDatabase(databaseConfig).setPlugins({
|
|
6450
6450
|
imports: [{
|
|
6451
6451
|
pkg: "hot-updater/plugins",
|
|
@@ -6457,11 +6457,11 @@ const getConfigScaffold = (build, resources) => {
|
|
|
6457
6457
|
}],
|
|
6458
6458
|
configString: "[apiKeys(), insights(), remoteConfig()]"
|
|
6459
6459
|
});
|
|
6460
|
-
|
|
6460
|
+
builder.addImport({
|
|
6461
6461
|
pkg: "hot-updater",
|
|
6462
|
-
named: ["
|
|
6463
|
-
})
|
|
6464
|
-
return createHotUpdaterConfigScaffoldFromBuilder(builder
|
|
6462
|
+
named: ["secret"]
|
|
6463
|
+
});
|
|
6464
|
+
return createHotUpdaterConfigScaffoldFromBuilder(builder);
|
|
6465
6465
|
};
|
|
6466
6466
|
//#endregion
|
|
6467
6467
|
//#region iac/init/index.ts
|
|
@@ -166,7 +166,15 @@ const isMissingObjectError = async (error) => {
|
|
|
166
166
|
};
|
|
167
167
|
/** A character Storage rejects in an object name, `?`, or `!`. */
|
|
168
168
|
const ESCAPED_CHARACTER = /[^A-Za-z0-9_/.*'() &$=@;:+,-]/gu;
|
|
169
|
+
const ESCAPE_SEQUENCE = /!([0-9A-F]{2})/gu;
|
|
169
170
|
const utf8 = new TextEncoder();
|
|
171
|
+
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
172
|
+
/** Objects Storage lists a page at a time. */
|
|
173
|
+
const LIST_PAGE_SIZE = 1e3;
|
|
174
|
+
/** Names Storage removes in one request. */
|
|
175
|
+
const DELETE_BATCH_SIZE = 1e3;
|
|
176
|
+
/** The object the Supabase dashboard keeps in a folder it creates. */
|
|
177
|
+
const EMPTY_FOLDER_PLACEHOLDER = ".emptyFolderPlaceholder";
|
|
170
178
|
/**
|
|
171
179
|
* The object name Storage keeps a key under. Storage accepts names of ASCII
|
|
172
180
|
* letters, digits and `_/!.*'() &$=@;:+,?-` only, and supabase-js puts a name
|
|
@@ -177,6 +185,34 @@ const utf8 = new TextEncoder();
|
|
|
177
185
|
* Deploy's keys, such as `<bundleId>/manifest.json`, are stored as they are.
|
|
178
186
|
*/
|
|
179
187
|
const toObjectName = (key) => key.replace(ESCAPED_CHARACTER, (character) => Array.from(utf8.encode(character), (byte) => `!${byte.toString(16).toUpperCase().padStart(2, "0")}`).join(""));
|
|
188
|
+
/** The key `toObjectName` stored under `name`, or null for a name it never makes. */
|
|
189
|
+
const fromObjectName = (name) => {
|
|
190
|
+
const bytes = [];
|
|
191
|
+
const parts = [];
|
|
192
|
+
const flush = () => {
|
|
193
|
+
if (bytes.length === 0) return true;
|
|
194
|
+
try {
|
|
195
|
+
parts.push(utf8Decoder.decode(new Uint8Array(bytes)));
|
|
196
|
+
} catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
bytes.length = 0;
|
|
200
|
+
return true;
|
|
201
|
+
};
|
|
202
|
+
let index = 0;
|
|
203
|
+
for (const match of name.matchAll(ESCAPE_SEQUENCE)) {
|
|
204
|
+
if (match.index !== index) {
|
|
205
|
+
if (!flush()) return null;
|
|
206
|
+
parts.push(name.slice(index, match.index));
|
|
207
|
+
}
|
|
208
|
+
bytes.push(Number.parseInt(match[1], 16));
|
|
209
|
+
index = match.index + match[0].length;
|
|
210
|
+
}
|
|
211
|
+
if (!flush()) return null;
|
|
212
|
+
parts.push(name.slice(index));
|
|
213
|
+
const key = parts.join("");
|
|
214
|
+
return toObjectName(key) === name ? key : null;
|
|
215
|
+
};
|
|
180
216
|
const supabaseStorage = (config) => {
|
|
181
217
|
let client;
|
|
182
218
|
const bucket = async () => (await (client ??= resolveSupabaseServiceRoleKey(config).then((key) => createClient(config.supabaseUrl, key), (error) => {
|
|
@@ -184,6 +220,7 @@ const supabaseStorage = (config) => {
|
|
|
184
220
|
throw error;
|
|
185
221
|
}))).storage.from(config.bucketName);
|
|
186
222
|
const getStorageKey = createStorageKeyBuilder(config.basePath);
|
|
223
|
+
const basePrefix = getStorageKey() ? `${getStorageKey()}/` : "";
|
|
187
224
|
const resolveSignedUrl = createSupabaseSignedUrlBatcher({
|
|
188
225
|
createSignedUrls: async (_bucketName, keys, expiresIn) => (await bucket()).createSignedUrls(keys, expiresIn),
|
|
189
226
|
expiresIn: config.signedUrlExpiresIn ?? 3600,
|
|
@@ -240,6 +277,51 @@ const supabaseStorage = (config) => {
|
|
|
240
277
|
const { error } = await (await bucket()).remove([toObjectName(key)]);
|
|
241
278
|
if (error && !isNotFoundError(error)) throw new Error(`Failed to delete storage object: ${error.message}`);
|
|
242
279
|
return { deleted: true };
|
|
280
|
+
},
|
|
281
|
+
async listObjects(prefix) {
|
|
282
|
+
const objects = [];
|
|
283
|
+
const folder = toObjectName(getStorageKey(prefix ?? ""));
|
|
284
|
+
let cursor;
|
|
285
|
+
do {
|
|
286
|
+
const { data, error } = await (await bucket()).listV2({
|
|
287
|
+
prefix: folder ? `${folder}/` : "",
|
|
288
|
+
limit: LIST_PAGE_SIZE,
|
|
289
|
+
...cursor === void 0 ? {} : { cursor }
|
|
290
|
+
});
|
|
291
|
+
if (error) throw new Error(`Failed to list storage objects: ${error.message}`);
|
|
292
|
+
for (const entry of data.objects) {
|
|
293
|
+
const name = entry.key ?? entry.name;
|
|
294
|
+
if (name.split("/").at(-1) === EMPTY_FOLDER_PLACEHOLDER) continue;
|
|
295
|
+
const key = fromObjectName(name);
|
|
296
|
+
if (key === null || !key.startsWith(basePrefix)) continue;
|
|
297
|
+
let storageUri;
|
|
298
|
+
try {
|
|
299
|
+
storageUri = createStorageUri({
|
|
300
|
+
protocol: "supabase-storage",
|
|
301
|
+
bucket: config.bucketName,
|
|
302
|
+
key
|
|
303
|
+
});
|
|
304
|
+
} catch {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const updatedAt = entry.updated_at ?? entry.created_at;
|
|
308
|
+
objects.push({
|
|
309
|
+
key: key.slice(basePrefix.length),
|
|
310
|
+
...updatedAt ? { lastModifiedAt: new Date(updatedAt) } : {},
|
|
311
|
+
size: Number(entry.metadata?.size ?? 0),
|
|
312
|
+
storageUri
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
cursor = data.hasNext ? data.nextCursor : void 0;
|
|
316
|
+
} while (cursor !== void 0);
|
|
317
|
+
return objects;
|
|
318
|
+
},
|
|
319
|
+
async deleteObjects(relativeKeys) {
|
|
320
|
+
const names = relativeKeys.map((key) => toObjectName(getStorageKey(key)));
|
|
321
|
+
for (let offset = 0; offset < names.length; offset += DELETE_BATCH_SIZE) {
|
|
322
|
+
const { error } = await (await bucket()).remove(names.slice(offset, offset + DELETE_BATCH_SIZE));
|
|
323
|
+
if (error && !isNotFoundError(error)) throw new Error(`Failed to delete storage objects: ${error.message}`);
|
|
324
|
+
}
|
|
243
325
|
}
|
|
244
326
|
});
|
|
245
327
|
};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { EngineDatabase, StorageAdapterWith } from "@hot-updater/plugin-core";
|
|
1
|
+
import { EngineDatabase, Lazy, StorageAdapterWith } from "@hot-updater/plugin-core";
|
|
2
2
|
//#region src/supabaseConfig.d.ts
|
|
3
3
|
type SupabaseServiceRoleConfig = {
|
|
4
4
|
supabaseUrl: string;
|
|
5
5
|
/**
|
|
6
6
|
* The project's service role key, or a function that returns it, such as
|
|
7
|
-
*
|
|
8
|
-
* config loads without the key.
|
|
7
|
+
* `secret("<name>")` in `hot-updater.config.ts`: called on the first
|
|
8
|
+
* request, so a config loads without the key.
|
|
9
9
|
*/
|
|
10
|
-
supabaseServiceRoleKey:
|
|
10
|
+
supabaseServiceRoleKey: Lazy<string>;
|
|
11
11
|
};
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region src/supabaseDatabase.d.ts
|
|
@@ -28,6 +28,6 @@ type SupabaseStorageConfig = SupabaseServiceRoleConfig & {
|
|
|
28
28
|
/** Signed download URL lifetime in seconds. @default 3600 */
|
|
29
29
|
signedUrlExpiresIn?: number;
|
|
30
30
|
};
|
|
31
|
-
declare const supabaseStorage: (config: SupabaseStorageConfig) => StorageAdapterWith<"put" | "get" | "getDownloadUrl" | "exists" | "delete">;
|
|
31
|
+
declare const supabaseStorage: (config: SupabaseStorageConfig) => StorageAdapterWith<"put" | "get" | "getDownloadUrl" | "exists" | "delete" | "listObjects" | "deleteObjects">;
|
|
32
32
|
//#endregion
|
|
33
33
|
export { supabaseDatabase as i, supabaseStorage as n, SupabaseDatabaseConfig as r, SupabaseStorageConfig as t };
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { EngineDatabase, StorageAdapterWith } from "@hot-updater/plugin-core";
|
|
1
|
+
import { EngineDatabase, Lazy, StorageAdapterWith } from "@hot-updater/plugin-core";
|
|
2
2
|
//#region src/supabaseConfig.d.ts
|
|
3
3
|
type SupabaseServiceRoleConfig = {
|
|
4
4
|
supabaseUrl: string;
|
|
5
5
|
/**
|
|
6
6
|
* The project's service role key, or a function that returns it, such as
|
|
7
|
-
*
|
|
8
|
-
* config loads without the key.
|
|
7
|
+
* `secret("<name>")` in `hot-updater.config.ts`: called on the first
|
|
8
|
+
* request, so a config loads without the key.
|
|
9
9
|
*/
|
|
10
|
-
supabaseServiceRoleKey:
|
|
10
|
+
supabaseServiceRoleKey: Lazy<string>;
|
|
11
11
|
};
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region src/supabaseDatabase.d.ts
|
|
@@ -28,6 +28,6 @@ type SupabaseStorageConfig = SupabaseServiceRoleConfig & {
|
|
|
28
28
|
/** Signed download URL lifetime in seconds. @default 3600 */
|
|
29
29
|
signedUrlExpiresIn?: number;
|
|
30
30
|
};
|
|
31
|
-
declare const supabaseStorage: (config: SupabaseStorageConfig) => StorageAdapterWith<"put" | "get" | "getDownloadUrl" | "exists" | "delete">;
|
|
31
|
+
declare const supabaseStorage: (config: SupabaseStorageConfig) => StorageAdapterWith<"put" | "get" | "getDownloadUrl" | "exists" | "delete" | "listObjects" | "deleteObjects">;
|
|
32
32
|
//#endregion
|
|
33
33
|
export { supabaseDatabase as i, supabaseStorage as n, SupabaseDatabaseConfig as r, SupabaseStorageConfig as t };
|
|
@@ -166,7 +166,15 @@ const isMissingObjectError = async (error) => {
|
|
|
166
166
|
};
|
|
167
167
|
/** A character Storage rejects in an object name, `?`, or `!`. */
|
|
168
168
|
const ESCAPED_CHARACTER = /[^A-Za-z0-9_/.*'() &$=@;:+,-]/gu;
|
|
169
|
+
const ESCAPE_SEQUENCE = /!([0-9A-F]{2})/gu;
|
|
169
170
|
const utf8 = new TextEncoder();
|
|
171
|
+
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
172
|
+
/** Objects Storage lists a page at a time. */
|
|
173
|
+
const LIST_PAGE_SIZE = 1e3;
|
|
174
|
+
/** Names Storage removes in one request. */
|
|
175
|
+
const DELETE_BATCH_SIZE = 1e3;
|
|
176
|
+
/** The object the Supabase dashboard keeps in a folder it creates. */
|
|
177
|
+
const EMPTY_FOLDER_PLACEHOLDER = ".emptyFolderPlaceholder";
|
|
170
178
|
/**
|
|
171
179
|
* The object name Storage keeps a key under. Storage accepts names of ASCII
|
|
172
180
|
* letters, digits and `_/!.*'() &$=@;:+,?-` only, and supabase-js puts a name
|
|
@@ -177,6 +185,34 @@ const utf8 = new TextEncoder();
|
|
|
177
185
|
* Deploy's keys, such as `<bundleId>/manifest.json`, are stored as they are.
|
|
178
186
|
*/
|
|
179
187
|
const toObjectName = (key) => key.replace(ESCAPED_CHARACTER, (character) => Array.from(utf8.encode(character), (byte) => `!${byte.toString(16).toUpperCase().padStart(2, "0")}`).join(""));
|
|
188
|
+
/** The key `toObjectName` stored under `name`, or null for a name it never makes. */
|
|
189
|
+
const fromObjectName = (name) => {
|
|
190
|
+
const bytes = [];
|
|
191
|
+
const parts = [];
|
|
192
|
+
const flush = () => {
|
|
193
|
+
if (bytes.length === 0) return true;
|
|
194
|
+
try {
|
|
195
|
+
parts.push(utf8Decoder.decode(new Uint8Array(bytes)));
|
|
196
|
+
} catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
bytes.length = 0;
|
|
200
|
+
return true;
|
|
201
|
+
};
|
|
202
|
+
let index = 0;
|
|
203
|
+
for (const match of name.matchAll(ESCAPE_SEQUENCE)) {
|
|
204
|
+
if (match.index !== index) {
|
|
205
|
+
if (!flush()) return null;
|
|
206
|
+
parts.push(name.slice(index, match.index));
|
|
207
|
+
}
|
|
208
|
+
bytes.push(Number.parseInt(match[1], 16));
|
|
209
|
+
index = match.index + match[0].length;
|
|
210
|
+
}
|
|
211
|
+
if (!flush()) return null;
|
|
212
|
+
parts.push(name.slice(index));
|
|
213
|
+
const key = parts.join("");
|
|
214
|
+
return toObjectName(key) === name ? key : null;
|
|
215
|
+
};
|
|
180
216
|
const supabaseStorage = (config) => {
|
|
181
217
|
let client;
|
|
182
218
|
const bucket = async () => (await (client ??= resolveSupabaseServiceRoleKey(config).then((key) => (0, _supabase_supabase_js.createClient)(config.supabaseUrl, key), (error) => {
|
|
@@ -184,6 +220,7 @@ const supabaseStorage = (config) => {
|
|
|
184
220
|
throw error;
|
|
185
221
|
}))).storage.from(config.bucketName);
|
|
186
222
|
const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(config.basePath);
|
|
223
|
+
const basePrefix = getStorageKey() ? `${getStorageKey()}/` : "";
|
|
187
224
|
const resolveSignedUrl = createSupabaseSignedUrlBatcher({
|
|
188
225
|
createSignedUrls: async (_bucketName, keys, expiresIn) => (await bucket()).createSignedUrls(keys, expiresIn),
|
|
189
226
|
expiresIn: config.signedUrlExpiresIn ?? 3600,
|
|
@@ -240,6 +277,51 @@ const supabaseStorage = (config) => {
|
|
|
240
277
|
const { error } = await (await bucket()).remove([toObjectName(key)]);
|
|
241
278
|
if (error && !isNotFoundError(error)) throw new Error(`Failed to delete storage object: ${error.message}`);
|
|
242
279
|
return { deleted: true };
|
|
280
|
+
},
|
|
281
|
+
async listObjects(prefix) {
|
|
282
|
+
const objects = [];
|
|
283
|
+
const folder = toObjectName(getStorageKey(prefix ?? ""));
|
|
284
|
+
let cursor;
|
|
285
|
+
do {
|
|
286
|
+
const { data, error } = await (await bucket()).listV2({
|
|
287
|
+
prefix: folder ? `${folder}/` : "",
|
|
288
|
+
limit: LIST_PAGE_SIZE,
|
|
289
|
+
...cursor === void 0 ? {} : { cursor }
|
|
290
|
+
});
|
|
291
|
+
if (error) throw new Error(`Failed to list storage objects: ${error.message}`);
|
|
292
|
+
for (const entry of data.objects) {
|
|
293
|
+
const name = entry.key ?? entry.name;
|
|
294
|
+
if (name.split("/").at(-1) === EMPTY_FOLDER_PLACEHOLDER) continue;
|
|
295
|
+
const key = fromObjectName(name);
|
|
296
|
+
if (key === null || !key.startsWith(basePrefix)) continue;
|
|
297
|
+
let storageUri;
|
|
298
|
+
try {
|
|
299
|
+
storageUri = (0, _hot_updater_plugin_core.createStorageUri)({
|
|
300
|
+
protocol: "supabase-storage",
|
|
301
|
+
bucket: config.bucketName,
|
|
302
|
+
key
|
|
303
|
+
});
|
|
304
|
+
} catch {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const updatedAt = entry.updated_at ?? entry.created_at;
|
|
308
|
+
objects.push({
|
|
309
|
+
key: key.slice(basePrefix.length),
|
|
310
|
+
...updatedAt ? { lastModifiedAt: new Date(updatedAt) } : {},
|
|
311
|
+
size: Number(entry.metadata?.size ?? 0),
|
|
312
|
+
storageUri
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
cursor = data.hasNext ? data.nextCursor : void 0;
|
|
316
|
+
} while (cursor !== void 0);
|
|
317
|
+
return objects;
|
|
318
|
+
},
|
|
319
|
+
async deleteObjects(relativeKeys) {
|
|
320
|
+
const names = relativeKeys.map((key) => toObjectName(getStorageKey(key)));
|
|
321
|
+
for (let offset = 0; offset < names.length; offset += DELETE_BATCH_SIZE) {
|
|
322
|
+
const { error } = await (await bucket()).remove(names.slice(offset, offset + DELETE_BATCH_SIZE));
|
|
323
|
+
if (error && !isNotFoundError(error)) throw new Error(`Failed to delete storage objects: ${error.message}`);
|
|
324
|
+
}
|
|
243
325
|
}
|
|
244
326
|
});
|
|
245
327
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/supabase",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.2.0-rc.
|
|
4
|
+
"version": "1.2.0-rc.9",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
7
7
|
"module": "dist/index.mjs",
|
|
@@ -51,10 +51,10 @@
|
|
|
51
51
|
"@supabase/supabase-js": "2.76.1",
|
|
52
52
|
"hono": "4.12.34",
|
|
53
53
|
"uuidv7": "^1.0.2",
|
|
54
|
-
"@hot-updater/protocol": "1.2.0-rc.
|
|
55
|
-
"@hot-updater/plugin-core": "1.2.0-rc.
|
|
56
|
-
"@hot-updater/
|
|
57
|
-
"@hot-updater/
|
|
54
|
+
"@hot-updater/protocol": "1.2.0-rc.9",
|
|
55
|
+
"@hot-updater/plugin-core": "1.2.0-rc.9",
|
|
56
|
+
"@hot-updater/server": "1.2.0-rc.9",
|
|
57
|
+
"@hot-updater/cli-tools": "1.2.0-rc.9"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@electric-sql/pglite": "0.4.1",
|
|
@@ -64,9 +64,9 @@
|
|
|
64
64
|
"execa": "9.5.2",
|
|
65
65
|
"@types/node": "^20",
|
|
66
66
|
"mime": "^4.0.4",
|
|
67
|
-
"@hot-updater/mock": "1.2.0-rc.
|
|
68
|
-
"@hot-updater/test-utils": "1.2.0-rc.
|
|
69
|
-
"@hot-updater/postgres": "1.2.0-rc.
|
|
67
|
+
"@hot-updater/mock": "1.2.0-rc.9",
|
|
68
|
+
"@hot-updater/test-utils": "1.2.0-rc.9",
|
|
69
|
+
"@hot-updater/postgres": "1.2.0-rc.9"
|
|
70
70
|
},
|
|
71
71
|
"scripts": {
|
|
72
72
|
"build": "tsdown",
|