@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
|
@@ -16,15 +16,16 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
16
16
|
import { resolvePackageVersion, transformEnv } from "@hot-updater/cli-tools";
|
|
17
17
|
import {
|
|
18
18
|
type Bundle,
|
|
19
|
-
type GetBundlesArgs,
|
|
20
19
|
NIL_UUID,
|
|
21
|
-
|
|
20
|
+
createReleaseCatalogScopeKey,
|
|
21
|
+
encodeChannelKey,
|
|
22
22
|
} from "@hot-updater/core";
|
|
23
|
-
import { createHotUpdater } from "@hot-updater/server";
|
|
24
23
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
commitReleaseCatalogMutations,
|
|
25
|
+
createDatabaseClient,
|
|
26
|
+
createUUIDv7,
|
|
27
|
+
} from "@hot-updater/plugin-core";
|
|
28
|
+
import { createHotUpdater, registerApiKey } from "@hot-updater/server";
|
|
28
29
|
import { createClient } from "@supabase/supabase-js";
|
|
29
30
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
30
31
|
|
|
@@ -34,10 +35,10 @@ import {
|
|
|
34
35
|
runCheckedCommand,
|
|
35
36
|
spawnRuntime,
|
|
36
37
|
stopRuntime,
|
|
37
|
-
formatRuntimeLogs,
|
|
38
38
|
waitForHttpOk,
|
|
39
39
|
} from "../../../../packages/test-utils/src/runtimeProcess";
|
|
40
40
|
import { supabaseDatabase } from "../../src/supabaseDatabase";
|
|
41
|
+
import { SUPABASE_V1_TABLE_NAMES } from "../../src/supabaseInfrastructureNames";
|
|
41
42
|
import { supabaseStorage } from "../../src/supabaseStorage";
|
|
42
43
|
|
|
43
44
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -45,8 +46,7 @@ const __dirname = path.dirname(__filename);
|
|
|
45
46
|
const WORKSPACE_ROOT = path.resolve(__dirname, "../../../..");
|
|
46
47
|
const FUNCTION_NAME = "hot-updater-function";
|
|
47
48
|
const FUNCTION_BASE_PATH = `/${FUNCTION_NAME}`;
|
|
48
|
-
const
|
|
49
|
-
const LEGACY_HOT_UPDATER_BASE_PATH = "/api/check-update";
|
|
49
|
+
const API_KEY = "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE";
|
|
50
50
|
const BUCKET_NAME = "hot-updater-bundles";
|
|
51
51
|
const DENO_DOCKER_IMAGE = "denoland/deno:alpine";
|
|
52
52
|
const DENO_CACHE_VOLUME = "hot-updater-supabase-deno-cache";
|
|
@@ -98,28 +98,6 @@ const ensureBuiltArtifacts = async (
|
|
|
98
98
|
}
|
|
99
99
|
};
|
|
100
100
|
|
|
101
|
-
const createCanonicalPath = (args: GetBundlesArgs) => {
|
|
102
|
-
const channel = args.channel ?? "production";
|
|
103
|
-
const minBundleId = args.minBundleId ?? NIL_UUID;
|
|
104
|
-
const cohortSegment = args.cohort
|
|
105
|
-
? `/${encodeURIComponent(args.cohort)}`
|
|
106
|
-
: "";
|
|
107
|
-
const joinHotUpdaterPath = (routePath: string) =>
|
|
108
|
-
HOT_UPDATER_BASE_PATH === "/"
|
|
109
|
-
? routePath
|
|
110
|
-
: `${HOT_UPDATER_BASE_PATH}${routePath}`;
|
|
111
|
-
|
|
112
|
-
if (args._updateStrategy === "appVersion") {
|
|
113
|
-
return joinHotUpdaterPath(
|
|
114
|
-
`/app-version/${encodeURIComponent(args.platform)}/${encodeURIComponent(args.appVersion)}/${encodeURIComponent(channel)}/${encodeURIComponent(minBundleId)}/${encodeURIComponent(args.bundleId)}${cohortSegment}`,
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return joinHotUpdaterPath(
|
|
119
|
-
`/fingerprint/${encodeURIComponent(args.platform)}/${encodeURIComponent(args.fingerprintHash)}/${encodeURIComponent(channel)}/${encodeURIComponent(minBundleId)}/${encodeURIComponent(args.bundleId)}${cohortSegment}`,
|
|
120
|
-
);
|
|
121
|
-
};
|
|
122
|
-
|
|
123
101
|
const toRuntimeBundle = (bundle: Bundle): Bundle => {
|
|
124
102
|
return {
|
|
125
103
|
...bundle,
|
|
@@ -127,6 +105,80 @@ const toRuntimeBundle = (bundle: Bundle): Bundle => {
|
|
|
127
105
|
};
|
|
128
106
|
};
|
|
129
107
|
|
|
108
|
+
const runtimeBundle = (id: string, overrides: Partial<Bundle> = {}): Bundle =>
|
|
109
|
+
toRuntimeBundle({
|
|
110
|
+
platform: "ios",
|
|
111
|
+
fileHash: `hash-${id}`,
|
|
112
|
+
gitCommitHash: null,
|
|
113
|
+
storageUri: "storage://unused",
|
|
114
|
+
archiveByteSize: 3_000_000_001,
|
|
115
|
+
...overrides,
|
|
116
|
+
id,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const seedProductionRelease = async ({
|
|
120
|
+
bundle,
|
|
121
|
+
database,
|
|
122
|
+
}: {
|
|
123
|
+
readonly bundle: Bundle;
|
|
124
|
+
readonly database: ReturnType<typeof supabaseDatabase>;
|
|
125
|
+
}) => {
|
|
126
|
+
const channelName = "production";
|
|
127
|
+
const channelKey = encodeChannelKey(channelName);
|
|
128
|
+
const channel = (
|
|
129
|
+
await database.models.channels.insert({
|
|
130
|
+
row: { id: `channel:${channelKey}`, name: channelName },
|
|
131
|
+
onConflict: "returnExisting",
|
|
132
|
+
})
|
|
133
|
+
).row;
|
|
134
|
+
const scopeKey = createReleaseCatalogScopeKey({
|
|
135
|
+
channelKey,
|
|
136
|
+
platform: bundle.platform,
|
|
137
|
+
strategy: "APP_VERSION",
|
|
138
|
+
});
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
await commitReleaseCatalogMutations({
|
|
141
|
+
database,
|
|
142
|
+
mutations: [
|
|
143
|
+
{
|
|
144
|
+
mutation: {
|
|
145
|
+
operation: "insert",
|
|
146
|
+
row: {
|
|
147
|
+
bundle_id: bundle.id,
|
|
148
|
+
channel_id: channel.id,
|
|
149
|
+
created_at_ms: now,
|
|
150
|
+
enabled: true,
|
|
151
|
+
fingerprint_hash: null,
|
|
152
|
+
id: createUUIDv7(),
|
|
153
|
+
kind: "BUNDLE",
|
|
154
|
+
message: "hello",
|
|
155
|
+
operation: "DEPLOY",
|
|
156
|
+
platform: bundle.platform,
|
|
157
|
+
revision: 1,
|
|
158
|
+
rollout_cohort_count: 1_000,
|
|
159
|
+
scope_key: scopeKey,
|
|
160
|
+
should_force_update: false,
|
|
161
|
+
source_release_id: null,
|
|
162
|
+
strategy: "APP_VERSION",
|
|
163
|
+
target_app_version: "1.0",
|
|
164
|
+
target_cohorts: [],
|
|
165
|
+
updated_at_ms: now,
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
scope: {
|
|
169
|
+
channelId: channel.id,
|
|
170
|
+
channelName,
|
|
171
|
+
fingerprintHash: null,
|
|
172
|
+
platform: bundle.platform,
|
|
173
|
+
scopeKey,
|
|
174
|
+
strategy: "APP_VERSION",
|
|
175
|
+
},
|
|
176
|
+
updatedAtMs: now,
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
|
|
130
182
|
describe.sequential("supabase edge runtime acceptance", () => {
|
|
131
183
|
let runtimeRoot: string | undefined;
|
|
132
184
|
let storageRepoPath = "";
|
|
@@ -136,9 +188,37 @@ describe.sequential("supabase edge runtime acceptance", () => {
|
|
|
136
188
|
let edgePort = 0;
|
|
137
189
|
let gatewayBaseUrl = "";
|
|
138
190
|
let edgeRuntime: ReturnType<typeof spawnRuntime> | undefined;
|
|
191
|
+
let database: ReturnType<typeof supabaseDatabase>;
|
|
139
192
|
let seedHotUpdater: ReturnType<typeof createHotUpdater>;
|
|
193
|
+
let databaseClient: ReturnType<typeof createDatabaseClient>;
|
|
140
194
|
let supabaseAdmin: ReturnType<typeof createClient>;
|
|
141
195
|
|
|
196
|
+
const runDatabaseSql = (statement: string): void => {
|
|
197
|
+
runCheckedCommand({
|
|
198
|
+
command: "docker",
|
|
199
|
+
args: [
|
|
200
|
+
"compose",
|
|
201
|
+
"-p",
|
|
202
|
+
composeProjectName,
|
|
203
|
+
"-f",
|
|
204
|
+
composeFilePath,
|
|
205
|
+
"exec",
|
|
206
|
+
"-T",
|
|
207
|
+
"db",
|
|
208
|
+
"psql",
|
|
209
|
+
"-U",
|
|
210
|
+
"postgres",
|
|
211
|
+
"-d",
|
|
212
|
+
POSTGRES_DB,
|
|
213
|
+
"-v",
|
|
214
|
+
"ON_ERROR_STOP=1",
|
|
215
|
+
"-c",
|
|
216
|
+
statement,
|
|
217
|
+
],
|
|
218
|
+
cwd: WORKSPACE_ROOT,
|
|
219
|
+
});
|
|
220
|
+
};
|
|
221
|
+
|
|
142
222
|
beforeAll(async () => {
|
|
143
223
|
await ensureBuiltArtifacts(REQUIRED_BUILD_ARTIFACTS);
|
|
144
224
|
|
|
@@ -226,23 +306,27 @@ describe.sequential("supabase edge runtime acceptance", () => {
|
|
|
226
306
|
supabaseAdmin = createClient(gatewayBaseUrl, SERVICE_ROLE_KEY);
|
|
227
307
|
await ensureBucketExists(supabaseAdmin);
|
|
228
308
|
|
|
309
|
+
database = supabaseDatabase({
|
|
310
|
+
supabaseUrl: gatewayBaseUrl,
|
|
311
|
+
supabaseServiceRoleKey: SERVICE_ROLE_KEY,
|
|
312
|
+
});
|
|
313
|
+
await registerApiKey({
|
|
314
|
+
apiKey: API_KEY,
|
|
315
|
+
apiKeys: database.models.apiKeys,
|
|
316
|
+
name: "Runtime acceptance",
|
|
317
|
+
});
|
|
318
|
+
databaseClient = createDatabaseClient(database);
|
|
319
|
+
|
|
229
320
|
seedHotUpdater = createHotUpdater({
|
|
230
|
-
database
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}),
|
|
234
|
-
storages: [
|
|
321
|
+
database,
|
|
322
|
+
clientAccess: { type: "public" },
|
|
323
|
+
storage: [
|
|
235
324
|
supabaseStorage({
|
|
236
325
|
supabaseUrl: gatewayBaseUrl,
|
|
237
|
-
|
|
326
|
+
supabaseServiceRoleKey: SERVICE_ROLE_KEY,
|
|
238
327
|
bucketName: BUCKET_NAME,
|
|
239
328
|
}),
|
|
240
329
|
],
|
|
241
|
-
basePath: HOT_UPDATER_BASE_PATH,
|
|
242
|
-
routes: {
|
|
243
|
-
updateCheck: true,
|
|
244
|
-
bundles: false,
|
|
245
|
-
},
|
|
246
330
|
});
|
|
247
331
|
|
|
248
332
|
edgeRuntime = spawnRuntime({
|
|
@@ -299,13 +383,18 @@ describe.sequential("supabase edge runtime acceptance", () => {
|
|
|
299
383
|
throw new Error("Supabase admin client was not initialized.");
|
|
300
384
|
}
|
|
301
385
|
|
|
302
|
-
const
|
|
303
|
-
.
|
|
304
|
-
.
|
|
305
|
-
.
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
386
|
+
for (const [table, key] of [
|
|
387
|
+
[SUPABASE_V1_TABLE_NAMES.releaseCatalogs, "scope_key"],
|
|
388
|
+
[SUPABASE_V1_TABLE_NAMES.releases, "id"],
|
|
389
|
+
[SUPABASE_V1_TABLE_NAMES.bundlePatches, "id"],
|
|
390
|
+
[SUPABASE_V1_TABLE_NAMES.bundles, "id"],
|
|
391
|
+
[SUPABASE_V1_TABLE_NAMES.channels, "id"],
|
|
392
|
+
] as const) {
|
|
393
|
+
const { error } = await supabaseAdmin
|
|
394
|
+
.from(table)
|
|
395
|
+
.delete()
|
|
396
|
+
.neq(key, NIL_UUID);
|
|
397
|
+
if (error) throw error;
|
|
309
398
|
}
|
|
310
399
|
});
|
|
311
400
|
|
|
@@ -336,193 +425,314 @@ describe.sequential("supabase edge runtime acceptance", () => {
|
|
|
336
425
|
}
|
|
337
426
|
}, 60_000);
|
|
338
427
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
428
|
+
it("returns one canonical Channel row under concurrent inserts", async () => {
|
|
429
|
+
const database = supabaseDatabase({
|
|
430
|
+
supabaseUrl: gatewayBaseUrl,
|
|
431
|
+
supabaseServiceRoleKey: SERVICE_ROLE_KEY,
|
|
432
|
+
});
|
|
433
|
+
const channelName = "concurrent-channel";
|
|
434
|
+
const results = await Promise.all([
|
|
435
|
+
database.models.channels.insert({
|
|
436
|
+
row: {
|
|
437
|
+
id: "00000000-0000-0000-0000-000000000091",
|
|
438
|
+
name: channelName,
|
|
439
|
+
},
|
|
440
|
+
onConflict: "returnExisting",
|
|
441
|
+
}),
|
|
442
|
+
database.models.channels.insert({
|
|
443
|
+
row: {
|
|
444
|
+
id: "00000000-0000-0000-0000-000000000092",
|
|
445
|
+
name: channelName,
|
|
446
|
+
},
|
|
447
|
+
onConflict: "returnExisting",
|
|
448
|
+
}),
|
|
449
|
+
]);
|
|
450
|
+
|
|
451
|
+
expect(new Set(results.map(({ row }) => row.id)).size).toBe(1);
|
|
452
|
+
expect(results.filter(({ inserted }) => inserted)).toHaveLength(1);
|
|
453
|
+
const stored = await supabaseAdmin
|
|
454
|
+
.from("hot_updater_v1_channels")
|
|
455
|
+
.select("id, name")
|
|
456
|
+
.eq("name", channelName);
|
|
457
|
+
if (stored.error) throw stored.error;
|
|
458
|
+
expect(stored.data).toEqual([results[0]?.row]);
|
|
459
|
+
});
|
|
344
460
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
461
|
+
it("rolls back a patch-bearing insert when one base bundle is missing", async () => {
|
|
462
|
+
const base = runtimeBundle("00000000-0000-0000-0000-000000000101");
|
|
463
|
+
const owner = {
|
|
464
|
+
...base,
|
|
465
|
+
id: "00000000-0000-0000-0000-000000000102",
|
|
466
|
+
fileHash: "hash-owner",
|
|
467
|
+
patches: [
|
|
468
|
+
{
|
|
469
|
+
baseBundleId: base.id,
|
|
470
|
+
baseFileHash: base.fileHash,
|
|
471
|
+
patchFileHash: "hash-valid-patch",
|
|
472
|
+
patchStorageUri: "storage://valid-patch",
|
|
473
|
+
byteSize: 3_000_000_002,
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
baseBundleId: "00000000-0000-0000-0000-000000000199",
|
|
477
|
+
baseFileHash: "hash-missing-base",
|
|
478
|
+
patchFileHash: "hash-invalid-patch",
|
|
479
|
+
patchStorageUri: "storage://invalid-patch",
|
|
480
|
+
byteSize: 3_000_000_003,
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
} satisfies Bundle;
|
|
484
|
+
await databaseClient.insertBundle(base);
|
|
485
|
+
|
|
486
|
+
await expect(
|
|
487
|
+
databaseClient.mutate((database) => database.insertBundle(owner)),
|
|
488
|
+
).rejects.toBeDefined();
|
|
489
|
+
|
|
490
|
+
const ownerResult = await supabaseAdmin
|
|
491
|
+
.from("hot_updater_v1_bundles")
|
|
492
|
+
.select("id")
|
|
493
|
+
.eq("id", owner.id)
|
|
494
|
+
.maybeSingle();
|
|
495
|
+
if (ownerResult.error) throw ownerResult.error;
|
|
496
|
+
expect(ownerResult.data).toBeNull();
|
|
497
|
+
const patchResult = await supabaseAdmin
|
|
498
|
+
.from("hot_updater_v1_bundle_patches")
|
|
499
|
+
.select("id")
|
|
500
|
+
.eq("bundle_id", owner.id);
|
|
501
|
+
if (patchResult.error) throw patchResult.error;
|
|
502
|
+
expect(patchResult.data).toEqual([]);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("preserves extension defaults and generated values during atomic inserts", async () => {
|
|
506
|
+
runDatabaseSql(
|
|
507
|
+
[
|
|
508
|
+
"ALTER TABLE public.hot_updater_v1_bundles",
|
|
509
|
+
"ADD COLUMN tenant_tag text NOT NULL DEFAULT 'default-tenant',",
|
|
510
|
+
"ADD COLUMN file_hash_upper text GENERATED ALWAYS AS (upper(file_hash)) STORED",
|
|
511
|
+
].join(" "),
|
|
350
512
|
);
|
|
351
513
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
514
|
+
try {
|
|
515
|
+
const base = runtimeBundle("00000000-0000-0000-0000-000000000151");
|
|
516
|
+
const owner = {
|
|
517
|
+
...base,
|
|
518
|
+
id: "00000000-0000-0000-0000-000000000152",
|
|
519
|
+
fileHash: "hash-owner",
|
|
520
|
+
patches: [
|
|
521
|
+
{
|
|
522
|
+
baseBundleId: base.id,
|
|
523
|
+
baseFileHash: base.fileHash,
|
|
524
|
+
patchFileHash: "hash-patch",
|
|
525
|
+
patchStorageUri: "storage://patch",
|
|
526
|
+
byteSize: 3_000_000_002,
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
} satisfies Bundle;
|
|
530
|
+
await databaseClient.insertBundle(base);
|
|
531
|
+
|
|
532
|
+
await databaseClient.insertBundle(owner);
|
|
533
|
+
|
|
534
|
+
const result = await supabaseAdmin
|
|
535
|
+
.from("hot_updater_v1_bundles")
|
|
536
|
+
.select("tenant_tag, file_hash_upper")
|
|
537
|
+
.eq("id", owner.id)
|
|
538
|
+
.single();
|
|
539
|
+
if (result.error) throw result.error;
|
|
540
|
+
expect(result.data).toEqual({
|
|
541
|
+
tenant_tag: "default-tenant",
|
|
542
|
+
file_hash_upper: "HASH-OWNER",
|
|
543
|
+
});
|
|
544
|
+
} finally {
|
|
545
|
+
runDatabaseSql(
|
|
546
|
+
"ALTER TABLE public.hot_updater_v1_bundles DROP COLUMN tenant_tag, DROP COLUMN file_hash_upper",
|
|
359
547
|
);
|
|
360
548
|
}
|
|
549
|
+
});
|
|
361
550
|
|
|
362
|
-
|
|
363
|
-
|
|
551
|
+
it("does not resolve public-schema JSON function shadows in atomic RPCs", async () => {
|
|
552
|
+
runDatabaseSql(`
|
|
553
|
+
CREATE FUNCTION public.jsonb_populate_record(public.hot_updater_v1_bundles, jsonb)
|
|
554
|
+
RETURNS public.hot_updater_v1_bundles
|
|
555
|
+
LANGUAGE plpgsql
|
|
556
|
+
AS $$
|
|
557
|
+
BEGIN
|
|
558
|
+
RAISE EXCEPTION 'public jsonb_populate_record shadow executed';
|
|
559
|
+
END;
|
|
560
|
+
$$
|
|
561
|
+
`);
|
|
364
562
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
563
|
+
try {
|
|
564
|
+
const base = runtimeBundle("00000000-0000-0000-0000-000000000551");
|
|
565
|
+
const owner = {
|
|
566
|
+
...base,
|
|
567
|
+
id: "00000000-0000-0000-0000-000000000552",
|
|
568
|
+
fileHash: "hash-owner",
|
|
569
|
+
patches: [
|
|
570
|
+
{
|
|
571
|
+
baseBundleId: base.id,
|
|
572
|
+
baseFileHash: base.fileHash,
|
|
573
|
+
patchFileHash: "hash-patch",
|
|
574
|
+
patchStorageUri: "storage://patch",
|
|
575
|
+
byteSize: 3_000_000_002,
|
|
576
|
+
},
|
|
577
|
+
],
|
|
578
|
+
} satisfies Bundle;
|
|
579
|
+
await databaseClient.insertBundle(base);
|
|
369
580
|
|
|
370
|
-
|
|
371
|
-
await uploadBundleObject(supabaseAdmin, bundle.id);
|
|
372
|
-
}
|
|
373
|
-
await seedRuntimeBundles(bundles);
|
|
374
|
-
return requestUpdateInfo(args);
|
|
375
|
-
};
|
|
581
|
+
await databaseClient.insertBundle(owner);
|
|
376
582
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
JSON.stringify(fixture.currentManifest),
|
|
386
|
-
"application/json",
|
|
387
|
-
),
|
|
388
|
-
uploadStorageObject(
|
|
389
|
-
supabaseAdmin,
|
|
390
|
-
`${fixture.nextBundleId}/manifest.json`,
|
|
391
|
-
JSON.stringify(fixture.nextManifest),
|
|
392
|
-
"application/json",
|
|
393
|
-
),
|
|
394
|
-
uploadStorageObject(
|
|
395
|
-
supabaseAdmin,
|
|
396
|
-
`${fixture.nextBundleId}/files/${fixture.changedAssetPath}.br`,
|
|
397
|
-
"next-bundle-bytes",
|
|
398
|
-
"application/javascript",
|
|
399
|
-
),
|
|
400
|
-
]);
|
|
401
|
-
|
|
402
|
-
return {
|
|
403
|
-
currentArtifacts: {
|
|
404
|
-
assetBaseStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.currentBundleId}/files`,
|
|
405
|
-
manifestFileHash: "sig:manifest-current",
|
|
406
|
-
manifestStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.currentBundleId}/manifest.json`,
|
|
407
|
-
},
|
|
408
|
-
nextArtifacts: {
|
|
409
|
-
assetBaseStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.nextBundleId}/files`,
|
|
410
|
-
manifestFileHash: "sig:manifest-next",
|
|
411
|
-
manifestStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.nextBundleId}/manifest.json`,
|
|
412
|
-
},
|
|
413
|
-
};
|
|
414
|
-
},
|
|
415
|
-
expectFileUrl: (fileUrl, fixture) => {
|
|
416
|
-
expect(fileUrl).toContain(
|
|
417
|
-
`/storage/v1/object/sign/${BUCKET_NAME}/${fixture.nextBundleId}/files/${fixture.changedAssetPath}.br`,
|
|
418
|
-
);
|
|
419
|
-
},
|
|
420
|
-
expectManifestUrl: (manifestUrl, fixture) => {
|
|
421
|
-
expect(manifestUrl).toContain(
|
|
422
|
-
`/storage/v1/object/sign/${BUCKET_NAME}/${fixture.nextBundleId}/manifest.json`,
|
|
423
|
-
);
|
|
424
|
-
},
|
|
425
|
-
},
|
|
583
|
+
await expect(
|
|
584
|
+
databaseClient.getBundleById(owner.id),
|
|
585
|
+
).resolves.toMatchObject({ id: owner.id, patches: owner.patches });
|
|
586
|
+
} finally {
|
|
587
|
+
runDatabaseSql(
|
|
588
|
+
"DROP FUNCTION public.jsonb_populate_record(public.hot_updater_v1_bundles, jsonb)",
|
|
589
|
+
);
|
|
590
|
+
}
|
|
426
591
|
});
|
|
427
592
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
JSON.stringify(fixture.nextManifest),
|
|
443
|
-
"application/json",
|
|
444
|
-
),
|
|
445
|
-
uploadStorageObject(
|
|
446
|
-
supabaseAdmin,
|
|
447
|
-
`${fixture.nextBundleId}/files/${fixture.assetPath}`,
|
|
448
|
-
"next-bundle-bytes",
|
|
449
|
-
"application/javascript",
|
|
450
|
-
),
|
|
451
|
-
uploadStorageObject(
|
|
452
|
-
supabaseAdmin,
|
|
453
|
-
fixture.patchPath,
|
|
454
|
-
"patch-bytes",
|
|
455
|
-
"application/octet-stream",
|
|
456
|
-
),
|
|
457
|
-
uploadBundleObject(supabaseAdmin, fixture.currentBundleId),
|
|
458
|
-
uploadBundleObject(supabaseAdmin, fixture.nextBundleId),
|
|
459
|
-
]);
|
|
460
|
-
|
|
461
|
-
return {
|
|
462
|
-
currentArtifacts: {
|
|
463
|
-
assetBaseStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.currentBundleId}/files`,
|
|
464
|
-
manifestFileHash: "sig:manifest-current",
|
|
465
|
-
manifestStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.currentBundleId}/manifest.json`,
|
|
593
|
+
it("rolls back scalar and patch replacement when a new base is missing", async () => {
|
|
594
|
+
const base = runtimeBundle("00000000-0000-0000-0000-000000000201");
|
|
595
|
+
const owner = {
|
|
596
|
+
...base,
|
|
597
|
+
id: "00000000-0000-0000-0000-000000000202",
|
|
598
|
+
fileHash: "hash-owner",
|
|
599
|
+
gitCommitHash: "before",
|
|
600
|
+
patches: [
|
|
601
|
+
{
|
|
602
|
+
baseBundleId: base.id,
|
|
603
|
+
baseFileHash: base.fileHash,
|
|
604
|
+
patchFileHash: "hash-old-patch",
|
|
605
|
+
patchStorageUri: "storage://old-patch",
|
|
606
|
+
byteSize: 3_000_000_002,
|
|
466
607
|
},
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
608
|
+
],
|
|
609
|
+
} satisfies Bundle;
|
|
610
|
+
await databaseClient.insertBundle(base);
|
|
611
|
+
await databaseClient.insertBundle(owner);
|
|
612
|
+
|
|
613
|
+
await expect(
|
|
614
|
+
databaseClient.mutate((database) =>
|
|
615
|
+
database.updateBundleById(owner.id, {
|
|
616
|
+
gitCommitHash: "after",
|
|
471
617
|
patches: [
|
|
472
618
|
{
|
|
473
|
-
baseBundleId:
|
|
474
|
-
baseFileHash: "hash-
|
|
475
|
-
patchFileHash: "hash-
|
|
476
|
-
patchStorageUri:
|
|
619
|
+
baseBundleId: "00000000-0000-0000-0000-000000000299",
|
|
620
|
+
baseFileHash: "hash-missing-base",
|
|
621
|
+
patchFileHash: "hash-invalid-patch",
|
|
622
|
+
patchStorageUri: "storage://invalid-patch",
|
|
623
|
+
byteSize: 3_000_000_003,
|
|
477
624
|
},
|
|
478
625
|
],
|
|
626
|
+
}),
|
|
627
|
+
),
|
|
628
|
+
).rejects.toBeDefined();
|
|
629
|
+
|
|
630
|
+
await expect(databaseClient.getBundleById(owner.id)).resolves.toMatchObject(
|
|
631
|
+
{
|
|
632
|
+
gitCommitHash: "before",
|
|
633
|
+
patches: owner.patches,
|
|
634
|
+
},
|
|
635
|
+
);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it("atomically applies explicit nulls and an empty patch list", async () => {
|
|
639
|
+
const base = runtimeBundle("00000000-0000-0000-0000-000000000301");
|
|
640
|
+
const owner = {
|
|
641
|
+
...base,
|
|
642
|
+
id: "00000000-0000-0000-0000-000000000302",
|
|
643
|
+
fileHash: "hash-owner",
|
|
644
|
+
gitCommitHash: "before",
|
|
645
|
+
patches: [
|
|
646
|
+
{
|
|
647
|
+
baseBundleId: base.id,
|
|
648
|
+
baseFileHash: base.fileHash,
|
|
649
|
+
patchFileHash: "hash-old-patch",
|
|
650
|
+
patchStorageUri: "storage://old-patch",
|
|
651
|
+
byteSize: 3_000_000_002,
|
|
479
652
|
},
|
|
480
|
-
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
653
|
+
],
|
|
654
|
+
} satisfies Bundle;
|
|
655
|
+
await databaseClient.insertBundle(base);
|
|
656
|
+
await databaseClient.insertBundle(owner);
|
|
657
|
+
|
|
658
|
+
await databaseClient.mutate((database) =>
|
|
659
|
+
database.updateBundleById(owner.id, {
|
|
660
|
+
gitCommitHash: null,
|
|
661
|
+
patches: [],
|
|
662
|
+
}),
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
await expect(databaseClient.getBundleById(owner.id)).resolves.toMatchObject(
|
|
666
|
+
{
|
|
667
|
+
gitCommitHash: null,
|
|
668
|
+
patches: [],
|
|
669
|
+
},
|
|
670
|
+
);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it("maps a missing aggregate update to the public not-found error", async () => {
|
|
674
|
+
const result = databaseClient.updateBundleById(
|
|
675
|
+
"00000000-0000-0000-0000-000000000401",
|
|
676
|
+
{ patches: [] },
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
await expect(result).rejects.toMatchObject({
|
|
680
|
+
name: "DatabaseBundleNotFoundError",
|
|
681
|
+
bundleId: "00000000-0000-0000-0000-000000000401",
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it("denies the generic commit RPC to the anonymous role", async () => {
|
|
686
|
+
const response = await fetch(
|
|
687
|
+
`${gatewayBaseUrl}/rest/v1/rpc/hot_updater_v1_commit`,
|
|
688
|
+
{
|
|
689
|
+
method: "POST",
|
|
690
|
+
headers: {
|
|
691
|
+
apikey: ANON_KEY,
|
|
692
|
+
Authorization: `Bearer ${ANON_KEY}`,
|
|
693
|
+
"Content-Type": "application/json",
|
|
694
|
+
},
|
|
695
|
+
body: JSON.stringify({ p_commit: { changes: [] } }),
|
|
696
|
+
},
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
expect(response.ok).toBe(false);
|
|
487
700
|
});
|
|
488
701
|
|
|
489
|
-
it("serves
|
|
702
|
+
it("serves unversioned Release Catalog routes from the edge function entrypoint", async () => {
|
|
490
703
|
const bundle = toRuntimeBundle({
|
|
491
704
|
id: "00000000-0000-0000-0000-000000000001",
|
|
492
705
|
platform: "ios",
|
|
493
|
-
targetAppVersion: "1.0",
|
|
494
|
-
shouldForceUpdate: false,
|
|
495
|
-
enabled: true,
|
|
496
706
|
fileHash: "hash",
|
|
497
707
|
gitCommitHash: null,
|
|
498
|
-
message: "hello",
|
|
499
|
-
channel: "production",
|
|
500
708
|
storageUri: "storage://unused",
|
|
501
|
-
|
|
709
|
+
archiveByteSize: 3_000_000_001,
|
|
502
710
|
});
|
|
503
711
|
|
|
504
712
|
await uploadBundleObject(supabaseAdmin, bundle.id);
|
|
505
713
|
await seedHotUpdater.insertBundle(bundle);
|
|
714
|
+
await seedProductionRelease({ bundle, database });
|
|
715
|
+
|
|
716
|
+
const unauthorized = await fetch(
|
|
717
|
+
`http://127.0.0.1:${edgePort}${FUNCTION_BASE_PATH}/release-catalogs/app-version/ios/cHJvZHVjdGlvbg/1.0.0`,
|
|
718
|
+
);
|
|
719
|
+
expect(unauthorized.status).toBe(401);
|
|
506
720
|
|
|
507
721
|
const response = await fetch(
|
|
508
|
-
`http://127.0.0.1:${edgePort}${FUNCTION_BASE_PATH}
|
|
509
|
-
|
|
510
|
-
bundleId: NIL_UUID,
|
|
511
|
-
platform: "ios",
|
|
512
|
-
_updateStrategy: "appVersion",
|
|
513
|
-
})}`,
|
|
722
|
+
`http://127.0.0.1:${edgePort}${FUNCTION_BASE_PATH}/release-catalogs/app-version/ios/cHJvZHVjdGlvbg/1.0.0`,
|
|
723
|
+
{ headers: { "x-api-key": API_KEY } },
|
|
514
724
|
);
|
|
515
725
|
|
|
516
726
|
expect(response.ok).toBe(true);
|
|
517
727
|
await expect(response.json()).resolves.toMatchObject({
|
|
518
|
-
|
|
519
|
-
|
|
728
|
+
catalogId: expect.stringMatching(/^[0-9a-f-]{36}$/),
|
|
729
|
+
releases: [{ bundleId: "00000000-0000-0000-0000-000000000001" }],
|
|
520
730
|
});
|
|
521
731
|
});
|
|
522
732
|
|
|
523
733
|
it("does not support the legacy exact path", async () => {
|
|
524
734
|
const response = await fetch(
|
|
525
|
-
`http://127.0.0.1:${edgePort}${FUNCTION_BASE_PATH}
|
|
735
|
+
`http://127.0.0.1:${edgePort}${FUNCTION_BASE_PATH}/api/check-update`,
|
|
526
736
|
);
|
|
527
737
|
|
|
528
738
|
expect(response.status).toBe(404);
|
|
@@ -530,7 +740,7 @@ describe.sequential("supabase edge runtime acceptance", () => {
|
|
|
530
740
|
|
|
531
741
|
it("does not expose management routes from the edge function entrypoint", async () => {
|
|
532
742
|
const response = await fetch(
|
|
533
|
-
`http://127.0.0.1:${edgePort}${FUNCTION_BASE_PATH}/
|
|
743
|
+
`http://127.0.0.1:${edgePort}${FUNCTION_BASE_PATH}/admin/bundles`,
|
|
534
744
|
);
|
|
535
745
|
|
|
536
746
|
expect(response.status).toBe(404);
|
|
@@ -597,7 +807,7 @@ const waitForRestApiReady = async (baseUrl: string, timeoutMs = 90_000) => {
|
|
|
597
807
|
while (Date.now() < deadline) {
|
|
598
808
|
try {
|
|
599
809
|
const response = await fetch(
|
|
600
|
-
`${baseUrl}/rest/v1
|
|
810
|
+
`${baseUrl}/rest/v1/${SUPABASE_V1_TABLE_NAMES.bundles}?select=id&limit=1`,
|
|
601
811
|
{
|
|
602
812
|
headers: {
|
|
603
813
|
apikey: SERVICE_ROLE_KEY,
|
|
@@ -747,12 +957,16 @@ ${storageMigrations.join("\n\n")}
|
|
|
747
957
|
SET search_path TO public;
|
|
748
958
|
|
|
749
959
|
GRANT USAGE ON SCHEMA public TO anon, authenticated, service_role;
|
|
750
|
-
GRANT USAGE ON TYPE platforms TO anon, authenticated, service_role;
|
|
751
960
|
GRANT SELECT ON ALL TABLES IN SCHEMA public TO anon, authenticated;
|
|
752
961
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO service_role;
|
|
753
962
|
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO anon, authenticated, service_role;
|
|
754
963
|
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO anon, authenticated, service_role;
|
|
755
964
|
|
|
965
|
+
REVOKE EXECUTE ON FUNCTION public.hot_updater_v1_commit(jsonb)
|
|
966
|
+
FROM anon, authenticated;
|
|
967
|
+
REVOKE EXECUTE ON FUNCTION public.hot_updater_v1_delete_channel(text)
|
|
968
|
+
FROM anon, authenticated;
|
|
969
|
+
|
|
756
970
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
|
757
971
|
GRANT SELECT ON TABLES TO anon, authenticated;
|
|
758
972
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
|
@@ -924,6 +1138,7 @@ const writeSupabaseRuntimeFiles = async ({
|
|
|
924
1138
|
"plugins/supabase/supabase/edge-functions/index.ts",
|
|
925
1139
|
),
|
|
926
1140
|
{
|
|
1141
|
+
BUCKET_NAME,
|
|
927
1142
|
FUNCTION_NAME,
|
|
928
1143
|
},
|
|
929
1144
|
);
|
|
@@ -932,7 +1147,7 @@ const writeSupabaseRuntimeFiles = async ({
|
|
|
932
1147
|
"@hot-updater/server": pathToFileURL(
|
|
933
1148
|
path.join(WORKSPACE_ROOT, "packages/server/dist/index.mjs"),
|
|
934
1149
|
).href,
|
|
935
|
-
"@hot-updater/supabase": pathToFileURL(
|
|
1150
|
+
"@hot-updater/supabase/edge": pathToFileURL(
|
|
936
1151
|
path.join(runtimeRoot, "hot-updater-supabase-edge.ts"),
|
|
937
1152
|
).href,
|
|
938
1153
|
hono: `npm:hono@${resolvePackageVersion("hono", {
|
|
@@ -944,8 +1159,8 @@ const writeSupabaseRuntimeFiles = async ({
|
|
|
944
1159
|
await writeFile(
|
|
945
1160
|
path.join(runtimeRoot, "hot-updater-supabase-edge.ts"),
|
|
946
1161
|
`
|
|
947
|
-
export {
|
|
948
|
-
export { supabaseEdgeFunctionStorage } from ${JSON.stringify(pathToFileURL(path.join(WORKSPACE_ROOT, "plugins/supabase/src/supabaseEdgeFunctionStorage.ts")).href)};
|
|
1162
|
+
export { supabaseDatabase } from ${JSON.stringify(pathToFileURL(path.join(WORKSPACE_ROOT, "plugins/supabase/src/supabaseDatabase.ts")).href)};
|
|
1163
|
+
export { supabaseEdgeFunctionStorage as supabaseStorage } from ${JSON.stringify(pathToFileURL(path.join(WORKSPACE_ROOT, "plugins/supabase/src/supabaseEdgeFunctionStorage.ts")).href)};
|
|
949
1164
|
`.trim(),
|
|
950
1165
|
);
|
|
951
1166
|
await writeFile(
|