@hot-updater/cloudflare 1.0.0-rc.2 → 1.0.0-rc.3

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.
@@ -6,6 +6,20 @@ import {
6
6
  } from "../../../packages/test-utils/src/databaseTestFixtures";
7
7
  import { parseD1Row } from "./d1Rows";
8
8
 
9
+ const installationD1Row = {
10
+ id: "event-1",
11
+ install_id: "install-1",
12
+ user_id: "user-1",
13
+ username: "Demo User",
14
+ to_bundle_id: "bundle-1",
15
+ type: "UPDATE_APPLIED",
16
+ platform: "ios",
17
+ app_version: "1.0.0",
18
+ channel: "production",
19
+ cohort: "cohort-1",
20
+ received_at_ms: 100,
21
+ } as const;
22
+
9
23
  const bundleD1Row = {
10
24
  id: "bundle-1",
11
25
  platform: "android",
@@ -109,3 +123,21 @@ it.each([
109
123
  }),
110
124
  ).toThrow("D1 returned an invalid bundle_events row");
111
125
  });
126
+
127
+ it("parses a current Insights installation", () => {
128
+ expect(parseD1Row("bundle_installations", installationD1Row)).toEqual(
129
+ installationD1Row,
130
+ );
131
+ });
132
+
133
+ it.each([{ type: "UNKNOWN" }, { platform: "web" }])(
134
+ "rejects an invalid current Insights installation",
135
+ (overrides) => {
136
+ expect(() =>
137
+ parseD1Row("bundle_installations", {
138
+ ...installationD1Row,
139
+ ...overrides,
140
+ }),
141
+ ).toThrow("D1 returned an invalid bundle_installations row");
142
+ },
143
+ );
package/src/d1Rows.ts CHANGED
@@ -3,6 +3,7 @@ import type {
3
3
  BundleRow,
4
4
  BundleEventRow,
5
5
  ChannelRow,
6
+ InsightsInstallationRow,
6
7
  ApiKeyRow,
7
8
  ReleaseCatalogRow,
8
9
  ReleaseRow,
@@ -188,6 +189,34 @@ const eventRow = (row: Record<string, unknown>): BundleEventRow => {
188
189
  } as BundleEventRow;
189
190
  };
190
191
 
192
+ const installationRow = (
193
+ row: Record<string, unknown>,
194
+ ): InsightsInstallationRow => {
195
+ const type = stringValue(row, "type", "bundle_installations");
196
+ const platform = stringValue(row, "platform", "bundle_installations");
197
+ if (
198
+ !["UPDATE_APPLIED", "RECOVERED", "RELEASE_ADOPTED", "UNCHANGED"].includes(
199
+ type,
200
+ ) ||
201
+ (platform !== "ios" && platform !== "android")
202
+ ) {
203
+ throw new InvalidD1RowError("bundle_installations");
204
+ }
205
+ return {
206
+ id: stringValue(row, "id", "bundle_installations"),
207
+ install_id: stringValue(row, "install_id", "bundle_installations"),
208
+ user_id: nullableString(row, "user_id", "bundle_installations"),
209
+ username: nullableString(row, "username", "bundle_installations"),
210
+ to_bundle_id: stringValue(row, "to_bundle_id", "bundle_installations"),
211
+ type: type as InsightsInstallationRow["type"],
212
+ platform,
213
+ app_version: stringValue(row, "app_version", "bundle_installations"),
214
+ channel: stringValue(row, "channel", "bundle_installations"),
215
+ cohort: stringValue(row, "cohort", "bundle_installations"),
216
+ received_at_ms: numberValue(row, "received_at_ms", "bundle_installations"),
217
+ };
218
+ };
219
+
191
220
  const apiKeyRow = (row: Record<string, unknown>): ApiKeyRow => {
192
221
  const role = stringValue(row, "role", "api_keys");
193
222
  if (role !== "client") throw new InvalidD1RowError("api_keys");
@@ -292,6 +321,10 @@ export function parseD1Row(
292
321
  model: "bundle_events",
293
322
  value: unknown,
294
323
  ): BundleEventRow;
324
+ export function parseD1Row(
325
+ model: "bundle_installations",
326
+ value: unknown,
327
+ ): InsightsInstallationRow;
295
328
  export function parseD1Row(model: "api_keys", value: unknown): ApiKeyRow;
296
329
  export function parseD1Row(model: "releases", value: unknown): ReleaseRow;
297
330
  export function parseD1Row(
@@ -316,6 +349,8 @@ export function parseD1Row(
316
349
  return channelRow(value);
317
350
  case "bundle_events":
318
351
  return eventRow(value);
352
+ case "bundle_installations":
353
+ return installationRow(value);
319
354
  case "api_keys":
320
355
  return apiKeyRow(value);
321
356
  case "releases":
@@ -1,5 +1,7 @@
1
+ import { toInsightsInstallationRow } from "@hot-updater/plugin-core";
1
2
  import { expect, it } from "vitest";
2
3
 
4
+ import { createBundleEventRowFixture } from "../../../../packages/test-utils/src/databaseTestFixtures";
3
5
  import { d1Database, type D1Like } from "./d1Database";
4
6
 
5
7
  const database: D1Like = {
@@ -31,3 +33,37 @@ it("uses the D1 binding supplied at the Worker composition boundary", async () =
31
33
  channels: [{ id: "channel-production", name: "production" }],
32
34
  });
33
35
  });
36
+
37
+ it("does not report a failed D1 query as empty Insights history", async () => {
38
+ const plugin = d1Database({
39
+ ...database,
40
+ prepare: () => ({
41
+ bind: () => ({ all: async () => ({ success: false, results: [] }) }),
42
+ }),
43
+ });
44
+ await expect(
45
+ plugin.models.insights.listEvents({
46
+ filter: { kind: "all" },
47
+ beforeReceivedAtMs: 100,
48
+ limit: 10,
49
+ }),
50
+ ).rejects.toThrow(
51
+ "D1 did not successfully execute every requested statement",
52
+ );
53
+ });
54
+
55
+ it("rejects an incomplete Insights batch response", async () => {
56
+ const plugin = d1Database({
57
+ ...database,
58
+ batch: async () => [{ success: true, results: [] }],
59
+ });
60
+ const event = createBundleEventRowFixture("1", 1);
61
+ await expect(
62
+ plugin.models.insights.record({
63
+ event,
64
+ installation: toInsightsInstallationRow(event),
65
+ }),
66
+ ).rejects.toThrow(
67
+ "D1 did not successfully execute every requested statement",
68
+ );
69
+ });
@@ -1,10 +1,11 @@
1
1
  import { createDatabasePlugin } from "@hot-updater/plugin-core";
2
2
  import { createDatabasePluginAdapter } from "@hot-updater/plugin-core/internal";
3
3
 
4
- import { createD1Implementation } from "../d1Implementation";
4
+ import { createD1Implementation, D1ExecutionError } from "../d1Implementation";
5
5
 
6
6
  type D1Result = {
7
7
  readonly results?: readonly unknown[];
8
+ readonly success?: boolean;
8
9
  };
9
10
 
10
11
  type D1BoundStatement = {
@@ -31,6 +32,7 @@ export const d1Database = (database: D1Like) => {
31
32
  .prepare(sql)
32
33
  .bind(...params)
33
34
  .all();
35
+ if (result.success === false) throw new D1ExecutionError();
34
36
  return result.results ?? [];
35
37
  },
36
38
  async batch(statements) {
@@ -39,6 +41,12 @@ export const d1Database = (database: D1Like) => {
39
41
  database.prepare(sql).bind(...params),
40
42
  ),
41
43
  );
44
+ if (
45
+ results.length !== statements.length ||
46
+ results.some(({ success }) => success === false)
47
+ ) {
48
+ throw new D1ExecutionError();
49
+ }
42
50
  return results.map(({ results }) => results ?? []);
43
51
  },
44
52
  });
@@ -1 +1 @@
1
- This folder contains the built output assets for the worker "hot-updater" generated at 2026-08-31T11:14:10.721Z.
1
+ This folder contains the built output assets for the worker "hot-updater" generated at 2026-09-07T08:27:12.590Z.