@hot-updater/cloudflare 0.29.5 → 0.29.6

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.
@@ -10837,8 +10837,8 @@ const getWranglerLoginAuthToken = () => {
10837
10837
  };
10838
10838
  //#endregion
10839
10839
  //#region iac/index.ts
10840
- const getConfigTemplate = (build) => {
10841
- return new _hot_updater_cli_tools.ConfigBuilder().setBuildType(build).setStorage({
10840
+ const getConfigScaffold = (build) => {
10841
+ return (0, _hot_updater_cli_tools.createHotUpdaterConfigScaffoldFromBuilder)(new _hot_updater_cli_tools.ConfigBuilder().setBuildType(build).setStorage({
10842
10842
  imports: [{
10843
10843
  pkg: "@hot-updater/cloudflare",
10844
10844
  named: ["r2Storage"]
@@ -10858,7 +10858,7 @@ const getConfigTemplate = (build) => {
10858
10858
  accountId: process.env.HOT_UPDATER_CLOUDFLARE_ACCOUNT_ID!,
10859
10859
  cloudflareApiToken: process.env.HOT_UPDATER_CLOUDFLARE_API_TOKEN!,
10860
10860
  })`
10861
- }).getResult();
10861
+ }));
10862
10862
  };
10863
10863
  const SOURCE_TEMPLATE = `// add this to your App.tsx
10864
10864
  import { HotUpdater } from "@hot-updater/react-native";
@@ -11066,7 +11066,7 @@ const runInit = async ({ build }) => {
11066
11066
  d1DatabaseName,
11067
11067
  r2BucketName: selectedBucketName
11068
11068
  });
11069
- await fs_promises.default.writeFile("hot-updater.config.ts", getConfigTemplate(build));
11069
+ const configWriteResult = await (0, _hot_updater_cli_tools.writeHotUpdaterConfig)(getConfigScaffold(build));
11070
11070
  await (0, _hot_updater_cli_tools.makeEnv)({
11071
11071
  HOT_UPDATER_CLOUDFLARE_API_TOKEN: apiToken,
11072
11072
  HOT_UPDATER_CLOUDFLARE_ACCOUNT_ID: accountId,
@@ -11074,7 +11074,9 @@ const runInit = async ({ build }) => {
11074
11074
  HOT_UPDATER_CLOUDFLARE_D1_DATABASE_ID: selectedD1DatabaseId
11075
11075
  });
11076
11076
  _hot_updater_cli_tools.p.log.success("Generated '.env.hotupdater' file with Cloudflare settings.");
11077
- _hot_updater_cli_tools.p.log.success("Generated 'hot-updater.config.ts' file with Cloudflare settings.");
11077
+ if (configWriteResult.status === "created") _hot_updater_cli_tools.p.log.success("Generated 'hot-updater.config.ts' file with Cloudflare settings.");
11078
+ else if (configWriteResult.status === "merged") _hot_updater_cli_tools.p.log.success("Updated 'hot-updater.config.ts' file with Cloudflare settings.");
11079
+ else _hot_updater_cli_tools.p.log.warn(`Kept existing 'hot-updater.config.ts' unchanged: ${configWriteResult.reason}`);
11078
11080
  if (subdomains.subdomain) _hot_updater_cli_tools.p.note((0, _hot_updater_cli_tools.transformTemplate)(SOURCE_TEMPLATE, { source: `https://${workerName}.${subdomains.subdomain}.workers.dev/api/check-update` }));
11079
11081
  _hot_updater_cli_tools.p.log.message(`Next step: ${(0, _hot_updater_cli_tools.link)("https://hot-updater.dev/docs/managed/cloudflare#step-4-add-hotupdater-to-your-project")}`);
11080
11082
  _hot_updater_cli_tools.p.log.success("Done! 🎉");
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
2
2
  import crypto from "crypto";
3
3
  import fs from "fs/promises";
4
4
  import path from "path";
5
- import { ConfigBuilder, copyDirToTmp, getCwd, link, makeEnv, p, transformTemplate } from "@hot-updater/cli-tools";
5
+ import { ConfigBuilder, copyDirToTmp, createHotUpdaterConfigScaffoldFromBuilder, getCwd, link, makeEnv, p, transformTemplate, writeHotUpdaterConfig } from "@hot-updater/cli-tools";
6
6
  import { Cloudflare } from "cloudflare";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
@@ -10829,8 +10829,8 @@ const getWranglerLoginAuthToken = () => {
10829
10829
  };
10830
10830
  //#endregion
10831
10831
  //#region iac/index.ts
10832
- const getConfigTemplate = (build) => {
10833
- return new ConfigBuilder().setBuildType(build).setStorage({
10832
+ const getConfigScaffold = (build) => {
10833
+ return createHotUpdaterConfigScaffoldFromBuilder(new ConfigBuilder().setBuildType(build).setStorage({
10834
10834
  imports: [{
10835
10835
  pkg: "@hot-updater/cloudflare",
10836
10836
  named: ["r2Storage"]
@@ -10850,7 +10850,7 @@ const getConfigTemplate = (build) => {
10850
10850
  accountId: process.env.HOT_UPDATER_CLOUDFLARE_ACCOUNT_ID!,
10851
10851
  cloudflareApiToken: process.env.HOT_UPDATER_CLOUDFLARE_API_TOKEN!,
10852
10852
  })`
10853
- }).getResult();
10853
+ }));
10854
10854
  };
10855
10855
  const SOURCE_TEMPLATE = `// add this to your App.tsx
10856
10856
  import { HotUpdater } from "@hot-updater/react-native";
@@ -11058,7 +11058,7 @@ const runInit = async ({ build }) => {
11058
11058
  d1DatabaseName,
11059
11059
  r2BucketName: selectedBucketName
11060
11060
  });
11061
- await fs.writeFile("hot-updater.config.ts", getConfigTemplate(build));
11061
+ const configWriteResult = await writeHotUpdaterConfig(getConfigScaffold(build));
11062
11062
  await makeEnv({
11063
11063
  HOT_UPDATER_CLOUDFLARE_API_TOKEN: apiToken,
11064
11064
  HOT_UPDATER_CLOUDFLARE_ACCOUNT_ID: accountId,
@@ -11066,7 +11066,9 @@ const runInit = async ({ build }) => {
11066
11066
  HOT_UPDATER_CLOUDFLARE_D1_DATABASE_ID: selectedD1DatabaseId
11067
11067
  });
11068
11068
  p.log.success("Generated '.env.hotupdater' file with Cloudflare settings.");
11069
- p.log.success("Generated 'hot-updater.config.ts' file with Cloudflare settings.");
11069
+ if (configWriteResult.status === "created") p.log.success("Generated 'hot-updater.config.ts' file with Cloudflare settings.");
11070
+ else if (configWriteResult.status === "merged") p.log.success("Updated 'hot-updater.config.ts' file with Cloudflare settings.");
11071
+ else p.log.warn(`Kept existing 'hot-updater.config.ts' unchanged: ${configWriteResult.reason}`);
11070
11072
  if (subdomains.subdomain) p.note(transformTemplate(SOURCE_TEMPLATE, { source: `https://${workerName}.${subdomains.subdomain}.workers.dev/api/check-update` }));
11071
11073
  p.log.message(`Next step: ${link("https://hot-updater.dev/docs/managed/cloudflare#step-4-add-hotupdater-to-your-project")}`);
11072
11074
  p.log.success("Done! 🎉");
package/dist/index.cjs CHANGED
@@ -404,7 +404,61 @@ const d1Database = (0, _hot_updater_plugin_core.createDatabasePlugin)({
404
404
  params
405
405
  }))).map(transformRowToBundle);
406
406
  }
407
+ async function queryBundlesForUpdateInfo(conditions) {
408
+ const { sql: whereClause, params } = buildWhereClause(conditions);
409
+ const sql = (0, import_lib.default)(`
410
+ SELECT * FROM bundles
411
+ ${whereClause}
412
+ `);
413
+ return (await resolvePage(await cf.d1.database.query(config.databaseId, {
414
+ account_id: config.accountId,
415
+ sql,
416
+ params
417
+ }))).map(transformRowToBundle);
418
+ }
419
+ async function getTargetAppVersionsForUpdateInfo({ platform, channel, minBundleId }) {
420
+ const sql = (0, import_lib.default)(`
421
+ SELECT target_app_version
422
+ FROM bundles
423
+ WHERE channel = ?
424
+ AND platform = ?
425
+ AND enabled = 1
426
+ AND id >= ?
427
+ AND target_app_version IS NOT NULL
428
+ GROUP BY target_app_version
429
+ `);
430
+ return (await resolvePage(await cf.d1.database.query(config.databaseId, {
431
+ account_id: config.accountId,
432
+ sql,
433
+ params: [
434
+ channel,
435
+ platform,
436
+ minBundleId
437
+ ]
438
+ }))).map((row) => row.target_app_version);
439
+ }
407
440
  return {
441
+ getUpdateInfo: (0, _hot_updater_plugin_core.createDatabasePluginGetUpdateInfo)({
442
+ listTargetAppVersions: getTargetAppVersionsForUpdateInfo,
443
+ getBundlesByTargetAppVersions({ platform, channel, minBundleId }, targetAppVersions) {
444
+ return queryBundlesForUpdateInfo({
445
+ enabled: true,
446
+ platform,
447
+ channel,
448
+ id: { gte: minBundleId },
449
+ targetAppVersionIn: targetAppVersions
450
+ });
451
+ },
452
+ getBundlesByFingerprint({ platform, channel, minBundleId, fingerprintHash }) {
453
+ return queryBundlesForUpdateInfo({
454
+ enabled: true,
455
+ platform,
456
+ channel,
457
+ id: { gte: minBundleId },
458
+ fingerprintHash
459
+ });
460
+ }
461
+ }),
408
462
  async getBundleById(bundleId) {
409
463
  const sql = (0, import_lib.default)(`
410
464
  SELECT * FROM bundles WHERE id = ? LIMIT 1`);
@@ -417,7 +471,8 @@ const d1Database = (0, _hot_updater_plugin_core.createDatabasePlugin)({
417
471
  return transformRowToBundle(rows[0]);
418
472
  },
419
473
  async getBundles(options) {
420
- const { where = {}, limit, offset, orderBy } = options;
474
+ const { where = {}, limit, orderBy } = options;
475
+ const offset = ("offset" in options ? options.offset : void 0) ?? 0;
421
476
  const totalCount = await getTotalCount(where);
422
477
  return {
423
478
  data: await getPaginatedBundles(where, limit, offset, orderBy),
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { DEFAULT_ROLLOUT_COHORT_COUNT } from "@hot-updater/core";
3
- import { calculatePagination, createDatabasePlugin, createStorageKeyBuilder, createStoragePlugin, getContentType, parseStorageUri } from "@hot-updater/plugin-core";
3
+ import { calculatePagination, createDatabasePlugin, createDatabasePluginGetUpdateInfo, createStorageKeyBuilder, createStoragePlugin, getContentType, parseStorageUri } from "@hot-updater/plugin-core";
4
4
  import Cloudflare from "cloudflare";
5
5
  import path from "path";
6
6
  import { fileURLToPath } from "node:url";
@@ -400,7 +400,61 @@ const d1Database = createDatabasePlugin({
400
400
  params
401
401
  }))).map(transformRowToBundle);
402
402
  }
403
+ async function queryBundlesForUpdateInfo(conditions) {
404
+ const { sql: whereClause, params } = buildWhereClause(conditions);
405
+ const sql = (0, import_lib.default)(`
406
+ SELECT * FROM bundles
407
+ ${whereClause}
408
+ `);
409
+ return (await resolvePage(await cf.d1.database.query(config.databaseId, {
410
+ account_id: config.accountId,
411
+ sql,
412
+ params
413
+ }))).map(transformRowToBundle);
414
+ }
415
+ async function getTargetAppVersionsForUpdateInfo({ platform, channel, minBundleId }) {
416
+ const sql = (0, import_lib.default)(`
417
+ SELECT target_app_version
418
+ FROM bundles
419
+ WHERE channel = ?
420
+ AND platform = ?
421
+ AND enabled = 1
422
+ AND id >= ?
423
+ AND target_app_version IS NOT NULL
424
+ GROUP BY target_app_version
425
+ `);
426
+ return (await resolvePage(await cf.d1.database.query(config.databaseId, {
427
+ account_id: config.accountId,
428
+ sql,
429
+ params: [
430
+ channel,
431
+ platform,
432
+ minBundleId
433
+ ]
434
+ }))).map((row) => row.target_app_version);
435
+ }
403
436
  return {
437
+ getUpdateInfo: createDatabasePluginGetUpdateInfo({
438
+ listTargetAppVersions: getTargetAppVersionsForUpdateInfo,
439
+ getBundlesByTargetAppVersions({ platform, channel, minBundleId }, targetAppVersions) {
440
+ return queryBundlesForUpdateInfo({
441
+ enabled: true,
442
+ platform,
443
+ channel,
444
+ id: { gte: minBundleId },
445
+ targetAppVersionIn: targetAppVersions
446
+ });
447
+ },
448
+ getBundlesByFingerprint({ platform, channel, minBundleId, fingerprintHash }) {
449
+ return queryBundlesForUpdateInfo({
450
+ enabled: true,
451
+ platform,
452
+ channel,
453
+ id: { gte: minBundleId },
454
+ fingerprintHash
455
+ });
456
+ }
457
+ }),
404
458
  async getBundleById(bundleId) {
405
459
  const sql = (0, import_lib.default)(`
406
460
  SELECT * FROM bundles WHERE id = ? LIMIT 1`);
@@ -413,7 +467,8 @@ const d1Database = createDatabasePlugin({
413
467
  return transformRowToBundle(rows[0]);
414
468
  },
415
469
  async getBundles(options) {
416
- const { where = {}, limit, offset, orderBy } = options;
470
+ const { where = {}, limit, orderBy } = options;
471
+ const offset = ("offset" in options ? options.offset : void 0) ?? 0;
417
472
  const totalCount = await getTotalCount(where);
418
473
  return {
419
474
  data: await getPaginatedBundles(where, limit, offset, orderBy),
@@ -111,13 +111,58 @@ const d1WorkerDatabase = () => (0, _hot_updater_plugin_core.createDatabasePlugin
111
111
  const queryFirst = async (sql, params = [], context) => {
112
112
  return await config.getDb(context).prepare(sql).bind(...params).first() ?? null;
113
113
  };
114
+ const queryBundlesForUpdateInfo = async (conditions, context) => {
115
+ const { sql: whereClause, params } = buildWhereClause(conditions);
116
+ return (await queryAll(`
117
+ SELECT * FROM bundles
118
+ ${whereClause}
119
+ `, params, context)).map(transformRowToBundle);
120
+ };
121
+ const getTargetAppVersionsForUpdateInfo = async ({ platform, channel, minBundleId }, context) => {
122
+ return (await queryAll(`
123
+ SELECT target_app_version
124
+ FROM bundles
125
+ WHERE channel = ?
126
+ AND platform = ?
127
+ AND enabled = 1
128
+ AND id >= ?
129
+ AND target_app_version IS NOT NULL
130
+ GROUP BY target_app_version
131
+ `, [
132
+ channel,
133
+ platform,
134
+ minBundleId
135
+ ], context)).map((row) => row.target_app_version);
136
+ };
114
137
  return {
138
+ getUpdateInfo: (0, _hot_updater_plugin_core.createDatabasePluginGetUpdateInfo)({
139
+ listTargetAppVersions: getTargetAppVersionsForUpdateInfo,
140
+ getBundlesByTargetAppVersions({ platform, channel, minBundleId }, targetAppVersions, context) {
141
+ return queryBundlesForUpdateInfo({
142
+ enabled: true,
143
+ platform,
144
+ channel,
145
+ id: { gte: minBundleId },
146
+ targetAppVersionIn: targetAppVersions
147
+ }, context);
148
+ },
149
+ getBundlesByFingerprint({ platform, channel, minBundleId, fingerprintHash }, context) {
150
+ return queryBundlesForUpdateInfo({
151
+ enabled: true,
152
+ platform,
153
+ channel,
154
+ id: { gte: minBundleId },
155
+ fingerprintHash
156
+ }, context);
157
+ }
158
+ }),
115
159
  async getBundleById(bundleId, context) {
116
160
  const row = await queryFirst("SELECT * FROM bundles WHERE id = ? LIMIT 1", [bundleId], context);
117
161
  return row ? transformRowToBundle(row) : null;
118
162
  },
119
163
  async getBundles(options, context) {
120
- const { where, limit, offset, orderBy } = options;
164
+ const { where, limit, orderBy } = options;
165
+ const offset = ("offset" in options ? options.offset : void 0) ?? 0;
121
166
  const { sql: whereClause, params } = buildWhereClause(where);
122
167
  const orderSql = orderBy?.direction === "asc" ? "ORDER BY id ASC" : "ORDER BY id DESC";
123
168
  const total = (await queryAll(`SELECT COUNT(*) as total FROM bundles${whereClause}`, params, context))[0]?.total ?? 0;
@@ -1,6 +1,6 @@
1
1
  import { signToken, verifyJwtSignedUrl } from "@hot-updater/js";
2
2
  import { DEFAULT_ROLLOUT_COHORT_COUNT } from "@hot-updater/core";
3
- import { calculatePagination, createDatabasePlugin } from "@hot-updater/plugin-core";
3
+ import { calculatePagination, createDatabasePlugin, createDatabasePluginGetUpdateInfo } from "@hot-updater/plugin-core";
4
4
  //#region src/cloudflareWorkerDatabase.ts
5
5
  function buildWhereClause(conditions) {
6
6
  if (!conditions) return {
@@ -110,13 +110,58 @@ const d1WorkerDatabase = () => createDatabasePlugin({
110
110
  const queryFirst = async (sql, params = [], context) => {
111
111
  return await config.getDb(context).prepare(sql).bind(...params).first() ?? null;
112
112
  };
113
+ const queryBundlesForUpdateInfo = async (conditions, context) => {
114
+ const { sql: whereClause, params } = buildWhereClause(conditions);
115
+ return (await queryAll(`
116
+ SELECT * FROM bundles
117
+ ${whereClause}
118
+ `, params, context)).map(transformRowToBundle);
119
+ };
120
+ const getTargetAppVersionsForUpdateInfo = async ({ platform, channel, minBundleId }, context) => {
121
+ return (await queryAll(`
122
+ SELECT target_app_version
123
+ FROM bundles
124
+ WHERE channel = ?
125
+ AND platform = ?
126
+ AND enabled = 1
127
+ AND id >= ?
128
+ AND target_app_version IS NOT NULL
129
+ GROUP BY target_app_version
130
+ `, [
131
+ channel,
132
+ platform,
133
+ minBundleId
134
+ ], context)).map((row) => row.target_app_version);
135
+ };
113
136
  return {
137
+ getUpdateInfo: createDatabasePluginGetUpdateInfo({
138
+ listTargetAppVersions: getTargetAppVersionsForUpdateInfo,
139
+ getBundlesByTargetAppVersions({ platform, channel, minBundleId }, targetAppVersions, context) {
140
+ return queryBundlesForUpdateInfo({
141
+ enabled: true,
142
+ platform,
143
+ channel,
144
+ id: { gte: minBundleId },
145
+ targetAppVersionIn: targetAppVersions
146
+ }, context);
147
+ },
148
+ getBundlesByFingerprint({ platform, channel, minBundleId, fingerprintHash }, context) {
149
+ return queryBundlesForUpdateInfo({
150
+ enabled: true,
151
+ platform,
152
+ channel,
153
+ id: { gte: minBundleId },
154
+ fingerprintHash
155
+ }, context);
156
+ }
157
+ }),
114
158
  async getBundleById(bundleId, context) {
115
159
  const row = await queryFirst("SELECT * FROM bundles WHERE id = ? LIMIT 1", [bundleId], context);
116
160
  return row ? transformRowToBundle(row) : null;
117
161
  },
118
162
  async getBundles(options, context) {
119
- const { where, limit, offset, orderBy } = options;
163
+ const { where, limit, orderBy } = options;
164
+ const offset = ("offset" in options ? options.offset : void 0) ?? 0;
120
165
  const { sql: whereClause, params } = buildWhereClause(where);
121
166
  const orderSql = orderBy?.direction === "asc" ? "ORDER BY id ASC" : "ORDER BY id DESC";
122
167
  const total = (await queryAll(`SELECT COUNT(*) as total FROM bundles${whereClause}`, params, context))[0]?.total ?? 0;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/cloudflare",
3
3
  "type": "module",
4
- "version": "0.29.5",
4
+ "version": "0.29.6",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.mjs",
@@ -50,11 +50,11 @@
50
50
  "cloudflare": "4.2.0",
51
51
  "hono": "4.12.9",
52
52
  "uuidv7": "^1.0.2",
53
- "@hot-updater/plugin-core": "0.29.5",
54
- "@hot-updater/js": "0.29.5",
55
- "@hot-updater/server": "0.29.5",
56
- "@hot-updater/cli-tools": "0.29.5",
57
- "@hot-updater/core": "0.29.5"
53
+ "@hot-updater/cli-tools": "0.29.6",
54
+ "@hot-updater/core": "0.29.6",
55
+ "@hot-updater/js": "0.29.6",
56
+ "@hot-updater/plugin-core": "0.29.6",
57
+ "@hot-updater/server": "0.29.6"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@cloudflare/vitest-pool-workers": "0.13.0",
@@ -71,7 +71,7 @@
71
71
  "vitest": "4.1.0",
72
72
  "wrangler": "^4.5.0",
73
73
  "xdg-app-paths": "^8.3.0",
74
- "@hot-updater/test-utils": "0.29.5"
74
+ "@hot-updater/test-utils": "0.29.6"
75
75
  },
76
76
  "scripts": {
77
77
  "build": "tsdown && pnpm build:worker",
@@ -12,6 +12,7 @@ import type {
12
12
  import {
13
13
  calculatePagination,
14
14
  createDatabasePlugin,
15
+ createDatabasePluginGetUpdateInfo,
15
16
  } from "@hot-updater/plugin-core";
16
17
 
17
18
  type D1Result<T> = {
@@ -235,7 +236,93 @@ export const d1WorkerDatabase = <
235
236
  return result ?? null;
236
237
  };
237
238
 
239
+ const queryBundlesForUpdateInfo = async (
240
+ conditions: QueryConditions,
241
+ context?: HotUpdaterContext<TContext>,
242
+ ): Promise<Bundle[]> => {
243
+ const { sql: whereClause, params } = buildWhereClause(conditions);
244
+ const rows = await queryAll<SnakeCaseBundle>(
245
+ `
246
+ SELECT * FROM bundles
247
+ ${whereClause}
248
+ `,
249
+ params,
250
+ context,
251
+ );
252
+
253
+ return rows.map(transformRowToBundle);
254
+ };
255
+
256
+ const getTargetAppVersionsForUpdateInfo = async (
257
+ {
258
+ platform,
259
+ channel,
260
+ minBundleId,
261
+ }: {
262
+ platform: Bundle["platform"];
263
+ channel: string;
264
+ minBundleId: string;
265
+ },
266
+ context?: HotUpdaterContext<TContext>,
267
+ ): Promise<string[]> => {
268
+ const rows = await queryAll<{ target_app_version: string }>(
269
+ `
270
+ SELECT target_app_version
271
+ FROM bundles
272
+ WHERE channel = ?
273
+ AND platform = ?
274
+ AND enabled = 1
275
+ AND id >= ?
276
+ AND target_app_version IS NOT NULL
277
+ GROUP BY target_app_version
278
+ `,
279
+ [channel, platform, minBundleId],
280
+ context,
281
+ );
282
+
283
+ return rows.map((row) => row.target_app_version);
284
+ };
285
+
238
286
  return {
287
+ getUpdateInfo: createDatabasePluginGetUpdateInfo({
288
+ listTargetAppVersions: getTargetAppVersionsForUpdateInfo,
289
+ getBundlesByTargetAppVersions(
290
+ { platform, channel, minBundleId },
291
+ targetAppVersions,
292
+ context,
293
+ ) {
294
+ return queryBundlesForUpdateInfo(
295
+ {
296
+ enabled: true,
297
+ platform,
298
+ channel,
299
+ id: {
300
+ gte: minBundleId,
301
+ },
302
+ targetAppVersionIn: targetAppVersions,
303
+ },
304
+ context,
305
+ );
306
+ },
307
+ getBundlesByFingerprint(
308
+ { platform, channel, minBundleId, fingerprintHash },
309
+ context,
310
+ ) {
311
+ return queryBundlesForUpdateInfo(
312
+ {
313
+ enabled: true,
314
+ platform,
315
+ channel,
316
+ id: {
317
+ gte: minBundleId,
318
+ },
319
+ fingerprintHash,
320
+ },
321
+ context,
322
+ );
323
+ },
324
+ }),
325
+
239
326
  async getBundleById(bundleId, context) {
240
327
  const row = await queryFirst<SnakeCaseBundle>(
241
328
  "SELECT * FROM bundles WHERE id = ? LIMIT 1",
@@ -247,7 +334,11 @@ export const d1WorkerDatabase = <
247
334
  },
248
335
 
249
336
  async getBundles(options, context) {
250
- const { where, limit, offset, orderBy } = options;
337
+ const { where, limit, orderBy } = options;
338
+ const offset =
339
+ (("offset" in options ? options.offset : undefined) as
340
+ | number
341
+ | undefined) ?? 0;
251
342
  const { sql: whereClause, params } = buildWhereClause(where);
252
343
  const orderSql =
253
344
  orderBy?.direction === "asc"
@@ -1,5 +1,8 @@
1
1
  import type { DatabasePlugin } from "@hot-updater/plugin-core";
2
- import { setupBundleMethodsTestSuite } from "@hot-updater/test-utils";
2
+ import {
3
+ setupBundleMethodsTestSuite,
4
+ setupGetUpdateInfoTestSuite,
5
+ } from "@hot-updater/test-utils";
3
6
  import { beforeEach, describe, expect, it, vi } from "vitest";
4
7
 
5
8
  import { d1Database } from "./d1Database";
@@ -178,6 +181,23 @@ vi.mock("cloudflare", () => ({
178
181
  return createPage(result);
179
182
  }
180
183
 
184
+ if (
185
+ normalizedSql.startsWith("select target_app_version from bundles")
186
+ ) {
187
+ const { filteredRows } = getFilteredRows(sql, params);
188
+ const result = Array.from(
189
+ new Set(
190
+ filteredRows
191
+ .map((row) => row.target_app_version)
192
+ .filter((version): version is string => Boolean(version)),
193
+ ),
194
+ ).map((targetAppVersion) => ({
195
+ target_app_version: targetAppVersion,
196
+ }));
197
+
198
+ return createPage(result);
199
+ }
200
+
181
201
  if (
182
202
  normalizedSql.startsWith(
183
203
  "select channel from bundles group by channel",
@@ -256,6 +276,19 @@ describe("d1Database plugin", () => {
256
276
  },
257
277
  });
258
278
 
279
+ setupGetUpdateInfoTestSuite({
280
+ getUpdateInfo: async (bundles, args) => {
281
+ rows.clear();
282
+
283
+ for (const bundle of bundles) {
284
+ await plugin.appendBundle(bundle);
285
+ }
286
+ await plugin.commitBundle();
287
+
288
+ return plugin.getUpdateInfo?.(args) ?? null;
289
+ },
290
+ });
291
+
259
292
  it("refreshes bundle data before merging an update after a previous list request", async () => {
260
293
  const bundleId = "bundle-stale-cache";
261
294
  const initialRow: D1Row = {
@@ -276,7 +309,7 @@ describe("d1Database plugin", () => {
276
309
  };
277
310
  rows.set(bundleId, initialRow);
278
311
 
279
- await plugin.getBundles({ limit: 20, offset: 0 });
312
+ await plugin.getBundles({ limit: 20 });
280
313
 
281
314
  rows.set(bundleId, {
282
315
  ...initialRow,