@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
@@ -1,46 +0,0 @@
1
- import { expect, it } from "vitest";
2
-
3
- import type { D1Executor } from "./d1Implementation";
4
- import { countD1Rows, D1QueryResultError } from "./d1Query";
5
-
6
- const countWithResult = (result: readonly unknown[]): Promise<number> => {
7
- const executor: D1Executor = {
8
- query: async () => result,
9
- batch: async () => [],
10
- };
11
-
12
- return countD1Rows(executor, { model: "bundles" });
13
- };
14
-
15
- it("preserves a zero count", async () => {
16
- const count = countWithResult([{ count: 0 }]);
17
-
18
- await expect(count).resolves.toBe(0);
19
- });
20
-
21
- it.each([
22
- ["a missing first row", []],
23
- ["a null first row", [null]],
24
- ["a primitive first row", ["invalid"]],
25
- ["an array first row", [[0]]],
26
- ["a missing count property", [{}]],
27
- ["a numeric string count", [{ count: "1" }]],
28
- ["a NaN count", [{ count: Number.NaN }]],
29
- ["an infinite count", [{ count: Number.POSITIVE_INFINITY }]],
30
- ] as const)("rejects %s", async (_description, rows) => {
31
- const count = countWithResult(rows);
32
-
33
- await expect(count).rejects.toBeInstanceOf(D1QueryResultError);
34
- });
35
-
36
- it("includes the malformed count row in the provider error", async () => {
37
- const row = { count: "1" };
38
-
39
- const count = countWithResult([row]);
40
-
41
- await expect(count).rejects.toMatchObject({
42
- name: "D1QueryResultError",
43
- operation: "count",
44
- result: row,
45
- });
46
- });
package/src/d1Query.ts DELETED
@@ -1,92 +0,0 @@
1
- import type {
2
- CountDatabaseImplementationInput,
3
- DatabaseImplementationResult,
4
- DatabaseModel,
5
- FindManyDatabaseImplementationInput,
6
- } from "@hot-updater/plugin-core/internal";
7
-
8
- import type { D1Executor } from "./d1Implementation";
9
- import { parseD1Row } from "./d1Rows";
10
- import { buildD1Order, buildD1Where, encodeD1Values } from "./d1Sql";
11
-
12
- export const d1TableNames = {
13
- bundles: "bundles",
14
- bundle_patches: "bundle_patches",
15
- channels: "channels",
16
- bundle_events: "bundle_events",
17
- bundle_installations: "bundle_installations",
18
- api_keys: "api_keys",
19
- release_catalogs: "release_catalogs",
20
- releases: "releases",
21
- } as const satisfies Record<DatabaseModel, string>;
22
-
23
- export class D1QueryResultError extends Error {
24
- readonly name = "D1QueryResultError";
25
- readonly operation = "count";
26
-
27
- constructor(readonly result: unknown) {
28
- super("D1 count query returned a malformed first row");
29
- }
30
- }
31
-
32
- export const countD1Rows = async (
33
- executor: D1Executor,
34
- input: CountDatabaseImplementationInput,
35
- ): Promise<number> => {
36
- const where = buildD1Where(input.where);
37
- const table = d1TableNames[input.model];
38
- const source =
39
- input.distinct === undefined
40
- ? `${table}${where.sql}`
41
- : `(SELECT DISTINCT ${input.distinct.join(", ")} FROM ${table}${where.sql}) AS distinct_rows`;
42
- const rows = await executor.query(
43
- `SELECT COUNT(*) AS count FROM ${source}`,
44
- where.params,
45
- );
46
- const row = rows[0];
47
- if (typeof row !== "object" || row === null || Array.isArray(row)) {
48
- throw new D1QueryResultError(row);
49
- }
50
- const count = Reflect.get(row, "count");
51
- if (typeof count !== "number" || !Number.isFinite(count)) {
52
- throw new D1QueryResultError(row);
53
- }
54
- return count;
55
- };
56
-
57
- export const findManyD1Rows = async (
58
- executor: D1Executor,
59
- input: FindManyDatabaseImplementationInput,
60
- ): Promise<readonly DatabaseImplementationResult[]> => {
61
- const where = buildD1Where(input.where);
62
- const orderBy = input.orderBy;
63
- const order = buildD1Order(orderBy);
64
- const table = d1TableNames[input.model];
65
- const source =
66
- input.distinctOn === undefined
67
- ? `SELECT * FROM ${table}${where.sql}${order}`
68
- : `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}`;
69
- const pageParams = encodeD1Values([input.limit, input.offset]);
70
- const rows = await executor.query(
71
- `${source} LIMIT json_extract(?, '$') OFFSET json_extract(?, '$')`,
72
- [...where.params, ...pageParams],
73
- );
74
- switch (input.model) {
75
- case "bundles":
76
- return rows.map((row) => parseD1Row("bundles", row));
77
- case "bundle_patches":
78
- return rows.map((row) => parseD1Row("bundle_patches", row));
79
- case "channels":
80
- return rows.map((row) => parseD1Row("channels", row));
81
- case "bundle_events":
82
- return rows.map((row) => parseD1Row("bundle_events", row));
83
- case "bundle_installations":
84
- return rows.map((row) => parseD1Row("bundle_installations", row));
85
- case "api_keys":
86
- return rows.map((row) => parseD1Row("api_keys", row));
87
- case "releases":
88
- return rows.map((row) => parseD1Row("releases", row));
89
- case "release_catalogs":
90
- return rows.map((row) => parseD1Row("release_catalogs", row));
91
- }
92
- };
@@ -1,143 +0,0 @@
1
- import { expect, it } from "vitest";
2
-
3
- import {
4
- createBundleEventRowFixture,
5
- createBundlePatchRowFixture,
6
- } from "../../../packages/test-utils/src/databaseTestFixtures";
7
- import { parseD1Row } from "./d1Rows";
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
-
23
- const bundleD1Row = {
24
- id: "bundle-1",
25
- platform: "android",
26
- file_hash: "hash",
27
- git_commit_hash: null,
28
- storage_uri: "storage://bundle",
29
- archive_byte_size: 3_000_000_001,
30
- metadata: '{"build":1}',
31
- manifest_storage_uri: null,
32
- manifest_file_hash: null,
33
- asset_base_storage_uri: null,
34
- } as const;
35
-
36
- it("parses SQLite JSON into an artifact-only Bundle row", () => {
37
- const row = parseD1Row("bundles", bundleD1Row);
38
-
39
- expect(row).toMatchObject({
40
- metadata: { build: 1 },
41
- });
42
- });
43
-
44
- it("defaults a legacy bundle's missing archive byte size", () => {
45
- const row: Record<string, unknown> = { ...bundleD1Row };
46
- delete row["archive_byte_size"];
47
-
48
- expect(parseD1Row("bundles", row)).toMatchObject({
49
- archive_byte_size: 0,
50
- });
51
- });
52
-
53
- it("rejects an invalid artifact platform", () => {
54
- expect(() =>
55
- parseD1Row("bundles", { ...bundleD1Row, platform: "web" }),
56
- ).toThrow("D1 returned an invalid bundles row");
57
- });
58
-
59
- it("rejects corrupt SQLite metadata JSON", () => {
60
- expect(() =>
61
- parseD1Row("bundles", { ...bundleD1Row, metadata: "{" }),
62
- ).toThrow("D1 returned an invalid bundles row");
63
- });
64
-
65
- it.each([-1, 1.5, Number.MAX_SAFE_INTEGER + 1, Number.NaN])(
66
- "rejects invalid SQLite archive size %s",
67
- (archiveByteSize) => {
68
- expect(() =>
69
- parseD1Row("bundles", {
70
- ...bundleD1Row,
71
- archive_byte_size: archiveByteSize,
72
- }),
73
- ).toThrow("D1 returned an invalid bundles row");
74
- },
75
- );
76
-
77
- it("preserves SQLite patch sizes above 2 GiB", () => {
78
- const patch = createBundlePatchRowFixture("large", "bundle", "base");
79
-
80
- expect(parseD1Row("bundle_patches", patch)).toMatchObject({
81
- byte_size: 3_000_000_002,
82
- });
83
- });
84
-
85
- it.each(["null", "[]", "1", '"metadata"'])(
86
- "rejects non-object SQLite metadata %s",
87
- (metadata) => {
88
- expect(() => parseD1Row("bundles", { ...bundleD1Row, metadata })).toThrow(
89
- "D1 returned an invalid bundles row",
90
- );
91
- },
92
- );
93
-
94
- it("preserves explicit null Release ids on an Insights row", () => {
95
- expect(
96
- parseD1Row("bundle_events", createBundleEventRowFixture("1", 1)),
97
- ).toMatchObject({ from_release_id: null, to_release_id: null });
98
- });
99
-
100
- it.each(["from_release_id", "to_release_id"])(
101
- "rejects an omitted Insights field: %s",
102
- (field) => {
103
- const row: Record<string, unknown> = {
104
- ...createBundleEventRowFixture("1", 1),
105
- };
106
- delete row[field];
107
-
108
- expect(() => parseD1Row("bundle_events", row)).toThrow(
109
- "D1 returned an invalid bundle_events row",
110
- );
111
- },
112
- );
113
-
114
- it.each([
115
- { from_bundle_id: null },
116
- { to_bundle_id: null },
117
- { type: "UNCHANGED", from_bundle_id: "bundle-old", update_strategy: null },
118
- ])("rejects an invalid Insights direction shape", (overrides) => {
119
- expect(() =>
120
- parseD1Row("bundle_events", {
121
- ...createBundleEventRowFixture("1", 1),
122
- ...overrides,
123
- }),
124
- ).toThrow("D1 returned an invalid bundle_events row");
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 DELETED
@@ -1,357 +0,0 @@
1
- import type {
2
- BundlePatchRow,
3
- BundleRow,
4
- BundleEventRow,
5
- ChannelRow,
6
- InsightsInstallationRow,
7
- ApiKeyRow,
8
- ReleaseCatalogRow,
9
- ReleaseRow,
10
- } from "@hot-updater/plugin-core";
11
- import { isDatabaseMetadataObject } from "@hot-updater/plugin-core";
12
- import type {
13
- DatabaseModel,
14
- DatabaseRow,
15
- } from "@hot-updater/plugin-core/internal";
16
-
17
- class InvalidD1RowError extends Error {
18
- readonly name = "InvalidD1RowError";
19
- readonly model: DatabaseModel;
20
-
21
- constructor(model: DatabaseModel) {
22
- super(`D1 returned an invalid ${model} row`);
23
- this.model = model;
24
- }
25
- }
26
-
27
- const isRecord = (value: unknown): value is Record<string, unknown> =>
28
- typeof value === "object" && value !== null && !Array.isArray(value);
29
-
30
- const stringValue = (
31
- row: Record<string, unknown>,
32
- field: string,
33
- model: DatabaseModel,
34
- ): string => {
35
- const value = row[field];
36
- if (typeof value !== "string") throw new InvalidD1RowError(model);
37
- return value;
38
- };
39
-
40
- const nullableString = (
41
- row: Record<string, unknown>,
42
- field: string,
43
- model: DatabaseModel,
44
- ): string | null => {
45
- const value = row[field];
46
- if (value === null) return null;
47
- if (typeof value !== "string") throw new InvalidD1RowError(model);
48
- return value;
49
- };
50
-
51
- const numberValue = (
52
- row: Record<string, unknown>,
53
- field: string,
54
- model: DatabaseModel,
55
- ): number => {
56
- const value = row[field];
57
- if (typeof value !== "number") throw new InvalidD1RowError(model);
58
- return value;
59
- };
60
-
61
- const byteSizeValue = (
62
- row: Record<string, unknown>,
63
- field: string,
64
- model: DatabaseModel,
65
- ): number => {
66
- const value = row[field];
67
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
68
- throw new InvalidD1RowError(model);
69
- }
70
- return value;
71
- };
72
-
73
- const booleanValue = (
74
- row: Record<string, unknown>,
75
- field: string,
76
- model: DatabaseModel,
77
- ): boolean => {
78
- const value = row[field];
79
- if (value === true || value === 1) return true;
80
- if (value === false || value === 0) return false;
81
- throw new InvalidD1RowError(model);
82
- };
83
-
84
- const jsonValue = (value: unknown, model: DatabaseModel): unknown => {
85
- if (typeof value !== "string") throw new InvalidD1RowError(model);
86
- try {
87
- return JSON.parse(value);
88
- } catch (error) {
89
- if (error instanceof SyntaxError) throw new InvalidD1RowError(model);
90
- throw error;
91
- }
92
- };
93
-
94
- const metadata = (row: Record<string, unknown>) => {
95
- const value = jsonValue(row["metadata"], "bundles");
96
- if (!isDatabaseMetadataObject(value)) {
97
- throw new InvalidD1RowError("bundles");
98
- }
99
- return value;
100
- };
101
-
102
- const bundleRow = (row: Record<string, unknown>): BundleRow => {
103
- const platform = stringValue(row, "platform", "bundles");
104
- if (platform !== "ios" && platform !== "android") {
105
- throw new InvalidD1RowError("bundles");
106
- }
107
- return {
108
- id: stringValue(row, "id", "bundles"),
109
- platform,
110
- file_hash: stringValue(row, "file_hash", "bundles"),
111
- git_commit_hash: nullableString(row, "git_commit_hash", "bundles"),
112
- storage_uri: stringValue(row, "storage_uri", "bundles"),
113
- archive_byte_size:
114
- row["archive_byte_size"] === undefined
115
- ? 0
116
- : byteSizeValue(row, "archive_byte_size", "bundles"),
117
- metadata: metadata(row),
118
- manifest_storage_uri: nullableString(
119
- row,
120
- "manifest_storage_uri",
121
- "bundles",
122
- ),
123
- manifest_file_hash: nullableString(row, "manifest_file_hash", "bundles"),
124
- asset_base_storage_uri: nullableString(
125
- row,
126
- "asset_base_storage_uri",
127
- "bundles",
128
- ),
129
- };
130
- };
131
-
132
- const patchRow = (row: Record<string, unknown>): BundlePatchRow => ({
133
- id: stringValue(row, "id", "bundle_patches"),
134
- bundle_id: stringValue(row, "bundle_id", "bundle_patches"),
135
- base_bundle_id: stringValue(row, "base_bundle_id", "bundle_patches"),
136
- base_file_hash: stringValue(row, "base_file_hash", "bundle_patches"),
137
- patch_file_hash: stringValue(row, "patch_file_hash", "bundle_patches"),
138
- patch_storage_uri: stringValue(row, "patch_storage_uri", "bundle_patches"),
139
- byte_size: byteSizeValue(row, "byte_size", "bundle_patches"),
140
- order_index: numberValue(row, "order_index", "bundle_patches"),
141
- });
142
-
143
- const channelRow = (row: Record<string, unknown>): ChannelRow => ({
144
- id: stringValue(row, "id", "channels"),
145
- name: stringValue(row, "name", "channels"),
146
- });
147
-
148
- const eventRow = (row: Record<string, unknown>): BundleEventRow => {
149
- const type = stringValue(row, "type", "bundle_events");
150
- const platform = stringValue(row, "platform", "bundle_events");
151
- const fromBundleId = nullableString(row, "from_bundle_id", "bundle_events");
152
- const updateStrategy = nullableString(
153
- row,
154
- "update_strategy",
155
- "bundle_events",
156
- );
157
- if (
158
- (platform !== "ios" && platform !== "android") ||
159
- !(
160
- ((type === "UPDATE_APPLIED" || type === "RECOVERED") &&
161
- typeof fromBundleId === "string" &&
162
- (updateStrategy === "fingerprint" ||
163
- updateStrategy === "appVersion")) ||
164
- (type === "UNCHANGED" && fromBundleId === null && updateStrategy === null)
165
- )
166
- ) {
167
- throw new InvalidD1RowError("bundle_events");
168
- }
169
- return {
170
- id: stringValue(row, "id", "bundle_events"),
171
- type,
172
- install_id: stringValue(row, "install_id", "bundle_events"),
173
- user_id: nullableString(row, "user_id", "bundle_events"),
174
- username: nullableString(row, "username", "bundle_events"),
175
- from_release_id: nullableString(row, "from_release_id", "bundle_events"),
176
- from_bundle_id: fromBundleId,
177
- to_release_id: nullableString(row, "to_release_id", "bundle_events"),
178
- to_bundle_id: stringValue(row, "to_bundle_id", "bundle_events"),
179
- platform,
180
- app_version: stringValue(row, "app_version", "bundle_events"),
181
- channel: stringValue(row, "channel", "bundle_events"),
182
- cohort: stringValue(row, "cohort", "bundle_events"),
183
- update_strategy: updateStrategy,
184
- fingerprint_hash: nullableString(row, "fingerprint_hash", "bundle_events"),
185
- sdk_version: nullableString(row, "sdk_version", "bundle_events"),
186
- received_at_ms: numberValue(row, "received_at_ms", "bundle_events"),
187
- } as BundleEventRow;
188
- };
189
-
190
- const installationRow = (
191
- row: Record<string, unknown>,
192
- ): InsightsInstallationRow => {
193
- const type = stringValue(row, "type", "bundle_installations");
194
- const platform = stringValue(row, "platform", "bundle_installations");
195
- if (
196
- !["UPDATE_APPLIED", "RECOVERED", "UNCHANGED"].includes(type) ||
197
- (platform !== "ios" && platform !== "android")
198
- ) {
199
- throw new InvalidD1RowError("bundle_installations");
200
- }
201
- return {
202
- id: stringValue(row, "id", "bundle_installations"),
203
- install_id: stringValue(row, "install_id", "bundle_installations"),
204
- user_id: nullableString(row, "user_id", "bundle_installations"),
205
- username: nullableString(row, "username", "bundle_installations"),
206
- to_bundle_id: stringValue(row, "to_bundle_id", "bundle_installations"),
207
- type: type as InsightsInstallationRow["type"],
208
- platform,
209
- app_version: stringValue(row, "app_version", "bundle_installations"),
210
- channel: stringValue(row, "channel", "bundle_installations"),
211
- cohort: stringValue(row, "cohort", "bundle_installations"),
212
- received_at_ms: numberValue(row, "received_at_ms", "bundle_installations"),
213
- };
214
- };
215
-
216
- const apiKeyRow = (row: Record<string, unknown>): ApiKeyRow => {
217
- const role = stringValue(row, "role", "api_keys");
218
- if (role !== "client") throw new InvalidD1RowError("api_keys");
219
- const revokedAt = row["revoked_at_ms"];
220
- return {
221
- id: stringValue(row, "id", "api_keys"),
222
- hash: stringValue(row, "hash", "api_keys"),
223
- name: stringValue(row, "name", "api_keys"),
224
- prefix: stringValue(row, "prefix", "api_keys"),
225
- role,
226
- created_at_ms: numberValue(row, "created_at_ms", "api_keys"),
227
- revoked_at_ms:
228
- revokedAt === null ? null : numberValue(row, "revoked_at_ms", "api_keys"),
229
- };
230
- };
231
-
232
- const releaseTargetCohorts = (
233
- row: Record<string, unknown>,
234
- ): readonly string[] => {
235
- const value = jsonValue(row["target_cohorts"], "releases");
236
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
237
- throw new InvalidD1RowError("releases");
238
- }
239
- return value.filter((item): item is string => typeof item === "string");
240
- };
241
-
242
- const releaseRow = (row: Record<string, unknown>): ReleaseRow => {
243
- const platform = stringValue(row, "platform", "releases");
244
- const kind = stringValue(row, "kind", "releases");
245
- const strategy = stringValue(row, "strategy", "releases");
246
- const operation = stringValue(row, "operation", "releases");
247
- if (
248
- (platform !== "ios" && platform !== "android") ||
249
- (kind !== "BUNDLE" && kind !== "EMBEDDED") ||
250
- (strategy !== "APP_VERSION" && strategy !== "FINGERPRINT") ||
251
- (operation !== "DEPLOY" &&
252
- operation !== "PROMOTE" &&
253
- operation !== "ROLLBACK")
254
- ) {
255
- throw new InvalidD1RowError("releases");
256
- }
257
- return {
258
- id: stringValue(row, "id", "releases"),
259
- revision: numberValue(row, "revision", "releases"),
260
- scope_key: stringValue(row, "scope_key", "releases"),
261
- channel_id: stringValue(row, "channel_id", "releases"),
262
- platform,
263
- kind,
264
- bundle_id: nullableString(row, "bundle_id", "releases"),
265
- strategy,
266
- target_app_version: nullableString(row, "target_app_version", "releases"),
267
- fingerprint_hash: nullableString(row, "fingerprint_hash", "releases"),
268
- enabled: booleanValue(row, "enabled", "releases"),
269
- should_force_update: booleanValue(row, "should_force_update", "releases"),
270
- message: nullableString(row, "message", "releases"),
271
- rollout_cohort_count: numberValue(row, "rollout_cohort_count", "releases"),
272
- target_cohorts: releaseTargetCohorts(row),
273
- operation,
274
- source_release_id: nullableString(row, "source_release_id", "releases"),
275
- created_at_ms: numberValue(row, "created_at_ms", "releases"),
276
- updated_at_ms: numberValue(row, "updated_at_ms", "releases"),
277
- };
278
- };
279
-
280
- const releaseCatalogRow = (row: Record<string, unknown>): ReleaseCatalogRow => {
281
- const platform = stringValue(row, "platform", "release_catalogs");
282
- const strategy = stringValue(row, "strategy", "release_catalogs");
283
- if (
284
- (platform !== "ios" && platform !== "android") ||
285
- (strategy !== "APP_VERSION" && strategy !== "FINGERPRINT")
286
- ) {
287
- throw new InvalidD1RowError("release_catalogs");
288
- }
289
- return {
290
- scope_key: stringValue(row, "scope_key", "release_catalogs"),
291
- catalog_id: stringValue(row, "catalog_id", "release_catalogs"),
292
- strategy,
293
- channel_id: stringValue(row, "channel_id", "release_catalogs"),
294
- channel_key: stringValue(row, "channel_key", "release_catalogs"),
295
- platform,
296
- fingerprint_hash: nullableString(
297
- row,
298
- "fingerprint_hash",
299
- "release_catalogs",
300
- ),
301
- generation: numberValue(row, "generation", "release_catalogs"),
302
- payload: stringValue(row, "payload", "release_catalogs"),
303
- catalog_hash: stringValue(row, "catalog_hash", "release_catalogs"),
304
- byte_size: numberValue(row, "byte_size", "release_catalogs"),
305
- is_tombstone: booleanValue(row, "is_tombstone", "release_catalogs"),
306
- updated_at_ms: numberValue(row, "updated_at_ms", "release_catalogs"),
307
- };
308
- };
309
-
310
- export function parseD1Row(model: "bundles", value: unknown): BundleRow;
311
- export function parseD1Row(model: "channels", value: unknown): ChannelRow;
312
- export function parseD1Row(
313
- model: "bundle_patches",
314
- value: unknown,
315
- ): BundlePatchRow;
316
- export function parseD1Row(
317
- model: "bundle_events",
318
- value: unknown,
319
- ): BundleEventRow;
320
- export function parseD1Row(
321
- model: "bundle_installations",
322
- value: unknown,
323
- ): InsightsInstallationRow;
324
- export function parseD1Row(model: "api_keys", value: unknown): ApiKeyRow;
325
- export function parseD1Row(model: "releases", value: unknown): ReleaseRow;
326
- export function parseD1Row(
327
- model: "release_catalogs",
328
- value: unknown,
329
- ): ReleaseCatalogRow;
330
- export function parseD1Row(
331
- model: DatabaseModel,
332
- value: unknown,
333
- ): DatabaseRow<DatabaseModel>;
334
- export function parseD1Row(
335
- model: DatabaseModel,
336
- value: unknown,
337
- ): DatabaseRow<DatabaseModel> {
338
- if (!isRecord(value)) throw new InvalidD1RowError(model);
339
- switch (model) {
340
- case "bundles":
341
- return bundleRow(value);
342
- case "bundle_patches":
343
- return patchRow(value);
344
- case "channels":
345
- return channelRow(value);
346
- case "bundle_events":
347
- return eventRow(value);
348
- case "bundle_installations":
349
- return installationRow(value);
350
- case "api_keys":
351
- return apiKeyRow(value);
352
- case "releases":
353
- return releaseRow(value);
354
- case "release_catalogs":
355
- return releaseCatalogRow(value);
356
- }
357
- }
package/src/d1Sql.spec.ts DELETED
@@ -1,30 +0,0 @@
1
- import { expect, it } from "vitest";
2
-
3
- import { buildD1Where } from "./d1Sql";
4
-
5
- it("composes predicates left to right with one json_each bind for sets", () => {
6
- const query = buildD1Where([
7
- { field: "enabled", value: true },
8
- { field: "id", operator: "in", value: ["a", "b"], connector: "OR" },
9
- {
10
- field: "target_app_version",
11
- operator: "ne",
12
- value: null,
13
- connector: "AND",
14
- },
15
- ]);
16
-
17
- expect(query.sql).toBe(
18
- " WHERE ((enabled = json_extract(?, '$') OR id IN (SELECT value FROM json_each(?))) AND target_app_version IS NOT NULL)",
19
- );
20
- expect(query.params).toEqual(["true", '["a","b"]']);
21
- });
22
-
23
- it("makes empty inclusion and exclusion predicates deterministic", () => {
24
- expect(buildD1Where([{ field: "id", operator: "in", value: [] }]).sql).toBe(
25
- " WHERE 1 = 0",
26
- );
27
- expect(
28
- buildD1Where([{ field: "id", operator: "not_in", value: [] }]).sql,
29
- ).toBe(" WHERE 1 = 1");
30
- });