@hot-updater/cloudflare 0.30.12 → 0.31.1

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.
@@ -1,6 +1,10 @@
1
1
  import {
2
2
  DEFAULT_ROLLOUT_COHORT_COUNT,
3
- type SnakeCaseBundle,
3
+ getAssetBaseStorageUri,
4
+ getBundlePatches,
5
+ getManifestFileHash,
6
+ getManifestStorageUri,
7
+ stripBundleArtifactMetadata,
4
8
  } from "@hot-updater/core";
5
9
  import type {
6
10
  Bundle,
@@ -48,6 +52,36 @@ interface BuildQueryResult {
48
52
  params: unknown[];
49
53
  }
50
54
 
55
+ interface D1WorkerBundleRow {
56
+ id: string;
57
+ channel: string;
58
+ enabled: number | boolean;
59
+ should_force_update: number | boolean;
60
+ file_hash: string;
61
+ git_commit_hash: string | null;
62
+ message: string | null;
63
+ platform: "ios" | "android";
64
+ target_app_version: string | null;
65
+ storage_uri: string;
66
+ fingerprint_hash: string | null;
67
+ metadata: unknown;
68
+ manifest_storage_uri?: string | null;
69
+ manifest_file_hash?: string | null;
70
+ asset_base_storage_uri?: string | null;
71
+ rollout_cohort_count: number | null;
72
+ target_cohorts: string | null;
73
+ }
74
+
75
+ interface D1WorkerBundlePatchRow {
76
+ id: string;
77
+ bundle_id: string;
78
+ base_bundle_id: string;
79
+ base_file_hash: string;
80
+ patch_file_hash: string;
81
+ patch_storage_uri: string;
82
+ order_index: number | null;
83
+ }
84
+
51
85
  function buildWhereClause(
52
86
  conditions: QueryConditions | undefined,
53
87
  ): BuildQueryResult {
@@ -168,7 +202,54 @@ function parseTargetCohorts(value: unknown): string[] | null {
168
202
  return null;
169
203
  }
170
204
 
171
- function transformRowToBundle(row: SnakeCaseBundle): Bundle {
205
+ const parseMetadata = (value: unknown): Bundle["metadata"] => {
206
+ if (!value) return undefined;
207
+ if (typeof value === "string") {
208
+ try {
209
+ return parseMetadata(JSON.parse(value) as unknown);
210
+ } catch {
211
+ return undefined;
212
+ }
213
+ }
214
+ return typeof value === "object" && !Array.isArray(value)
215
+ ? (value as Bundle["metadata"])
216
+ : undefined;
217
+ };
218
+
219
+ const buildBundlePatchId = (bundleId: string, baseBundleId: string) =>
220
+ `${bundleId}:${baseBundleId}`;
221
+
222
+ const bundleToPatchRows = (bundle: Bundle): D1WorkerBundlePatchRow[] =>
223
+ getBundlePatches(bundle).map((patch, index) => ({
224
+ id: buildBundlePatchId(bundle.id, patch.baseBundleId),
225
+ bundle_id: bundle.id,
226
+ base_bundle_id: patch.baseBundleId,
227
+ base_file_hash: patch.baseFileHash,
228
+ patch_file_hash: patch.patchFileHash,
229
+ patch_storage_uri: patch.patchStorageUri,
230
+ order_index: index,
231
+ }));
232
+
233
+ function transformRowToBundle(
234
+ row: D1WorkerBundleRow,
235
+ patchRows: D1WorkerBundlePatchRow[] = [],
236
+ ): Bundle {
237
+ const rawMetadata = parseMetadata(row.metadata);
238
+ const patches = patchRows
239
+ .slice()
240
+ .sort(
241
+ (left, right) =>
242
+ (left.order_index ?? 0) - (right.order_index ?? 0) ||
243
+ left.base_bundle_id.localeCompare(right.base_bundle_id),
244
+ )
245
+ .map((patch) => ({
246
+ baseBundleId: patch.base_bundle_id,
247
+ baseFileHash: patch.base_file_hash,
248
+ patchFileHash: patch.patch_file_hash,
249
+ patchStorageUri: patch.patch_storage_uri,
250
+ }));
251
+ const primaryPatch = patches[0] ?? null;
252
+
172
253
  return {
173
254
  id: row.id,
174
255
  channel: row.channel,
@@ -181,7 +262,15 @@ function transformRowToBundle(row: SnakeCaseBundle): Bundle {
181
262
  targetAppVersion: row.target_app_version,
182
263
  storageUri: row.storage_uri,
183
264
  fingerprintHash: row.fingerprint_hash,
184
- metadata: row?.metadata ? JSON.parse(row.metadata as string) : {},
265
+ metadata: stripBundleArtifactMetadata(rawMetadata),
266
+ manifestStorageUri: row.manifest_storage_uri ?? null,
267
+ manifestFileHash: row.manifest_file_hash ?? null,
268
+ assetBaseStorageUri: row.asset_base_storage_uri ?? null,
269
+ patches,
270
+ patchBaseBundleId: primaryPatch?.baseBundleId ?? null,
271
+ patchBaseFileHash: primaryPatch?.baseFileHash ?? null,
272
+ patchFileHash: primaryPatch?.patchFileHash ?? null,
273
+ patchStorageUri: primaryPatch?.patchStorageUri ?? null,
185
274
  rolloutCohortCount:
186
275
  (row.rollout_cohort_count as number | null) ??
187
276
  DEFAULT_ROLLOUT_COHORT_COUNT,
@@ -236,12 +325,43 @@ export const d1WorkerDatabase = <
236
325
  return result ?? null;
237
326
  };
238
327
 
328
+ const getPatchMap = async (
329
+ bundleIds: string[],
330
+ context?: HotUpdaterContext<TContext>,
331
+ ) => {
332
+ const patchMap = new Map<string, D1WorkerBundlePatchRow[]>();
333
+
334
+ if (bundleIds.length === 0) {
335
+ return patchMap;
336
+ }
337
+
338
+ const placeholders = bundleIds.map(() => "?").join(", ");
339
+ const rows = await queryAll<D1WorkerBundlePatchRow>(
340
+ `
341
+ SELECT *
342
+ FROM bundle_patches
343
+ WHERE bundle_id IN (${placeholders})
344
+ ORDER BY order_index ASC, base_bundle_id ASC
345
+ `,
346
+ bundleIds,
347
+ context,
348
+ );
349
+
350
+ for (const row of rows) {
351
+ const current = patchMap.get(row.bundle_id) ?? [];
352
+ current.push(row);
353
+ patchMap.set(row.bundle_id, current);
354
+ }
355
+
356
+ return patchMap;
357
+ };
358
+
239
359
  const queryBundlesForUpdateInfo = async (
240
360
  conditions: QueryConditions,
241
361
  context?: HotUpdaterContext<TContext>,
242
362
  ): Promise<Bundle[]> => {
243
363
  const { sql: whereClause, params } = buildWhereClause(conditions);
244
- const rows = await queryAll<SnakeCaseBundle>(
364
+ const rows = await queryAll<D1WorkerBundleRow>(
245
365
  `
246
366
  SELECT * FROM bundles
247
367
  ${whereClause}
@@ -249,8 +369,14 @@ export const d1WorkerDatabase = <
249
369
  params,
250
370
  context,
251
371
  );
372
+ const patchMap = await getPatchMap(
373
+ rows.map((row) => row.id),
374
+ context,
375
+ );
252
376
 
253
- return rows.map(transformRowToBundle);
377
+ return rows.map((row) =>
378
+ transformRowToBundle(row, patchMap.get(row.id)),
379
+ );
254
380
  };
255
381
 
256
382
  const getTargetAppVersionsForUpdateInfo = async (
@@ -324,13 +450,16 @@ export const d1WorkerDatabase = <
324
450
  }),
325
451
 
326
452
  async getBundleById(bundleId, context) {
327
- const row = await queryFirst<SnakeCaseBundle>(
328
- "SELECT * FROM bundles WHERE id = ? LIMIT 1",
329
- [bundleId],
330
- context,
331
- );
453
+ const [row, patchMap] = await Promise.all([
454
+ queryFirst<D1WorkerBundleRow>(
455
+ "SELECT * FROM bundles WHERE id = ? LIMIT 1",
456
+ [bundleId],
457
+ context,
458
+ ),
459
+ getPatchMap([bundleId], context),
460
+ ]);
332
461
 
333
- return row ? transformRowToBundle(row) : null;
462
+ return row ? transformRowToBundle(row, patchMap.get(bundleId)) : null;
334
463
  },
335
464
 
336
465
  async getBundles(options, context) {
@@ -352,13 +481,19 @@ export const d1WorkerDatabase = <
352
481
  );
353
482
  const total = countRows[0]?.total ?? 0;
354
483
 
355
- const rows = await queryAll<SnakeCaseBundle>(
484
+ const rows = await queryAll<D1WorkerBundleRow>(
356
485
  `SELECT * FROM bundles${whereClause} ${orderSql} LIMIT ? OFFSET ?`,
357
486
  [...params, limit, offset],
358
487
  context,
359
488
  );
360
489
 
361
- const bundles = rows.map(transformRowToBundle);
490
+ const patchMap = await getPatchMap(
491
+ rows.map((row) => row.id),
492
+ context,
493
+ );
494
+ const bundles = rows.map((row) =>
495
+ transformRowToBundle(row, patchMap.get(row.id)),
496
+ );
362
497
 
363
498
  const paginationOptions: PaginationOptions = { limit, offset };
364
499
  return {
@@ -385,6 +520,14 @@ export const d1WorkerDatabase = <
385
520
 
386
521
  for (const operation of changedSets) {
387
522
  if (operation.operation === "delete") {
523
+ await db
524
+ .prepare("DELETE FROM bundle_patches WHERE bundle_id = ?")
525
+ .bind(operation.data.id)
526
+ .run();
527
+ await db
528
+ .prepare("DELETE FROM bundle_patches WHERE base_bundle_id = ?")
529
+ .bind(operation.data.id)
530
+ .run();
388
531
  await db
389
532
  .prepare("DELETE FROM bundles WHERE id = ?")
390
533
  .bind(operation.data.id)
@@ -408,10 +551,13 @@ export const d1WorkerDatabase = <
408
551
  storage_uri,
409
552
  fingerprint_hash,
410
553
  metadata,
554
+ manifest_storage_uri,
555
+ manifest_file_hash,
556
+ asset_base_storage_uri,
411
557
  rollout_cohort_count,
412
558
  target_cohorts
413
559
  )
414
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
560
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
415
561
  `)
416
562
  .bind(
417
563
  bundle.id,
@@ -425,13 +571,50 @@ export const d1WorkerDatabase = <
425
571
  bundle.targetAppVersion,
426
572
  bundle.storageUri,
427
573
  bundle.fingerprintHash,
428
- JSON.stringify(bundle.metadata ?? {}),
574
+ JSON.stringify(
575
+ stripBundleArtifactMetadata(bundle.metadata) ?? {},
576
+ ),
577
+ getManifestStorageUri(bundle),
578
+ getManifestFileHash(bundle),
579
+ getAssetBaseStorageUri(bundle),
429
580
  bundle.rolloutCohortCount ?? DEFAULT_ROLLOUT_COHORT_COUNT,
430
581
  bundle.targetCohorts
431
582
  ? JSON.stringify(bundle.targetCohorts)
432
583
  : null,
433
584
  )
434
585
  .run();
586
+
587
+ await db
588
+ .prepare("DELETE FROM bundle_patches WHERE bundle_id = ?")
589
+ .bind(bundle.id)
590
+ .run();
591
+
592
+ const patchRows = bundleToPatchRows(bundle);
593
+ for (const patchRow of patchRows) {
594
+ await db
595
+ .prepare(`
596
+ INSERT OR REPLACE INTO bundle_patches (
597
+ id,
598
+ bundle_id,
599
+ base_bundle_id,
600
+ base_file_hash,
601
+ patch_file_hash,
602
+ patch_storage_uri,
603
+ order_index
604
+ )
605
+ VALUES (?, ?, ?, ?, ?, ?, ?)
606
+ `)
607
+ .bind(
608
+ patchRow.id,
609
+ patchRow.bundle_id,
610
+ patchRow.base_bundle_id,
611
+ patchRow.base_file_hash,
612
+ patchRow.patch_file_hash,
613
+ patchRow.patch_storage_uri,
614
+ patchRow.order_index ?? 0,
615
+ )
616
+ .run();
617
+ }
435
618
  }
436
619
  },
437
620
  };
@@ -20,12 +20,26 @@ type D1Row = {
20
20
  storage_uri: string;
21
21
  fingerprint_hash: string | null;
22
22
  metadata: string;
23
+ manifest_storage_uri?: string | null;
24
+ manifest_file_hash?: string | null;
25
+ asset_base_storage_uri?: string | null;
23
26
  rollout_cohort_count: number | null;
24
27
  target_cohorts: string | null;
25
28
  };
26
29
 
27
- const { rows } = vi.hoisted(() => ({
30
+ type D1PatchRow = {
31
+ id: string;
32
+ bundle_id: string;
33
+ base_bundle_id: string;
34
+ base_file_hash: string;
35
+ patch_file_hash: string;
36
+ patch_storage_uri: string;
37
+ order_index: number | null;
38
+ };
39
+
40
+ const { rows, patchRows } = vi.hoisted(() => ({
28
41
  rows: new Map<string, D1Row>(),
42
+ patchRows: new Map<string, D1PatchRow>(),
29
43
  }));
30
44
 
31
45
  vi.mock("pg-minify", () => ({
@@ -170,6 +184,25 @@ vi.mock("cloudflare", () => ({
170
184
  return createPage(row ? [row] : []);
171
185
  }
172
186
 
187
+ if (
188
+ normalizedSql.startsWith(
189
+ "select * from bundle_patches where bundle_id in",
190
+ )
191
+ ) {
192
+ const selectedBundleIds = new Set(
193
+ params.map((value) => String(value)),
194
+ );
195
+ const result = Array.from(patchRows.values())
196
+ .filter((row) => selectedBundleIds.has(row.bundle_id))
197
+ .sort(
198
+ (a, b) =>
199
+ Number(a.order_index ?? 0) - Number(b.order_index ?? 0) ||
200
+ a.base_bundle_id.localeCompare(b.base_bundle_id),
201
+ );
202
+
203
+ return createPage(result);
204
+ }
205
+
173
206
  if (normalizedSql.startsWith("select * from bundles")) {
174
207
  const { filteredRows, index } = getFilteredRows(sql, params);
175
208
  const limit = Number(params[index] ?? filteredRows.length);
@@ -214,6 +247,34 @@ vi.mock("cloudflare", () => ({
214
247
  return createPage([]);
215
248
  }
216
249
 
250
+ if (
251
+ normalizedSql.startsWith(
252
+ "delete from bundle_patches where bundle_id = ?",
253
+ )
254
+ ) {
255
+ const bundleId = String(params[0]);
256
+ for (const [id, row] of patchRows.entries()) {
257
+ if (row.bundle_id === bundleId) {
258
+ patchRows.delete(id);
259
+ }
260
+ }
261
+ return createPage([]);
262
+ }
263
+
264
+ if (
265
+ normalizedSql.startsWith(
266
+ "delete from bundle_patches where base_bundle_id = ?",
267
+ )
268
+ ) {
269
+ const baseBundleId = String(params[0]);
270
+ for (const [id, row] of patchRows.entries()) {
271
+ if (row.base_bundle_id === baseBundleId) {
272
+ patchRows.delete(id);
273
+ }
274
+ }
275
+ return createPage([]);
276
+ }
277
+
217
278
  if (normalizedSql.startsWith("insert or replace into bundles")) {
218
279
  const row: D1Row = {
219
280
  id: params[0],
@@ -228,13 +289,32 @@ vi.mock("cloudflare", () => ({
228
289
  storage_uri: params[9],
229
290
  fingerprint_hash: params[10],
230
291
  metadata: params[11],
231
- rollout_cohort_count: params[12],
232
- target_cohorts: params[13],
292
+ manifest_storage_uri: params[12],
293
+ manifest_file_hash: params[13],
294
+ asset_base_storage_uri: params[14],
295
+ rollout_cohort_count: params[15],
296
+ target_cohorts: params[16],
233
297
  };
234
298
  rows.set(row.id, row);
235
299
  return createPage([]);
236
300
  }
237
301
 
302
+ if (
303
+ normalizedSql.startsWith("insert or replace into bundle_patches")
304
+ ) {
305
+ const row: D1PatchRow = {
306
+ id: params[0],
307
+ bundle_id: params[1],
308
+ base_bundle_id: params[2],
309
+ base_file_hash: params[3],
310
+ patch_file_hash: params[4],
311
+ patch_storage_uri: params[5],
312
+ order_index: Number(params[6] ?? 0),
313
+ };
314
+ patchRows.set(row.id, row);
315
+ return createPage([]);
316
+ }
317
+
238
318
  throw new Error(`Unsupported SQL in D1 mock: ${sql}`);
239
319
  },
240
320
  },
@@ -247,6 +327,7 @@ describe("d1Database plugin", () => {
247
327
 
248
328
  beforeEach(() => {
249
329
  rows.clear();
330
+ patchRows.clear();
250
331
  plugin = d1Database({
251
332
  databaseId: "test-db-id",
252
333
  accountId: "test-account-id",
@@ -279,6 +360,7 @@ describe("d1Database plugin", () => {
279
360
  setupGetUpdateInfoTestSuite({
280
361
  getUpdateInfo: async (bundles, args) => {
281
362
  rows.clear();
363
+ patchRows.clear();
282
364
 
283
365
  for (const bundle of bundles) {
284
366
  await plugin.appendBundle(bundle);