@hot-updater/cloudflare 1.0.0-rc.14 → 1.0.0-rc.17

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 (44) hide show
  1. package/dist/iac/index.cjs +90 -906
  2. package/dist/iac/index.mjs +91 -907
  3. package/dist/index.cjs +90 -902
  4. package/dist/index.d.cts +353 -2
  5. package/dist/index.d.mts +354 -3
  6. package/dist/index.mjs +89 -903
  7. package/dist/worker/index.cjs +55 -894
  8. package/dist/worker/index.d.cts +354 -8
  9. package/dist/worker/index.d.mts +354 -8
  10. package/dist/worker/index.mjs +56 -896
  11. package/package.json +6 -7
  12. package/sql/bundles.sql +81 -199
  13. package/src/d1Database.spec.ts +143 -249
  14. package/src/d1Database.ts +55 -43
  15. package/src/d1Executor.ts +84 -0
  16. package/src/d1Schema.spec.ts +84 -0
  17. package/src/d1Schema.ts +25 -0
  18. package/src/d1TestDatabase.ts +38 -0
  19. package/src/index.ts +1 -0
  20. package/src/plugins.ts +8 -0
  21. package/src/worker/d1Database.spec.ts +51 -61
  22. package/src/worker/d1Database.ts +21 -35
  23. package/src/worker/index.ts +1 -0
  24. package/worker/dist/README.md +1 -1
  25. package/worker/dist/index.js +7640 -5987
  26. package/worker/dist/index.js.map +4 -4
  27. package/worker/migrations/0001_hot-updater_1.0.0.sql +81 -207
  28. package/worker/src/d1.conformance.integration.spec.ts +53 -0
  29. package/worker/src/d1.readBudgets.integration.spec.ts +85 -0
  30. package/worker/src/databaseMigration.integration.spec.ts +49 -188
  31. package/worker/src/databasePlugins.integration.spec.ts +118 -156
  32. package/worker/src/index.integration.spec.ts +29 -84
  33. package/worker/src/index.ts +2 -2
  34. package/worker/src/readPlans.integration.spec.ts +91 -0
  35. package/src/d1Implementation.spec.ts +0 -274
  36. package/src/d1Implementation.ts +0 -803
  37. package/src/d1Query.spec.ts +0 -46
  38. package/src/d1Query.ts +0 -91
  39. package/src/d1Rows.spec.ts +0 -118
  40. package/src/d1Rows.ts +0 -323
  41. package/src/d1Sql.spec.ts +0 -30
  42. package/src/d1Sql.ts +0 -175
  43. package/worker/src/insightsMovement.integration.spec.ts +0 -73
  44. package/worker/src/insightsReadCost.integration.spec.ts +0 -108
@@ -29,11 +29,12 @@ fs_promises = __toESM(fs_promises, 1);
29
29
  let path = require("path");
30
30
  path = __toESM(path, 1);
31
31
  let _hot_updater_cli_tools = require("@hot-updater/cli-tools");
32
- let _hot_updater_server = require("@hot-updater/server");
32
+ let _hot_updater_server_db = require("@hot-updater/server/db");
33
33
  let cloudflare = require("cloudflare");
34
34
  cloudflare = __toESM(cloudflare, 1);
35
- let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
36
- let _hot_updater_plugin_core_internal = require("@hot-updater/plugin-core/internal");
35
+ let _hot_updater_server_database = require("@hot-updater/server/database");
36
+ let _hot_updater_server_plugins_api_keys = require("@hot-updater/server/plugins/api-keys");
37
+ let _hot_updater_server_plugins_insights = require("@hot-updater/server/plugins/insights");
37
38
  let node_url = require("node:url");
38
39
  let node_child_process = require("node:child_process");
39
40
  let node_string_decoder = require("node:string_decoder");
@@ -57,934 +58,117 @@ let node_buffer = require("node:buffer");
57
58
  let cloudflare_error = require("cloudflare/error");
58
59
  let os = require("os");
59
60
  os = __toESM(os, 1);
60
- //#region src/d1Rows.ts
61
- var InvalidD1RowError = class extends Error {
62
- name = "InvalidD1RowError";
63
- model;
64
- constructor(model) {
65
- super(`D1 returned an invalid ${model} row`);
66
- this.model = model;
67
- }
68
- };
69
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
70
- const stringValue = (row, field, model) => {
71
- const value = row[field];
72
- if (typeof value !== "string") throw new InvalidD1RowError(model);
73
- return value;
74
- };
75
- const nullableString = (row, field, model) => {
76
- const value = row[field];
77
- if (value === null) return null;
78
- if (typeof value !== "string") throw new InvalidD1RowError(model);
79
- return value;
80
- };
81
- const numberValue = (row, field, model) => {
82
- const value = row[field];
83
- if (typeof value !== "number") throw new InvalidD1RowError(model);
84
- return value;
85
- };
86
- const byteSizeValue = (row, field, model) => {
87
- const value = row[field];
88
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new InvalidD1RowError(model);
89
- return value;
90
- };
91
- const booleanValue = (row, field, model) => {
92
- const value = row[field];
93
- if (value === true || value === 1) return true;
94
- if (value === false || value === 0) return false;
95
- throw new InvalidD1RowError(model);
96
- };
97
- const jsonValue = (value, model) => {
98
- if (typeof value !== "string") throw new InvalidD1RowError(model);
99
- try {
100
- return JSON.parse(value);
101
- } catch (error) {
102
- if (error instanceof SyntaxError) throw new InvalidD1RowError(model);
103
- throw error;
104
- }
105
- };
106
- const metadata = (row) => {
107
- const value = jsonValue(row["metadata"], "bundles");
108
- if (!(0, _hot_updater_plugin_core.isDatabaseMetadataObject)(value)) throw new InvalidD1RowError("bundles");
109
- return value;
110
- };
111
- const bundleRow = (row) => {
112
- const platform = stringValue(row, "platform", "bundles");
113
- if (platform !== "ios" && platform !== "android") throw new InvalidD1RowError("bundles");
114
- return {
115
- id: stringValue(row, "id", "bundles"),
116
- platform,
117
- file_hash: stringValue(row, "file_hash", "bundles"),
118
- git_commit_hash: nullableString(row, "git_commit_hash", "bundles"),
119
- storage_uri: stringValue(row, "storage_uri", "bundles"),
120
- archive_byte_size: row["archive_byte_size"] === void 0 ? 0 : byteSizeValue(row, "archive_byte_size", "bundles"),
121
- metadata: metadata(row),
122
- manifest_storage_uri: nullableString(row, "manifest_storage_uri", "bundles"),
123
- manifest_file_hash: nullableString(row, "manifest_file_hash", "bundles"),
124
- asset_base_storage_uri: nullableString(row, "asset_base_storage_uri", "bundles")
125
- };
126
- };
127
- const patchRow = (row) => ({
128
- id: stringValue(row, "id", "bundle_patches"),
129
- bundle_id: stringValue(row, "bundle_id", "bundle_patches"),
130
- base_bundle_id: stringValue(row, "base_bundle_id", "bundle_patches"),
131
- base_file_hash: stringValue(row, "base_file_hash", "bundle_patches"),
132
- patch_file_hash: stringValue(row, "patch_file_hash", "bundle_patches"),
133
- patch_storage_uri: stringValue(row, "patch_storage_uri", "bundle_patches"),
134
- byte_size: byteSizeValue(row, "byte_size", "bundle_patches"),
135
- order_index: numberValue(row, "order_index", "bundle_patches")
136
- });
137
- const channelRow = (row) => ({
138
- id: stringValue(row, "id", "channels"),
139
- name: stringValue(row, "name", "channels")
140
- });
141
- const eventRow = (row) => {
142
- const type = stringValue(row, "type", "bundle_events");
143
- const platform = stringValue(row, "platform", "bundle_events");
144
- const fromBundleId = nullableString(row, "from_bundle_id", "bundle_events");
145
- const metadata = jsonValue(row["metadata"], "bundle_events");
146
- if (!(0, _hot_updater_plugin_core.isDatabaseBundleEventMetadata)(metadata)) throw new InvalidD1RowError("bundle_events");
147
- const updateStrategy = metadata.update_strategy;
148
- if (platform !== "ios" && platform !== "android" || !((type === "UPDATE_DOWNLOADED" || type === "UPDATE_APPLIED" || type === "RECOVERED") && typeof fromBundleId === "string" && (updateStrategy === "fingerprint" || updateStrategy === "appVersion") || type === "UNCHANGED" && fromBundleId === null && updateStrategy === null)) throw new InvalidD1RowError("bundle_events");
149
- return {
150
- id: stringValue(row, "id", "bundle_events"),
151
- type,
152
- install_id: stringValue(row, "install_id", "bundle_events"),
153
- user_id: nullableString(row, "user_id", "bundle_events"),
154
- from_release_id: nullableString(row, "from_release_id", "bundle_events"),
155
- from_bundle_id: fromBundleId,
156
- to_release_id: nullableString(row, "to_release_id", "bundle_events"),
157
- to_bundle_id: stringValue(row, "to_bundle_id", "bundle_events"),
158
- platform,
159
- app_version: stringValue(row, "app_version", "bundle_events"),
160
- channel: stringValue(row, "channel", "bundle_events"),
161
- metadata,
162
- received_at_ms: numberValue(row, "received_at_ms", "bundle_events")
163
- };
164
- };
165
- const apiKeyRow = (row) => {
166
- const role = stringValue(row, "role", "api_keys");
167
- if (role !== "client") throw new InvalidD1RowError("api_keys");
168
- const revokedAt = row["revoked_at_ms"];
169
- return {
170
- id: stringValue(row, "id", "api_keys"),
171
- hash: stringValue(row, "hash", "api_keys"),
172
- name: stringValue(row, "name", "api_keys"),
173
- prefix: stringValue(row, "prefix", "api_keys"),
174
- role,
175
- created_at_ms: numberValue(row, "created_at_ms", "api_keys"),
176
- revoked_at_ms: revokedAt === null ? null : numberValue(row, "revoked_at_ms", "api_keys")
177
- };
178
- };
179
- const releaseTargetCohorts = (row) => {
180
- const value = jsonValue(row["target_cohorts"], "releases");
181
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw new InvalidD1RowError("releases");
182
- return value.filter((item) => typeof item === "string");
183
- };
184
- const releaseRow = (row) => {
185
- const platform = stringValue(row, "platform", "releases");
186
- const kind = stringValue(row, "kind", "releases");
187
- const strategy = stringValue(row, "strategy", "releases");
188
- const operation = stringValue(row, "operation", "releases");
189
- if (platform !== "ios" && platform !== "android" || kind !== "BUNDLE" && kind !== "EMBEDDED" || strategy !== "APP_VERSION" && strategy !== "FINGERPRINT" || operation !== "DEPLOY" && operation !== "PROMOTE" && operation !== "ROLLBACK") throw new InvalidD1RowError("releases");
190
- return {
191
- id: stringValue(row, "id", "releases"),
192
- revision: numberValue(row, "revision", "releases"),
193
- scope_key: stringValue(row, "scope_key", "releases"),
194
- channel_id: stringValue(row, "channel_id", "releases"),
195
- platform,
196
- kind,
197
- bundle_id: nullableString(row, "bundle_id", "releases"),
198
- strategy,
199
- target_app_version: nullableString(row, "target_app_version", "releases"),
200
- fingerprint_hash: nullableString(row, "fingerprint_hash", "releases"),
201
- enabled: booleanValue(row, "enabled", "releases"),
202
- should_force_update: booleanValue(row, "should_force_update", "releases"),
203
- message: nullableString(row, "message", "releases"),
204
- rollout_cohort_count: numberValue(row, "rollout_cohort_count", "releases"),
205
- target_cohorts: releaseTargetCohorts(row),
206
- operation,
207
- source_release_id: nullableString(row, "source_release_id", "releases"),
208
- created_at_ms: numberValue(row, "created_at_ms", "releases"),
209
- updated_at_ms: numberValue(row, "updated_at_ms", "releases")
210
- };
211
- };
212
- const releaseCatalogRow = (row) => {
213
- const platform = stringValue(row, "platform", "release_catalogs");
214
- const strategy = stringValue(row, "strategy", "release_catalogs");
215
- if (platform !== "ios" && platform !== "android" || strategy !== "APP_VERSION" && strategy !== "FINGERPRINT") throw new InvalidD1RowError("release_catalogs");
216
- return {
217
- scope_key: stringValue(row, "scope_key", "release_catalogs"),
218
- catalog_id: stringValue(row, "catalog_id", "release_catalogs"),
219
- strategy,
220
- channel_id: stringValue(row, "channel_id", "release_catalogs"),
221
- channel_key: stringValue(row, "channel_key", "release_catalogs"),
222
- platform,
223
- fingerprint_hash: nullableString(row, "fingerprint_hash", "release_catalogs"),
224
- generation: numberValue(row, "generation", "release_catalogs"),
225
- payload: stringValue(row, "payload", "release_catalogs"),
226
- catalog_hash: stringValue(row, "catalog_hash", "release_catalogs"),
227
- byte_size: numberValue(row, "byte_size", "release_catalogs"),
228
- is_tombstone: booleanValue(row, "is_tombstone", "release_catalogs"),
229
- updated_at_ms: numberValue(row, "updated_at_ms", "release_catalogs")
230
- };
231
- };
232
- function parseD1Row(model, value) {
233
- if (!isRecord(value)) throw new InvalidD1RowError(model);
234
- switch (model) {
235
- case "bundles": return bundleRow(value);
236
- case "bundle_patches": return patchRow(value);
237
- case "channels": return channelRow(value);
238
- case "bundle_events": return eventRow(value);
239
- case "api_keys": return apiKeyRow(value);
240
- case "releases": return releaseRow(value);
241
- case "release_catalogs": return releaseCatalogRow(value);
242
- }
243
- }
244
- //#endregion
245
- //#region src/d1Sql.ts
246
- var InvalidD1PredicateError = class extends Error {
247
- name = "InvalidD1PredicateError";
248
- };
249
- const encodeD1Value = (value) => {
250
- const encoded = JSON.stringify(value);
251
- if (encoded === void 0) throw new TypeError("D1 values must be JSON-serializable.");
252
- return encoded;
253
- };
254
- const bind = (value) => ({
255
- sql: "json_extract(?, '$')",
256
- params: [encodeD1Value(value)]
257
- });
258
- const escapeLikePattern = (value) => value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
259
- const stringPredicate = (condition, operator) => {
260
- if (typeof condition.value !== "string") throw new InvalidD1PredicateError();
261
- if (condition.mode !== "insensitive") {
262
- const parameter = bind(condition.value);
263
- const column = `${condition.field} COLLATE BINARY`;
264
- const value = `${parameter.sql} COLLATE BINARY`;
265
- if (operator === "contains" || operator === "starts_with") return {
266
- sql: `instr(${column}, ${value}) ${operator === "contains" ? "> 0" : "= 1"}`,
267
- params: parameter.params
268
- };
269
- return {
270
- sql: `substr(${column}, length(${condition.field}) - length(${parameter.sql}) + 1) = ${value}`,
271
- params: [...parameter.params, ...parameter.params]
272
- };
273
- }
274
- const literal = escapeLikePattern(condition.value);
275
- const pattern = operator === "contains" ? `%${literal}%` : operator === "starts_with" ? `${literal}%` : `%${literal}`;
276
- const parameter = bind(pattern);
277
- return {
278
- sql: `lower(${condition.field}) LIKE lower(${parameter.sql}) ESCAPE '\\'`,
279
- params: parameter.params
280
- };
281
- };
282
- const predicate = (condition) => {
283
- const operator = condition.operator ?? "eq";
284
- switch (operator) {
285
- case "eq":
286
- case "ne": {
287
- if (condition.value === null) return {
288
- sql: `${condition.field} IS ${operator === "ne" ? "NOT " : ""}NULL`,
289
- params: []
290
- };
291
- const parameter = bind(condition.value);
292
- const insensitive = "mode" in condition && condition.mode === "insensitive";
293
- const column = insensitive ? `lower(${condition.field})` : condition.field;
294
- const value = insensitive ? `lower(${parameter.sql})` : parameter.sql;
295
- return {
296
- sql: `${column} ${operator === "eq" ? "=" : "<>"} ${value}`,
297
- params: parameter.params
298
- };
299
- }
300
- case "gt":
301
- case "gte":
302
- case "lt":
303
- case "lte": {
304
- const sqlOperator = {
305
- gt: ">",
306
- gte: ">=",
307
- lt: "<",
308
- lte: "<="
309
- }[operator];
310
- const parameter = bind(condition.value);
311
- return {
312
- sql: `${condition.field} ${sqlOperator} ${parameter.sql}`,
313
- params: parameter.params
314
- };
315
- }
316
- case "in":
317
- case "not_in":
318
- if (!Array.isArray(condition.value)) throw new InvalidD1PredicateError();
319
- if (condition.value.length === 0) return {
320
- sql: operator === "not_in" ? "1 = 1" : "1 = 0",
321
- params: []
322
- };
323
- return {
324
- sql: `${condition.field} ${operator === "not_in" ? "NOT " : ""}IN (SELECT value FROM json_each(?))`,
325
- params: [encodeD1Value(condition.value)]
326
- };
327
- case "contains":
328
- case "starts_with":
329
- case "ends_with": return stringPredicate(condition, operator);
330
- }
331
- };
332
- const buildD1Where = (where) => {
333
- const [first, ...rest] = where ?? [];
334
- if (first === void 0) return {
335
- sql: "",
336
- params: []
337
- };
338
- const initial = predicate(first);
339
- let sql = initial.sql;
340
- const params = [...initial.params];
341
- for (const condition of rest) {
342
- const next = predicate(condition);
343
- sql = `(${sql} ${condition.connector === "OR" ? "OR" : "AND"} ${next.sql})`;
344
- params.push(...next.params);
345
- }
346
- return {
347
- sql: ` WHERE ${sql}`,
348
- params
349
- };
350
- };
351
- const buildD1Order = (orderBy) => orderBy === void 0 ? "" : ` ORDER BY ${orderBy.map((clause) => `${clause.field} ${clause.direction.toUpperCase()}${clause.nulls ? ` NULLS ${clause.nulls.toUpperCase()}` : ""}`).join(", ")}`;
352
- const d1Placeholders = (count) => Array.from({ length: count }, () => "json_extract(?, '$')").join(", ");
353
- const encodeD1Values = (values) => values.map(encodeD1Value);
354
- //#endregion
355
- //#region src/d1Query.ts
356
- const d1TableNames = {
357
- bundles: "bundles",
358
- bundle_patches: "bundle_patches",
359
- channels: "channels",
360
- bundle_events: "bundle_events",
361
- api_keys: "api_keys",
362
- release_catalogs: "release_catalogs",
363
- releases: "releases"
364
- };
365
- var D1QueryResultError = class extends Error {
366
- result;
367
- name = "D1QueryResultError";
368
- operation = "count";
369
- constructor(result) {
370
- super("D1 count query returned a malformed first row");
371
- this.result = result;
372
- }
373
- };
374
- const countD1Rows = async (executor, input) => {
375
- const where = buildD1Where(input.where);
376
- const table = d1TableNames[input.model];
377
- const source = input.distinct === void 0 ? `${table}${where.sql}` : `(SELECT DISTINCT ${input.distinct.join(", ")} FROM ${table}${where.sql}) AS distinct_rows`;
378
- const row = (await executor.query(`SELECT COUNT(*) AS count FROM ${source}`, where.params))[0];
379
- if (typeof row !== "object" || row === null || Array.isArray(row)) throw new D1QueryResultError(row);
380
- const count = Reflect.get(row, "count");
381
- if (typeof count !== "number" || !Number.isFinite(count)) throw new D1QueryResultError(row);
382
- return count;
383
- };
384
- const findManyD1Rows = async (executor, input) => {
385
- const where = buildD1Where(input.where);
386
- const orderBy = input.orderBy;
387
- const order = buildD1Order(orderBy);
388
- const table = d1TableNames[input.model];
389
- 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}`;
390
- const pageParams = encodeD1Values([input.limit, input.offset]);
391
- const rows = await executor.query(`${source} LIMIT json_extract(?, '$') OFFSET json_extract(?, '$')`, [...where.params, ...pageParams]);
392
- switch (input.model) {
393
- case "bundles": return rows.map((row) => parseD1Row("bundles", row));
394
- case "bundle_patches": return rows.map((row) => parseD1Row("bundle_patches", row));
395
- case "channels": return rows.map((row) => parseD1Row("channels", row));
396
- case "bundle_events": return rows.map((row) => parseD1Row("bundle_events", row));
397
- case "api_keys": return rows.map((row) => parseD1Row("api_keys", row));
398
- case "releases": return rows.map((row) => parseD1Row("releases", row));
399
- case "release_catalogs": return rows.map((row) => parseD1Row("release_catalogs", row));
400
- }
401
- };
402
- //#endregion
403
- //#region src/d1Implementation.ts
61
+ //#region src/d1Executor.ts
404
62
  var D1ExecutionError = class extends Error {
405
63
  name = "D1ExecutionError";
406
64
  constructor() {
407
65
  super("D1 did not successfully execute every requested statement");
408
66
  }
409
67
  };
410
- const EXPECTATION_CONFLICT_MARKER = "HOT_UPDATER_COMMIT_EXPECTATION_CONFLICT";
411
- const expectationGuard = (expectations) => {
412
- const clauses = [];
413
- const params = [];
414
- for (const expectation of expectations) {
415
- const isRelease = expectation.model === "releases";
416
- const table = isRelease ? "releases" : "release_catalogs";
417
- const keyField = isRelease ? "id" : "scope_key";
418
- const versionField = isRelease ? "revision" : "generation";
419
- const key = isRelease ? expectation.id : expectation.scopeKey;
420
- const version = isRelease ? expectation.revision : expectation.generation;
421
- if (version === null) {
422
- clauses.push(`NOT EXISTS (SELECT 1 FROM ${table} WHERE ${keyField} = json_extract(?, '$'))`);
423
- params.push(...encodeD1Values([key]));
424
- } else {
425
- clauses.push(`EXISTS (SELECT 1 FROM ${table} WHERE ${keyField} = json_extract(?, '$') AND ${versionField} = json_extract(?, '$'))`);
426
- params.push(...encodeD1Values([key, version]));
427
- }
428
- }
68
+ const toSqlResult = (result) => {
69
+ if (result.success === false) throw new D1ExecutionError();
429
70
  return {
430
- sql: clauses.join(" AND ") || "1",
431
- params
71
+ rows: result.results ?? [],
72
+ changes: result.meta?.changes ?? 0
432
73
  };
433
74
  };
434
- const readVersion = (row, field) => {
435
- if (typeof row !== "object" || row === null) return null;
436
- const value = Reflect.get(row, field);
437
- return typeof value === "number" ? value : null;
438
- };
439
- const expectationConflict = async (executor, expectations) => {
440
- for (const expectation of expectations) {
441
- const isRelease = expectation.model === "releases";
442
- const table = isRelease ? "releases" : "release_catalogs";
443
- const keyField = isRelease ? "id" : "scope_key";
444
- const versionField = isRelease ? "revision" : "generation";
445
- const key = isRelease ? expectation.id : expectation.scopeKey;
446
- const expectedVersion = isRelease ? expectation.revision : expectation.generation;
447
- const rows = await executor.query(`SELECT ${versionField} FROM ${table} WHERE ${keyField} = json_extract(?, '$') LIMIT 1`, encodeD1Values([key]));
448
- const actualVersion = readVersion(rows[0], versionField);
449
- if (actualVersion !== expectedVersion) return {
450
- committed: false,
451
- conflict: {
452
- actualVersion,
453
- changeIndex: -1,
454
- expectedVersion,
455
- key,
456
- model: expectation.model,
457
- reason: "version_conflict"
458
- }
459
- };
460
- }
461
- return null;
462
- };
463
- const isExpectationConflictError = (error) => {
75
+ /** D1 reports SQLite errors in the message; the SQL core classifies codes. */
76
+ const withSqliteCode = (error) => {
464
77
  const message = error instanceof Error ? error.message : String(error);
465
- return message.includes(EXPECTATION_CONFLICT_MARKER) || message.toLowerCase().includes("malformed json");
466
- };
467
- const bundleValues = (row) => [
468
- row.id,
469
- row.platform,
470
- row.file_hash,
471
- row.git_commit_hash,
472
- row.storage_uri,
473
- row.archive_byte_size,
474
- row.metadata,
475
- row.manifest_storage_uri,
476
- row.manifest_file_hash,
477
- row.asset_base_storage_uri
478
- ];
479
- const patchValues = (row) => [
480
- row.id,
481
- row.bundle_id,
482
- row.base_bundle_id,
483
- row.base_file_hash,
484
- row.patch_file_hash,
485
- row.patch_storage_uri,
486
- row.byte_size,
487
- row.order_index
488
- ];
489
- const channelValues = (row) => [row.id, row.name];
490
- const insertQuery = (input, guard, conflictMode = "returnExisting") => {
491
- let columns;
492
- let values;
493
- switch (input.model) {
494
- case "bundles":
495
- columns = [
496
- "id",
497
- "platform",
498
- "file_hash",
499
- "git_commit_hash",
500
- "storage_uri",
501
- "archive_byte_size",
502
- "metadata",
503
- "manifest_storage_uri",
504
- "manifest_file_hash",
505
- "asset_base_storage_uri"
506
- ];
507
- values = bundleValues(input.data);
508
- break;
509
- case "bundle_patches":
510
- columns = [
511
- "id",
512
- "bundle_id",
513
- "base_bundle_id",
514
- "base_file_hash",
515
- "patch_file_hash",
516
- "patch_storage_uri",
517
- "byte_size",
518
- "order_index"
519
- ];
520
- values = patchValues(input.data);
521
- break;
522
- case "channels":
523
- columns = ["id", "name"];
524
- values = channelValues(input.data);
525
- break;
526
- case "bundle_events":
527
- case "api_keys":
528
- case "release_catalogs":
529
- case "releases":
530
- columns = Object.keys(input.data);
531
- values = Object.values(input.data);
532
- }
533
- 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";
534
- return {
535
- sql: `INSERT INTO ${d1TableNames[input.model]} (${columns.join(", ")}) ${guard === void 0 ? `VALUES (${d1Placeholders(values.length)})` : `SELECT ${d1Placeholders(values.length)} WHERE ${guard.sql}`}${conflict} RETURNING *`,
536
- params: [...encodeD1Values(values), ...guard?.params ?? []]
537
- };
538
- };
539
- const updateEntries = (update) => Object.entries(update);
540
- const updateQuery = (input, guard) => {
541
- const entries = updateEntries(input.update);
542
- const where = buildD1Where(input.where);
543
- if (entries.length === 0) return {
544
- sql: `SELECT * FROM ${d1TableNames[input.model]}${where.sql}${guard ? ` AND ${guard.sql}` : ""} LIMIT 1`,
545
- params: [...where.params, ...guard?.params ?? []]
546
- };
547
- const assignments = entries.map(([field]) => `${field} = json_extract(?, '$')`).join(", ");
548
- return {
549
- sql: `UPDATE ${d1TableNames[input.model]} SET ${assignments}${where.sql}${guard ? ` AND ${guard.sql}` : ""} RETURNING *`,
550
- params: [
551
- ...encodeD1Values(entries.map(([, value]) => value)),
552
- ...where.params,
553
- ...guard?.params ?? []
554
- ]
555
- };
556
- };
557
- const deleteQuery = (input, guard) => {
558
- const where = buildD1Where(input.where);
559
- return {
560
- sql: `DELETE FROM ${d1TableNames[input.model]}${where.sql}${guard ? ` AND ${guard.sql}` : ""}`,
561
- params: [...where.params, ...guard?.params ?? []]
562
- };
563
- };
564
- const insertedKey = (change) => {
565
- if (change.operation !== "insert") return void 0;
566
- if (change.model === "bundles") return `bundles:${change.row.id}`;
567
- if (change.model === "apiKeys") return `api_keys:${change.row.id}`;
568
- if (change.model === "releases") return `releases:${change.row.id}`;
569
- };
570
- const requiredRow = (change, changeIndex) => {
571
- if (change.operation !== "update") return void 0;
572
- if (change.model === "bundles") return {
573
- model: "bundles",
574
- id: change.where.id,
575
- changeIndex
576
- };
577
- if (change.model === "apiKeys") return {
578
- model: "api_keys",
579
- id: change.where.id,
580
- changeIndex
581
- };
582
- if (change.model === "releases") return {
583
- model: "releases",
584
- id: change.where.id,
585
- changeIndex
586
- };
587
- };
588
- const channelDeletePrecondition = (changes, changeIndex, channelId) => {
589
- const releaseEffects = /* @__PURE__ */ new Map();
590
- for (const change of changes.slice(0, changeIndex)) {
591
- if (change.model !== "releases") continue;
592
- switch (change.operation) {
593
- case "insert":
594
- releaseEffects.set(change.row.id, change.row.channel_id);
595
- break;
596
- case "update": break;
597
- case "delete": releaseEffects.set(change.where.id, null);
598
- }
599
- }
600
- return {
601
- id: channelId,
602
- excludedReleaseIds: [...releaseEffects.keys()],
603
- addedReferenceReleaseIds: [...releaseEffects].filter(([, finalChannelId]) => finalChannelId === channelId).map(([releaseId]) => releaseId)
604
- };
605
- };
606
- const changeQuery = (change, guard) => {
607
- switch (change.model) {
608
- case "bundles": switch (change.operation) {
609
- case "insert": return insertQuery({
610
- model: "bundles",
611
- data: change.row
612
- }, guard);
613
- case "update": return updateQuery({
614
- model: "bundles",
615
- where: [{
616
- field: "id",
617
- value: change.where.id
618
- }],
619
- update: change.update
620
- }, guard);
621
- case "delete": return deleteQuery({
622
- model: "bundles",
623
- where: [{
624
- field: "id",
625
- value: change.where.id
626
- }]
627
- }, guard);
628
- }
629
- case "bundlePatches": return change.operation === "insert" ? insertQuery({
630
- model: "bundle_patches",
631
- data: change.row
632
- }, guard) : deleteQuery({
633
- model: "bundle_patches",
634
- where: [{
635
- field: "bundle_id",
636
- value: change.where.bundleId
637
- }]
638
- }, guard);
639
- case "releases": switch (change.operation) {
640
- case "insert": return insertQuery({
641
- model: "releases",
642
- data: change.row
643
- }, guard);
644
- case "update": return updateQuery({
645
- model: "releases",
646
- where: [{
647
- field: "id",
648
- value: change.where.id
649
- }],
650
- update: change.update
651
- }, guard);
652
- case "delete": return deleteQuery({
653
- model: "releases",
654
- where: [{
655
- field: "id",
656
- value: change.where.id
657
- }]
658
- }, guard);
659
- }
660
- case "releaseCatalogs": return {
661
- 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 *`,
662
- params: [...encodeD1Values(Object.values(change.row)), ...guard.params]
663
- };
664
- case "channels": return change.operation === "insert" ? insertQuery({
665
- model: "channels",
666
- data: change.row,
667
- onConflict: change.onConflict
668
- }, guard, "ignore") : {
669
- sql: `DELETE FROM channels WHERE id = json_extract(?, '$') AND NOT EXISTS (SELECT 1 FROM releases WHERE channel_id = json_extract(?, '$')) AND ${guard.sql}`,
670
- params: [...encodeD1Values([change.where.id, change.where.id]), ...guard.params]
671
- };
672
- case "apiKeys": return change.operation === "insert" ? insertQuery({
673
- model: "api_keys",
674
- data: change.row,
675
- onConflict: change.onConflict
676
- }, guard, "ignore") : updateQuery({
677
- model: "api_keys",
678
- where: [{
679
- field: "id",
680
- value: change.where.id
681
- }],
682
- update: { revoked_at_ms: change.update.revokedAtMs }
683
- }, guard);
684
- }
685
- };
686
- const createCommitPlan = (input) => {
687
- const checks = [];
688
- const statements = [];
689
- const expectations = input.expectations ?? [];
690
- if (expectations.length > 0) {
691
- const guard = expectationGuard(expectations);
692
- statements.push({
693
- sql: `SELECT CASE WHEN ${guard.sql} THEN 1 ELSE json_extract('${EXPECTATION_CONFLICT_MARKER}', '$') END AS expectation_guard`,
694
- params: guard.params
695
- });
696
- }
697
- const requiredRows = [];
698
- const channelDeletes = [];
699
- const inserted = /* @__PURE__ */ new Set();
700
- for (const [changeIndex, change] of input.changes.entries()) {
701
- const required = requiredRow(change, changeIndex);
702
- if (required !== void 0) {
703
- if (!inserted.has(`${required.model}:${required.id}`)) {
704
- requiredRows.push(required);
705
- checks.push({
706
- changeIndex,
707
- resultIndex: statements.length,
708
- conflictWhen: "empty",
709
- reason: "not_found"
710
- });
711
- statements.push({
712
- sql: `SELECT id FROM ${d1TableNames[required.model]} WHERE id = json_extract(?, '$') LIMIT 1`,
713
- params: encodeD1Values([required.id])
714
- });
715
- }
716
- }
717
- if (change.model === "channels" && change.operation === "delete") {
718
- const precondition = channelDeletePrecondition(input.changes, changeIndex, change.where.id);
719
- channelDeletes.push(precondition);
720
- checks.push({
721
- changeIndex,
722
- resultIndex: statements.length,
723
- conflictWhen: "nonempty",
724
- reason: "referenced"
725
- });
726
- statements.push({
727
- 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",
728
- params: encodeD1Values([
729
- precondition.id,
730
- precondition.excludedReleaseIds,
731
- precondition.addedReferenceReleaseIds
732
- ])
733
- });
734
- }
735
- const key = insertedKey(change);
736
- if (key !== void 0) inserted.add(key);
737
- }
738
- const guard = {
739
- sql: `NOT EXISTS (
740
- SELECT 1 FROM json_each(?) AS required
741
- WHERE NOT EXISTS (
742
- SELECT 1 FROM bundles
743
- WHERE json_extract(required.value, '$.model') = 'bundles'
744
- AND bundles.id = json_extract(required.value, '$.id')
745
- UNION ALL
746
- SELECT 1 FROM api_keys
747
- WHERE json_extract(required.value, '$.model') = 'api_keys'
748
- AND api_keys.id = json_extract(required.value, '$.id')
749
- UNION ALL
750
- SELECT 1 FROM channels
751
- WHERE json_extract(required.value, '$.model') = 'channels'
752
- AND channels.id = json_extract(required.value, '$.id')
753
- UNION ALL
754
- SELECT 1 FROM releases
755
- WHERE json_extract(required.value, '$.model') = 'releases'
756
- AND releases.id = json_extract(required.value, '$.id')
757
- )
758
- ) AND NOT EXISTS (
759
- SELECT 1 FROM json_each(?) AS channel_delete
760
- WHERE EXISTS (
761
- SELECT 1 FROM releases
762
- WHERE releases.channel_id = json_extract(channel_delete.value, '$.id')
763
- AND releases.id NOT IN (
764
- SELECT value FROM json_each(
765
- json_extract(channel_delete.value, '$.excludedReleaseIds')
766
- )
767
- )
768
- UNION ALL
769
- SELECT value FROM json_each(
770
- json_extract(channel_delete.value, '$.addedReferenceReleaseIds')
771
- )
772
- )
773
- )`,
774
- params: encodeD1Values([requiredRows.map(({ model, id }) => ({
775
- model,
776
- id
777
- })), channelDeletes])
778
- };
779
- statements.push(...input.changes.map((change) => changeQuery(change, guard)));
780
- return {
781
- checks,
782
- statements
783
- };
784
- };
785
- const resultForPlan = (plan, results) => {
786
- const missing = plan.checks.find(({ conflictWhen, resultIndex }) => conflictWhen === "empty" ? (results[resultIndex]?.length ?? 0) === 0 : (results[resultIndex]?.length ?? 0) > 0);
787
- return missing === void 0 ? { committed: true } : {
788
- committed: false,
789
- conflict: {
790
- changeIndex: missing.changeIndex,
791
- reason: missing.reason
792
- }
793
- };
78
+ 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;
79
+ return code === void 0 ? error : Object.assign(new Error(message, { cause: error }), { code });
794
80
  };
795
- const insertChannel = async (executor, input) => {
796
- const insert = insertQuery({
797
- model: "channels",
798
- data: input.row,
799
- onConflict: "ignore"
800
- }, void 0, "ignore");
801
- const select = {
802
- sql: "SELECT id, name FROM channels WHERE name = json_extract(?, '$') LIMIT 1",
803
- params: encodeD1Values([input.row.name])
804
- };
805
- const [insertedRows = [], canonicalRows = []] = await executor.batch([insert, select]);
806
- return {
807
- row: parseD1Row("channels", canonicalRows[0]),
808
- inserted: insertedRows.length > 0
809
- };
810
- };
811
- const deleteChannel = async (executor, input) => {
812
- const [channelRows = [], referenceRows = [], deletedRows = []] = await executor.batch([
813
- {
814
- sql: "SELECT id FROM channels WHERE id = json_extract(?, '$') LIMIT 1",
815
- params: encodeD1Values([input.id])
816
- },
817
- {
818
- sql: "SELECT id FROM releases WHERE channel_id = json_extract(?, '$') LIMIT 1",
819
- params: encodeD1Values([input.id])
820
- },
821
- {
822
- sql: "DELETE FROM channels WHERE id = json_extract(?, '$') AND NOT EXISTS (SELECT 1 FROM releases WHERE channel_id = json_extract(?, '$')) RETURNING id",
823
- params: encodeD1Values([input.id, input.id])
824
- }
825
- ]);
826
- if (channelRows.length === 0) return {
827
- deleted: false,
828
- reason: "not_found"
829
- };
830
- if (referenceRows.length > 0) return {
831
- deleted: false,
832
- reason: "not_empty"
833
- };
834
- return deletedRows.length > 0 ? { deleted: true } : {
835
- deleted: false,
836
- reason: "not_empty"
837
- };
838
- };
839
- const createD1Implementation = (executor) => ({
840
- async recordInsights({ event }) {
841
- const query = insertQuery({
842
- model: "bundle_events",
843
- data: event
844
- });
845
- await executor.batch([{
846
- sql: query.sql.replace(" RETURNING *", " ON CONFLICT(id) DO NOTHING"),
847
- params: query.params
848
- }, {
849
- sql: `INSERT INTO bundle_event_heads (install_id, id, received_at_ms, user_id, platform, channel, type, from_bundle_id, to_bundle_id)
850
- SELECT install_id, id, received_at_ms, user_id, platform, channel, type, from_bundle_id, to_bundle_id
851
- FROM bundle_events WHERE id = json_extract(?, '$')
852
- ON CONFLICT(install_id) DO UPDATE SET
853
- id = excluded.id, received_at_ms = excluded.received_at_ms, user_id = excluded.user_id,
854
- platform = excluded.platform, channel = excluded.channel, type = excluded.type,
855
- from_bundle_id = excluded.from_bundle_id, to_bundle_id = excluded.to_bundle_id
856
- WHERE (excluded.received_at_ms, excluded.id) > (bundle_event_heads.received_at_ms, bundle_event_heads.id)`,
857
- params: encodeD1Values([event.id])
858
- }]);
859
- },
860
- async findLatestInsightsEvents(input) {
861
- const where = buildD1Where((0, _hot_updater_plugin_core_internal.latestInsightsWhere)(input));
862
- return (await executor.query(`SELECT event.* FROM (SELECT id, install_id FROM bundle_event_heads${where.sql} ORDER BY install_id ASC LIMIT json_extract(?, '$')) AS head JOIN bundle_events AS event ON event.id = head.id ORDER BY head.install_id ASC`, [...where.params, ...encodeD1Values(["installId" in input ? 1 : input.limit])])).map((row) => parseD1Row("bundle_events", row));
863
- },
864
- async countLatestInsightsEvents(input) {
865
- const groups = (0, _hot_updater_plugin_core_internal.latestInsightsCountGroups)(input).map(buildD1Where);
866
- const where = {
867
- sql: ` WHERE (${groups.map((group) => `(${group.sql.replace(/^ WHERE /, "")})`).join(" OR ")})`,
868
- params: groups.flatMap((group) => group.params)
869
- };
870
- const first = (await executor.query(`SELECT COUNT(*) AS count FROM bundle_event_heads${where.sql}`, where.params))[0];
871
- const count = typeof first === "object" && first !== null ? Reflect.get(first, "count") : void 0;
872
- if (typeof count !== "number") throw new Error("Invalid Insights count result.");
873
- return count;
874
- },
875
- async create(input) {
876
- const query = insertQuery(input);
877
- const rows = await executor.query(query.sql, query.params);
878
- switch (input.model) {
879
- case "bundles": return parseD1Row("bundles", rows[0]);
880
- case "bundle_patches": return parseD1Row("bundle_patches", rows[0]);
881
- case "channels": return parseD1Row("channels", rows[0]);
882
- case "bundle_events": return parseD1Row("bundle_events", rows[0]);
883
- case "api_keys": return parseD1Row("api_keys", rows[0]);
884
- case "releases": return parseD1Row("releases", rows[0]);
885
- case "release_catalogs": return parseD1Row("release_catalogs", rows[0]);
886
- }
887
- },
888
- async update(input) {
889
- const query = updateQuery(input);
890
- const rows = await executor.query(query.sql, query.params);
891
- if (rows[0] === void 0) return null;
892
- switch (input.model) {
893
- case "bundles": return parseD1Row("bundles", rows[0]);
894
- case "api_keys": return parseD1Row("api_keys", rows[0]);
895
- case "releases": return parseD1Row("releases", rows[0]);
896
- case "release_catalogs": return parseD1Row("release_catalogs", rows[0]);
897
- }
898
- },
899
- async delete(input) {
900
- const query = deleteQuery(input);
901
- await executor.query(query.sql, query.params);
902
- },
903
- count: (input) => countD1Rows(executor, input),
904
- async findOne(input) {
905
- const where = buildD1Where(input.where);
906
- const rows = await executor.query(`SELECT * FROM ${d1TableNames[input.model]}${where.sql} LIMIT 1`, where.params);
907
- if (rows[0] === void 0) return null;
908
- switch (input.model) {
909
- case "bundles": return parseD1Row("bundles", rows[0]);
910
- case "bundle_patches": return parseD1Row("bundle_patches", rows[0]);
911
- case "channels": return parseD1Row("channels", rows[0]);
912
- case "api_keys": return parseD1Row("api_keys", rows[0]);
913
- case "releases": return parseD1Row("releases", rows[0]);
914
- case "release_catalogs": return parseD1Row("release_catalogs", rows[0]);
915
- }
916
- },
917
- findMany: (input) => findManyD1Rows(executor, input),
918
- insertChannel: (input) => insertChannel(executor, input),
919
- deleteChannel: (input) => deleteChannel(executor, input),
920
- async commit(input) {
921
- if (input.changes.length === 0) return { committed: true };
922
- const expectations = input.expectations ?? [];
923
- const conflict = await expectationConflict(executor, expectations);
924
- if (conflict !== null) return conflict;
925
- const plan = createCommitPlan(input);
926
- try {
927
- return resultForPlan(plan, await executor.batch(plan.statements));
928
- } catch (error) {
929
- if (expectations.length === 0 || !isExpectationConflictError(error)) throw error;
930
- return await expectationConflict(executor, expectations) ?? {
931
- committed: false,
932
- conflict: {
933
- actualVersion: expectations[0].model === "releases" ? expectations[0].revision : expectations[0].generation,
934
- changeIndex: -1,
935
- expectedVersion: expectations[0].model === "releases" ? expectations[0].revision : expectations[0].generation,
936
- key: expectations[0].model === "releases" ? expectations[0].id : expectations[0].scopeKey,
937
- model: expectations[0].model,
938
- reason: "version_conflict"
81
+ /**
82
+ * Hot Updater's database on D1: the storage engine through the shared SQL
83
+ * core, fenced by the schema settings. D1 has no interactive transactions,
84
+ * so each write is one atomic batch that evaluates its guards first
85
+ * (`_hu_write`).
86
+ */
87
+ const createD1Database = (runner) => {
88
+ return (0, _hot_updater_server_database.createEngineDatabase)({
89
+ name: "d1Database",
90
+ adapter: (0, _hot_updater_server_database.createSqlAdapter)({
91
+ executor: {
92
+ dialect: "sqlite",
93
+ execute: (statement) => runner.query(statement).catch((error) => {
94
+ throw withSqliteCode(error);
95
+ }),
96
+ batch: (statements) => runner.batch(statements).catch((error) => {
97
+ throw withSqliteCode(error);
98
+ }),
99
+ transaction: () => {
100
+ throw new Error("D1 has no interactive transactions; writes are batches.");
939
101
  }
940
- };
941
- }
942
- }
943
- });
102
+ },
103
+ maxOps: 450,
104
+ maxParams: 100
105
+ })
106
+ });
107
+ };
108
+ //#endregion
109
+ //#region src/d1Schema.ts
110
+ /**
111
+ * D1's schema: the guard table batch writes need, then the shared SQL
112
+ * schema, settings last. Every statement can run again, so a migration for a
113
+ * server's plugin tables repeats the built-in ones.
114
+ */
115
+ const d1SchemaStatements = ({ schema, settings } = _hot_updater_server_database.builtInTarget) => [...(0, _hot_updater_server_database.createTableStatements)("sqlite", [_hot_updater_server_database.WRITE_GUARD_TABLE]), ...(0, _hot_updater_server_db.generateEngineSql)("sqlite", schema, settings)];
116
+ /** The schema as a D1 migration, which `wrangler d1 migrations apply` runs. */
117
+ const d1SchemaSql = (target) => `-- HotUpdater.schema\n\n${d1SchemaStatements(target).map((statement) => `${statement};`).join("\n\n")}\n`;
944
118
  //#endregion
945
119
  //#region src/d1Database.ts
120
+ /** The REST API binds text: each value goes as JSON and is read back with `json_extract`. */
121
+ const encode = ({ sql, params }) => ({
122
+ sql: sql.replaceAll("?", () => "json_extract(?, '$')"),
123
+ params: params.map((value) => JSON.stringify(value ?? null))
124
+ });
125
+ /**
126
+ * Hot Updater's database on D1 through the Cloudflare REST API, for the CLI
127
+ * and console. `hot-updater db generate` writes a D1 migration of the built-in
128
+ * tables and the server's plugin tables to `migrations`, and
129
+ * `hot-updater db migrate` applies them through the API.
130
+ */
946
131
  const d1Database = (config) => {
947
132
  const cloudflare$1 = new cloudflare.default({ apiToken: config.cloudflareApiToken });
948
133
  const execute = async (statements) => {
949
- const first = statements[0];
134
+ const [first] = statements;
950
135
  if (first === void 0) return [];
951
136
  const body = statements.length === 1 ? {
952
137
  account_id: config.accountId,
953
- sql: first.sql,
954
- params: [...first.params]
138
+ ...encode(first)
955
139
  } : {
956
140
  account_id: config.accountId,
957
- batch: statements.map(({ sql, params }) => ({
958
- sql,
959
- params: [...params]
960
- }))
141
+ batch: statements.map(encode)
961
142
  };
962
- const page = await cloudflare$1.d1.database.query(config.databaseId, { ...body });
143
+ const page = await cloudflare$1.d1.database.query(config.databaseId, body);
963
144
  const results = [];
964
- for await (const resultPage of page.iterPages()) for (const result of resultPage.result) {
965
- if (result.success === false) throw new D1ExecutionError();
966
- results.push(result.results ?? []);
967
- }
145
+ for await (const resultPage of page.iterPages()) for (const result of resultPage.result) results.push(toSqlResult(result));
968
146
  if (results.length !== statements.length) throw new D1ExecutionError();
969
147
  return results;
970
148
  };
971
- const adapter = (0, _hot_updater_plugin_core_internal.createDatabasePluginAdapter)("d1Database", createD1Implementation({
972
- async query(sql, params) {
973
- return (await execute([{
974
- sql,
975
- params
976
- }])).flat();
977
- },
978
- batch: execute
979
- }));
980
- return (0, _hot_updater_plugin_core.createDatabasePlugin)({
981
- name: "d1Database",
982
- models: adapter.models,
983
- commit: adapter.commit,
984
- ...adapter.dispose ? { dispose: adapter.dispose } : {}
985
- });
149
+ return {
150
+ ...createD1Database({
151
+ query: async (statement) => (await execute([statement]))[0],
152
+ batch: execute
153
+ }),
154
+ generateSchema: ((version, _name, target = _hot_updater_server_database.builtInTarget) => {
155
+ if (version !== "latest" && version !== _hot_updater_server_database.builtInSettings["schema.core"]) throw new Error(`Invalid version ${version}`);
156
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\D/g, "").slice(0, 14);
157
+ return {
158
+ code: d1SchemaSql(target),
159
+ path: `migrations/${timestamp}_hot-updater.sql`
160
+ };
161
+ })
162
+ };
986
163
  };
987
164
  //#endregion
165
+ //#region src/plugins.ts
166
+ /**
167
+ * The plugins the managed Cloudflare server runs: Insights, and API keys on every
168
+ * client route. Its Worker imports this list.
169
+ */
170
+ const plugins = [(0, _hot_updater_server_plugins_insights.insights)(), (0, _hot_updater_server_plugins_api_keys.apiKeys)()];
171
+ //#endregion
988
172
  //#region ../../node_modules/.pnpm/is-plain-obj@4.1.0/node_modules/is-plain-obj/index.js
989
173
  function isPlainObject(value) {
990
174
  if (typeof value !== "object" || value === null) return false;
@@ -12638,27 +11822,27 @@ const runInit = async ({ build, envFile }) => {
12638
11822
  r2BucketName: selectedBucketName,
12639
11823
  workerName
12640
11824
  });
12641
- const databasePlugin = d1Database({
11825
+ const database = d1Database({
12642
11826
  accountId,
12643
11827
  cloudflareApiToken: apiToken,
12644
11828
  databaseId: selectedD1DatabaseId
12645
11829
  });
12646
11830
  let apiKey;
12647
11831
  try {
12648
- apiKey = (await (0, _hot_updater_server.provisionApiKey)({
12649
- apiKeys: databasePlugin.models.apiKeys,
11832
+ apiKey = (await (0, _hot_updater_server_db.createDatabasePluginApis)(database, plugins).apiKeys.provision({
12650
11833
  existingApiKey: initInputEnv.HOT_UPDATER_API_KEY,
12651
11834
  name: "Cloudflare init"
12652
11835
  })).apiKey;
12653
11836
  await (0, _hot_updater_cli_tools.makeEnv)({ HOT_UPDATER_API_KEY: apiKey });
12654
11837
  } finally {
12655
- await databasePlugin.dispose?.();
11838
+ await database.dispose?.();
12656
11839
  }
12657
11840
  const configWriteResult = await (0, _hot_updater_cli_tools.writeHotUpdaterConfig)(getConfigScaffold(build));
12658
11841
  _hot_updater_cli_tools.p.log.success("Generated '.env.hotupdater' file with Cloudflare settings.");
12659
11842
  if (configWriteResult.status === "created") _hot_updater_cli_tools.p.log.success("Generated 'hot-updater.config.ts' file with Cloudflare settings.");
12660
11843
  else if (configWriteResult.status === "merged") _hot_updater_cli_tools.p.log.success("Updated 'hot-updater.config.ts' file with Cloudflare settings.");
12661
11844
  else _hot_updater_cli_tools.p.log.warn(`Kept existing 'hot-updater.config.ts' unchanged: ${configWriteResult.reason}`);
11845
+ await (0, _hot_updater_cli_tools.generateHotUpdaterPlugins)("@hot-updater/cloudflare");
12662
11846
  if (subdomains.subdomain) _hot_updater_cli_tools.p.note((0, _hot_updater_cli_tools.transformTemplate)(SOURCE_TEMPLATE, {
12663
11847
  apiKey: JSON.stringify(apiKey),
12664
11848
  source: `https://${workerName}.${subdomains.subdomain}.workers.dev`