@hot-updater/cloudflare 0.36.6 → 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/iac/index.cjs +989 -11
- package/dist/iac/index.mjs +992 -15
- package/dist/index.cjs +919 -6989
- package/dist/index.d.cts +11 -47
- package/dist/index.d.mts +11 -47
- package/dist/index.mjs +917 -7006
- package/dist/worker/index.cjs +898 -336
- package/dist/worker/index.d.cts +18 -33
- package/dist/worker/index.d.mts +18 -33
- package/dist/worker/index.mjs +899 -331
- package/package.json +7 -10
- package/sql/bundles.sql +183 -35
- package/src/d1Database.spec.ts +143 -392
- package/src/d1Database.ts +55 -650
- package/src/d1Implementation.spec.ts +252 -0
- package/src/d1Implementation.ts +742 -0
- package/src/d1Query.spec.ts +46 -0
- package/src/d1Query.ts +89 -0
- package/src/d1Rows.spec.ts +111 -0
- package/src/d1Rows.ts +326 -0
- package/src/d1Sql.spec.ts +30 -0
- package/src/d1Sql.ts +175 -0
- package/src/r2S3Storage.ts +93 -137
- package/src/r2Storage.spec.ts +120 -218
- package/src/r2Storage.ts +13 -61
- package/src/r2WorkerStorage.spec.ts +161 -50
- package/src/r2WorkerStorage.ts +85 -118
- package/src/worker/d1Database.spec.ts +33 -0
- package/src/worker/d1Database.ts +52 -0
- package/src/worker/index.ts +3 -31
- package/worker/dist/README.md +1 -1
- package/worker/dist/index.js +5968 -4701
- package/worker/dist/index.js.map +4 -4
- package/worker/migrations/0001_hot-updater_1.0.0.sql +197 -0
- package/worker/migrations/0002_hot-updater_artifact_byte_sizes.sql +11 -0
- package/worker/src/databaseMigration.integration.spec.ts +114 -0
- package/worker/src/databasePlugins.integration.spec.ts +292 -0
- package/worker/src/index.integration.spec.ts +102 -294
- package/worker/src/index.ts +14 -66
- package/worker/src/storagePlugin.integration.spec.ts +59 -0
- package/worker/wrangler.json +5 -2
- package/src/cloudflareWorkerDatabase.spec.ts +0 -260
- package/src/cloudflareWorkerDatabase.ts +0 -651
- package/src/r2WranglerStorage.ts +0 -193
- package/worker/migrations/0001_hot-updater_init.sql +0 -16
- package/worker/migrations/0002_hot-updater_0.13.0.sql +0 -8
- package/worker/migrations/0003_hot-updater_0.18.0.sql +0 -38
- package/worker/migrations/0004_hot-updater_0.29.0.sql +0 -11
- package/worker/migrations/0005_hot-updater_0.31.0.sql +0 -24
package/dist/iac/index.mjs
CHANGED
|
@@ -2,8 +2,11 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import crypto from "crypto";
|
|
3
3
|
import fs from "fs/promises";
|
|
4
4
|
import path from "path";
|
|
5
|
-
import { ConfigBuilder, InitError, assertInitInputs, assertInitProviderInputs, confirmInitInputPersistence, copyDirToTmp, createHotUpdaterConfigScaffoldFromBuilder, getCwd, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, link, makeEnv, p, readHotUpdaterInitEnv, resolveInitProviderInput, transformTemplate, writeHotUpdaterConfig } from "@hot-updater/cli-tools";
|
|
6
|
-
import {
|
|
5
|
+
import { ConfigBuilder, InitError, LegacyInfrastructureError, assertInfrastructureGenerationAtUrl, assertInitInputs, assertInitProviderInputs, confirmInitInputPersistence, copyDirToTmp, createHotUpdaterConfigScaffoldFromBuilder, formatApiKeyNote, getCwd, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, link, makeEnv, p, readHotUpdaterInitEnv, resolveInitProviderInput, transformTemplate, writeHotUpdaterConfig } from "@hot-updater/cli-tools";
|
|
6
|
+
import { provisionApiKey } from "@hot-updater/server";
|
|
7
|
+
import Cloudflare, { Cloudflare as Cloudflare$1 } from "cloudflare";
|
|
8
|
+
import { createDatabasePlugin, isDatabaseMetadataObject } from "@hot-updater/plugin-core";
|
|
9
|
+
import { createDatabasePluginAdapter } from "@hot-updater/plugin-core/internal";
|
|
7
10
|
import { fileURLToPath } from "node:url";
|
|
8
11
|
import { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
|
|
9
12
|
import { StringDecoder } from "node:string_decoder";
|
|
@@ -46,6 +49,895 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
46
49
|
}) : target, mod));
|
|
47
50
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
48
51
|
//#endregion
|
|
52
|
+
//#region src/d1Rows.ts
|
|
53
|
+
var InvalidD1RowError = class extends Error {
|
|
54
|
+
name = "InvalidD1RowError";
|
|
55
|
+
model;
|
|
56
|
+
constructor(model) {
|
|
57
|
+
super(`D1 returned an invalid ${model} row`);
|
|
58
|
+
this.model = model;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
62
|
+
const stringValue = (row, field, model) => {
|
|
63
|
+
const value = row[field];
|
|
64
|
+
if (typeof value !== "string") throw new InvalidD1RowError(model);
|
|
65
|
+
return value;
|
|
66
|
+
};
|
|
67
|
+
const nullableString = (row, field, model) => {
|
|
68
|
+
const value = row[field];
|
|
69
|
+
if (value === null) return null;
|
|
70
|
+
if (typeof value !== "string") throw new InvalidD1RowError(model);
|
|
71
|
+
return value;
|
|
72
|
+
};
|
|
73
|
+
const numberValue = (row, field, model) => {
|
|
74
|
+
const value = row[field];
|
|
75
|
+
if (typeof value !== "number") throw new InvalidD1RowError(model);
|
|
76
|
+
return value;
|
|
77
|
+
};
|
|
78
|
+
const byteSizeValue = (row, field, model) => {
|
|
79
|
+
const value = row[field];
|
|
80
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new InvalidD1RowError(model);
|
|
81
|
+
return value;
|
|
82
|
+
};
|
|
83
|
+
const booleanValue = (row, field, model) => {
|
|
84
|
+
const value = row[field];
|
|
85
|
+
if (value === true || value === 1) return true;
|
|
86
|
+
if (value === false || value === 0) return false;
|
|
87
|
+
throw new InvalidD1RowError(model);
|
|
88
|
+
};
|
|
89
|
+
const jsonValue = (value, model) => {
|
|
90
|
+
if (typeof value !== "string") throw new InvalidD1RowError(model);
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(value);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error instanceof SyntaxError) throw new InvalidD1RowError(model);
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const metadata = (row) => {
|
|
99
|
+
const value = jsonValue(row["metadata"], "bundles");
|
|
100
|
+
if (!isDatabaseMetadataObject(value)) throw new InvalidD1RowError("bundles");
|
|
101
|
+
return value;
|
|
102
|
+
};
|
|
103
|
+
const bundleRow = (row) => {
|
|
104
|
+
const platform = stringValue(row, "platform", "bundles");
|
|
105
|
+
if (platform !== "ios" && platform !== "android") throw new InvalidD1RowError("bundles");
|
|
106
|
+
return {
|
|
107
|
+
id: stringValue(row, "id", "bundles"),
|
|
108
|
+
platform,
|
|
109
|
+
file_hash: stringValue(row, "file_hash", "bundles"),
|
|
110
|
+
git_commit_hash: nullableString(row, "git_commit_hash", "bundles"),
|
|
111
|
+
storage_uri: stringValue(row, "storage_uri", "bundles"),
|
|
112
|
+
archive_byte_size: row["archive_byte_size"] === void 0 ? 0 : byteSizeValue(row, "archive_byte_size", "bundles"),
|
|
113
|
+
metadata: metadata(row),
|
|
114
|
+
manifest_storage_uri: nullableString(row, "manifest_storage_uri", "bundles"),
|
|
115
|
+
manifest_file_hash: nullableString(row, "manifest_file_hash", "bundles"),
|
|
116
|
+
asset_base_storage_uri: nullableString(row, "asset_base_storage_uri", "bundles")
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
const patchRow = (row) => ({
|
|
120
|
+
id: stringValue(row, "id", "bundle_patches"),
|
|
121
|
+
bundle_id: stringValue(row, "bundle_id", "bundle_patches"),
|
|
122
|
+
base_bundle_id: stringValue(row, "base_bundle_id", "bundle_patches"),
|
|
123
|
+
base_file_hash: stringValue(row, "base_file_hash", "bundle_patches"),
|
|
124
|
+
patch_file_hash: stringValue(row, "patch_file_hash", "bundle_patches"),
|
|
125
|
+
patch_storage_uri: stringValue(row, "patch_storage_uri", "bundle_patches"),
|
|
126
|
+
byte_size: byteSizeValue(row, "byte_size", "bundle_patches"),
|
|
127
|
+
order_index: numberValue(row, "order_index", "bundle_patches")
|
|
128
|
+
});
|
|
129
|
+
const channelRow = (row) => ({
|
|
130
|
+
id: stringValue(row, "id", "channels"),
|
|
131
|
+
name: stringValue(row, "name", "channels")
|
|
132
|
+
});
|
|
133
|
+
const eventRow = (row) => {
|
|
134
|
+
const type = stringValue(row, "type", "bundle_events");
|
|
135
|
+
const platform = stringValue(row, "platform", "bundle_events");
|
|
136
|
+
const fromBundleId = nullableString(row, "from_bundle_id", "bundle_events");
|
|
137
|
+
const updateStrategy = nullableString(row, "update_strategy", "bundle_events");
|
|
138
|
+
if (platform !== "ios" && platform !== "android" || !((type === "UPDATE_APPLIED" || type === "RECOVERED" || type === "RELEASE_ADOPTED") && typeof fromBundleId === "string" && (updateStrategy === "fingerprint" || updateStrategy === "appVersion") || type === "UNCHANGED" && fromBundleId === null && updateStrategy === null)) throw new InvalidD1RowError("bundle_events");
|
|
139
|
+
return {
|
|
140
|
+
id: stringValue(row, "id", "bundle_events"),
|
|
141
|
+
type,
|
|
142
|
+
install_id: stringValue(row, "install_id", "bundle_events"),
|
|
143
|
+
user_id: nullableString(row, "user_id", "bundle_events"),
|
|
144
|
+
username: nullableString(row, "username", "bundle_events"),
|
|
145
|
+
from_release_id: nullableString(row, "from_release_id", "bundle_events"),
|
|
146
|
+
from_bundle_id: fromBundleId,
|
|
147
|
+
to_release_id: nullableString(row, "to_release_id", "bundle_events"),
|
|
148
|
+
to_bundle_id: stringValue(row, "to_bundle_id", "bundle_events"),
|
|
149
|
+
platform,
|
|
150
|
+
app_version: stringValue(row, "app_version", "bundle_events"),
|
|
151
|
+
channel: stringValue(row, "channel", "bundle_events"),
|
|
152
|
+
cohort: stringValue(row, "cohort", "bundle_events"),
|
|
153
|
+
update_strategy: updateStrategy,
|
|
154
|
+
fingerprint_hash: nullableString(row, "fingerprint_hash", "bundle_events"),
|
|
155
|
+
sdk_version: nullableString(row, "sdk_version", "bundle_events"),
|
|
156
|
+
received_at_ms: numberValue(row, "received_at_ms", "bundle_events")
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
const apiKeyRow = (row) => {
|
|
160
|
+
const role = stringValue(row, "role", "api_keys");
|
|
161
|
+
if (role !== "client") throw new InvalidD1RowError("api_keys");
|
|
162
|
+
const revokedAt = row["revoked_at_ms"];
|
|
163
|
+
return {
|
|
164
|
+
id: stringValue(row, "id", "api_keys"),
|
|
165
|
+
hash: stringValue(row, "hash", "api_keys"),
|
|
166
|
+
name: stringValue(row, "name", "api_keys"),
|
|
167
|
+
prefix: stringValue(row, "prefix", "api_keys"),
|
|
168
|
+
role,
|
|
169
|
+
created_at_ms: numberValue(row, "created_at_ms", "api_keys"),
|
|
170
|
+
revoked_at_ms: revokedAt === null ? null : numberValue(row, "revoked_at_ms", "api_keys")
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
const releaseTargetCohorts = (row) => {
|
|
174
|
+
const value = jsonValue(row["target_cohorts"], "releases");
|
|
175
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw new InvalidD1RowError("releases");
|
|
176
|
+
return value.filter((item) => typeof item === "string");
|
|
177
|
+
};
|
|
178
|
+
const releaseRow = (row) => {
|
|
179
|
+
const platform = stringValue(row, "platform", "releases");
|
|
180
|
+
const kind = stringValue(row, "kind", "releases");
|
|
181
|
+
const strategy = stringValue(row, "strategy", "releases");
|
|
182
|
+
const operation = stringValue(row, "operation", "releases");
|
|
183
|
+
if (platform !== "ios" && platform !== "android" || kind !== "BUNDLE" && kind !== "EMBEDDED" || strategy !== "APP_VERSION" && strategy !== "FINGERPRINT" || operation !== "DEPLOY" && operation !== "PROMOTE" && operation !== "ROLLBACK") throw new InvalidD1RowError("releases");
|
|
184
|
+
return {
|
|
185
|
+
id: stringValue(row, "id", "releases"),
|
|
186
|
+
revision: numberValue(row, "revision", "releases"),
|
|
187
|
+
scope_key: stringValue(row, "scope_key", "releases"),
|
|
188
|
+
channel_id: stringValue(row, "channel_id", "releases"),
|
|
189
|
+
platform,
|
|
190
|
+
kind,
|
|
191
|
+
bundle_id: nullableString(row, "bundle_id", "releases"),
|
|
192
|
+
strategy,
|
|
193
|
+
target_app_version: nullableString(row, "target_app_version", "releases"),
|
|
194
|
+
fingerprint_hash: nullableString(row, "fingerprint_hash", "releases"),
|
|
195
|
+
enabled: booleanValue(row, "enabled", "releases"),
|
|
196
|
+
should_force_update: booleanValue(row, "should_force_update", "releases"),
|
|
197
|
+
message: nullableString(row, "message", "releases"),
|
|
198
|
+
rollout_cohort_count: numberValue(row, "rollout_cohort_count", "releases"),
|
|
199
|
+
target_cohorts: releaseTargetCohorts(row),
|
|
200
|
+
operation,
|
|
201
|
+
source_release_id: nullableString(row, "source_release_id", "releases"),
|
|
202
|
+
created_at_ms: numberValue(row, "created_at_ms", "releases"),
|
|
203
|
+
updated_at_ms: numberValue(row, "updated_at_ms", "releases")
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
const releaseCatalogRow = (row) => {
|
|
207
|
+
const platform = stringValue(row, "platform", "release_catalogs");
|
|
208
|
+
const strategy = stringValue(row, "strategy", "release_catalogs");
|
|
209
|
+
if (platform !== "ios" && platform !== "android" || strategy !== "APP_VERSION" && strategy !== "FINGERPRINT") throw new InvalidD1RowError("release_catalogs");
|
|
210
|
+
return {
|
|
211
|
+
scope_key: stringValue(row, "scope_key", "release_catalogs"),
|
|
212
|
+
catalog_id: stringValue(row, "catalog_id", "release_catalogs"),
|
|
213
|
+
strategy,
|
|
214
|
+
channel_id: stringValue(row, "channel_id", "release_catalogs"),
|
|
215
|
+
channel_key: stringValue(row, "channel_key", "release_catalogs"),
|
|
216
|
+
platform,
|
|
217
|
+
fingerprint_hash: nullableString(row, "fingerprint_hash", "release_catalogs"),
|
|
218
|
+
generation: numberValue(row, "generation", "release_catalogs"),
|
|
219
|
+
payload: stringValue(row, "payload", "release_catalogs"),
|
|
220
|
+
catalog_hash: stringValue(row, "catalog_hash", "release_catalogs"),
|
|
221
|
+
byte_size: numberValue(row, "byte_size", "release_catalogs"),
|
|
222
|
+
is_tombstone: booleanValue(row, "is_tombstone", "release_catalogs"),
|
|
223
|
+
updated_at_ms: numberValue(row, "updated_at_ms", "release_catalogs")
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
function parseD1Row(model, value) {
|
|
227
|
+
if (!isRecord(value)) throw new InvalidD1RowError(model);
|
|
228
|
+
switch (model) {
|
|
229
|
+
case "bundles": return bundleRow(value);
|
|
230
|
+
case "bundle_patches": return patchRow(value);
|
|
231
|
+
case "channels": return channelRow(value);
|
|
232
|
+
case "bundle_events": return eventRow(value);
|
|
233
|
+
case "api_keys": return apiKeyRow(value);
|
|
234
|
+
case "releases": return releaseRow(value);
|
|
235
|
+
case "release_catalogs": return releaseCatalogRow(value);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/d1Sql.ts
|
|
240
|
+
var InvalidD1PredicateError = class extends Error {
|
|
241
|
+
name = "InvalidD1PredicateError";
|
|
242
|
+
};
|
|
243
|
+
const encodeD1Value = (value) => {
|
|
244
|
+
const encoded = JSON.stringify(value);
|
|
245
|
+
if (encoded === void 0) throw new TypeError("D1 values must be JSON-serializable.");
|
|
246
|
+
return encoded;
|
|
247
|
+
};
|
|
248
|
+
const bind = (value) => ({
|
|
249
|
+
sql: "json_extract(?, '$')",
|
|
250
|
+
params: [encodeD1Value(value)]
|
|
251
|
+
});
|
|
252
|
+
const escapeLikePattern = (value) => value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
|
253
|
+
const stringPredicate = (condition, operator) => {
|
|
254
|
+
if (typeof condition.value !== "string") throw new InvalidD1PredicateError();
|
|
255
|
+
if (condition.mode !== "insensitive") {
|
|
256
|
+
const parameter = bind(condition.value);
|
|
257
|
+
const column = `${condition.field} COLLATE BINARY`;
|
|
258
|
+
const value = `${parameter.sql} COLLATE BINARY`;
|
|
259
|
+
if (operator === "contains" || operator === "starts_with") return {
|
|
260
|
+
sql: `instr(${column}, ${value}) ${operator === "contains" ? "> 0" : "= 1"}`,
|
|
261
|
+
params: parameter.params
|
|
262
|
+
};
|
|
263
|
+
return {
|
|
264
|
+
sql: `substr(${column}, length(${condition.field}) - length(${parameter.sql}) + 1) = ${value}`,
|
|
265
|
+
params: [...parameter.params, ...parameter.params]
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const literal = escapeLikePattern(condition.value);
|
|
269
|
+
const parameter = bind(operator === "contains" ? `%${literal}%` : operator === "starts_with" ? `${literal}%` : `%${literal}`);
|
|
270
|
+
return {
|
|
271
|
+
sql: `lower(${condition.field}) LIKE lower(${parameter.sql}) ESCAPE '\\'`,
|
|
272
|
+
params: parameter.params
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
const predicate = (condition) => {
|
|
276
|
+
const operator = condition.operator ?? "eq";
|
|
277
|
+
switch (operator) {
|
|
278
|
+
case "eq":
|
|
279
|
+
case "ne": {
|
|
280
|
+
if (condition.value === null) return {
|
|
281
|
+
sql: `${condition.field} IS ${operator === "ne" ? "NOT " : ""}NULL`,
|
|
282
|
+
params: []
|
|
283
|
+
};
|
|
284
|
+
const parameter = bind(condition.value);
|
|
285
|
+
const insensitive = "mode" in condition && condition.mode === "insensitive";
|
|
286
|
+
const column = insensitive ? `lower(${condition.field})` : condition.field;
|
|
287
|
+
const value = insensitive ? `lower(${parameter.sql})` : parameter.sql;
|
|
288
|
+
return {
|
|
289
|
+
sql: `${column} ${operator === "eq" ? "=" : "<>"} ${value}`,
|
|
290
|
+
params: parameter.params
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
case "gt":
|
|
294
|
+
case "gte":
|
|
295
|
+
case "lt":
|
|
296
|
+
case "lte": {
|
|
297
|
+
const sqlOperator = {
|
|
298
|
+
gt: ">",
|
|
299
|
+
gte: ">=",
|
|
300
|
+
lt: "<",
|
|
301
|
+
lte: "<="
|
|
302
|
+
}[operator];
|
|
303
|
+
const parameter = bind(condition.value);
|
|
304
|
+
return {
|
|
305
|
+
sql: `${condition.field} ${sqlOperator} ${parameter.sql}`,
|
|
306
|
+
params: parameter.params
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
case "in":
|
|
310
|
+
case "not_in":
|
|
311
|
+
if (!Array.isArray(condition.value)) throw new InvalidD1PredicateError();
|
|
312
|
+
if (condition.value.length === 0) return {
|
|
313
|
+
sql: operator === "not_in" ? "1 = 1" : "1 = 0",
|
|
314
|
+
params: []
|
|
315
|
+
};
|
|
316
|
+
return {
|
|
317
|
+
sql: `${condition.field} ${operator === "not_in" ? "NOT " : ""}IN (SELECT value FROM json_each(?))`,
|
|
318
|
+
params: [encodeD1Value(condition.value)]
|
|
319
|
+
};
|
|
320
|
+
case "contains":
|
|
321
|
+
case "starts_with":
|
|
322
|
+
case "ends_with": return stringPredicate(condition, operator);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
const buildD1Where = (where) => {
|
|
326
|
+
const [first, ...rest] = where ?? [];
|
|
327
|
+
if (first === void 0) return {
|
|
328
|
+
sql: "",
|
|
329
|
+
params: []
|
|
330
|
+
};
|
|
331
|
+
const initial = predicate(first);
|
|
332
|
+
let sql = initial.sql;
|
|
333
|
+
const params = [...initial.params];
|
|
334
|
+
for (const condition of rest) {
|
|
335
|
+
const next = predicate(condition);
|
|
336
|
+
sql = `(${sql} ${condition.connector === "OR" ? "OR" : "AND"} ${next.sql})`;
|
|
337
|
+
params.push(...next.params);
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
sql: ` WHERE ${sql}`,
|
|
341
|
+
params
|
|
342
|
+
};
|
|
343
|
+
};
|
|
344
|
+
const buildD1Order = (orderBy) => orderBy === void 0 ? "" : ` ORDER BY ${orderBy.map((clause) => `${clause.field} ${clause.direction.toUpperCase()}${clause.nulls ? ` NULLS ${clause.nulls.toUpperCase()}` : ""}`).join(", ")}`;
|
|
345
|
+
const d1Placeholders = (count) => Array.from({ length: count }, () => "json_extract(?, '$')").join(", ");
|
|
346
|
+
const encodeD1Values = (values) => values.map(encodeD1Value);
|
|
347
|
+
//#endregion
|
|
348
|
+
//#region src/d1Query.ts
|
|
349
|
+
const d1TableNames = {
|
|
350
|
+
bundles: "bundles",
|
|
351
|
+
bundle_patches: "bundle_patches",
|
|
352
|
+
channels: "channels",
|
|
353
|
+
bundle_events: "bundle_events",
|
|
354
|
+
api_keys: "api_keys",
|
|
355
|
+
release_catalogs: "release_catalogs",
|
|
356
|
+
releases: "releases"
|
|
357
|
+
};
|
|
358
|
+
var D1QueryResultError = class extends Error {
|
|
359
|
+
name = "D1QueryResultError";
|
|
360
|
+
operation = "count";
|
|
361
|
+
constructor(result) {
|
|
362
|
+
super("D1 count query returned a malformed first row");
|
|
363
|
+
this.result = result;
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
const countD1Rows = async (executor, input) => {
|
|
367
|
+
const where = buildD1Where(input.where);
|
|
368
|
+
const table = d1TableNames[input.model];
|
|
369
|
+
const source = input.distinct === void 0 ? `${table}${where.sql}` : `(SELECT DISTINCT ${input.distinct.join(", ")} FROM ${table}${where.sql}) AS distinct_rows`;
|
|
370
|
+
const row = (await executor.query(`SELECT COUNT(*) AS count FROM ${source}`, where.params))[0];
|
|
371
|
+
if (typeof row !== "object" || row === null || Array.isArray(row)) throw new D1QueryResultError(row);
|
|
372
|
+
const count = Reflect.get(row, "count");
|
|
373
|
+
if (typeof count !== "number" || !Number.isFinite(count)) throw new D1QueryResultError(row);
|
|
374
|
+
return count;
|
|
375
|
+
};
|
|
376
|
+
const findManyD1Rows = async (executor, input) => {
|
|
377
|
+
const where = buildD1Where(input.where);
|
|
378
|
+
const orderBy = input.orderBy;
|
|
379
|
+
const order = buildD1Order(orderBy);
|
|
380
|
+
const table = d1TableNames[input.model];
|
|
381
|
+
const source = input.distinctOn === void 0 ? `SELECT * FROM ${table}${where.sql}${order}` : `SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY ${input.distinctOn.fields.join(", ")}${order}) AS __hot_updater_rank FROM ${table}${where.sql}) AS distinct_rows WHERE __hot_updater_rank = 1${order}`;
|
|
382
|
+
const pageParams = encodeD1Values([input.limit, input.offset]);
|
|
383
|
+
const rows = await executor.query(`${source} LIMIT json_extract(?, '$') OFFSET json_extract(?, '$')`, [...where.params, ...pageParams]);
|
|
384
|
+
switch (input.model) {
|
|
385
|
+
case "bundles": return rows.map((row) => parseD1Row("bundles", row));
|
|
386
|
+
case "bundle_patches": return rows.map((row) => parseD1Row("bundle_patches", row));
|
|
387
|
+
case "channels": return rows.map((row) => parseD1Row("channels", row));
|
|
388
|
+
case "bundle_events": return rows.map((row) => parseD1Row("bundle_events", row));
|
|
389
|
+
case "api_keys": return rows.map((row) => parseD1Row("api_keys", row));
|
|
390
|
+
case "releases": return rows.map((row) => parseD1Row("releases", row));
|
|
391
|
+
case "release_catalogs": return rows.map((row) => parseD1Row("release_catalogs", row));
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
//#endregion
|
|
395
|
+
//#region src/d1Implementation.ts
|
|
396
|
+
const EXPECTATION_CONFLICT_MARKER = "HOT_UPDATER_COMMIT_EXPECTATION_CONFLICT";
|
|
397
|
+
const expectationGuard = (expectations) => {
|
|
398
|
+
const clauses = [];
|
|
399
|
+
const params = [];
|
|
400
|
+
for (const expectation of expectations) {
|
|
401
|
+
const isRelease = expectation.model === "releases";
|
|
402
|
+
const table = isRelease ? "releases" : "release_catalogs";
|
|
403
|
+
const keyField = isRelease ? "id" : "scope_key";
|
|
404
|
+
const versionField = isRelease ? "revision" : "generation";
|
|
405
|
+
const key = isRelease ? expectation.id : expectation.scopeKey;
|
|
406
|
+
const version = isRelease ? expectation.revision : expectation.generation;
|
|
407
|
+
if (version === null) {
|
|
408
|
+
clauses.push(`NOT EXISTS (SELECT 1 FROM ${table} WHERE ${keyField} = json_extract(?, '$'))`);
|
|
409
|
+
params.push(...encodeD1Values([key]));
|
|
410
|
+
} else {
|
|
411
|
+
clauses.push(`EXISTS (SELECT 1 FROM ${table} WHERE ${keyField} = json_extract(?, '$') AND ${versionField} = json_extract(?, '$'))`);
|
|
412
|
+
params.push(...encodeD1Values([key, version]));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
sql: clauses.join(" AND ") || "1",
|
|
417
|
+
params
|
|
418
|
+
};
|
|
419
|
+
};
|
|
420
|
+
const readVersion = (row, field) => {
|
|
421
|
+
if (typeof row !== "object" || row === null) return null;
|
|
422
|
+
const value = Reflect.get(row, field);
|
|
423
|
+
return typeof value === "number" ? value : null;
|
|
424
|
+
};
|
|
425
|
+
const expectationConflict = async (executor, expectations) => {
|
|
426
|
+
for (const expectation of expectations) {
|
|
427
|
+
const isRelease = expectation.model === "releases";
|
|
428
|
+
const table = isRelease ? "releases" : "release_catalogs";
|
|
429
|
+
const keyField = isRelease ? "id" : "scope_key";
|
|
430
|
+
const versionField = isRelease ? "revision" : "generation";
|
|
431
|
+
const key = isRelease ? expectation.id : expectation.scopeKey;
|
|
432
|
+
const expectedVersion = isRelease ? expectation.revision : expectation.generation;
|
|
433
|
+
const actualVersion = readVersion((await executor.query(`SELECT ${versionField} FROM ${table} WHERE ${keyField} = json_extract(?, '$') LIMIT 1`, encodeD1Values([key])))[0], versionField);
|
|
434
|
+
if (actualVersion !== expectedVersion) return {
|
|
435
|
+
committed: false,
|
|
436
|
+
conflict: {
|
|
437
|
+
actualVersion,
|
|
438
|
+
changeIndex: -1,
|
|
439
|
+
expectedVersion,
|
|
440
|
+
key,
|
|
441
|
+
model: expectation.model,
|
|
442
|
+
reason: "version_conflict"
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return null;
|
|
447
|
+
};
|
|
448
|
+
const isExpectationConflictError = (error) => {
|
|
449
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
450
|
+
return message.includes(EXPECTATION_CONFLICT_MARKER) || message.toLowerCase().includes("malformed json");
|
|
451
|
+
};
|
|
452
|
+
const bundleValues = (row) => [
|
|
453
|
+
row.id,
|
|
454
|
+
row.platform,
|
|
455
|
+
row.file_hash,
|
|
456
|
+
row.git_commit_hash,
|
|
457
|
+
row.storage_uri,
|
|
458
|
+
row.archive_byte_size,
|
|
459
|
+
row.metadata,
|
|
460
|
+
row.manifest_storage_uri,
|
|
461
|
+
row.manifest_file_hash,
|
|
462
|
+
row.asset_base_storage_uri
|
|
463
|
+
];
|
|
464
|
+
const patchValues = (row) => [
|
|
465
|
+
row.id,
|
|
466
|
+
row.bundle_id,
|
|
467
|
+
row.base_bundle_id,
|
|
468
|
+
row.base_file_hash,
|
|
469
|
+
row.patch_file_hash,
|
|
470
|
+
row.patch_storage_uri,
|
|
471
|
+
row.byte_size,
|
|
472
|
+
row.order_index
|
|
473
|
+
];
|
|
474
|
+
const channelValues = (row) => [row.id, row.name];
|
|
475
|
+
const insertQuery = (input, guard, conflictMode = "returnExisting") => {
|
|
476
|
+
let columns;
|
|
477
|
+
let values;
|
|
478
|
+
switch (input.model) {
|
|
479
|
+
case "bundles":
|
|
480
|
+
columns = [
|
|
481
|
+
"id",
|
|
482
|
+
"platform",
|
|
483
|
+
"file_hash",
|
|
484
|
+
"git_commit_hash",
|
|
485
|
+
"storage_uri",
|
|
486
|
+
"archive_byte_size",
|
|
487
|
+
"metadata",
|
|
488
|
+
"manifest_storage_uri",
|
|
489
|
+
"manifest_file_hash",
|
|
490
|
+
"asset_base_storage_uri"
|
|
491
|
+
];
|
|
492
|
+
values = bundleValues(input.data);
|
|
493
|
+
break;
|
|
494
|
+
case "bundle_patches":
|
|
495
|
+
columns = [
|
|
496
|
+
"id",
|
|
497
|
+
"bundle_id",
|
|
498
|
+
"base_bundle_id",
|
|
499
|
+
"base_file_hash",
|
|
500
|
+
"patch_file_hash",
|
|
501
|
+
"patch_storage_uri",
|
|
502
|
+
"byte_size",
|
|
503
|
+
"order_index"
|
|
504
|
+
];
|
|
505
|
+
values = patchValues(input.data);
|
|
506
|
+
break;
|
|
507
|
+
case "channels":
|
|
508
|
+
columns = ["id", "name"];
|
|
509
|
+
values = channelValues(input.data);
|
|
510
|
+
break;
|
|
511
|
+
case "bundle_events":
|
|
512
|
+
case "api_keys":
|
|
513
|
+
case "release_catalogs":
|
|
514
|
+
case "releases":
|
|
515
|
+
columns = Object.keys(input.data);
|
|
516
|
+
values = Object.values(input.data);
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
const conflict = input.onConflict !== "ignore" ? "" : input.model === "channels" ? conflictMode === "ignore" ? " ON CONFLICT(name) DO NOTHING" : " ON CONFLICT(name) DO UPDATE SET name = excluded.name" : input.model === "api_keys" ? conflictMode === "ignore" ? " ON CONFLICT(hash) DO NOTHING" : " ON CONFLICT(hash) DO UPDATE SET hash = excluded.hash" : "";
|
|
520
|
+
return {
|
|
521
|
+
sql: `INSERT INTO ${d1TableNames[input.model]} (${columns.join(", ")}) ${guard === void 0 ? `VALUES (${d1Placeholders(values.length)})` : `SELECT ${d1Placeholders(values.length)} WHERE ${guard.sql}`}${conflict} RETURNING *`,
|
|
522
|
+
params: [...encodeD1Values(values), ...guard?.params ?? []]
|
|
523
|
+
};
|
|
524
|
+
};
|
|
525
|
+
const updateEntries = (update) => Object.entries(update);
|
|
526
|
+
const updateQuery = (input, guard) => {
|
|
527
|
+
const entries = updateEntries(input.update);
|
|
528
|
+
const where = buildD1Where(input.where);
|
|
529
|
+
if (entries.length === 0) return {
|
|
530
|
+
sql: `SELECT * FROM ${d1TableNames[input.model]}${where.sql}${guard ? ` AND ${guard.sql}` : ""} LIMIT 1`,
|
|
531
|
+
params: [...where.params, ...guard?.params ?? []]
|
|
532
|
+
};
|
|
533
|
+
const assignments = entries.map(([field]) => `${field} = json_extract(?, '$')`).join(", ");
|
|
534
|
+
return {
|
|
535
|
+
sql: `UPDATE ${d1TableNames[input.model]} SET ${assignments}${where.sql}${guard ? ` AND ${guard.sql}` : ""} RETURNING *`,
|
|
536
|
+
params: [
|
|
537
|
+
...encodeD1Values(entries.map(([, value]) => value)),
|
|
538
|
+
...where.params,
|
|
539
|
+
...guard?.params ?? []
|
|
540
|
+
]
|
|
541
|
+
};
|
|
542
|
+
};
|
|
543
|
+
const deleteQuery = (input, guard) => {
|
|
544
|
+
const where = buildD1Where(input.where);
|
|
545
|
+
return {
|
|
546
|
+
sql: `DELETE FROM ${d1TableNames[input.model]}${where.sql}${guard ? ` AND ${guard.sql}` : ""}`,
|
|
547
|
+
params: [...where.params, ...guard?.params ?? []]
|
|
548
|
+
};
|
|
549
|
+
};
|
|
550
|
+
const insertedKey = (change) => {
|
|
551
|
+
if (change.operation !== "insert") return void 0;
|
|
552
|
+
if (change.model === "bundles") return `bundles:${change.row.id}`;
|
|
553
|
+
if (change.model === "apiKeys") return `api_keys:${change.row.id}`;
|
|
554
|
+
if (change.model === "releases") return `releases:${change.row.id}`;
|
|
555
|
+
};
|
|
556
|
+
const requiredRow = (change, changeIndex) => {
|
|
557
|
+
if (change.operation !== "update") return void 0;
|
|
558
|
+
if (change.model === "bundles") return {
|
|
559
|
+
model: "bundles",
|
|
560
|
+
id: change.where.id,
|
|
561
|
+
changeIndex
|
|
562
|
+
};
|
|
563
|
+
if (change.model === "apiKeys") return {
|
|
564
|
+
model: "api_keys",
|
|
565
|
+
id: change.where.id,
|
|
566
|
+
changeIndex
|
|
567
|
+
};
|
|
568
|
+
if (change.model === "releases") return {
|
|
569
|
+
model: "releases",
|
|
570
|
+
id: change.where.id,
|
|
571
|
+
changeIndex
|
|
572
|
+
};
|
|
573
|
+
};
|
|
574
|
+
const channelDeletePrecondition = (changes, changeIndex, channelId) => {
|
|
575
|
+
const releaseEffects = /* @__PURE__ */ new Map();
|
|
576
|
+
for (const change of changes.slice(0, changeIndex)) {
|
|
577
|
+
if (change.model !== "releases") continue;
|
|
578
|
+
switch (change.operation) {
|
|
579
|
+
case "insert":
|
|
580
|
+
releaseEffects.set(change.row.id, change.row.channel_id);
|
|
581
|
+
break;
|
|
582
|
+
case "update": break;
|
|
583
|
+
case "delete":
|
|
584
|
+
releaseEffects.set(change.where.id, null);
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
id: channelId,
|
|
590
|
+
excludedReleaseIds: [...releaseEffects.keys()],
|
|
591
|
+
addedReferenceReleaseIds: [...releaseEffects].filter(([, finalChannelId]) => finalChannelId === channelId).map(([releaseId]) => releaseId)
|
|
592
|
+
};
|
|
593
|
+
};
|
|
594
|
+
const changeQuery = (change, guard) => {
|
|
595
|
+
switch (change.model) {
|
|
596
|
+
case "bundles": switch (change.operation) {
|
|
597
|
+
case "insert": return insertQuery({
|
|
598
|
+
model: "bundles",
|
|
599
|
+
data: change.row
|
|
600
|
+
}, guard);
|
|
601
|
+
case "update": return updateQuery({
|
|
602
|
+
model: "bundles",
|
|
603
|
+
where: [{
|
|
604
|
+
field: "id",
|
|
605
|
+
value: change.where.id
|
|
606
|
+
}],
|
|
607
|
+
update: change.update
|
|
608
|
+
}, guard);
|
|
609
|
+
case "delete": return deleteQuery({
|
|
610
|
+
model: "bundles",
|
|
611
|
+
where: [{
|
|
612
|
+
field: "id",
|
|
613
|
+
value: change.where.id
|
|
614
|
+
}]
|
|
615
|
+
}, guard);
|
|
616
|
+
}
|
|
617
|
+
case "bundlePatches": return change.operation === "insert" ? insertQuery({
|
|
618
|
+
model: "bundle_patches",
|
|
619
|
+
data: change.row
|
|
620
|
+
}, guard) : deleteQuery({
|
|
621
|
+
model: "bundle_patches",
|
|
622
|
+
where: [{
|
|
623
|
+
field: "bundle_id",
|
|
624
|
+
value: change.where.bundleId
|
|
625
|
+
}]
|
|
626
|
+
}, guard);
|
|
627
|
+
case "releases": switch (change.operation) {
|
|
628
|
+
case "insert": return insertQuery({
|
|
629
|
+
model: "releases",
|
|
630
|
+
data: change.row
|
|
631
|
+
}, guard);
|
|
632
|
+
case "update": return updateQuery({
|
|
633
|
+
model: "releases",
|
|
634
|
+
where: [{
|
|
635
|
+
field: "id",
|
|
636
|
+
value: change.where.id
|
|
637
|
+
}],
|
|
638
|
+
update: change.update
|
|
639
|
+
}, guard);
|
|
640
|
+
case "delete": return deleteQuery({
|
|
641
|
+
model: "releases",
|
|
642
|
+
where: [{
|
|
643
|
+
field: "id",
|
|
644
|
+
value: change.where.id
|
|
645
|
+
}]
|
|
646
|
+
}, guard);
|
|
647
|
+
}
|
|
648
|
+
case "releaseCatalogs": return {
|
|
649
|
+
sql: `INSERT INTO release_catalogs (${Object.keys(change.row).join(", ")}) SELECT ${d1Placeholders(Object.keys(change.row).length)} WHERE ${guard.sql} ON CONFLICT(scope_key) DO UPDATE SET ${Object.keys(change.row).filter((field) => field !== "scope_key").map((field) => `${field} = excluded.${field}`).join(", ")} RETURNING *`,
|
|
650
|
+
params: [...encodeD1Values(Object.values(change.row)), ...guard.params]
|
|
651
|
+
};
|
|
652
|
+
case "channels": return change.operation === "insert" ? insertQuery({
|
|
653
|
+
model: "channels",
|
|
654
|
+
data: change.row,
|
|
655
|
+
onConflict: change.onConflict
|
|
656
|
+
}, guard, "ignore") : {
|
|
657
|
+
sql: `DELETE FROM channels WHERE id = json_extract(?, '$') AND NOT EXISTS (SELECT 1 FROM releases WHERE channel_id = json_extract(?, '$')) AND ${guard.sql}`,
|
|
658
|
+
params: [...encodeD1Values([change.where.id, change.where.id]), ...guard.params]
|
|
659
|
+
};
|
|
660
|
+
case "analytics": return insertQuery({
|
|
661
|
+
model: "bundle_events",
|
|
662
|
+
data: change.row
|
|
663
|
+
}, guard);
|
|
664
|
+
case "apiKeys": return change.operation === "insert" ? insertQuery({
|
|
665
|
+
model: "api_keys",
|
|
666
|
+
data: change.row,
|
|
667
|
+
onConflict: change.onConflict
|
|
668
|
+
}, guard, "ignore") : updateQuery({
|
|
669
|
+
model: "api_keys",
|
|
670
|
+
where: [{
|
|
671
|
+
field: "id",
|
|
672
|
+
value: change.where.id
|
|
673
|
+
}],
|
|
674
|
+
update: { revoked_at_ms: change.update.revokedAtMs }
|
|
675
|
+
}, guard);
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
const createCommitPlan = (input) => {
|
|
679
|
+
const checks = [];
|
|
680
|
+
const statements = [];
|
|
681
|
+
const expectations = input.expectations ?? [];
|
|
682
|
+
if (expectations.length > 0) {
|
|
683
|
+
const guard = expectationGuard(expectations);
|
|
684
|
+
statements.push({
|
|
685
|
+
sql: `SELECT CASE WHEN ${guard.sql} THEN 1 ELSE json_extract('${EXPECTATION_CONFLICT_MARKER}', '$') END AS expectation_guard`,
|
|
686
|
+
params: guard.params
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
const requiredRows = [];
|
|
690
|
+
const channelDeletes = [];
|
|
691
|
+
const inserted = /* @__PURE__ */ new Set();
|
|
692
|
+
for (const [changeIndex, change] of input.changes.entries()) {
|
|
693
|
+
const required = requiredRow(change, changeIndex);
|
|
694
|
+
if (required !== void 0) {
|
|
695
|
+
if (!inserted.has(`${required.model}:${required.id}`)) {
|
|
696
|
+
requiredRows.push(required);
|
|
697
|
+
checks.push({
|
|
698
|
+
changeIndex,
|
|
699
|
+
resultIndex: statements.length,
|
|
700
|
+
conflictWhen: "empty",
|
|
701
|
+
reason: "not_found"
|
|
702
|
+
});
|
|
703
|
+
statements.push({
|
|
704
|
+
sql: `SELECT id FROM ${d1TableNames[required.model]} WHERE id = json_extract(?, '$') LIMIT 1`,
|
|
705
|
+
params: encodeD1Values([required.id])
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (change.model === "channels" && change.operation === "delete") {
|
|
710
|
+
const precondition = channelDeletePrecondition(input.changes, changeIndex, change.where.id);
|
|
711
|
+
channelDeletes.push(precondition);
|
|
712
|
+
checks.push({
|
|
713
|
+
changeIndex,
|
|
714
|
+
resultIndex: statements.length,
|
|
715
|
+
conflictWhen: "nonempty",
|
|
716
|
+
reason: "referenced"
|
|
717
|
+
});
|
|
718
|
+
statements.push({
|
|
719
|
+
sql: "SELECT id FROM releases WHERE channel_id = json_extract(?, '$') AND id NOT IN (SELECT value FROM json_each(?)) UNION ALL SELECT value AS id FROM json_each(?) LIMIT 1",
|
|
720
|
+
params: encodeD1Values([
|
|
721
|
+
precondition.id,
|
|
722
|
+
precondition.excludedReleaseIds,
|
|
723
|
+
precondition.addedReferenceReleaseIds
|
|
724
|
+
])
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
const key = insertedKey(change);
|
|
728
|
+
if (key !== void 0) inserted.add(key);
|
|
729
|
+
}
|
|
730
|
+
const guard = {
|
|
731
|
+
sql: `NOT EXISTS (
|
|
732
|
+
SELECT 1 FROM json_each(?) AS required
|
|
733
|
+
WHERE NOT EXISTS (
|
|
734
|
+
SELECT 1 FROM bundles
|
|
735
|
+
WHERE json_extract(required.value, '$.model') = 'bundles'
|
|
736
|
+
AND bundles.id = json_extract(required.value, '$.id')
|
|
737
|
+
UNION ALL
|
|
738
|
+
SELECT 1 FROM api_keys
|
|
739
|
+
WHERE json_extract(required.value, '$.model') = 'api_keys'
|
|
740
|
+
AND api_keys.id = json_extract(required.value, '$.id')
|
|
741
|
+
UNION ALL
|
|
742
|
+
SELECT 1 FROM channels
|
|
743
|
+
WHERE json_extract(required.value, '$.model') = 'channels'
|
|
744
|
+
AND channels.id = json_extract(required.value, '$.id')
|
|
745
|
+
UNION ALL
|
|
746
|
+
SELECT 1 FROM releases
|
|
747
|
+
WHERE json_extract(required.value, '$.model') = 'releases'
|
|
748
|
+
AND releases.id = json_extract(required.value, '$.id')
|
|
749
|
+
)
|
|
750
|
+
) AND NOT EXISTS (
|
|
751
|
+
SELECT 1 FROM json_each(?) AS channel_delete
|
|
752
|
+
WHERE EXISTS (
|
|
753
|
+
SELECT 1 FROM releases
|
|
754
|
+
WHERE releases.channel_id = json_extract(channel_delete.value, '$.id')
|
|
755
|
+
AND releases.id NOT IN (
|
|
756
|
+
SELECT value FROM json_each(
|
|
757
|
+
json_extract(channel_delete.value, '$.excludedReleaseIds')
|
|
758
|
+
)
|
|
759
|
+
)
|
|
760
|
+
UNION ALL
|
|
761
|
+
SELECT value FROM json_each(
|
|
762
|
+
json_extract(channel_delete.value, '$.addedReferenceReleaseIds')
|
|
763
|
+
)
|
|
764
|
+
)
|
|
765
|
+
)`,
|
|
766
|
+
params: encodeD1Values([requiredRows.map(({ model, id }) => ({
|
|
767
|
+
model,
|
|
768
|
+
id
|
|
769
|
+
})), channelDeletes])
|
|
770
|
+
};
|
|
771
|
+
statements.push(...input.changes.map((change) => changeQuery(change, guard)));
|
|
772
|
+
return {
|
|
773
|
+
checks,
|
|
774
|
+
statements
|
|
775
|
+
};
|
|
776
|
+
};
|
|
777
|
+
const resultForPlan = (plan, results) => {
|
|
778
|
+
const missing = plan.checks.find(({ conflictWhen, resultIndex }) => conflictWhen === "empty" ? (results[resultIndex]?.length ?? 0) === 0 : (results[resultIndex]?.length ?? 0) > 0);
|
|
779
|
+
return missing === void 0 ? { committed: true } : {
|
|
780
|
+
committed: false,
|
|
781
|
+
conflict: {
|
|
782
|
+
changeIndex: missing.changeIndex,
|
|
783
|
+
reason: missing.reason
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
};
|
|
787
|
+
const insertChannel = async (executor, input) => {
|
|
788
|
+
const insert = insertQuery({
|
|
789
|
+
model: "channels",
|
|
790
|
+
data: input.row,
|
|
791
|
+
onConflict: "ignore"
|
|
792
|
+
}, void 0, "ignore");
|
|
793
|
+
const select = {
|
|
794
|
+
sql: "SELECT id, name FROM channels WHERE name = json_extract(?, '$') LIMIT 1",
|
|
795
|
+
params: encodeD1Values([input.row.name])
|
|
796
|
+
};
|
|
797
|
+
const [insertedRows = [], canonicalRows = []] = await executor.batch([insert, select]);
|
|
798
|
+
return {
|
|
799
|
+
row: parseD1Row("channels", canonicalRows[0]),
|
|
800
|
+
inserted: insertedRows.length > 0
|
|
801
|
+
};
|
|
802
|
+
};
|
|
803
|
+
const deleteChannel = async (executor, input) => {
|
|
804
|
+
const [channelRows = [], referenceRows = [], deletedRows = []] = await executor.batch([
|
|
805
|
+
{
|
|
806
|
+
sql: "SELECT id FROM channels WHERE id = json_extract(?, '$') LIMIT 1",
|
|
807
|
+
params: encodeD1Values([input.id])
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
sql: "SELECT id FROM releases WHERE channel_id = json_extract(?, '$') LIMIT 1",
|
|
811
|
+
params: encodeD1Values([input.id])
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
sql: "DELETE FROM channels WHERE id = json_extract(?, '$') AND NOT EXISTS (SELECT 1 FROM releases WHERE channel_id = json_extract(?, '$')) RETURNING id",
|
|
815
|
+
params: encodeD1Values([input.id, input.id])
|
|
816
|
+
}
|
|
817
|
+
]);
|
|
818
|
+
if (channelRows.length === 0) return {
|
|
819
|
+
deleted: false,
|
|
820
|
+
reason: "not_found"
|
|
821
|
+
};
|
|
822
|
+
if (referenceRows.length > 0) return {
|
|
823
|
+
deleted: false,
|
|
824
|
+
reason: "not_empty"
|
|
825
|
+
};
|
|
826
|
+
return deletedRows.length > 0 ? { deleted: true } : {
|
|
827
|
+
deleted: false,
|
|
828
|
+
reason: "not_empty"
|
|
829
|
+
};
|
|
830
|
+
};
|
|
831
|
+
const createD1Implementation = (executor) => ({
|
|
832
|
+
async create(input) {
|
|
833
|
+
const query = insertQuery(input);
|
|
834
|
+
const rows = await executor.query(query.sql, query.params);
|
|
835
|
+
switch (input.model) {
|
|
836
|
+
case "bundles": return parseD1Row("bundles", rows[0]);
|
|
837
|
+
case "bundle_patches": return parseD1Row("bundle_patches", rows[0]);
|
|
838
|
+
case "channels": return parseD1Row("channels", rows[0]);
|
|
839
|
+
case "bundle_events": return parseD1Row("bundle_events", rows[0]);
|
|
840
|
+
case "api_keys": return parseD1Row("api_keys", rows[0]);
|
|
841
|
+
case "releases": return parseD1Row("releases", rows[0]);
|
|
842
|
+
case "release_catalogs": return parseD1Row("release_catalogs", rows[0]);
|
|
843
|
+
}
|
|
844
|
+
},
|
|
845
|
+
async update(input) {
|
|
846
|
+
const query = updateQuery(input);
|
|
847
|
+
const rows = await executor.query(query.sql, query.params);
|
|
848
|
+
if (rows[0] === void 0) return null;
|
|
849
|
+
switch (input.model) {
|
|
850
|
+
case "bundles": return parseD1Row("bundles", rows[0]);
|
|
851
|
+
case "api_keys": return parseD1Row("api_keys", rows[0]);
|
|
852
|
+
case "releases": return parseD1Row("releases", rows[0]);
|
|
853
|
+
case "release_catalogs": return parseD1Row("release_catalogs", rows[0]);
|
|
854
|
+
}
|
|
855
|
+
},
|
|
856
|
+
async delete(input) {
|
|
857
|
+
const query = deleteQuery(input);
|
|
858
|
+
await executor.query(query.sql, query.params);
|
|
859
|
+
},
|
|
860
|
+
count: (input) => countD1Rows(executor, input),
|
|
861
|
+
async findOne(input) {
|
|
862
|
+
const where = buildD1Where(input.where);
|
|
863
|
+
const rows = await executor.query(`SELECT * FROM ${d1TableNames[input.model]}${where.sql} LIMIT 1`, where.params);
|
|
864
|
+
if (rows[0] === void 0) return null;
|
|
865
|
+
switch (input.model) {
|
|
866
|
+
case "bundles": return parseD1Row("bundles", rows[0]);
|
|
867
|
+
case "bundle_patches": return parseD1Row("bundle_patches", rows[0]);
|
|
868
|
+
case "channels": return parseD1Row("channels", rows[0]);
|
|
869
|
+
case "api_keys": return parseD1Row("api_keys", rows[0]);
|
|
870
|
+
case "releases": return parseD1Row("releases", rows[0]);
|
|
871
|
+
case "release_catalogs": return parseD1Row("release_catalogs", rows[0]);
|
|
872
|
+
}
|
|
873
|
+
},
|
|
874
|
+
findMany: (input) => findManyD1Rows(executor, input),
|
|
875
|
+
insertChannel: (input) => insertChannel(executor, input),
|
|
876
|
+
deleteChannel: (input) => deleteChannel(executor, input),
|
|
877
|
+
async commit(input) {
|
|
878
|
+
if (input.changes.length === 0) return { committed: true };
|
|
879
|
+
const expectations = input.expectations ?? [];
|
|
880
|
+
const conflict = await expectationConflict(executor, expectations);
|
|
881
|
+
if (conflict !== null) return conflict;
|
|
882
|
+
const plan = createCommitPlan(input);
|
|
883
|
+
try {
|
|
884
|
+
return resultForPlan(plan, await executor.batch(plan.statements));
|
|
885
|
+
} catch (error) {
|
|
886
|
+
if (expectations.length === 0 || !isExpectationConflictError(error)) throw error;
|
|
887
|
+
return await expectationConflict(executor, expectations) ?? {
|
|
888
|
+
committed: false,
|
|
889
|
+
conflict: {
|
|
890
|
+
actualVersion: expectations[0].model === "releases" ? expectations[0].revision : expectations[0].generation,
|
|
891
|
+
changeIndex: -1,
|
|
892
|
+
expectedVersion: expectations[0].model === "releases" ? expectations[0].revision : expectations[0].generation,
|
|
893
|
+
key: expectations[0].model === "releases" ? expectations[0].id : expectations[0].scopeKey,
|
|
894
|
+
model: expectations[0].model,
|
|
895
|
+
reason: "version_conflict"
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
//#endregion
|
|
902
|
+
//#region src/d1Database.ts
|
|
903
|
+
const d1Database = (config) => {
|
|
904
|
+
const cloudflare = new Cloudflare({ apiToken: config.cloudflareApiToken });
|
|
905
|
+
const execute = async (statements) => {
|
|
906
|
+
const first = statements[0];
|
|
907
|
+
if (first === void 0) return [];
|
|
908
|
+
const body = statements.length === 1 ? {
|
|
909
|
+
account_id: config.accountId,
|
|
910
|
+
sql: first.sql,
|
|
911
|
+
params: [...first.params]
|
|
912
|
+
} : {
|
|
913
|
+
account_id: config.accountId,
|
|
914
|
+
batch: statements.map(({ sql, params }) => ({
|
|
915
|
+
sql,
|
|
916
|
+
params: [...params]
|
|
917
|
+
}))
|
|
918
|
+
};
|
|
919
|
+
const page = await cloudflare.d1.database.query(config.databaseId, { ...body });
|
|
920
|
+
const results = [];
|
|
921
|
+
for await (const resultPage of page.iterPages()) for (const result of resultPage.result) results.push(result.results ?? []);
|
|
922
|
+
return results;
|
|
923
|
+
};
|
|
924
|
+
const adapter = createDatabasePluginAdapter("d1Database", createD1Implementation({
|
|
925
|
+
async query(sql, params) {
|
|
926
|
+
return (await execute([{
|
|
927
|
+
sql,
|
|
928
|
+
params
|
|
929
|
+
}])).flat();
|
|
930
|
+
},
|
|
931
|
+
batch: execute
|
|
932
|
+
}));
|
|
933
|
+
return createDatabasePlugin({
|
|
934
|
+
name: "d1Database",
|
|
935
|
+
models: adapter.models,
|
|
936
|
+
commit: adapter.commit,
|
|
937
|
+
...adapter.dispose ? { dispose: adapter.dispose } : {}
|
|
938
|
+
});
|
|
939
|
+
};
|
|
940
|
+
//#endregion
|
|
49
941
|
//#region ../../node_modules/.pnpm/is-plain-obj@4.1.0/node_modules/is-plain-obj/index.js
|
|
50
942
|
function isPlainObject(value) {
|
|
51
943
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -6227,7 +7119,7 @@ const CLOUDFLARE_INIT_PERMISSIONS = [CLOUDFLARE_INIT_PERMISSION.d1];
|
|
|
6227
7119
|
const getCredentialRemediation = (source) => {
|
|
6228
7120
|
switch (source.kind) {
|
|
6229
7121
|
case "environment": return ["Update HOT_UPDATER_CLOUDFLARE_API_TOKEN in the environment, then rerun init."];
|
|
6230
|
-
case "env-file": return [`Update HOT_UPDATER_CLOUDFLARE_API_TOKEN in ${source.envFile}, then rerun init.`, "Alternatively, rerun without --env-file to enter a new token."];
|
|
7122
|
+
case "env-file": return [`Update HOT_UPDATER_CLOUDFLARE_API_TOKEN in ${source.envFile}, then rerun init.`, "Alternatively, rerun without --from-env-file to enter a new token."];
|
|
6231
7123
|
case "prompt": return ["Create a new API token, then rerun init and enter it again."];
|
|
6232
7124
|
case "wrangler-oauth": return ["Refresh Wrangler authentication with `npx wrangler login`, then rerun init."];
|
|
6233
7125
|
}
|
|
@@ -11026,6 +11918,26 @@ const resolveCloudflareInfrastructureApiToken = async ({ cwd, nonInteractive })
|
|
|
11026
11918
|
return authenticatedToken;
|
|
11027
11919
|
};
|
|
11028
11920
|
//#endregion
|
|
11921
|
+
//#region iac/cloudflareInfrastructureState.ts
|
|
11922
|
+
const resolveCloudflareInfrastructureState = (tableNames) => {
|
|
11923
|
+
if (tableNames.includes("release_catalogs")) return "v1";
|
|
11924
|
+
if (tableNames.includes("bundles")) return "v0";
|
|
11925
|
+
return "fresh";
|
|
11926
|
+
};
|
|
11927
|
+
const assertCloudflareInfrastructureCanInitialize = (tableNames, databaseName) => {
|
|
11928
|
+
if (resolveCloudflareInfrastructureState(tableNames) === "v0") throw new LegacyInfrastructureError("Cloudflare", `D1 database ${databaseName}`);
|
|
11929
|
+
};
|
|
11930
|
+
const assertCloudflareWorkerCanInitialize = async ({ fetchImpl, scriptNames, workerName, workersSubdomain }) => {
|
|
11931
|
+
if (!scriptNames.includes(workerName)) return;
|
|
11932
|
+
await assertInfrastructureGenerationAtUrl({
|
|
11933
|
+
fetchImpl,
|
|
11934
|
+
legacyStatuses: [400, 404],
|
|
11935
|
+
provider: "Cloudflare",
|
|
11936
|
+
resource: `Worker ${workerName}`,
|
|
11937
|
+
versionUrl: `https://${workerName}.${workersSubdomain}.workers.dev/version`
|
|
11938
|
+
});
|
|
11939
|
+
};
|
|
11940
|
+
//#endregion
|
|
11029
11941
|
//#region iac/init/index.ts
|
|
11030
11942
|
const initProvider = {
|
|
11031
11943
|
label: "Cloudflare D1 + R2 + Worker",
|
|
@@ -11229,10 +12141,16 @@ function App() {
|
|
|
11229
12141
|
return null; // Replace with your app root
|
|
11230
12142
|
}
|
|
11231
12143
|
|
|
11232
|
-
|
|
12144
|
+
HotUpdater.init({
|
|
11233
12145
|
baseURL: "%%source%%",
|
|
11234
|
-
|
|
11235
|
-
|
|
12146
|
+
requestHeaders: {
|
|
12147
|
+
"x-api-key": %%apiKey%%,
|
|
12148
|
+
},
|
|
12149
|
+
});
|
|
12150
|
+
|
|
12151
|
+
// Call HotUpdater.checkForUpdate({ updateStrategy: "appVersion" })
|
|
12152
|
+
// when your app is ready to check.
|
|
12153
|
+
export default App;`;
|
|
11236
12154
|
const deployWorker = async (apiToken, accountId, { credentialSource, d1DatabaseId, d1DatabaseName, nonInteractive, r2BucketName, workerName }) => {
|
|
11237
12155
|
const cwd = getCwd();
|
|
11238
12156
|
const cloudflarePackagePath = __require.resolve("@hot-updater/cloudflare/package.json", { paths: [cwd] });
|
|
@@ -11249,7 +12167,7 @@ const deployWorker = async (apiToken, accountId, { credentialSource, d1DatabaseI
|
|
|
11249
12167
|
binding: "BUCKET",
|
|
11250
12168
|
bucket_name: r2BucketName
|
|
11251
12169
|
}];
|
|
11252
|
-
wranglerConfig.vars = {
|
|
12170
|
+
wranglerConfig.vars = { BUCKET_NAME: r2BucketName };
|
|
11253
12171
|
await fs.writeFile(path.join(workerRoot, "wrangler.json"), JSON.stringify(wranglerConfig, null, 2));
|
|
11254
12172
|
const wrangler = await createWrangler({
|
|
11255
12173
|
stdio: "inherit",
|
|
@@ -11267,6 +12185,16 @@ const deployWorker = async (apiToken, accountId, { credentialSource, d1DatabaseI
|
|
|
11267
12185
|
}
|
|
11268
12186
|
await wrangler("d1", "migrations", "apply", d1DatabaseName, "--remote");
|
|
11269
12187
|
await wrangler("deploy", "--name", workerName);
|
|
12188
|
+
const secretCommand = createWrangler({
|
|
12189
|
+
stdio: "pipe",
|
|
12190
|
+
cloudflareApiToken: apiToken,
|
|
12191
|
+
cwd: workerRoot,
|
|
12192
|
+
accountId,
|
|
12193
|
+
nonInteractive
|
|
12194
|
+
})("secret", "put", "STORAGE_DOWNLOAD_URL_SIGNING_KEY", "--name", workerName);
|
|
12195
|
+
if (!secretCommand.stdin) throw new Error("Failed to open Wrangler secret input.");
|
|
12196
|
+
secretCommand.stdin.end(crypto.randomBytes(32).toString("hex"));
|
|
12197
|
+
await secretCommand;
|
|
11270
12198
|
return workerName;
|
|
11271
12199
|
} catch (error) {
|
|
11272
12200
|
if (error instanceof Error) throw toCloudflareDeploymentError(error, credentialSource);
|
|
@@ -11280,7 +12208,8 @@ const runInit = async ({ build, envFile }) => {
|
|
|
11280
12208
|
const nonInteractive = envFile !== void 0;
|
|
11281
12209
|
const initEnvSources = await readHotUpdaterInitEnv(cwd, envFile);
|
|
11282
12210
|
const { managedEnv } = initEnvSources;
|
|
11283
|
-
const
|
|
12211
|
+
const initInputEnv = getHotUpdaterInitInputEnv(initEnvSources, nonInteractive);
|
|
12212
|
+
const existingInputs = resolveCloudflareInitInputs(initInputEnv);
|
|
11284
12213
|
assertCloudflareNonInteractiveInputs(existingInputs, nonInteractive);
|
|
11285
12214
|
const { accessKeyId: existingR2AccessKeyId, accountId: existingAccountId, apiToken: existingApiToken, bucketName: existingBucketName, d1DatabaseId: existingD1DatabaseId, d1DatabaseName: existingD1DatabaseName, r2Private: savedPrivateSetting, secretAccessKey: existingR2SecretAccessKey, workerName: existingWorkerName } = existingInputs;
|
|
11286
12215
|
const infrastructureCredentialSource = { kind: "wrangler-oauth" };
|
|
@@ -11288,7 +12217,7 @@ const runInit = async ({ build, envFile }) => {
|
|
|
11288
12217
|
cwd,
|
|
11289
12218
|
nonInteractive
|
|
11290
12219
|
});
|
|
11291
|
-
const cf = new Cloudflare({ apiToken: infrastructureApiToken });
|
|
12220
|
+
const cf = new Cloudflare$1({ apiToken: infrastructureApiToken });
|
|
11292
12221
|
const createKey = `create/${Math.random().toString(36).substring(2, 15)}`;
|
|
11293
12222
|
let accountId;
|
|
11294
12223
|
if (nonInteractive && existingAccountId) {
|
|
@@ -11469,7 +12398,7 @@ const runInit = async ({ build, envFile }) => {
|
|
|
11469
12398
|
envFile,
|
|
11470
12399
|
kind: "env-file"
|
|
11471
12400
|
} : { kind: "environment" } : { kind: "prompt" };
|
|
11472
|
-
const credentialClient = new Cloudflare({ apiToken });
|
|
12401
|
+
const credentialClient = new Cloudflare$1({ apiToken });
|
|
11473
12402
|
await validateCloudflareApiToken({
|
|
11474
12403
|
accountId,
|
|
11475
12404
|
probes: [{
|
|
@@ -11485,6 +12414,37 @@ const runInit = async ({ build, envFile }) => {
|
|
|
11485
12414
|
verifyUserToken: () => credentialClient.user.tokens.verify()
|
|
11486
12415
|
})
|
|
11487
12416
|
});
|
|
12417
|
+
const queryD1 = async (sql) => {
|
|
12418
|
+
if (!selectedD1DatabaseId) return [];
|
|
12419
|
+
const page = await cf.d1.database.query(selectedD1DatabaseId, {
|
|
12420
|
+
account_id: accountId,
|
|
12421
|
+
sql
|
|
12422
|
+
});
|
|
12423
|
+
const rows = [];
|
|
12424
|
+
for await (const resultPage of page.iterPages()) for (const result of resultPage.result) rows.push(...result.results ?? []);
|
|
12425
|
+
return rows;
|
|
12426
|
+
};
|
|
12427
|
+
if (!createD1Database) assertCloudflareInfrastructureCanInitialize((await queryD1(`
|
|
12428
|
+
SELECT name
|
|
12429
|
+
FROM sqlite_schema
|
|
12430
|
+
WHERE type = 'table'
|
|
12431
|
+
AND name IN ('bundles', 'release_catalogs')
|
|
12432
|
+
ORDER BY name
|
|
12433
|
+
`)).flatMap(({ name }) => typeof name === "string" ? [name] : []), d1DatabaseName);
|
|
12434
|
+
const workerScripts = await runCloudflareApiRequest({
|
|
12435
|
+
request: () => cf.workers.scripts.list({ account_id: accountId }),
|
|
12436
|
+
source: infrastructureCredentialSource
|
|
12437
|
+
});
|
|
12438
|
+
const subdomains = await runCloudflareApiRequest({
|
|
12439
|
+
request: () => cf.workers.subdomains.get({ account_id: accountId }),
|
|
12440
|
+
source: infrastructureCredentialSource
|
|
12441
|
+
});
|
|
12442
|
+
if (!subdomains.subdomain) throw new Error("Cloudflare Workers subdomain is required to configure the storage download URL.");
|
|
12443
|
+
await assertCloudflareWorkerCanInitialize({
|
|
12444
|
+
scriptNames: workerScripts.result.flatMap(({ id }) => typeof id === "string" ? [id] : []),
|
|
12445
|
+
workerName,
|
|
12446
|
+
workersSubdomain: subdomains.subdomain
|
|
12447
|
+
});
|
|
11488
12448
|
const resolvedInputs = {
|
|
11489
12449
|
...existingInputs,
|
|
11490
12450
|
accessKeyId,
|
|
@@ -11556,10 +12516,6 @@ const runInit = async ({ build, envFile }) => {
|
|
|
11556
12516
|
[initProvider.inputs.d1DatabaseId.envKey]: selectedD1DatabaseId,
|
|
11557
12517
|
[initProvider.inputs.d1DatabaseName.envKey]: d1DatabaseName
|
|
11558
12518
|
});
|
|
11559
|
-
const subdomains = await runCloudflareApiRequest({
|
|
11560
|
-
request: () => cf.workers.subdomains.get({ account_id: accountId }),
|
|
11561
|
-
source: infrastructureCredentialSource
|
|
11562
|
-
});
|
|
11563
12519
|
await deployWorker(infrastructureApiToken, accountId, {
|
|
11564
12520
|
credentialSource: infrastructureCredentialSource,
|
|
11565
12521
|
d1DatabaseId: selectedD1DatabaseId,
|
|
@@ -11568,12 +12524,33 @@ const runInit = async ({ build, envFile }) => {
|
|
|
11568
12524
|
r2BucketName: selectedBucketName,
|
|
11569
12525
|
workerName
|
|
11570
12526
|
});
|
|
12527
|
+
const databasePlugin = d1Database({
|
|
12528
|
+
accountId,
|
|
12529
|
+
cloudflareApiToken: apiToken,
|
|
12530
|
+
databaseId: selectedD1DatabaseId
|
|
12531
|
+
});
|
|
12532
|
+
let apiKey;
|
|
12533
|
+
try {
|
|
12534
|
+
apiKey = (await provisionApiKey({
|
|
12535
|
+
apiKeys: databasePlugin.models.apiKeys,
|
|
12536
|
+
existingApiKey: initInputEnv.HOT_UPDATER_API_KEY,
|
|
12537
|
+
name: "Cloudflare init"
|
|
12538
|
+
})).apiKey;
|
|
12539
|
+
await makeEnv({ HOT_UPDATER_API_KEY: apiKey });
|
|
12540
|
+
} finally {
|
|
12541
|
+
await databasePlugin.dispose?.();
|
|
12542
|
+
}
|
|
11571
12543
|
const configWriteResult = await writeHotUpdaterConfig(getConfigScaffold(build));
|
|
11572
12544
|
p.log.success("Generated '.env.hotupdater' file with Cloudflare settings.");
|
|
11573
12545
|
if (configWriteResult.status === "created") p.log.success("Generated 'hot-updater.config.ts' file with Cloudflare settings.");
|
|
11574
12546
|
else if (configWriteResult.status === "merged") p.log.success("Updated 'hot-updater.config.ts' file with Cloudflare settings.");
|
|
11575
12547
|
else p.log.warn(`Kept existing 'hot-updater.config.ts' unchanged: ${configWriteResult.reason}`);
|
|
11576
|
-
if (subdomains.subdomain) p.note(transformTemplate(SOURCE_TEMPLATE, {
|
|
12548
|
+
if (subdomains.subdomain) p.note(transformTemplate(SOURCE_TEMPLATE, {
|
|
12549
|
+
apiKey: JSON.stringify(apiKey),
|
|
12550
|
+
source: `https://${workerName}.${subdomains.subdomain}.workers.dev`
|
|
12551
|
+
}));
|
|
12552
|
+
p.note(formatApiKeyNote(apiKey), "API Key");
|
|
12553
|
+
p.log.message("Store this API key separately in a secure place.");
|
|
11577
12554
|
p.log.message(`Next step: ${link("https://hot-updater.dev/docs/managed/cloudflare#step-4-add-hotupdater-to-your-project")}`);
|
|
11578
12555
|
p.log.success("Done! 🎉");
|
|
11579
12556
|
};
|