@hot-updater/cloudflare 1.0.0-rc.5 → 1.0.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.
Files changed (51) hide show
  1. package/dist/index.cjs +90 -924
  2. package/dist/index.d.cts +16 -6
  3. package/dist/index.d.mts +16 -6
  4. package/dist/index.mjs +91 -926
  5. package/dist/init/index.cjs +11777 -0
  6. package/dist/init/index.d.cts +5 -1
  7. package/dist/init/index.d.mts +5 -1
  8. package/dist/init/index.mjs +11771 -1
  9. package/dist/worker/index.cjs +117 -916
  10. package/dist/worker/index.d.cts +45 -11
  11. package/dist/worker/index.d.mts +45 -11
  12. package/dist/worker/index.mjs +118 -917
  13. package/package.json +9 -14
  14. package/sql/bundles.sql +121 -212
  15. package/src/d1Database.ts +58 -43
  16. package/src/d1Executor.ts +84 -0
  17. package/src/d1Schema.ts +26 -0
  18. package/src/presignR2Url.ts +103 -0
  19. package/src/r2S3Storage.ts +20 -19
  20. package/src/r2Storage.ts +5 -3
  21. package/src/r2WorkerStorage.ts +55 -17
  22. package/src/worker/d1Database.ts +21 -39
  23. package/worker/dist/README.md +1 -1
  24. package/worker/dist/index.js +9859 -5696
  25. package/worker/dist/index.js.map +4 -4
  26. package/worker/migrations/0001_hot-updater_1.0.0.sql +121 -222
  27. package/worker/src/index.ts +15 -9
  28. package/worker/wrangler.json +1 -0
  29. package/dist/iac/index.cjs +0 -12687
  30. package/dist/iac/index.d.cts +0 -5
  31. package/dist/iac/index.d.mts +0 -5
  32. package/dist/iac/index.mjs +0 -12679
  33. package/src/d1Database.spec.ts +0 -277
  34. package/src/d1Implementation.spec.ts +0 -342
  35. package/src/d1Implementation.ts +0 -781
  36. package/src/d1Query.spec.ts +0 -46
  37. package/src/d1Query.ts +0 -92
  38. package/src/d1Rows.spec.ts +0 -143
  39. package/src/d1Rows.ts +0 -357
  40. package/src/d1Sql.spec.ts +0 -30
  41. package/src/d1Sql.ts +0 -175
  42. package/src/r2Storage.spec.ts +0 -346
  43. package/src/r2WorkerStorage.spec.ts +0 -184
  44. package/src/utils/createWrangler.spec.ts +0 -75
  45. package/src/worker/d1Database.spec.ts +0 -69
  46. package/worker/src/databaseMigration.integration.spec.ts +0 -191
  47. package/worker/src/databasePlugins.integration.spec.ts +0 -326
  48. package/worker/src/index.integration.spec.ts +0 -190
  49. package/worker/src/insightsMovement.integration.spec.ts +0 -73
  50. package/worker/src/storagePlugin.integration.spec.ts +0 -59
  51. package/worker/src/testWorker.ts +0 -5
package/dist/index.cjs CHANGED
@@ -22,961 +22,124 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  }) : target, mod));
23
23
  //#endregion
24
24
  let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
25
- let _hot_updater_plugin_core_internal = require("@hot-updater/plugin-core/internal");
26
25
  let cloudflare = require("cloudflare");
27
26
  cloudflare = __toESM(cloudflare, 1);
28
27
  let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
29
28
  let _aws_sdk_lib_storage = require("@aws-sdk/lib-storage");
30
- //#region src/d1Rows.ts
31
- var InvalidD1RowError = class extends Error {
32
- name = "InvalidD1RowError";
33
- model;
34
- constructor(model) {
35
- super(`D1 returned an invalid ${model} row`);
36
- this.model = model;
37
- }
38
- };
39
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
40
- const stringValue = (row, field, model) => {
41
- const value = row[field];
42
- if (typeof value !== "string") throw new InvalidD1RowError(model);
43
- return value;
44
- };
45
- const nullableString = (row, field, model) => {
46
- const value = row[field];
47
- if (value === null) return null;
48
- if (typeof value !== "string") throw new InvalidD1RowError(model);
49
- return value;
50
- };
51
- const numberValue = (row, field, model) => {
52
- const value = row[field];
53
- if (typeof value !== "number") throw new InvalidD1RowError(model);
54
- return value;
55
- };
56
- const byteSizeValue = (row, field, model) => {
57
- const value = row[field];
58
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new InvalidD1RowError(model);
59
- return value;
60
- };
61
- const booleanValue = (row, field, model) => {
62
- const value = row[field];
63
- if (value === true || value === 1) return true;
64
- if (value === false || value === 0) return false;
65
- throw new InvalidD1RowError(model);
66
- };
67
- const jsonValue = (value, model) => {
68
- if (typeof value !== "string") throw new InvalidD1RowError(model);
69
- try {
70
- return JSON.parse(value);
71
- } catch (error) {
72
- if (error instanceof SyntaxError) throw new InvalidD1RowError(model);
73
- throw error;
74
- }
75
- };
76
- const metadata = (row) => {
77
- const value = jsonValue(row["metadata"], "bundles");
78
- if (!(0, _hot_updater_plugin_core.isDatabaseMetadataObject)(value)) throw new InvalidD1RowError("bundles");
79
- return value;
80
- };
81
- const bundleRow = (row) => {
82
- const platform = stringValue(row, "platform", "bundles");
83
- if (platform !== "ios" && platform !== "android") throw new InvalidD1RowError("bundles");
84
- return {
85
- id: stringValue(row, "id", "bundles"),
86
- platform,
87
- file_hash: stringValue(row, "file_hash", "bundles"),
88
- git_commit_hash: nullableString(row, "git_commit_hash", "bundles"),
89
- storage_uri: stringValue(row, "storage_uri", "bundles"),
90
- archive_byte_size: row["archive_byte_size"] === void 0 ? 0 : byteSizeValue(row, "archive_byte_size", "bundles"),
91
- metadata: metadata(row),
92
- manifest_storage_uri: nullableString(row, "manifest_storage_uri", "bundles"),
93
- manifest_file_hash: nullableString(row, "manifest_file_hash", "bundles"),
94
- asset_base_storage_uri: nullableString(row, "asset_base_storage_uri", "bundles")
95
- };
96
- };
97
- const patchRow = (row) => ({
98
- id: stringValue(row, "id", "bundle_patches"),
99
- bundle_id: stringValue(row, "bundle_id", "bundle_patches"),
100
- base_bundle_id: stringValue(row, "base_bundle_id", "bundle_patches"),
101
- base_file_hash: stringValue(row, "base_file_hash", "bundle_patches"),
102
- patch_file_hash: stringValue(row, "patch_file_hash", "bundle_patches"),
103
- patch_storage_uri: stringValue(row, "patch_storage_uri", "bundle_patches"),
104
- byte_size: byteSizeValue(row, "byte_size", "bundle_patches"),
105
- order_index: numberValue(row, "order_index", "bundle_patches")
106
- });
107
- const channelRow = (row) => ({
108
- id: stringValue(row, "id", "channels"),
109
- name: stringValue(row, "name", "channels")
110
- });
111
- const eventRow = (row) => {
112
- const type = stringValue(row, "type", "bundle_events");
113
- const platform = stringValue(row, "platform", "bundle_events");
114
- const fromBundleId = nullableString(row, "from_bundle_id", "bundle_events");
115
- const updateStrategy = nullableString(row, "update_strategy", "bundle_events");
116
- if (platform !== "ios" && platform !== "android" || !((type === "UPDATE_APPLIED" || type === "RECOVERED") && typeof fromBundleId === "string" && (updateStrategy === "fingerprint" || updateStrategy === "appVersion") || type === "UNCHANGED" && fromBundleId === null && updateStrategy === null)) throw new InvalidD1RowError("bundle_events");
117
- return {
118
- id: stringValue(row, "id", "bundle_events"),
119
- type,
120
- install_id: stringValue(row, "install_id", "bundle_events"),
121
- user_id: nullableString(row, "user_id", "bundle_events"),
122
- username: nullableString(row, "username", "bundle_events"),
123
- from_release_id: nullableString(row, "from_release_id", "bundle_events"),
124
- from_bundle_id: fromBundleId,
125
- to_release_id: nullableString(row, "to_release_id", "bundle_events"),
126
- to_bundle_id: stringValue(row, "to_bundle_id", "bundle_events"),
127
- platform,
128
- app_version: stringValue(row, "app_version", "bundle_events"),
129
- channel: stringValue(row, "channel", "bundle_events"),
130
- cohort: stringValue(row, "cohort", "bundle_events"),
131
- update_strategy: updateStrategy,
132
- fingerprint_hash: nullableString(row, "fingerprint_hash", "bundle_events"),
133
- sdk_version: nullableString(row, "sdk_version", "bundle_events"),
134
- received_at_ms: numberValue(row, "received_at_ms", "bundle_events")
135
- };
136
- };
137
- const installationRow = (row) => {
138
- const type = stringValue(row, "type", "bundle_installations");
139
- const platform = stringValue(row, "platform", "bundle_installations");
140
- if (![
141
- "UPDATE_APPLIED",
142
- "RECOVERED",
143
- "UNCHANGED"
144
- ].includes(type) || platform !== "ios" && platform !== "android") throw new InvalidD1RowError("bundle_installations");
145
- return {
146
- id: stringValue(row, "id", "bundle_installations"),
147
- install_id: stringValue(row, "install_id", "bundle_installations"),
148
- user_id: nullableString(row, "user_id", "bundle_installations"),
149
- username: nullableString(row, "username", "bundle_installations"),
150
- to_bundle_id: stringValue(row, "to_bundle_id", "bundle_installations"),
151
- type,
152
- platform,
153
- app_version: stringValue(row, "app_version", "bundle_installations"),
154
- channel: stringValue(row, "channel", "bundle_installations"),
155
- cohort: stringValue(row, "cohort", "bundle_installations"),
156
- received_at_ms: numberValue(row, "received_at_ms", "bundle_installations")
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 "bundle_installations": return installationRow(value);
234
- case "api_keys": return apiKeyRow(value);
235
- case "releases": return releaseRow(value);
236
- case "release_catalogs": return releaseCatalogRow(value);
237
- }
238
- }
239
- //#endregion
240
- //#region src/d1Sql.ts
241
- var InvalidD1PredicateError = class extends Error {
242
- name = "InvalidD1PredicateError";
243
- };
244
- const encodeD1Value = (value) => {
245
- const encoded = JSON.stringify(value);
246
- if (encoded === void 0) throw new TypeError("D1 values must be JSON-serializable.");
247
- return encoded;
248
- };
249
- const bind = (value) => ({
250
- sql: "json_extract(?, '$')",
251
- params: [encodeD1Value(value)]
252
- });
253
- const escapeLikePattern = (value) => value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
254
- const stringPredicate = (condition, operator) => {
255
- if (typeof condition.value !== "string") throw new InvalidD1PredicateError();
256
- if (condition.mode !== "insensitive") {
257
- const parameter = bind(condition.value);
258
- const column = `${condition.field} COLLATE BINARY`;
259
- const value = `${parameter.sql} COLLATE BINARY`;
260
- if (operator === "contains" || operator === "starts_with") return {
261
- sql: `instr(${column}, ${value}) ${operator === "contains" ? "> 0" : "= 1"}`,
262
- params: parameter.params
263
- };
264
- return {
265
- sql: `substr(${column}, length(${condition.field}) - length(${parameter.sql}) + 1) = ${value}`,
266
- params: [...parameter.params, ...parameter.params]
267
- };
268
- }
269
- const literal = escapeLikePattern(condition.value);
270
- const pattern = operator === "contains" ? `%${literal}%` : operator === "starts_with" ? `${literal}%` : `%${literal}`;
271
- const parameter = bind(pattern);
272
- return {
273
- sql: `lower(${condition.field}) LIKE lower(${parameter.sql}) ESCAPE '\\'`,
274
- params: parameter.params
275
- };
276
- };
277
- const predicate = (condition) => {
278
- const operator = condition.operator ?? "eq";
279
- switch (operator) {
280
- case "eq":
281
- case "ne": {
282
- if (condition.value === null) return {
283
- sql: `${condition.field} IS ${operator === "ne" ? "NOT " : ""}NULL`,
284
- params: []
285
- };
286
- const parameter = bind(condition.value);
287
- const insensitive = "mode" in condition && condition.mode === "insensitive";
288
- const column = insensitive ? `lower(${condition.field})` : condition.field;
289
- const value = insensitive ? `lower(${parameter.sql})` : parameter.sql;
290
- return {
291
- sql: `${column} ${operator === "eq" ? "=" : "<>"} ${value}`,
292
- params: parameter.params
293
- };
294
- }
295
- case "gt":
296
- case "gte":
297
- case "lt":
298
- case "lte": {
299
- const sqlOperator = {
300
- gt: ">",
301
- gte: ">=",
302
- lt: "<",
303
- lte: "<="
304
- }[operator];
305
- const parameter = bind(condition.value);
306
- return {
307
- sql: `${condition.field} ${sqlOperator} ${parameter.sql}`,
308
- params: parameter.params
309
- };
310
- }
311
- case "in":
312
- case "not_in":
313
- if (!Array.isArray(condition.value)) throw new InvalidD1PredicateError();
314
- if (condition.value.length === 0) return {
315
- sql: operator === "not_in" ? "1 = 1" : "1 = 0",
316
- params: []
317
- };
318
- return {
319
- sql: `${condition.field} ${operator === "not_in" ? "NOT " : ""}IN (SELECT value FROM json_each(?))`,
320
- params: [encodeD1Value(condition.value)]
321
- };
322
- case "contains":
323
- case "starts_with":
324
- case "ends_with": return stringPredicate(condition, operator);
325
- }
326
- };
327
- const buildD1Where = (where) => {
328
- const [first, ...rest] = where ?? [];
329
- if (first === void 0) return {
330
- sql: "",
331
- params: []
332
- };
333
- const initial = predicate(first);
334
- let sql = initial.sql;
335
- const params = [...initial.params];
336
- for (const condition of rest) {
337
- const next = predicate(condition);
338
- sql = `(${sql} ${condition.connector === "OR" ? "OR" : "AND"} ${next.sql})`;
339
- params.push(...next.params);
340
- }
341
- return {
342
- sql: ` WHERE ${sql}`,
343
- params
344
- };
345
- };
346
- const buildD1Order = (orderBy) => orderBy === void 0 ? "" : ` ORDER BY ${orderBy.map((clause) => `${clause.field} ${clause.direction.toUpperCase()}${clause.nulls ? ` NULLS ${clause.nulls.toUpperCase()}` : ""}`).join(", ")}`;
347
- const d1Placeholders = (count) => Array.from({ length: count }, () => "json_extract(?, '$')").join(", ");
348
- const encodeD1Values = (values) => values.map(encodeD1Value);
349
- //#endregion
350
- //#region src/d1Query.ts
351
- const d1TableNames = {
352
- bundles: "bundles",
353
- bundle_patches: "bundle_patches",
354
- channels: "channels",
355
- bundle_events: "bundle_events",
356
- bundle_installations: "bundle_installations",
357
- api_keys: "api_keys",
358
- release_catalogs: "release_catalogs",
359
- releases: "releases"
360
- };
361
- var D1QueryResultError = class extends Error {
362
- result;
363
- name = "D1QueryResultError";
364
- operation = "count";
365
- constructor(result) {
366
- super("D1 count query returned a malformed first row");
367
- this.result = result;
368
- }
369
- };
370
- const countD1Rows = async (executor, input) => {
371
- const where = buildD1Where(input.where);
372
- const table = d1TableNames[input.model];
373
- const source = input.distinct === void 0 ? `${table}${where.sql}` : `(SELECT DISTINCT ${input.distinct.join(", ")} FROM ${table}${where.sql}) AS distinct_rows`;
374
- const row = (await executor.query(`SELECT COUNT(*) AS count FROM ${source}`, where.params))[0];
375
- if (typeof row !== "object" || row === null || Array.isArray(row)) throw new D1QueryResultError(row);
376
- const count = Reflect.get(row, "count");
377
- if (typeof count !== "number" || !Number.isFinite(count)) throw new D1QueryResultError(row);
378
- return count;
379
- };
380
- const findManyD1Rows = async (executor, input) => {
381
- const where = buildD1Where(input.where);
382
- const orderBy = input.orderBy;
383
- const order = buildD1Order(orderBy);
384
- const table = d1TableNames[input.model];
385
- 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}`;
386
- const pageParams = encodeD1Values([input.limit, input.offset]);
387
- const rows = await executor.query(`${source} LIMIT json_extract(?, '$') OFFSET json_extract(?, '$')`, [...where.params, ...pageParams]);
388
- switch (input.model) {
389
- case "bundles": return rows.map((row) => parseD1Row("bundles", row));
390
- case "bundle_patches": return rows.map((row) => parseD1Row("bundle_patches", row));
391
- case "channels": return rows.map((row) => parseD1Row("channels", row));
392
- case "bundle_events": return rows.map((row) => parseD1Row("bundle_events", row));
393
- case "bundle_installations": return rows.map((row) => parseD1Row("bundle_installations", row));
394
- case "api_keys": return rows.map((row) => parseD1Row("api_keys", row));
395
- case "releases": return rows.map((row) => parseD1Row("releases", row));
396
- case "release_catalogs": return rows.map((row) => parseD1Row("release_catalogs", row));
397
- }
398
- };
399
- //#endregion
400
- //#region src/d1Implementation.ts
29
+ let _aws_sdk_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
30
+ //#region src/d1Executor.ts
401
31
  var D1ExecutionError = class extends Error {
402
32
  name = "D1ExecutionError";
403
33
  constructor() {
404
34
  super("D1 did not successfully execute every requested statement");
405
35
  }
406
36
  };
407
- const EXPECTATION_CONFLICT_MARKER = "HOT_UPDATER_COMMIT_EXPECTATION_CONFLICT";
408
- const expectationGuard = (expectations) => {
409
- const clauses = [];
410
- const params = [];
411
- for (const expectation of expectations) {
412
- const isRelease = expectation.model === "releases";
413
- const table = isRelease ? "releases" : "release_catalogs";
414
- const keyField = isRelease ? "id" : "scope_key";
415
- const versionField = isRelease ? "revision" : "generation";
416
- const key = isRelease ? expectation.id : expectation.scopeKey;
417
- const version = isRelease ? expectation.revision : expectation.generation;
418
- if (version === null) {
419
- clauses.push(`NOT EXISTS (SELECT 1 FROM ${table} WHERE ${keyField} = json_extract(?, '$'))`);
420
- params.push(...encodeD1Values([key]));
421
- } else {
422
- clauses.push(`EXISTS (SELECT 1 FROM ${table} WHERE ${keyField} = json_extract(?, '$') AND ${versionField} = json_extract(?, '$'))`);
423
- params.push(...encodeD1Values([key, version]));
424
- }
425
- }
37
+ const toSqlResult = (result) => {
38
+ if (result.success === false) throw new D1ExecutionError();
426
39
  return {
427
- sql: clauses.join(" AND ") || "1",
428
- params
40
+ rows: result.results ?? [],
41
+ changes: result.meta?.changes ?? 0
429
42
  };
430
43
  };
431
- const readVersion = (row, field) => {
432
- if (typeof row !== "object" || row === null) return null;
433
- const value = Reflect.get(row, field);
434
- return typeof value === "number" ? value : null;
435
- };
436
- const expectationConflict = async (executor, expectations) => {
437
- for (const expectation of expectations) {
438
- const isRelease = expectation.model === "releases";
439
- const table = isRelease ? "releases" : "release_catalogs";
440
- const keyField = isRelease ? "id" : "scope_key";
441
- const versionField = isRelease ? "revision" : "generation";
442
- const key = isRelease ? expectation.id : expectation.scopeKey;
443
- const expectedVersion = isRelease ? expectation.revision : expectation.generation;
444
- const rows = await executor.query(`SELECT ${versionField} FROM ${table} WHERE ${keyField} = json_extract(?, '$') LIMIT 1`, encodeD1Values([key]));
445
- const actualVersion = readVersion(rows[0], versionField);
446
- if (actualVersion !== expectedVersion) return {
447
- committed: false,
448
- conflict: {
449
- actualVersion,
450
- changeIndex: -1,
451
- expectedVersion,
452
- key,
453
- model: expectation.model,
454
- reason: "version_conflict"
455
- }
456
- };
457
- }
458
- return null;
459
- };
460
- const isExpectationConflictError = (error) => {
44
+ /** D1 reports SQLite errors in the message; the SQL core classifies codes. */
45
+ const withSqliteCode = (error) => {
461
46
  const message = error instanceof Error ? error.message : String(error);
462
- return message.includes(EXPECTATION_CONFLICT_MARKER) || message.toLowerCase().includes("malformed json");
463
- };
464
- const bundleValues = (row) => [
465
- row.id,
466
- row.platform,
467
- row.file_hash,
468
- row.git_commit_hash,
469
- row.storage_uri,
470
- row.archive_byte_size,
471
- row.metadata,
472
- row.manifest_storage_uri,
473
- row.manifest_file_hash,
474
- row.asset_base_storage_uri
475
- ];
476
- const patchValues = (row) => [
477
- row.id,
478
- row.bundle_id,
479
- row.base_bundle_id,
480
- row.base_file_hash,
481
- row.patch_file_hash,
482
- row.patch_storage_uri,
483
- row.byte_size,
484
- row.order_index
485
- ];
486
- const channelValues = (row) => [row.id, row.name];
487
- const insertQuery = (input, guard, conflictMode = "returnExisting") => {
488
- let columns;
489
- let values;
490
- switch (input.model) {
491
- case "bundles":
492
- columns = [
493
- "id",
494
- "platform",
495
- "file_hash",
496
- "git_commit_hash",
497
- "storage_uri",
498
- "archive_byte_size",
499
- "metadata",
500
- "manifest_storage_uri",
501
- "manifest_file_hash",
502
- "asset_base_storage_uri"
503
- ];
504
- values = bundleValues(input.data);
505
- break;
506
- case "bundle_patches":
507
- columns = [
508
- "id",
509
- "bundle_id",
510
- "base_bundle_id",
511
- "base_file_hash",
512
- "patch_file_hash",
513
- "patch_storage_uri",
514
- "byte_size",
515
- "order_index"
516
- ];
517
- values = patchValues(input.data);
518
- break;
519
- case "channels":
520
- columns = ["id", "name"];
521
- values = channelValues(input.data);
522
- break;
523
- case "bundle_events":
524
- case "bundle_installations":
525
- case "api_keys":
526
- case "release_catalogs":
527
- case "releases":
528
- columns = Object.keys(input.data);
529
- values = Object.values(input.data);
530
- }
531
- 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" : " ON CONFLICT(install_id) DO UPDATE SET install_id = excluded.install_id";
532
- return {
533
- sql: `INSERT INTO ${d1TableNames[input.model]} (${columns.join(", ")}) ${guard === void 0 ? `VALUES (${d1Placeholders(values.length)})` : `SELECT ${d1Placeholders(values.length)} WHERE ${guard.sql}`}${conflict} RETURNING *`,
534
- params: [...encodeD1Values(values), ...guard?.params ?? []]
535
- };
536
- };
537
- const updateEntries = (update) => Object.entries(update);
538
- const updateQuery = (input, guard) => {
539
- const entries = updateEntries(input.update);
540
- const where = buildD1Where(input.where);
541
- if (entries.length === 0) return {
542
- sql: `SELECT * FROM ${d1TableNames[input.model]}${where.sql}${guard ? ` AND ${guard.sql}` : ""} LIMIT 1`,
543
- params: [...where.params, ...guard?.params ?? []]
544
- };
545
- const assignments = entries.map(([field]) => `${field} = json_extract(?, '$')`).join(", ");
546
- return {
547
- sql: `UPDATE ${d1TableNames[input.model]} SET ${assignments}${where.sql}${guard ? ` AND ${guard.sql}` : ""} RETURNING *`,
548
- params: [
549
- ...encodeD1Values(entries.map(([, value]) => value)),
550
- ...where.params,
551
- ...guard?.params ?? []
552
- ]
553
- };
554
- };
555
- const deleteQuery = (input, guard) => {
556
- const where = buildD1Where(input.where);
557
- return {
558
- sql: `DELETE FROM ${d1TableNames[input.model]}${where.sql}${guard ? ` AND ${guard.sql}` : ""}`,
559
- params: [...where.params, ...guard?.params ?? []]
560
- };
561
- };
562
- const insertedKey = (change) => {
563
- if (change.operation !== "insert") return void 0;
564
- if (change.model === "bundles") return `bundles:${change.row.id}`;
565
- if (change.model === "apiKeys") return `api_keys:${change.row.id}`;
566
- if (change.model === "releases") return `releases:${change.row.id}`;
567
- };
568
- const requiredRow = (change, changeIndex) => {
569
- if (change.operation !== "update") return void 0;
570
- if (change.model === "bundles") return {
571
- model: "bundles",
572
- id: change.where.id,
573
- changeIndex
574
- };
575
- if (change.model === "apiKeys") return {
576
- model: "api_keys",
577
- id: change.where.id,
578
- changeIndex
579
- };
580
- if (change.model === "releases") return {
581
- model: "releases",
582
- id: change.where.id,
583
- changeIndex
584
- };
585
- };
586
- const channelDeletePrecondition = (changes, changeIndex, channelId) => {
587
- const releaseEffects = /* @__PURE__ */ new Map();
588
- for (const change of changes.slice(0, changeIndex)) {
589
- if (change.model !== "releases") continue;
590
- switch (change.operation) {
591
- case "insert":
592
- releaseEffects.set(change.row.id, change.row.channel_id);
593
- break;
594
- case "update": break;
595
- case "delete": releaseEffects.set(change.where.id, null);
596
- }
597
- }
598
- return {
599
- id: channelId,
600
- excludedReleaseIds: [...releaseEffects.keys()],
601
- addedReferenceReleaseIds: [...releaseEffects].filter(([, finalChannelId]) => finalChannelId === channelId).map(([releaseId]) => releaseId)
602
- };
603
- };
604
- const changeQuery = (change, guard) => {
605
- switch (change.model) {
606
- case "bundles": switch (change.operation) {
607
- case "insert": return insertQuery({
608
- model: "bundles",
609
- data: change.row
610
- }, guard);
611
- case "update": return updateQuery({
612
- model: "bundles",
613
- where: [{
614
- field: "id",
615
- value: change.where.id
616
- }],
617
- update: change.update
618
- }, guard);
619
- case "delete": return deleteQuery({
620
- model: "bundles",
621
- where: [{
622
- field: "id",
623
- value: change.where.id
624
- }]
625
- }, guard);
626
- }
627
- case "bundlePatches": return change.operation === "insert" ? insertQuery({
628
- model: "bundle_patches",
629
- data: change.row
630
- }, guard) : deleteQuery({
631
- model: "bundle_patches",
632
- where: [{
633
- field: "bundle_id",
634
- value: change.where.bundleId
635
- }]
636
- }, guard);
637
- case "releases": switch (change.operation) {
638
- case "insert": return insertQuery({
639
- model: "releases",
640
- data: change.row
641
- }, guard);
642
- case "update": return updateQuery({
643
- model: "releases",
644
- where: [{
645
- field: "id",
646
- value: change.where.id
647
- }],
648
- update: change.update
649
- }, guard);
650
- case "delete": return deleteQuery({
651
- model: "releases",
652
- where: [{
653
- field: "id",
654
- value: change.where.id
655
- }]
656
- }, guard);
657
- }
658
- case "releaseCatalogs": return {
659
- 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 *`,
660
- params: [...encodeD1Values(Object.values(change.row)), ...guard.params]
661
- };
662
- case "channels": return change.operation === "insert" ? insertQuery({
663
- model: "channels",
664
- data: change.row,
665
- onConflict: change.onConflict
666
- }, guard, "ignore") : {
667
- sql: `DELETE FROM channels WHERE id = json_extract(?, '$') AND NOT EXISTS (SELECT 1 FROM releases WHERE channel_id = json_extract(?, '$')) AND ${guard.sql}`,
668
- params: [...encodeD1Values([change.where.id, change.where.id]), ...guard.params]
669
- };
670
- case "apiKeys": return change.operation === "insert" ? insertQuery({
671
- model: "api_keys",
672
- data: change.row,
673
- onConflict: change.onConflict
674
- }, guard, "ignore") : updateQuery({
675
- model: "api_keys",
676
- where: [{
677
- field: "id",
678
- value: change.where.id
679
- }],
680
- update: { revoked_at_ms: change.update.revokedAtMs }
681
- }, guard);
682
- }
683
- };
684
- const createCommitPlan = (input) => {
685
- const checks = [];
686
- const statements = [];
687
- const expectations = input.expectations ?? [];
688
- if (expectations.length > 0) {
689
- const guard = expectationGuard(expectations);
690
- statements.push({
691
- sql: `SELECT CASE WHEN ${guard.sql} THEN 1 ELSE json_extract('${EXPECTATION_CONFLICT_MARKER}', '$') END AS expectation_guard`,
692
- params: guard.params
693
- });
694
- }
695
- const requiredRows = [];
696
- const channelDeletes = [];
697
- const inserted = /* @__PURE__ */ new Set();
698
- for (const [changeIndex, change] of input.changes.entries()) {
699
- const required = requiredRow(change, changeIndex);
700
- if (required !== void 0) {
701
- if (!inserted.has(`${required.model}:${required.id}`)) {
702
- requiredRows.push(required);
703
- checks.push({
704
- changeIndex,
705
- resultIndex: statements.length,
706
- conflictWhen: "empty",
707
- reason: "not_found"
708
- });
709
- statements.push({
710
- sql: `SELECT id FROM ${d1TableNames[required.model]} WHERE id = json_extract(?, '$') LIMIT 1`,
711
- params: encodeD1Values([required.id])
712
- });
713
- }
714
- }
715
- if (change.model === "channels" && change.operation === "delete") {
716
- const precondition = channelDeletePrecondition(input.changes, changeIndex, change.where.id);
717
- channelDeletes.push(precondition);
718
- checks.push({
719
- changeIndex,
720
- resultIndex: statements.length,
721
- conflictWhen: "nonempty",
722
- reason: "referenced"
723
- });
724
- statements.push({
725
- 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",
726
- params: encodeD1Values([
727
- precondition.id,
728
- precondition.excludedReleaseIds,
729
- precondition.addedReferenceReleaseIds
730
- ])
731
- });
732
- }
733
- const key = insertedKey(change);
734
- if (key !== void 0) inserted.add(key);
735
- }
736
- const guard = {
737
- sql: `NOT EXISTS (
738
- SELECT 1 FROM json_each(?) AS required
739
- WHERE NOT EXISTS (
740
- SELECT 1 FROM bundles
741
- WHERE json_extract(required.value, '$.model') = 'bundles'
742
- AND bundles.id = json_extract(required.value, '$.id')
743
- UNION ALL
744
- SELECT 1 FROM api_keys
745
- WHERE json_extract(required.value, '$.model') = 'api_keys'
746
- AND api_keys.id = json_extract(required.value, '$.id')
747
- UNION ALL
748
- SELECT 1 FROM channels
749
- WHERE json_extract(required.value, '$.model') = 'channels'
750
- AND channels.id = json_extract(required.value, '$.id')
751
- UNION ALL
752
- SELECT 1 FROM releases
753
- WHERE json_extract(required.value, '$.model') = 'releases'
754
- AND releases.id = json_extract(required.value, '$.id')
755
- )
756
- ) AND NOT EXISTS (
757
- SELECT 1 FROM json_each(?) AS channel_delete
758
- WHERE EXISTS (
759
- SELECT 1 FROM releases
760
- WHERE releases.channel_id = json_extract(channel_delete.value, '$.id')
761
- AND releases.id NOT IN (
762
- SELECT value FROM json_each(
763
- json_extract(channel_delete.value, '$.excludedReleaseIds')
764
- )
765
- )
766
- UNION ALL
767
- SELECT value FROM json_each(
768
- json_extract(channel_delete.value, '$.addedReferenceReleaseIds')
769
- )
770
- )
771
- )`,
772
- params: encodeD1Values([requiredRows.map(({ model, id }) => ({
773
- model,
774
- id
775
- })), channelDeletes])
776
- };
777
- statements.push(...input.changes.map((change) => changeQuery(change, guard)));
778
- return {
779
- checks,
780
- statements
781
- };
782
- };
783
- const resultForPlan = (plan, results) => {
784
- const missing = plan.checks.find(({ conflictWhen, resultIndex }) => conflictWhen === "empty" ? (results[resultIndex]?.length ?? 0) === 0 : (results[resultIndex]?.length ?? 0) > 0);
785
- return missing === void 0 ? { committed: true } : {
786
- committed: false,
787
- conflict: {
788
- changeIndex: missing.changeIndex,
789
- reason: missing.reason
790
- }
791
- };
792
- };
793
- const insertChannel = async (executor, input) => {
794
- const insert = insertQuery({
795
- model: "channels",
796
- data: input.row,
797
- onConflict: "ignore"
798
- }, void 0, "ignore");
799
- const select = {
800
- sql: "SELECT id, name FROM channels WHERE name = json_extract(?, '$') LIMIT 1",
801
- params: encodeD1Values([input.row.name])
802
- };
803
- const [insertedRows = [], canonicalRows = []] = await executor.batch([insert, select]);
804
- return {
805
- row: parseD1Row("channels", canonicalRows[0]),
806
- inserted: insertedRows.length > 0
807
- };
808
- };
809
- const deleteChannel = async (executor, input) => {
810
- const [channelRows = [], referenceRows = [], deletedRows = []] = await executor.batch([
811
- {
812
- sql: "SELECT id FROM channels WHERE id = json_extract(?, '$') LIMIT 1",
813
- params: encodeD1Values([input.id])
814
- },
815
- {
816
- sql: "SELECT id FROM releases WHERE channel_id = json_extract(?, '$') LIMIT 1",
817
- params: encodeD1Values([input.id])
818
- },
819
- {
820
- sql: "DELETE FROM channels WHERE id = json_extract(?, '$') AND NOT EXISTS (SELECT 1 FROM releases WHERE channel_id = json_extract(?, '$')) RETURNING id",
821
- params: encodeD1Values([input.id, input.id])
822
- }
823
- ]);
824
- if (channelRows.length === 0) return {
825
- deleted: false,
826
- reason: "not_found"
827
- };
828
- if (referenceRows.length > 0) return {
829
- deleted: false,
830
- reason: "not_empty"
831
- };
832
- return deletedRows.length > 0 ? { deleted: true } : {
833
- deleted: false,
834
- reason: "not_empty"
835
- };
836
- };
837
- const createD1Implementation = (executor) => ({
838
- async recordInsights({ event, installation }) {
839
- const columns = Object.keys(installation);
840
- const eventInsert = insertQuery({
841
- model: "bundle_events",
842
- data: event
843
- });
844
- await executor.batch([{
845
- sql: `INSERT INTO bundle_installations (${columns.join(", ")})
846
- SELECT ${d1Placeholders(columns.length)}
847
- WHERE NOT EXISTS (SELECT 1 FROM bundle_events WHERE id = json_extract(?, '$'))
848
- ON CONFLICT(install_id) DO UPDATE SET ${columns.filter((column) => column !== "install_id").map((column) => `${column} = excluded.${column}`).join(", ")}
849
- WHERE (excluded.received_at_ms, excluded.id) >
850
- (bundle_installations.received_at_ms, bundle_installations.id)`,
851
- params: encodeD1Values([...Object.values(installation), event.id])
852
- }, {
853
- ...eventInsert,
854
- sql: eventInsert.sql.replace(" RETURNING *", " ON CONFLICT(id) DO NOTHING")
855
- }]);
856
- },
857
- async create(input) {
858
- const query = insertQuery(input);
859
- const rows = await executor.query(query.sql, query.params);
860
- switch (input.model) {
861
- case "bundles": return parseD1Row("bundles", rows[0]);
862
- case "bundle_patches": return parseD1Row("bundle_patches", rows[0]);
863
- case "channels": return parseD1Row("channels", rows[0]);
864
- case "bundle_events": return parseD1Row("bundle_events", rows[0]);
865
- case "bundle_installations": return parseD1Row("bundle_installations", rows[0]);
866
- case "api_keys": return parseD1Row("api_keys", rows[0]);
867
- case "releases": return parseD1Row("releases", rows[0]);
868
- case "release_catalogs": return parseD1Row("release_catalogs", rows[0]);
869
- }
870
- },
871
- async update(input) {
872
- const query = updateQuery(input);
873
- const rows = await executor.query(query.sql, query.params);
874
- if (rows[0] === void 0) return null;
875
- switch (input.model) {
876
- case "bundles": return parseD1Row("bundles", rows[0]);
877
- case "api_keys": return parseD1Row("api_keys", rows[0]);
878
- case "releases": return parseD1Row("releases", rows[0]);
879
- case "release_catalogs": return parseD1Row("release_catalogs", rows[0]);
880
- case "bundle_installations": return parseD1Row("bundle_installations", rows[0]);
881
- }
882
- },
883
- async delete(input) {
884
- const query = deleteQuery(input);
885
- await executor.query(query.sql, query.params);
886
- },
887
- count: (input) => countD1Rows(executor, input),
888
- async findOne(input) {
889
- const where = buildD1Where(input.where);
890
- const rows = await executor.query(`SELECT * FROM ${d1TableNames[input.model]}${where.sql} LIMIT 1`, where.params);
891
- if (rows[0] === void 0) return null;
892
- switch (input.model) {
893
- case "bundles": return parseD1Row("bundles", rows[0]);
894
- case "bundle_patches": return parseD1Row("bundle_patches", rows[0]);
895
- case "channels": return parseD1Row("channels", rows[0]);
896
- case "api_keys": return parseD1Row("api_keys", rows[0]);
897
- case "releases": return parseD1Row("releases", rows[0]);
898
- case "release_catalogs": return parseD1Row("release_catalogs", rows[0]);
899
- case "bundle_installations": return parseD1Row("bundle_installations", rows[0]);
900
- }
901
- },
902
- findMany: (input) => findManyD1Rows(executor, input),
903
- insertChannel: (input) => insertChannel(executor, input),
904
- deleteChannel: (input) => deleteChannel(executor, input),
905
- async commit(input) {
906
- if (input.changes.length === 0) return { committed: true };
907
- const expectations = input.expectations ?? [];
908
- const conflict = await expectationConflict(executor, expectations);
909
- if (conflict !== null) return conflict;
910
- const plan = createCommitPlan(input);
911
- try {
912
- return resultForPlan(plan, await executor.batch(plan.statements));
913
- } catch (error) {
914
- if (expectations.length === 0 || !isExpectationConflictError(error)) throw error;
915
- return await expectationConflict(executor, expectations) ?? {
916
- committed: false,
917
- conflict: {
918
- actualVersion: expectations[0].model === "releases" ? expectations[0].revision : expectations[0].generation,
919
- changeIndex: -1,
920
- expectedVersion: expectations[0].model === "releases" ? expectations[0].revision : expectations[0].generation,
921
- key: expectations[0].model === "releases" ? expectations[0].id : expectations[0].scopeKey,
922
- model: expectations[0].model,
923
- reason: "version_conflict"
47
+ const code = /UNIQUE constraint failed|constraint failed: .*PRIMARY/iu.test(message) ? "SQLITE_CONSTRAINT_UNIQUE" : /SQLITE_BUSY|database is locked/iu.test(message) ? "SQLITE_BUSY" : void 0;
48
+ return code === void 0 ? error : Object.assign(new Error(message, { cause: error }), { code });
49
+ };
50
+ /**
51
+ * Hot Updater's database on D1: the storage engine through the shared SQL
52
+ * core, fenced by the schema settings. D1 has no interactive transactions,
53
+ * so each write is one atomic batch that evaluates its guards first
54
+ * (`_hu_write`).
55
+ */
56
+ const createD1Database = (runner) => {
57
+ return (0, _hot_updater_plugin_core.createEngineDatabase)({
58
+ name: "d1Database",
59
+ adapter: (0, _hot_updater_plugin_core.createSqlAdapter)({
60
+ executor: {
61
+ dialect: "sqlite",
62
+ execute: (statement) => runner.query(statement).catch((error) => {
63
+ throw withSqliteCode(error);
64
+ }),
65
+ batch: (statements) => runner.batch(statements).catch((error) => {
66
+ throw withSqliteCode(error);
67
+ }),
68
+ transaction: () => {
69
+ throw new Error("D1 has no interactive transactions; writes are batches.");
924
70
  }
925
- };
926
- }
927
- }
928
- });
71
+ },
72
+ maxOps: 450,
73
+ maxParams: 100
74
+ })
75
+ });
76
+ };
77
+ //#endregion
78
+ //#region src/d1Schema.ts
79
+ /**
80
+ * D1's schema: the guard table batch writes need, then the shared SQL
81
+ * schema of core and the target's plugins, settings last. Every statement can
82
+ * run again, so a migration for a server's plugin tables repeats core's.
83
+ */
84
+ const d1SchemaStatements = ({ schema, settings } = _hot_updater_plugin_core.coreTarget) => [...(0, _hot_updater_plugin_core.createTableStatements)("sqlite", [_hot_updater_plugin_core.WRITE_GUARD_TABLE]), ...(0, _hot_updater_plugin_core.generateEngineSql)("sqlite", schema, settings)];
85
+ /** The schema as a D1 migration, which `wrangler d1 migrations apply` runs. */
86
+ const d1SchemaSql = (target) => `-- HotUpdater.schema\n\n${d1SchemaStatements(target).map((statement) => `${statement};`).join("\n\n")}\n`;
929
87
  //#endregion
930
88
  //#region src/d1Database.ts
89
+ /** The REST API binds text: each value goes as JSON and is read back with `json_extract`. */
90
+ const encode = ({ sql, params }) => ({
91
+ sql: sql.replaceAll("?", () => "json_extract(?, '$')"),
92
+ params: params.map((value) => JSON.stringify(value ?? null))
93
+ });
94
+ /**
95
+ * Hot Updater's database on D1 through the Cloudflare REST API, for the CLI
96
+ * and console. `hot-updater db generate` writes a D1 migration of the built-in
97
+ * tables and the server's plugin tables to `migrations`, and
98
+ * `hot-updater db migrate` applies them through the API.
99
+ */
931
100
  const d1Database = (config) => {
932
101
  const cloudflare$1 = new cloudflare.default({ apiToken: config.cloudflareApiToken });
933
102
  const execute = async (statements) => {
934
- const first = statements[0];
103
+ const [first] = statements;
935
104
  if (first === void 0) return [];
936
105
  const body = statements.length === 1 ? {
937
106
  account_id: config.accountId,
938
- sql: first.sql,
939
- params: [...first.params]
107
+ ...encode(first)
940
108
  } : {
941
109
  account_id: config.accountId,
942
- batch: statements.map(({ sql, params }) => ({
943
- sql,
944
- params: [...params]
945
- }))
110
+ batch: statements.map(encode)
946
111
  };
947
- const page = await cloudflare$1.d1.database.query(config.databaseId, { ...body });
112
+ const page = await cloudflare$1.d1.database.query(config.databaseId, body);
948
113
  const results = [];
949
- for await (const resultPage of page.iterPages()) for (const result of resultPage.result) {
950
- if (result.success === false) throw new D1ExecutionError();
951
- results.push(result.results ?? []);
952
- }
114
+ for await (const resultPage of page.iterPages()) for (const result of resultPage.result) results.push(toSqlResult(result));
953
115
  if (results.length !== statements.length) throw new D1ExecutionError();
954
116
  return results;
955
117
  };
956
- const adapter = (0, _hot_updater_plugin_core_internal.createDatabasePluginAdapter)("d1Database", createD1Implementation({
957
- async query(sql, params) {
958
- return (await execute([{
959
- sql,
960
- params
961
- }])).flat();
962
- },
963
- batch: execute
964
- }));
965
- return (0, _hot_updater_plugin_core.createDatabasePlugin)({
966
- name: "d1Database",
967
- models: adapter.models,
968
- commit: adapter.commit,
969
- ...adapter.dispose ? { dispose: adapter.dispose } : {}
970
- });
118
+ return {
119
+ ...createD1Database({
120
+ query: async (statement) => (await execute([statement]))[0],
121
+ batch: execute
122
+ }),
123
+ generateSchema: ((version, _name, target = _hot_updater_plugin_core.coreTarget) => {
124
+ if (version !== "latest" && version !== _hot_updater_plugin_core.coreSettings["schema.core"]) throw new Error(`Invalid version ${version}`);
125
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\D/g, "").slice(0, 14);
126
+ return {
127
+ code: d1SchemaSql(target),
128
+ path: `migrations/${timestamp}_hot-updater.sql`
129
+ };
130
+ })
131
+ };
971
132
  };
972
133
  //#endregion
973
134
  //#region src/r2S3Storage.ts
135
+ /** Devices download right after the update check that returns the URL. */
136
+ const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
974
137
  const isObjectNotFoundError = (error) => {
975
138
  if (error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey")) return true;
976
139
  return typeof error === "object" && error !== null && "$metadata" in error && error.$metadata?.httpStatusCode === 404;
977
140
  };
978
141
  const createR2S3Storage = (config) => {
979
- const { accountId, basePath, bucketName, downloadUrlSigningKey, endpoint, forcePathStyle, region, ...s3Config } = config;
142
+ const { accountId, basePath, bucketName, endpoint, forcePathStyle, region, ...s3Config } = config;
980
143
  const client = new _aws_sdk_client_s3.S3Client({
981
144
  ...s3Config,
982
145
  endpoint: endpoint ?? `https://${accountId}.r2.cloudflarestorage.com`,
@@ -984,13 +147,12 @@ const createR2S3Storage = (config) => {
984
147
  region: region ?? "auto"
985
148
  });
986
149
  const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(basePath);
987
- const getDownloadUrl = downloadUrlSigningKey ? (0, _hot_updater_plugin_core.createStorageDownloadUrl)(downloadUrlSigningKey) : void 0;
988
150
  const parseAndValidate = (storageUri) => {
989
151
  const parsed = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "r2");
990
152
  if (parsed.bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${parsed.bucket}".`);
991
153
  return parsed;
992
154
  };
993
- return (0, _hot_updater_plugin_core.createStoragePlugin)({
155
+ return (0, _hot_updater_plugin_core.createStorageAdapter)({
994
156
  name: "r2Storage",
995
157
  protocol: "r2",
996
158
  async put({ key, body, contentLength, contentType }) {
@@ -1029,10 +191,13 @@ const createR2S3Storage = (config) => {
1029
191
  throw error;
1030
192
  }
1031
193
  },
1032
- ...getDownloadUrl ? { async getDownloadUrl({ storageUri }) {
1033
- parseAndValidate(storageUri);
1034
- return getDownloadUrl({ storageUri });
1035
- } } : {},
194
+ async getDownloadUrl({ storageUri }) {
195
+ const { key } = parseAndValidate(storageUri);
196
+ return { url: await (0, _aws_sdk_s3_request_presigner.getSignedUrl)(client, new _aws_sdk_client_s3.GetObjectCommand({
197
+ Bucket: bucketName,
198
+ Key: key
199
+ }), { expiresIn: PRESIGNED_URL_EXPIRES_IN_SECONDS }) };
200
+ },
1036
201
  async exists({ storageUri }) {
1037
202
  const { key } = parseAndValidate(storageUri);
1038
203
  try {
@@ -1059,9 +224,10 @@ const createR2S3Storage = (config) => {
1059
224
  //#endregion
1060
225
  //#region src/r2Storage.ts
1061
226
  const r2Storage = (config) => {
1062
- if (!config.credentials) throw new Error("r2Storage requires S3-compatible credentials. The Wrangler fallback was removed.");
227
+ if (!config.credentials) throw new Error("r2Storage requires S3-compatible credentials: pass credentials with an R2 access key ID and secret access key.");
1063
228
  return createR2S3Storage(config);
1064
229
  };
1065
230
  //#endregion
231
+ exports.D1ExecutionError = D1ExecutionError;
1066
232
  exports.d1Database = d1Database;
1067
233
  exports.r2Storage = r2Storage;