@hot-updater/supabase 1.0.0-rc.2 → 1.0.0-rc.21
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 +4 -3
- package/dist/edge.d.cts +4 -4
- package/dist/edge.d.mts +4 -4
- package/dist/edge.mjs +2 -2
- package/dist/index.cjs +143 -3
- package/dist/index.d.cts +13 -4
- package/dist/index.d.mts +13 -4
- package/dist/index.mjs +141 -2
- package/dist/init/index.cjs +7593 -4
- package/dist/init/index.d.cts +97 -4
- package/dist/init/index.d.mts +97 -4
- package/dist/init/index.mjs +7578 -1
- package/dist/plugins-B8grmU79.mjs +209 -0
- package/dist/plugins-DJUmZYvT.cjs +238 -0
- package/dist/supabaseDatabase-DVb3-y2x.d.cts +726 -0
- package/dist/supabaseDatabase-DVb3-y2x.d.mts +726 -0
- package/package.json +9 -14
- package/supabase/edge-functions/index.ts +6 -2
- package/supabase/migrations/20260818000000_hot-updater_1.0.0.sql +190 -604
- package/dist/iac/index.cjs +0 -8096
- package/dist/iac/index.d.cts +0 -102
- package/dist/iac/index.d.mts +0 -102
- package/dist/iac/index.mjs +0 -8079
- package/dist/supabaseDatabase-3d90EtUD.d.cts +0 -11
- package/dist/supabaseDatabase-3d90EtUD.d.mts +0 -11
- package/dist/supabaseStorage-CbzHzqCa.cjs +0 -622
- package/dist/supabaseStorage-CjteZ2zX.mjs +0 -611
- package/supabase/edge-functions/runtime.docker.integration.spec.ts +0 -1183
- package/supabase/migrations.spec.ts +0 -281
|
@@ -1,1183 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { createHmac } from "node:crypto";
|
|
3
|
-
import {
|
|
4
|
-
access,
|
|
5
|
-
mkdir,
|
|
6
|
-
mkdtemp,
|
|
7
|
-
readdir,
|
|
8
|
-
readFile,
|
|
9
|
-
rm,
|
|
10
|
-
symlink,
|
|
11
|
-
writeFile,
|
|
12
|
-
} from "node:fs/promises";
|
|
13
|
-
import path from "node:path";
|
|
14
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
|
-
|
|
16
|
-
import { resolvePackageVersion, transformEnv } from "@hot-updater/cli-tools";
|
|
17
|
-
import {
|
|
18
|
-
type Bundle,
|
|
19
|
-
NIL_UUID,
|
|
20
|
-
createReleaseCatalogScopeKey,
|
|
21
|
-
encodeChannelKey,
|
|
22
|
-
} from "@hot-updater/core";
|
|
23
|
-
import {
|
|
24
|
-
commitReleaseCatalogMutations,
|
|
25
|
-
createDatabaseClient,
|
|
26
|
-
createUUIDv7,
|
|
27
|
-
} from "@hot-updater/plugin-core";
|
|
28
|
-
import { createHotUpdater, registerApiKey } from "@hot-updater/server";
|
|
29
|
-
import { createClient } from "@supabase/supabase-js";
|
|
30
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
31
|
-
|
|
32
|
-
import {
|
|
33
|
-
assertDockerComposeAvailable,
|
|
34
|
-
findOpenPort,
|
|
35
|
-
runCheckedCommand,
|
|
36
|
-
spawnRuntime,
|
|
37
|
-
stopRuntime,
|
|
38
|
-
waitForHttpOk,
|
|
39
|
-
} from "../../../../packages/test-utils/src/runtimeProcess";
|
|
40
|
-
import { supabaseDatabase } from "../../src/supabaseDatabase";
|
|
41
|
-
import { SUPABASE_V1_TABLE_NAMES } from "../../src/supabaseInfrastructureNames";
|
|
42
|
-
import { supabaseStorage } from "../../src/supabaseStorage";
|
|
43
|
-
|
|
44
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
45
|
-
const __dirname = path.dirname(__filename);
|
|
46
|
-
const WORKSPACE_ROOT = path.resolve(__dirname, "../../../..");
|
|
47
|
-
const FUNCTION_NAME = "hot-updater-function";
|
|
48
|
-
const FUNCTION_BASE_PATH = `/${FUNCTION_NAME}`;
|
|
49
|
-
const API_KEY = "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE";
|
|
50
|
-
const BUCKET_NAME = "hot-updater-bundles";
|
|
51
|
-
const DENO_DOCKER_IMAGE = "denoland/deno:alpine";
|
|
52
|
-
const DENO_CACHE_VOLUME = "hot-updater-supabase-deno-cache";
|
|
53
|
-
const POSTGRES_IMAGE = "postgres:15-alpine";
|
|
54
|
-
const POSTGREST_IMAGE = "postgrest/postgrest:v14.6";
|
|
55
|
-
const STORAGE_IMAGE = "supabase/storage-api:v1.44.2";
|
|
56
|
-
const IMGPROXY_IMAGE = "darthsim/imgproxy:v3.30.1";
|
|
57
|
-
const NGINX_IMAGE = "nginx:1.27-alpine";
|
|
58
|
-
const POSTGRES_PASSWORD = "postgres";
|
|
59
|
-
const POSTGRES_DB = "postgres";
|
|
60
|
-
const JWT_SECRET = "super-secret-jwt-token-with-at-least-32-chars";
|
|
61
|
-
const JWT_EXPIRY_SECONDS = 60 * 60 * 24 * 365;
|
|
62
|
-
const ANON_KEY = createLegacyJwt("anon");
|
|
63
|
-
const SERVICE_ROLE_KEY = createLegacyJwt("service_role");
|
|
64
|
-
const REQUIRED_BUILD_ARTIFACTS = [
|
|
65
|
-
{
|
|
66
|
-
command: "pnpm --filter @hot-updater/core build",
|
|
67
|
-
path: path.join(WORKSPACE_ROOT, "packages/core/dist/index.mjs"),
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
command: "pnpm --filter @hot-updater/server build",
|
|
71
|
-
path: path.join(WORKSPACE_ROOT, "packages/server/dist/index.mjs"),
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
command: "pnpm --filter @hot-updater/plugin-core build",
|
|
75
|
-
path: path.join(WORKSPACE_ROOT, "plugins/plugin-core/dist/index.mjs"),
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
command: "pnpm --filter @hot-updater/supabase build",
|
|
79
|
-
path: path.join(WORKSPACE_ROOT, "plugins/supabase/dist/index.mjs"),
|
|
80
|
-
},
|
|
81
|
-
] as const;
|
|
82
|
-
|
|
83
|
-
assertDockerComposeAvailable(
|
|
84
|
-
"supabase edge runtime acceptance requires Docker Compose and a running Docker daemon.",
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
const ensureBuiltArtifacts = async (
|
|
88
|
-
artifacts: ReadonlyArray<{ command: string; path: string }>,
|
|
89
|
-
) => {
|
|
90
|
-
for (const artifact of artifacts) {
|
|
91
|
-
try {
|
|
92
|
-
await access(artifact.path);
|
|
93
|
-
} catch {
|
|
94
|
-
throw new Error(
|
|
95
|
-
`Missing built artifact at ${artifact.path}. Run \`${artifact.command}\` before running this test.`,
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const toRuntimeBundle = (bundle: Bundle): Bundle => {
|
|
102
|
-
return {
|
|
103
|
-
...bundle,
|
|
104
|
-
storageUri: `supabase-storage://${BUCKET_NAME}/${bundle.id}/bundle.zip`,
|
|
105
|
-
};
|
|
106
|
-
};
|
|
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
|
-
|
|
182
|
-
describe.sequential("supabase edge runtime acceptance", () => {
|
|
183
|
-
let runtimeRoot: string | undefined;
|
|
184
|
-
let storageRepoPath = "";
|
|
185
|
-
let composeFilePath = "";
|
|
186
|
-
let composeProjectName = "";
|
|
187
|
-
let gatewayPort = 0;
|
|
188
|
-
let edgePort = 0;
|
|
189
|
-
let gatewayBaseUrl = "";
|
|
190
|
-
let edgeRuntime: ReturnType<typeof spawnRuntime> | undefined;
|
|
191
|
-
let database: ReturnType<typeof supabaseDatabase>;
|
|
192
|
-
let seedHotUpdater: ReturnType<typeof createHotUpdater>;
|
|
193
|
-
let databaseClient: ReturnType<typeof createDatabaseClient>;
|
|
194
|
-
let supabaseAdmin: ReturnType<typeof createClient>;
|
|
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
|
-
|
|
222
|
-
beforeAll(async () => {
|
|
223
|
-
await ensureBuiltArtifacts(REQUIRED_BUILD_ARTIFACTS);
|
|
224
|
-
|
|
225
|
-
runtimeRoot = await mkdtemp(
|
|
226
|
-
path.join(WORKSPACE_ROOT, "plugins/supabase/runtime-acceptance-"),
|
|
227
|
-
);
|
|
228
|
-
storageRepoPath = path.join(runtimeRoot, "storage-repo");
|
|
229
|
-
gatewayPort = await findOpenPort();
|
|
230
|
-
edgePort = await findOpenPort();
|
|
231
|
-
gatewayBaseUrl = `http://127.0.0.1:${gatewayPort}`;
|
|
232
|
-
composeProjectName = `hot-updater-supabase-${process.pid}-${Date.now()}`;
|
|
233
|
-
composeFilePath = path.join(runtimeRoot, "docker-compose.yml");
|
|
234
|
-
|
|
235
|
-
runCheckedCommand({
|
|
236
|
-
command: "git",
|
|
237
|
-
args: [
|
|
238
|
-
"clone",
|
|
239
|
-
"--depth",
|
|
240
|
-
"1",
|
|
241
|
-
"https://github.com/supabase/storage.git",
|
|
242
|
-
storageRepoPath,
|
|
243
|
-
],
|
|
244
|
-
cwd: WORKSPACE_ROOT,
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
await writeSupabaseRuntimeFiles({
|
|
248
|
-
runtimeRoot,
|
|
249
|
-
gatewayPort,
|
|
250
|
-
storageRepoPath,
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
try {
|
|
254
|
-
runCheckedCommand({
|
|
255
|
-
command: "docker",
|
|
256
|
-
args: [
|
|
257
|
-
"compose",
|
|
258
|
-
"-p",
|
|
259
|
-
composeProjectName,
|
|
260
|
-
"-f",
|
|
261
|
-
composeFilePath,
|
|
262
|
-
"up",
|
|
263
|
-
"-d",
|
|
264
|
-
],
|
|
265
|
-
cwd: WORKSPACE_ROOT,
|
|
266
|
-
});
|
|
267
|
-
} catch (error) {
|
|
268
|
-
let dbLogs = "";
|
|
269
|
-
|
|
270
|
-
try {
|
|
271
|
-
const result = spawnSync(
|
|
272
|
-
"docker",
|
|
273
|
-
[
|
|
274
|
-
"compose",
|
|
275
|
-
"-p",
|
|
276
|
-
composeProjectName,
|
|
277
|
-
"-f",
|
|
278
|
-
composeFilePath,
|
|
279
|
-
"logs",
|
|
280
|
-
"--no-color",
|
|
281
|
-
"db",
|
|
282
|
-
],
|
|
283
|
-
{
|
|
284
|
-
cwd: WORKSPACE_ROOT,
|
|
285
|
-
encoding: "utf8",
|
|
286
|
-
},
|
|
287
|
-
);
|
|
288
|
-
dbLogs = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
289
|
-
} catch {
|
|
290
|
-
dbLogs = "failed to collect database logs";
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
throw new Error(
|
|
294
|
-
[
|
|
295
|
-
error instanceof Error ? error.message : String(error),
|
|
296
|
-
"",
|
|
297
|
-
"Database logs:",
|
|
298
|
-
dbLogs,
|
|
299
|
-
].join("\n"),
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
await waitForRestApiReady(gatewayBaseUrl, 180_000);
|
|
304
|
-
await waitForUrlOk(`${gatewayBaseUrl}/storage/v1/status`, 180_000);
|
|
305
|
-
|
|
306
|
-
supabaseAdmin = createClient(gatewayBaseUrl, SERVICE_ROLE_KEY);
|
|
307
|
-
await ensureBucketExists(supabaseAdmin);
|
|
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
|
-
|
|
320
|
-
seedHotUpdater = createHotUpdater({
|
|
321
|
-
database,
|
|
322
|
-
clientAccess: { type: "public" },
|
|
323
|
-
storage: [
|
|
324
|
-
supabaseStorage({
|
|
325
|
-
supabaseUrl: gatewayBaseUrl,
|
|
326
|
-
supabaseServiceRoleKey: SERVICE_ROLE_KEY,
|
|
327
|
-
bucketName: BUCKET_NAME,
|
|
328
|
-
}),
|
|
329
|
-
],
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
edgeRuntime = spawnRuntime({
|
|
333
|
-
command: "docker",
|
|
334
|
-
args: [
|
|
335
|
-
"run",
|
|
336
|
-
"--rm",
|
|
337
|
-
"--network",
|
|
338
|
-
`${composeProjectName}_default`,
|
|
339
|
-
"--add-host",
|
|
340
|
-
"host.docker.internal:host-gateway",
|
|
341
|
-
"-p",
|
|
342
|
-
`127.0.0.1:${edgePort}:8000`,
|
|
343
|
-
"-e",
|
|
344
|
-
`SUPABASE_URL=http://host.docker.internal:${gatewayPort}`,
|
|
345
|
-
"-e",
|
|
346
|
-
`SUPABASE_SERVICE_ROLE_KEY=${SERVICE_ROLE_KEY}`,
|
|
347
|
-
"-e",
|
|
348
|
-
"DENO_DIR=/deno-dir",
|
|
349
|
-
"-v",
|
|
350
|
-
`${WORKSPACE_ROOT}:${WORKSPACE_ROOT}:ro`,
|
|
351
|
-
"-v",
|
|
352
|
-
`${runtimeRoot}:${runtimeRoot}`,
|
|
353
|
-
"-v",
|
|
354
|
-
`${DENO_CACHE_VOLUME}:/deno-dir`,
|
|
355
|
-
"-w",
|
|
356
|
-
runtimeRoot,
|
|
357
|
-
DENO_DOCKER_IMAGE,
|
|
358
|
-
"run",
|
|
359
|
-
"--no-lock",
|
|
360
|
-
"--node-modules-dir=manual",
|
|
361
|
-
"--allow-env",
|
|
362
|
-
"--allow-net",
|
|
363
|
-
"--allow-read",
|
|
364
|
-
"--allow-sys",
|
|
365
|
-
"--unstable-sloppy-imports",
|
|
366
|
-
"--import-map",
|
|
367
|
-
path.join(runtimeRoot, "import_map.json"),
|
|
368
|
-
path.join(runtimeRoot, "supabase/edge-functions/index.ts"),
|
|
369
|
-
],
|
|
370
|
-
cwd: WORKSPACE_ROOT,
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
await waitForHttpOk({
|
|
374
|
-
url: `http://127.0.0.1:${edgePort}${FUNCTION_BASE_PATH}/ping`,
|
|
375
|
-
child: edgeRuntime.child,
|
|
376
|
-
logs: edgeRuntime.logs,
|
|
377
|
-
timeoutMs: 90_000,
|
|
378
|
-
});
|
|
379
|
-
}, 300_000);
|
|
380
|
-
|
|
381
|
-
beforeEach(async () => {
|
|
382
|
-
if (!supabaseAdmin) {
|
|
383
|
-
throw new Error("Supabase admin client was not initialized.");
|
|
384
|
-
}
|
|
385
|
-
|
|
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;
|
|
398
|
-
}
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
afterAll(async () => {
|
|
402
|
-
if (edgeRuntime) {
|
|
403
|
-
await stopRuntime(edgeRuntime.child);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
if (composeFilePath) {
|
|
407
|
-
runCheckedCommand({
|
|
408
|
-
command: "docker",
|
|
409
|
-
args: [
|
|
410
|
-
"compose",
|
|
411
|
-
"-p",
|
|
412
|
-
composeProjectName,
|
|
413
|
-
"-f",
|
|
414
|
-
composeFilePath,
|
|
415
|
-
"down",
|
|
416
|
-
"-v",
|
|
417
|
-
"--remove-orphans",
|
|
418
|
-
],
|
|
419
|
-
cwd: WORKSPACE_ROOT,
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (runtimeRoot) {
|
|
424
|
-
await rm(runtimeRoot, { recursive: true, force: true });
|
|
425
|
-
}
|
|
426
|
-
}, 60_000);
|
|
427
|
-
|
|
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
|
-
});
|
|
460
|
-
|
|
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(" "),
|
|
512
|
-
);
|
|
513
|
-
|
|
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",
|
|
547
|
-
);
|
|
548
|
-
}
|
|
549
|
-
});
|
|
550
|
-
|
|
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
|
-
`);
|
|
562
|
-
|
|
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);
|
|
580
|
-
|
|
581
|
-
await databaseClient.insertBundle(owner);
|
|
582
|
-
|
|
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
|
-
}
|
|
591
|
-
});
|
|
592
|
-
|
|
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,
|
|
607
|
-
},
|
|
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",
|
|
617
|
-
patches: [
|
|
618
|
-
{
|
|
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,
|
|
624
|
-
},
|
|
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,
|
|
652
|
-
},
|
|
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);
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
it("serves unversioned Release Catalog routes from the edge function entrypoint", async () => {
|
|
703
|
-
const bundle = toRuntimeBundle({
|
|
704
|
-
id: "00000000-0000-0000-0000-000000000001",
|
|
705
|
-
platform: "ios",
|
|
706
|
-
fileHash: "hash",
|
|
707
|
-
gitCommitHash: null,
|
|
708
|
-
storageUri: "storage://unused",
|
|
709
|
-
archiveByteSize: 3_000_000_001,
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
await uploadBundleObject(supabaseAdmin, bundle.id);
|
|
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);
|
|
720
|
-
|
|
721
|
-
const response = await fetch(
|
|
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 } },
|
|
724
|
-
);
|
|
725
|
-
|
|
726
|
-
expect(response.ok).toBe(true);
|
|
727
|
-
await expect(response.json()).resolves.toMatchObject({
|
|
728
|
-
catalogId: expect.stringMatching(/^[0-9a-f-]{36}$/),
|
|
729
|
-
releases: [{ bundleId: "00000000-0000-0000-0000-000000000001" }],
|
|
730
|
-
});
|
|
731
|
-
});
|
|
732
|
-
|
|
733
|
-
it("does not support the legacy exact path", async () => {
|
|
734
|
-
const response = await fetch(
|
|
735
|
-
`http://127.0.0.1:${edgePort}${FUNCTION_BASE_PATH}/api/check-update`,
|
|
736
|
-
);
|
|
737
|
-
|
|
738
|
-
expect(response.status).toBe(404);
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
it("does not expose management routes from the edge function entrypoint", async () => {
|
|
742
|
-
const response = await fetch(
|
|
743
|
-
`http://127.0.0.1:${edgePort}${FUNCTION_BASE_PATH}/admin/bundles`,
|
|
744
|
-
);
|
|
745
|
-
|
|
746
|
-
expect(response.status).toBe(404);
|
|
747
|
-
await expect(response.json()).resolves.toEqual({
|
|
748
|
-
error: "Not found",
|
|
749
|
-
});
|
|
750
|
-
});
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
function base64UrlEncode(value: string | Buffer) {
|
|
754
|
-
return Buffer.from(value)
|
|
755
|
-
.toString("base64")
|
|
756
|
-
.replace(/\+/g, "-")
|
|
757
|
-
.replace(/\//g, "_")
|
|
758
|
-
.replace(/=+$/g, "");
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
function createLegacyJwt(role: "anon" | "service_role") {
|
|
762
|
-
const header = base64UrlEncode(JSON.stringify({ alg: "HS256", typ: "JWT" }));
|
|
763
|
-
const payload = base64UrlEncode(
|
|
764
|
-
JSON.stringify({
|
|
765
|
-
role,
|
|
766
|
-
iss: "supabase-test",
|
|
767
|
-
iat: Math.floor(Date.now() / 1000),
|
|
768
|
-
exp: Math.floor(Date.now() / 1000) + JWT_EXPIRY_SECONDS,
|
|
769
|
-
}),
|
|
770
|
-
);
|
|
771
|
-
const signature = createHmac("sha256", JWT_SECRET)
|
|
772
|
-
.update(`${header}.${payload}`)
|
|
773
|
-
.digest("base64")
|
|
774
|
-
.replace(/\+/g, "-")
|
|
775
|
-
.replace(/\//g, "_")
|
|
776
|
-
.replace(/=+$/g, "");
|
|
777
|
-
|
|
778
|
-
return `${header}.${payload}.${signature}`;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
const waitForUrlOk = async (url: string, timeoutMs = 90_000) => {
|
|
782
|
-
const deadline = Date.now() + timeoutMs;
|
|
783
|
-
let lastError = "no response";
|
|
784
|
-
|
|
785
|
-
while (Date.now() < deadline) {
|
|
786
|
-
try {
|
|
787
|
-
const response = await fetch(url);
|
|
788
|
-
if (response.ok) {
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
lastError = `${response.status} ${response.statusText}`;
|
|
793
|
-
} catch (error) {
|
|
794
|
-
lastError = error instanceof Error ? error.message : String(error);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
await sleep(500);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
throw new Error(`Timed out waiting for ${url}: ${lastError}`);
|
|
801
|
-
};
|
|
802
|
-
|
|
803
|
-
const waitForRestApiReady = async (baseUrl: string, timeoutMs = 90_000) => {
|
|
804
|
-
const deadline = Date.now() + timeoutMs;
|
|
805
|
-
let lastError = "no response";
|
|
806
|
-
|
|
807
|
-
while (Date.now() < deadline) {
|
|
808
|
-
try {
|
|
809
|
-
const response = await fetch(
|
|
810
|
-
`${baseUrl}/rest/v1/${SUPABASE_V1_TABLE_NAMES.bundles}?select=id&limit=1`,
|
|
811
|
-
{
|
|
812
|
-
headers: {
|
|
813
|
-
apikey: SERVICE_ROLE_KEY,
|
|
814
|
-
Authorization: `Bearer ${SERVICE_ROLE_KEY}`,
|
|
815
|
-
},
|
|
816
|
-
},
|
|
817
|
-
);
|
|
818
|
-
if (response.ok) {
|
|
819
|
-
return;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
lastError = `${response.status} ${response.statusText}: ${await response.text()}`;
|
|
823
|
-
} catch (error) {
|
|
824
|
-
lastError = error instanceof Error ? error.message : String(error);
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
await sleep(500);
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
throw new Error(`Timed out waiting for PostgREST: ${lastError}`);
|
|
831
|
-
};
|
|
832
|
-
|
|
833
|
-
const sleep = async (ms: number) => {
|
|
834
|
-
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
835
|
-
};
|
|
836
|
-
|
|
837
|
-
const ensureBucketExists = async (
|
|
838
|
-
supabaseAdmin: ReturnType<typeof createClient>,
|
|
839
|
-
) => {
|
|
840
|
-
const { data: buckets, error: listError } =
|
|
841
|
-
await supabaseAdmin.storage.listBuckets();
|
|
842
|
-
|
|
843
|
-
if (listError) {
|
|
844
|
-
throw listError;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
if (buckets.some((bucket) => bucket.name === BUCKET_NAME)) {
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
const { error } = await supabaseAdmin.storage.createBucket(BUCKET_NAME);
|
|
852
|
-
|
|
853
|
-
if (error) {
|
|
854
|
-
throw error;
|
|
855
|
-
}
|
|
856
|
-
};
|
|
857
|
-
|
|
858
|
-
const uploadBundleObject = async (
|
|
859
|
-
supabaseAdmin: ReturnType<typeof createClient>,
|
|
860
|
-
bundleId: string,
|
|
861
|
-
) => {
|
|
862
|
-
await uploadStorageObject(
|
|
863
|
-
supabaseAdmin,
|
|
864
|
-
`${bundleId}/bundle.zip`,
|
|
865
|
-
Buffer.from("zip"),
|
|
866
|
-
"application/zip",
|
|
867
|
-
);
|
|
868
|
-
};
|
|
869
|
-
|
|
870
|
-
const uploadStorageObject = async (
|
|
871
|
-
supabaseAdmin: ReturnType<typeof createClient>,
|
|
872
|
-
key: string,
|
|
873
|
-
body: string | Buffer,
|
|
874
|
-
contentType: string,
|
|
875
|
-
) => {
|
|
876
|
-
const { error } = await supabaseAdmin.storage
|
|
877
|
-
.from(BUCKET_NAME)
|
|
878
|
-
.upload(key, body, {
|
|
879
|
-
contentType,
|
|
880
|
-
cacheControl: "31536000",
|
|
881
|
-
upsert: true,
|
|
882
|
-
});
|
|
883
|
-
|
|
884
|
-
if (error) {
|
|
885
|
-
throw error;
|
|
886
|
-
}
|
|
887
|
-
};
|
|
888
|
-
|
|
889
|
-
const loadSupabaseInitSql = async (storageRepoPath: string) => {
|
|
890
|
-
const storageMigrationsDir = path.join(storageRepoPath, "migrations/tenant");
|
|
891
|
-
const storageMigrationFiles = (await readdir(storageMigrationsDir))
|
|
892
|
-
.filter((file) => file.endsWith(".sql"))
|
|
893
|
-
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
|
894
|
-
const storageMigrations = await Promise.all(
|
|
895
|
-
storageMigrationFiles.map(async (file) => {
|
|
896
|
-
const contents = await readFile(
|
|
897
|
-
path.join(storageMigrationsDir, file),
|
|
898
|
-
"utf8",
|
|
899
|
-
);
|
|
900
|
-
const trimmed = contents.trimEnd();
|
|
901
|
-
return trimmed.endsWith(";") ? trimmed : `${trimmed};`;
|
|
902
|
-
}),
|
|
903
|
-
);
|
|
904
|
-
|
|
905
|
-
const migrationsDir = path.join(
|
|
906
|
-
WORKSPACE_ROOT,
|
|
907
|
-
"plugins/supabase/supabase/migrations",
|
|
908
|
-
);
|
|
909
|
-
const migrationFiles = (await readdir(migrationsDir))
|
|
910
|
-
.filter((file) => file.endsWith(".sql"))
|
|
911
|
-
.sort();
|
|
912
|
-
const migrations = await Promise.all(
|
|
913
|
-
migrationFiles.map(async (file) => {
|
|
914
|
-
const contents = await readFile(path.join(migrationsDir, file), "utf8");
|
|
915
|
-
return contents.replaceAll("%%BUCKET_NAME%%", BUCKET_NAME);
|
|
916
|
-
}),
|
|
917
|
-
);
|
|
918
|
-
|
|
919
|
-
return `
|
|
920
|
-
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
921
|
-
|
|
922
|
-
DO $$
|
|
923
|
-
BEGIN
|
|
924
|
-
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN
|
|
925
|
-
CREATE ROLE anon NOLOGIN NOINHERIT;
|
|
926
|
-
END IF;
|
|
927
|
-
|
|
928
|
-
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated') THEN
|
|
929
|
-
CREATE ROLE authenticated NOLOGIN NOINHERIT;
|
|
930
|
-
END IF;
|
|
931
|
-
|
|
932
|
-
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN
|
|
933
|
-
CREATE ROLE service_role NOLOGIN NOINHERIT BYPASSRLS;
|
|
934
|
-
END IF;
|
|
935
|
-
|
|
936
|
-
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticator') THEN
|
|
937
|
-
CREATE ROLE authenticator LOGIN PASSWORD '${POSTGRES_PASSWORD}' NOINHERIT;
|
|
938
|
-
END IF;
|
|
939
|
-
|
|
940
|
-
IF NOT EXISTS (
|
|
941
|
-
SELECT 1 FROM pg_roles WHERE rolname = 'supabase_storage_admin'
|
|
942
|
-
) THEN
|
|
943
|
-
CREATE ROLE supabase_storage_admin LOGIN PASSWORD '${POSTGRES_PASSWORD}' SUPERUSER;
|
|
944
|
-
END IF;
|
|
945
|
-
END $$;
|
|
946
|
-
|
|
947
|
-
GRANT anon TO authenticator;
|
|
948
|
-
GRANT authenticated TO authenticator;
|
|
949
|
-
GRANT service_role TO authenticator;
|
|
950
|
-
|
|
951
|
-
${migrations.join("\n\n")}
|
|
952
|
-
|
|
953
|
-
SET search_path TO storage, public, extensions;
|
|
954
|
-
|
|
955
|
-
${storageMigrations.join("\n\n")}
|
|
956
|
-
|
|
957
|
-
SET search_path TO public;
|
|
958
|
-
|
|
959
|
-
GRANT USAGE ON SCHEMA public TO anon, authenticated, service_role;
|
|
960
|
-
GRANT SELECT ON ALL TABLES IN SCHEMA public TO anon, authenticated;
|
|
961
|
-
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO service_role;
|
|
962
|
-
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO anon, authenticated, service_role;
|
|
963
|
-
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO anon, authenticated, service_role;
|
|
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
|
-
|
|
970
|
-
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
|
971
|
-
GRANT SELECT ON TABLES TO anon, authenticated;
|
|
972
|
-
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
|
973
|
-
GRANT ALL PRIVILEGES ON TABLES TO service_role;
|
|
974
|
-
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
|
975
|
-
GRANT USAGE, SELECT ON SEQUENCES TO anon, authenticated, service_role;
|
|
976
|
-
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
|
977
|
-
GRANT EXECUTE ON FUNCTIONS TO anon, authenticated, service_role;
|
|
978
|
-
`.trim();
|
|
979
|
-
};
|
|
980
|
-
|
|
981
|
-
const createComposeFile = ({
|
|
982
|
-
gatewayPort,
|
|
983
|
-
runtimeRoot,
|
|
984
|
-
}: {
|
|
985
|
-
gatewayPort: number;
|
|
986
|
-
runtimeRoot: string;
|
|
987
|
-
}) => {
|
|
988
|
-
return `
|
|
989
|
-
services:
|
|
990
|
-
db:
|
|
991
|
-
image: ${POSTGRES_IMAGE}
|
|
992
|
-
environment:
|
|
993
|
-
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
994
|
-
POSTGRES_DB: ${POSTGRES_DB}
|
|
995
|
-
healthcheck:
|
|
996
|
-
test: ["CMD-SHELL", "pg_isready -U postgres -d ${POSTGRES_DB}"]
|
|
997
|
-
interval: 5s
|
|
998
|
-
timeout: 5s
|
|
999
|
-
retries: 20
|
|
1000
|
-
volumes:
|
|
1001
|
-
- ${path.join(runtimeRoot, "db-init")}:/docker-entrypoint-initdb.d:ro
|
|
1002
|
-
|
|
1003
|
-
rest:
|
|
1004
|
-
image: ${POSTGREST_IMAGE}
|
|
1005
|
-
depends_on:
|
|
1006
|
-
db:
|
|
1007
|
-
condition: service_healthy
|
|
1008
|
-
environment:
|
|
1009
|
-
PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
|
1010
|
-
PGRST_DB_SCHEMAS: public,storage
|
|
1011
|
-
PGRST_DB_MAX_ROWS: 1000
|
|
1012
|
-
PGRST_DB_EXTRA_SEARCH_PATH: public
|
|
1013
|
-
PGRST_DB_ANON_ROLE: anon
|
|
1014
|
-
PGRST_JWT_SECRET: ${JWT_SECRET}
|
|
1015
|
-
PGRST_DB_USE_LEGACY_GUCS: "false"
|
|
1016
|
-
PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
|
|
1017
|
-
PGRST_APP_SETTINGS_JWT_EXP: "3600"
|
|
1018
|
-
|
|
1019
|
-
imgproxy:
|
|
1020
|
-
image: ${IMGPROXY_IMAGE}
|
|
1021
|
-
environment:
|
|
1022
|
-
IMGPROXY_BIND: ":5001"
|
|
1023
|
-
IMGPROXY_LOCAL_FILESYSTEM_ROOT: /
|
|
1024
|
-
IMGPROXY_USE_ETAG: "true"
|
|
1025
|
-
|
|
1026
|
-
storage:
|
|
1027
|
-
image: ${STORAGE_IMAGE}
|
|
1028
|
-
restart: on-failure
|
|
1029
|
-
depends_on:
|
|
1030
|
-
db:
|
|
1031
|
-
condition: service_healthy
|
|
1032
|
-
rest:
|
|
1033
|
-
condition: service_started
|
|
1034
|
-
imgproxy:
|
|
1035
|
-
condition: service_started
|
|
1036
|
-
environment:
|
|
1037
|
-
ANON_KEY: ${ANON_KEY}
|
|
1038
|
-
SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
|
1039
|
-
POSTGREST_URL: http://rest:3000
|
|
1040
|
-
AUTH_JWT_SECRET: ${JWT_SECRET}
|
|
1041
|
-
DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
|
1042
|
-
STORAGE_PUBLIC_URL: http://gateway:8000
|
|
1043
|
-
REQUEST_ALLOW_X_FORWARDED_PATH: "true"
|
|
1044
|
-
FILE_SIZE_LIMIT: 52428800
|
|
1045
|
-
STORAGE_BACKEND: file
|
|
1046
|
-
GLOBAL_S3_BUCKET: ${BUCKET_NAME}
|
|
1047
|
-
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
|
|
1048
|
-
TENANT_ID: stub
|
|
1049
|
-
REGION: stub
|
|
1050
|
-
ENABLE_IMAGE_TRANSFORMATION: "false"
|
|
1051
|
-
IMGPROXY_URL: http://imgproxy:5001
|
|
1052
|
-
S3_PROTOCOL_ACCESS_KEY_ID: stub
|
|
1053
|
-
S3_PROTOCOL_ACCESS_KEY_SECRET: stub
|
|
1054
|
-
volumes:
|
|
1055
|
-
- storage-data:/var/lib/storage
|
|
1056
|
-
|
|
1057
|
-
gateway:
|
|
1058
|
-
image: ${NGINX_IMAGE}
|
|
1059
|
-
depends_on:
|
|
1060
|
-
storage:
|
|
1061
|
-
condition: service_started
|
|
1062
|
-
rest:
|
|
1063
|
-
condition: service_started
|
|
1064
|
-
ports:
|
|
1065
|
-
- "0.0.0.0:${gatewayPort}:8000"
|
|
1066
|
-
volumes:
|
|
1067
|
-
- ${path.join(runtimeRoot, "nginx.conf")}:/etc/nginx/nginx.conf:ro
|
|
1068
|
-
|
|
1069
|
-
volumes:
|
|
1070
|
-
storage-data:
|
|
1071
|
-
`.trim();
|
|
1072
|
-
};
|
|
1073
|
-
|
|
1074
|
-
const createNginxConfig = () => {
|
|
1075
|
-
return `
|
|
1076
|
-
events {}
|
|
1077
|
-
|
|
1078
|
-
http {
|
|
1079
|
-
client_max_body_size 100m;
|
|
1080
|
-
|
|
1081
|
-
server {
|
|
1082
|
-
listen 8000;
|
|
1083
|
-
|
|
1084
|
-
location /rest/v1/ {
|
|
1085
|
-
proxy_pass http://rest:3000/;
|
|
1086
|
-
proxy_http_version 1.1;
|
|
1087
|
-
proxy_set_header Host $host;
|
|
1088
|
-
proxy_set_header Authorization $http_authorization;
|
|
1089
|
-
proxy_set_header apikey $http_apikey;
|
|
1090
|
-
proxy_set_header Content-Profile $http_content_profile;
|
|
1091
|
-
proxy_set_header Accept-Profile $http_accept_profile;
|
|
1092
|
-
proxy_set_header Prefer $http_prefer;
|
|
1093
|
-
proxy_set_header Range $http_range;
|
|
1094
|
-
proxy_set_header Range-Unit $http_range_unit;
|
|
1095
|
-
proxy_set_header Content-Type $http_content_type;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
location /storage/v1/ {
|
|
1099
|
-
proxy_pass http://storage:5000/;
|
|
1100
|
-
proxy_http_version 1.1;
|
|
1101
|
-
proxy_set_header Host $host;
|
|
1102
|
-
proxy_set_header Authorization $http_authorization;
|
|
1103
|
-
proxy_set_header apikey $http_apikey;
|
|
1104
|
-
proxy_set_header x-forwarded-path $request_uri;
|
|
1105
|
-
proxy_set_header Content-Type $http_content_type;
|
|
1106
|
-
proxy_set_header Content-Length $content_length;
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
`.trim();
|
|
1111
|
-
};
|
|
1112
|
-
|
|
1113
|
-
const writeSupabaseRuntimeFiles = async ({
|
|
1114
|
-
runtimeRoot,
|
|
1115
|
-
gatewayPort,
|
|
1116
|
-
storageRepoPath,
|
|
1117
|
-
}: {
|
|
1118
|
-
runtimeRoot: string;
|
|
1119
|
-
gatewayPort: number;
|
|
1120
|
-
storageRepoPath: string;
|
|
1121
|
-
}) => {
|
|
1122
|
-
await mkdir(path.join(runtimeRoot, "db-init"), { recursive: true });
|
|
1123
|
-
await mkdir(path.join(runtimeRoot, "supabase/edge-functions"), {
|
|
1124
|
-
recursive: true,
|
|
1125
|
-
});
|
|
1126
|
-
await symlink(
|
|
1127
|
-
path.join(WORKSPACE_ROOT, "plugins/supabase/src"),
|
|
1128
|
-
path.join(runtimeRoot, "src"),
|
|
1129
|
-
);
|
|
1130
|
-
await symlink(
|
|
1131
|
-
path.join(WORKSPACE_ROOT, "plugins/supabase/node_modules"),
|
|
1132
|
-
path.join(runtimeRoot, "node_modules"),
|
|
1133
|
-
);
|
|
1134
|
-
|
|
1135
|
-
const transformedEntry = transformEnv(
|
|
1136
|
-
path.join(
|
|
1137
|
-
WORKSPACE_ROOT,
|
|
1138
|
-
"plugins/supabase/supabase/edge-functions/index.ts",
|
|
1139
|
-
),
|
|
1140
|
-
{
|
|
1141
|
-
BUCKET_NAME,
|
|
1142
|
-
FUNCTION_NAME,
|
|
1143
|
-
},
|
|
1144
|
-
);
|
|
1145
|
-
const importMap = {
|
|
1146
|
-
imports: {
|
|
1147
|
-
"@hot-updater/server": pathToFileURL(
|
|
1148
|
-
path.join(WORKSPACE_ROOT, "packages/server/dist/index.mjs"),
|
|
1149
|
-
).href,
|
|
1150
|
-
"@hot-updater/supabase/edge": pathToFileURL(
|
|
1151
|
-
path.join(runtimeRoot, "hot-updater-supabase-edge.ts"),
|
|
1152
|
-
).href,
|
|
1153
|
-
hono: `npm:hono@${resolvePackageVersion("hono", {
|
|
1154
|
-
searchFrom: path.join(WORKSPACE_ROOT, "plugins/supabase"),
|
|
1155
|
-
})}`,
|
|
1156
|
-
},
|
|
1157
|
-
};
|
|
1158
|
-
|
|
1159
|
-
await writeFile(
|
|
1160
|
-
path.join(runtimeRoot, "hot-updater-supabase-edge.ts"),
|
|
1161
|
-
`
|
|
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)};
|
|
1164
|
-
`.trim(),
|
|
1165
|
-
);
|
|
1166
|
-
await writeFile(
|
|
1167
|
-
path.join(runtimeRoot, "supabase/edge-functions/index.ts"),
|
|
1168
|
-
transformedEntry,
|
|
1169
|
-
);
|
|
1170
|
-
await writeFile(
|
|
1171
|
-
path.join(runtimeRoot, "import_map.json"),
|
|
1172
|
-
JSON.stringify(importMap),
|
|
1173
|
-
);
|
|
1174
|
-
await writeFile(
|
|
1175
|
-
path.join(runtimeRoot, "db-init/00-init.sql"),
|
|
1176
|
-
await loadSupabaseInitSql(storageRepoPath),
|
|
1177
|
-
);
|
|
1178
|
-
await writeFile(
|
|
1179
|
-
path.join(runtimeRoot, "docker-compose.yml"),
|
|
1180
|
-
createComposeFile({ runtimeRoot, gatewayPort }),
|
|
1181
|
-
);
|
|
1182
|
-
await writeFile(path.join(runtimeRoot, "nginx.conf"), createNginxConfig());
|
|
1183
|
-
};
|