@hot-updater/server 0.28.0 → 0.29.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 (84) hide show
  1. package/dist/adapters/drizzle.cjs +7 -7
  2. package/dist/adapters/drizzle.mjs +2 -0
  3. package/dist/adapters/kysely.cjs +7 -7
  4. package/dist/adapters/kysely.mjs +2 -0
  5. package/dist/adapters/mongodb.cjs +7 -7
  6. package/dist/adapters/mongodb.mjs +2 -0
  7. package/dist/adapters/prisma.cjs +7 -7
  8. package/dist/adapters/prisma.mjs +2 -0
  9. package/dist/calculatePagination.cjs +1 -3
  10. package/dist/{calculatePagination.js → calculatePagination.mjs} +1 -2
  11. package/dist/db/index.cjs +24 -15
  12. package/dist/db/index.d.cts +12 -9
  13. package/dist/db/index.d.mts +30 -0
  14. package/dist/db/index.mjs +45 -0
  15. package/dist/db/ormCore.cjs +247 -138
  16. package/dist/db/ormCore.d.cts +35 -17
  17. package/dist/db/ormCore.d.mts +44 -0
  18. package/dist/db/ormCore.mjs +386 -0
  19. package/dist/db/pluginCore.cjs +145 -40
  20. package/dist/db/pluginCore.mjs +176 -0
  21. package/dist/db/types.cjs +1 -3
  22. package/dist/db/types.d.cts +14 -21
  23. package/dist/db/types.d.mts +24 -0
  24. package/dist/db/{types.js → types.mjs} +1 -2
  25. package/dist/handler.cjs +117 -48
  26. package/dist/handler.d.cts +28 -18
  27. package/dist/handler.d.mts +47 -0
  28. package/dist/handler.mjs +217 -0
  29. package/dist/index.cjs +5 -5
  30. package/dist/index.d.cts +3 -3
  31. package/dist/index.d.mts +5 -0
  32. package/dist/index.mjs +4 -0
  33. package/dist/internalRouter.cjs +54 -0
  34. package/dist/internalRouter.mjs +52 -0
  35. package/dist/node.cjs +2 -3
  36. package/dist/node.d.cts +0 -1
  37. package/dist/{node.d.ts → node.d.mts} +1 -2
  38. package/dist/{node.js → node.mjs} +1 -2
  39. package/dist/route.cjs +7 -0
  40. package/dist/route.mjs +7 -0
  41. package/dist/runtime.cjs +42 -0
  42. package/dist/runtime.d.cts +21 -0
  43. package/dist/runtime.d.mts +21 -0
  44. package/dist/runtime.mjs +40 -0
  45. package/dist/schema/v0_21_0.cjs +1 -5
  46. package/dist/schema/{v0_21_0.js → v0_21_0.mjs} +1 -3
  47. package/dist/schema/v0_29_0.cjs +24 -0
  48. package/dist/schema/v0_29_0.mjs +24 -0
  49. package/dist/types/{index.d.ts → index.d.mts} +1 -1
  50. package/package.json +18 -18
  51. package/src/db/index.spec.ts +64 -29
  52. package/src/db/index.ts +55 -35
  53. package/src/db/ormCore.ts +438 -210
  54. package/src/db/ormUpdateCheck.bench.ts +261 -0
  55. package/src/db/pluginCore.ts +298 -49
  56. package/src/db/pluginUpdateCheck.bench.ts +250 -0
  57. package/src/db/types.ts +52 -27
  58. package/src/{handler-standalone-integration.spec.ts → handler-standalone.integration.spec.ts} +106 -0
  59. package/src/handler.spec.ts +156 -0
  60. package/src/handler.ts +296 -77
  61. package/src/internalRouter.ts +104 -0
  62. package/src/route.ts +7 -0
  63. package/src/runtime.spec.ts +277 -0
  64. package/src/runtime.ts +121 -0
  65. package/src/schema/v0_29_0.ts +26 -0
  66. package/dist/_virtual/rolldown_runtime.cjs +0 -25
  67. package/dist/adapters/drizzle.js +0 -3
  68. package/dist/adapters/kysely.js +0 -3
  69. package/dist/adapters/mongodb.js +0 -3
  70. package/dist/adapters/prisma.js +0 -3
  71. package/dist/db/index.d.ts +0 -27
  72. package/dist/db/index.js +0 -36
  73. package/dist/db/ormCore.d.ts +0 -26
  74. package/dist/db/ormCore.js +0 -273
  75. package/dist/db/pluginCore.js +0 -69
  76. package/dist/db/types.d.ts +0 -31
  77. package/dist/handler.d.ts +0 -37
  78. package/dist/handler.js +0 -146
  79. package/dist/index.d.ts +0 -5
  80. package/dist/index.js +0 -5
  81. /package/dist/adapters/{drizzle.d.ts → drizzle.d.mts} +0 -0
  82. /package/dist/adapters/{kysely.d.ts → kysely.d.mts} +0 -0
  83. /package/dist/adapters/{mongodb.d.ts → mongodb.d.mts} +0 -0
  84. /package/dist/adapters/{prisma.d.ts → prisma.d.mts} +0 -0
@@ -0,0 +1,176 @@
1
+ import { NIL_UUID, isCohortEligibleForUpdate } from "@hot-updater/core";
2
+ import { semverSatisfies } from "@hot-updater/plugin-core";
3
+ //#region src/db/pluginCore.ts
4
+ const PAGE_SIZE = 100;
5
+ const DESC_ORDER = {
6
+ field: "id",
7
+ direction: "desc"
8
+ };
9
+ const bundleMatchesQueryWhere = (bundle, where) => {
10
+ if (!where) return true;
11
+ if (where.channel !== void 0 && bundle.channel !== where.channel) return false;
12
+ if (where.platform !== void 0 && bundle.platform !== where.platform) return false;
13
+ if (where.enabled !== void 0 && bundle.enabled !== where.enabled) return false;
14
+ if (where.id?.eq !== void 0 && bundle.id !== where.id.eq) return false;
15
+ if (where.id?.gt !== void 0 && bundle.id.localeCompare(where.id.gt) <= 0) return false;
16
+ if (where.id?.gte !== void 0 && bundle.id.localeCompare(where.id.gte) < 0) return false;
17
+ if (where.id?.lt !== void 0 && bundle.id.localeCompare(where.id.lt) >= 0) return false;
18
+ if (where.id?.lte !== void 0 && bundle.id.localeCompare(where.id.lte) > 0) return false;
19
+ if (where.id?.in && !where.id.in.includes(bundle.id)) return false;
20
+ if (where.targetAppVersionNotNull && bundle.targetAppVersion === null) return false;
21
+ if (where.targetAppVersion !== void 0 && bundle.targetAppVersion !== where.targetAppVersion) return false;
22
+ if (where.targetAppVersionIn && !where.targetAppVersionIn.includes(bundle.targetAppVersion ?? "")) return false;
23
+ if (where.fingerprintHash !== void 0 && bundle.fingerprintHash !== where.fingerprintHash) return false;
24
+ return true;
25
+ };
26
+ const sortBundles = (bundles, orderBy) => {
27
+ const direction = orderBy?.direction ?? "desc";
28
+ return bundles.slice().sort((a, b) => {
29
+ const result = a.id.localeCompare(b.id);
30
+ return direction === "asc" ? result : -result;
31
+ });
32
+ };
33
+ const makeResponse = (bundle, status) => ({
34
+ id: bundle.id,
35
+ message: bundle.message,
36
+ shouldForceUpdate: status === "ROLLBACK" ? true : bundle.shouldForceUpdate,
37
+ status,
38
+ storageUri: bundle.storageUri,
39
+ fileHash: bundle.fileHash
40
+ });
41
+ const INIT_BUNDLE_ROLLBACK_UPDATE_INFO = {
42
+ message: null,
43
+ id: NIL_UUID,
44
+ shouldForceUpdate: true,
45
+ status: "ROLLBACK",
46
+ storageUri: null,
47
+ fileHash: null
48
+ };
49
+ function createPluginDatabaseCore(plugin, resolveFileUrl) {
50
+ const getSortedBundlePage = async (options, context) => {
51
+ const result = await plugin.getBundles({
52
+ ...options,
53
+ orderBy: options.orderBy ?? DESC_ORDER
54
+ }, context);
55
+ return {
56
+ ...result,
57
+ data: sortBundles(result.data, options.orderBy ?? DESC_ORDER)
58
+ };
59
+ };
60
+ const isEligibleForUpdate = (bundle, cohort) => {
61
+ return isCohortEligibleForUpdate(bundle.id, cohort, bundle.rolloutCohortCount, bundle.targetCohorts);
62
+ };
63
+ const findUpdateInfoByScanning = async ({ args, queryWhere, isCandidate, context }) => {
64
+ let offset = 0;
65
+ while (true) {
66
+ const { data, pagination } = await getSortedBundlePage({
67
+ where: queryWhere,
68
+ limit: PAGE_SIZE,
69
+ offset,
70
+ orderBy: DESC_ORDER
71
+ }, context);
72
+ for (const bundle of data) {
73
+ if (!bundleMatchesQueryWhere(bundle, queryWhere) || !isCandidate(bundle)) continue;
74
+ if (args.bundleId === NIL_UUID) {
75
+ if (isEligibleForUpdate(bundle, args.cohort)) return makeResponse(bundle, "UPDATE");
76
+ continue;
77
+ }
78
+ const compareResult = bundle.id.localeCompare(args.bundleId);
79
+ if (compareResult > 0) {
80
+ if (isEligibleForUpdate(bundle, args.cohort)) return makeResponse(bundle, "UPDATE");
81
+ continue;
82
+ }
83
+ if (compareResult === 0) {
84
+ if (isEligibleForUpdate(bundle, args.cohort)) return null;
85
+ continue;
86
+ }
87
+ return makeResponse(bundle, "ROLLBACK");
88
+ }
89
+ if (!pagination.hasNextPage) break;
90
+ offset += PAGE_SIZE;
91
+ }
92
+ if (args.bundleId === NIL_UUID) return null;
93
+ if (args.minBundleId && args.bundleId.localeCompare(args.minBundleId) <= 0) return null;
94
+ return INIT_BUNDLE_ROLLBACK_UPDATE_INFO;
95
+ };
96
+ const getBaseWhere = ({ platform, channel, minBundleId }) => ({
97
+ platform,
98
+ channel,
99
+ enabled: true,
100
+ id: { gte: minBundleId }
101
+ });
102
+ return {
103
+ api: {
104
+ async getBundleById(id, context) {
105
+ return plugin.getBundleById(id, context);
106
+ },
107
+ async getUpdateInfo(args, context) {
108
+ const channel = args.channel ?? "production";
109
+ const minBundleId = args.minBundleId ?? NIL_UUID;
110
+ const baseWhere = getBaseWhere({
111
+ platform: args.platform,
112
+ channel,
113
+ minBundleId
114
+ });
115
+ if (args._updateStrategy === "fingerprint") return findUpdateInfoByScanning({
116
+ args,
117
+ queryWhere: {
118
+ ...baseWhere,
119
+ fingerprintHash: args.fingerprintHash
120
+ },
121
+ context,
122
+ isCandidate: (bundle) => {
123
+ return bundle.enabled && bundle.platform === args.platform && bundle.channel === channel && bundle.id.localeCompare(minBundleId) >= 0 && bundle.fingerprintHash === args.fingerprintHash;
124
+ }
125
+ });
126
+ return findUpdateInfoByScanning({
127
+ args,
128
+ queryWhere: { ...baseWhere },
129
+ context,
130
+ isCandidate: (bundle) => {
131
+ return bundle.enabled && bundle.platform === args.platform && bundle.channel === channel && bundle.id.localeCompare(minBundleId) >= 0 && !!bundle.targetAppVersion && semverSatisfies(bundle.targetAppVersion, args.appVersion);
132
+ }
133
+ });
134
+ },
135
+ async getAppUpdateInfo(args, context) {
136
+ const info = await this.getUpdateInfo(args, context);
137
+ if (!info) return null;
138
+ const { storageUri, ...rest } = info;
139
+ const fileUrl = await resolveFileUrl(storageUri ?? null, context);
140
+ return {
141
+ ...rest,
142
+ fileUrl
143
+ };
144
+ },
145
+ async getChannels(context) {
146
+ return plugin.getChannels(context);
147
+ },
148
+ async getBundles(options, context) {
149
+ return plugin.getBundles(options, context);
150
+ },
151
+ async insertBundle(bundle, context) {
152
+ await plugin.appendBundle(bundle, context);
153
+ await plugin.commitBundle(context);
154
+ },
155
+ async updateBundleById(bundleId, newBundle, context) {
156
+ await plugin.updateBundle(bundleId, newBundle, context);
157
+ await plugin.commitBundle(context);
158
+ },
159
+ async deleteBundleById(bundleId, context) {
160
+ const bundle = await plugin.getBundleById(bundleId, context);
161
+ if (!bundle) return;
162
+ await plugin.deleteBundle(bundle, context);
163
+ await plugin.commitBundle(context);
164
+ }
165
+ },
166
+ adapterName: plugin.name,
167
+ createMigrator: () => {
168
+ throw new Error("createMigrator is only available for Kysely/Prisma/Drizzle database adapters.");
169
+ },
170
+ generateSchema: () => {
171
+ throw new Error("generateSchema is only available for Kysely/Prisma/Drizzle database adapters.");
172
+ }
173
+ };
174
+ }
175
+ //#endregion
176
+ export { createPluginDatabaseCore };
package/dist/db/types.cjs CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  //#region src/db/types.ts
3
2
  function isDatabasePluginFactory(adapter) {
4
3
  return typeof adapter === "function";
@@ -6,7 +5,6 @@ function isDatabasePluginFactory(adapter) {
6
5
  function isDatabasePlugin(adapter) {
7
6
  return typeof adapter === "object" && adapter !== null && "getBundleById" in adapter && "getBundles" in adapter && "getChannels" in adapter;
8
7
  }
9
-
10
8
  //#endregion
11
9
  exports.isDatabasePlugin = isDatabasePlugin;
12
- exports.isDatabasePluginFactory = isDatabasePluginFactory;
10
+ exports.isDatabasePluginFactory = isDatabasePluginFactory;
@@ -1,31 +1,24 @@
1
1
  import { PaginationInfo } from "../types/index.cjs";
2
- import { DatabasePlugin, StoragePlugin } from "@hot-updater/plugin-core";
3
- import { FumaDBAdapter } from "fumadb/adapters";
2
+ import { DatabaseBundleQueryOptions, DatabasePlugin, HotUpdaterContext, StoragePlugin } from "@hot-updater/plugin-core";
4
3
  import { AppUpdateInfo, Bundle, GetBundlesArgs, UpdateInfo } from "@hot-updater/core";
4
+ import { FumaDBAdapter } from "fumadb/adapters";
5
5
 
6
6
  //#region src/db/types.d.ts
7
- type DatabasePluginFactory = () => DatabasePlugin;
8
- type DatabaseAdapter = FumaDBAdapter | DatabasePlugin | DatabasePluginFactory;
9
- interface DatabaseAPI {
10
- getBundleById(id: string): Promise<Bundle | null>;
11
- getUpdateInfo(args: GetBundlesArgs): Promise<UpdateInfo | null>;
12
- getAppUpdateInfo(args: GetBundlesArgs): Promise<AppUpdateInfo | null>;
13
- getChannels(): Promise<string[]>;
14
- getBundles(options: {
15
- where?: {
16
- channel?: string;
17
- platform?: string;
18
- };
19
- limit: number;
20
- offset: number;
21
- }): Promise<{
7
+ type DatabasePluginFactory<TContext = unknown> = () => DatabasePlugin<TContext>;
8
+ type DatabaseAdapter<TContext = unknown> = FumaDBAdapter | DatabasePlugin<TContext> | DatabasePluginFactory<TContext>;
9
+ interface DatabaseAPI<TContext = unknown> {
10
+ getBundleById(id: string, context?: HotUpdaterContext<TContext>): Promise<Bundle | null>;
11
+ getUpdateInfo(args: GetBundlesArgs, context?: HotUpdaterContext<TContext>): Promise<UpdateInfo | null>;
12
+ getAppUpdateInfo(args: GetBundlesArgs, context?: HotUpdaterContext<TContext>): Promise<AppUpdateInfo | null>;
13
+ getChannels(context?: HotUpdaterContext<TContext>): Promise<string[]>;
14
+ getBundles(options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>): Promise<{
22
15
  data: Bundle[];
23
16
  pagination: PaginationInfo;
24
17
  }>;
25
- insertBundle(bundle: Bundle): Promise<void>;
26
- updateBundleById(bundleId: string, newBundle: Partial<Bundle>): Promise<void>;
27
- deleteBundleById(bundleId: string): Promise<void>;
18
+ insertBundle(bundle: Bundle, context?: HotUpdaterContext<TContext>): Promise<void>;
19
+ updateBundleById(bundleId: string, newBundle: Partial<Bundle>, context?: HotUpdaterContext<TContext>): Promise<void>;
20
+ deleteBundleById(bundleId: string, context?: HotUpdaterContext<TContext>): Promise<void>;
28
21
  }
29
- type StoragePluginFactory = () => StoragePlugin;
22
+ type StoragePluginFactory<TContext = unknown> = () => StoragePlugin<TContext>;
30
23
  //#endregion
31
24
  export { DatabaseAPI, DatabaseAdapter, StoragePluginFactory };
@@ -0,0 +1,24 @@
1
+ import { PaginationInfo } from "../types/index.mjs";
2
+ import { AppUpdateInfo, Bundle, GetBundlesArgs, UpdateInfo } from "@hot-updater/core";
3
+ import { DatabaseBundleQueryOptions, DatabasePlugin, HotUpdaterContext, StoragePlugin } from "@hot-updater/plugin-core";
4
+ import { FumaDBAdapter } from "fumadb/adapters";
5
+
6
+ //#region src/db/types.d.ts
7
+ type DatabasePluginFactory<TContext = unknown> = () => DatabasePlugin<TContext>;
8
+ type DatabaseAdapter<TContext = unknown> = FumaDBAdapter | DatabasePlugin<TContext> | DatabasePluginFactory<TContext>;
9
+ interface DatabaseAPI<TContext = unknown> {
10
+ getBundleById(id: string, context?: HotUpdaterContext<TContext>): Promise<Bundle | null>;
11
+ getUpdateInfo(args: GetBundlesArgs, context?: HotUpdaterContext<TContext>): Promise<UpdateInfo | null>;
12
+ getAppUpdateInfo(args: GetBundlesArgs, context?: HotUpdaterContext<TContext>): Promise<AppUpdateInfo | null>;
13
+ getChannels(context?: HotUpdaterContext<TContext>): Promise<string[]>;
14
+ getBundles(options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>): Promise<{
15
+ data: Bundle[];
16
+ pagination: PaginationInfo;
17
+ }>;
18
+ insertBundle(bundle: Bundle, context?: HotUpdaterContext<TContext>): Promise<void>;
19
+ updateBundleById(bundleId: string, newBundle: Partial<Bundle>, context?: HotUpdaterContext<TContext>): Promise<void>;
20
+ deleteBundleById(bundleId: string, context?: HotUpdaterContext<TContext>): Promise<void>;
21
+ }
22
+ type StoragePluginFactory<TContext = unknown> = () => StoragePlugin<TContext>;
23
+ //#endregion
24
+ export { DatabaseAPI, DatabaseAdapter, StoragePluginFactory };
@@ -5,6 +5,5 @@ function isDatabasePluginFactory(adapter) {
5
5
  function isDatabasePlugin(adapter) {
6
6
  return typeof adapter === "object" && adapter !== null && "getBundleById" in adapter && "getBundles" in adapter && "getChannels" in adapter;
7
7
  }
8
-
9
8
  //#endregion
10
- export { isDatabasePlugin, isDatabasePluginFactory };
9
+ export { isDatabasePlugin, isDatabasePluginFactory };
package/dist/handler.cjs CHANGED
@@ -1,44 +1,88 @@
1
- const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
2
- let rou3 = require("rou3");
3
- rou3 = require_rolldown_runtime.__toESM(rou3);
4
-
1
+ const require_internalRouter = require("./internalRouter.cjs");
5
2
  //#region src/handler.ts
3
+ var HandlerBadRequestError = class extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = "HandlerBadRequestError";
7
+ }
8
+ };
6
9
  const handleVersion = async () => {
7
- return new Response(JSON.stringify({ version: "0.28.0" }), {
10
+ return new Response(JSON.stringify({ version: "0.29.0" }), {
8
11
  status: 200,
9
12
  headers: { "Content-Type": "application/json" }
10
13
  });
11
14
  };
12
- const handleFingerprintUpdate = async (params, _request, api) => {
15
+ const decodeMaybe = (value) => {
16
+ if (value === void 0) return void 0;
17
+ try {
18
+ return decodeURIComponent(value);
19
+ } catch {
20
+ return value;
21
+ }
22
+ };
23
+ const isPlatform = (value) => {
24
+ return value === "ios" || value === "android";
25
+ };
26
+ const requireRouteParam = (params, key) => {
27
+ const value = params[key];
28
+ if (!value) throw new HandlerBadRequestError(`Missing route parameter: ${key}`);
29
+ return value;
30
+ };
31
+ const requirePlatformParam = (params) => {
32
+ const platform = requireRouteParam(params, "platform");
33
+ if (!isPlatform(platform)) throw new HandlerBadRequestError(`Invalid platform: ${platform}. Expected 'ios' or 'android'.`);
34
+ return platform;
35
+ };
36
+ const requireBundlePatchPayload = (payload, bundleId) => {
37
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new HandlerBadRequestError("Invalid bundle payload");
38
+ const bundlePatch = payload;
39
+ if (bundlePatch.id !== void 0 && bundlePatch.id !== bundleId) throw new HandlerBadRequestError("Bundle id mismatch");
40
+ const { id: _ignoredId, ...rest } = bundlePatch;
41
+ return rest;
42
+ };
43
+ const handleFingerprintUpdateWithCohort = async (params, _request, api, context) => {
44
+ const platform = requirePlatformParam(params);
45
+ const fingerprintHash = requireRouteParam(params, "fingerprintHash");
46
+ const channel = requireRouteParam(params, "channel");
47
+ const minBundleId = requireRouteParam(params, "minBundleId");
48
+ const bundleId = requireRouteParam(params, "bundleId");
13
49
  const updateInfo = await api.getAppUpdateInfo({
14
50
  _updateStrategy: "fingerprint",
15
- platform: params.platform,
16
- fingerprintHash: params.fingerprintHash,
17
- channel: params.channel,
18
- minBundleId: params.minBundleId,
19
- bundleId: params.bundleId
20
- });
51
+ platform,
52
+ fingerprintHash,
53
+ channel,
54
+ minBundleId,
55
+ bundleId,
56
+ cohort: decodeMaybe(params.cohort)
57
+ }, context);
21
58
  return new Response(JSON.stringify(updateInfo), {
22
59
  status: 200,
23
60
  headers: { "Content-Type": "application/json" }
24
61
  });
25
62
  };
26
- const handleAppVersionUpdate = async (params, _request, api) => {
63
+ const handleAppVersionUpdateWithCohort = async (params, _request, api, context) => {
64
+ const platform = requirePlatformParam(params);
65
+ const appVersion = requireRouteParam(params, "appVersion");
66
+ const channel = requireRouteParam(params, "channel");
67
+ const minBundleId = requireRouteParam(params, "minBundleId");
68
+ const bundleId = requireRouteParam(params, "bundleId");
27
69
  const updateInfo = await api.getAppUpdateInfo({
28
70
  _updateStrategy: "appVersion",
29
- platform: params.platform,
30
- appVersion: params.appVersion,
31
- channel: params.channel,
32
- minBundleId: params.minBundleId,
33
- bundleId: params.bundleId
34
- });
71
+ platform,
72
+ appVersion,
73
+ channel,
74
+ minBundleId,
75
+ bundleId,
76
+ cohort: decodeMaybe(params.cohort)
77
+ }, context);
35
78
  return new Response(JSON.stringify(updateInfo), {
36
79
  status: 200,
37
80
  headers: { "Content-Type": "application/json" }
38
81
  });
39
82
  };
40
- const handleGetBundle = async (params, _request, api) => {
41
- const bundle = await api.getBundleById(params.id);
83
+ const handleGetBundle = async (params, _request, api, context) => {
84
+ const bundleId = requireRouteParam(params, "id");
85
+ const bundle = await api.getBundleById(bundleId, context);
42
86
  if (!bundle) return new Response(JSON.stringify({ error: "Bundle not found" }), {
43
87
  status: 404,
44
88
  headers: { "Content-Type": "application/json" }
@@ -48,12 +92,13 @@ const handleGetBundle = async (params, _request, api) => {
48
92
  headers: { "Content-Type": "application/json" }
49
93
  });
50
94
  };
51
- const handleGetBundles = async (_params, request, api) => {
95
+ const handleGetBundles = async (_params, request, api, context) => {
52
96
  const url = new URL(request.url);
53
97
  const channel = url.searchParams.get("channel") ?? void 0;
54
- const platform = url.searchParams.get("platform") ?? void 0;
98
+ const platform = url.searchParams.get("platform");
55
99
  const limit = Number(url.searchParams.get("limit")) || 50;
56
100
  const offset = Number(url.searchParams.get("offset")) || 0;
101
+ if (platform !== null && !isPlatform(platform)) throw new HandlerBadRequestError(`Invalid platform: ${platform}. Expected 'ios' or 'android'.`);
57
102
  const result = await api.getBundles({
58
103
  where: {
59
104
  ...channel && { channel },
@@ -61,30 +106,41 @@ const handleGetBundles = async (_params, request, api) => {
61
106
  },
62
107
  limit,
63
108
  offset
64
- });
109
+ }, context);
65
110
  return new Response(JSON.stringify(result.data), {
66
111
  status: 200,
67
112
  headers: { "Content-Type": "application/json" }
68
113
  });
69
114
  };
70
- const handleCreateBundles = async (_params, request, api) => {
115
+ const handleCreateBundles = async (_params, request, api, context) => {
71
116
  const body = await request.json();
72
117
  const bundles = Array.isArray(body) ? body : [body];
73
- for (const bundle of bundles) await api.insertBundle(bundle);
118
+ for (const bundle of bundles) await api.insertBundle(bundle, context);
74
119
  return new Response(JSON.stringify({ success: true }), {
75
120
  status: 201,
76
121
  headers: { "Content-Type": "application/json" }
77
122
  });
78
123
  };
79
- const handleDeleteBundle = async (params, _request, api) => {
80
- await api.deleteBundleById(params.id);
124
+ const handleUpdateBundle = async (params, request, api, context) => {
125
+ const bundleId = requireRouteParam(params, "id");
126
+ const body = await request.json();
127
+ const bundlePatch = requireBundlePatchPayload(Array.isArray(body) ? body[0] : body, bundleId);
128
+ await api.updateBundleById(bundleId, bundlePatch, context);
129
+ return new Response(JSON.stringify({ success: true }), {
130
+ status: 200,
131
+ headers: { "Content-Type": "application/json" }
132
+ });
133
+ };
134
+ const handleDeleteBundle = async (params, _request, api, context) => {
135
+ const bundleId = requireRouteParam(params, "id");
136
+ await api.deleteBundleById(bundleId, context);
81
137
  return new Response(JSON.stringify({ success: true }), {
82
138
  status: 200,
83
139
  headers: { "Content-Type": "application/json" }
84
140
  });
85
141
  };
86
- const handleGetChannels = async (_params, _request, api) => {
87
- const channels = await api.getChannels();
142
+ const handleGetChannels = async (_params, _request, api, context) => {
143
+ const channels = await api.getChannels(context);
88
144
  return new Response(JSON.stringify({ channels }), {
89
145
  status: 200,
90
146
  headers: { "Content-Type": "application/json" }
@@ -92,35 +148,45 @@ const handleGetChannels = async (_params, _request, api) => {
92
148
  };
93
149
  const routes = {
94
150
  version: handleVersion,
95
- fingerprintUpdate: handleFingerprintUpdate,
96
- appVersionUpdate: handleAppVersionUpdate,
151
+ fingerprintUpdateWithCohort: handleFingerprintUpdateWithCohort,
152
+ appVersionUpdateWithCohort: handleAppVersionUpdateWithCohort,
97
153
  getBundle: handleGetBundle,
98
154
  getBundles: handleGetBundles,
99
155
  createBundles: handleCreateBundles,
156
+ updateBundle: handleUpdateBundle,
100
157
  deleteBundle: handleDeleteBundle,
101
158
  getChannels: handleGetChannels
102
159
  };
103
160
  /**
104
161
  * Creates a Web Standard Request handler for Hot Updater API
105
- * This handler is framework-agnostic and works with any framework
106
- * that supports Web Standard Request/Response (Hono, Elysia, etc.)
162
+ * This handler is framework-agnostic and works with any runtime that
163
+ * supports standard Request/Response objects.
107
164
  */
108
165
  function createHandler(api, options = {}) {
109
166
  const basePath = options.basePath ?? "/api";
110
- const router = (0, rou3.createRouter)();
111
- (0, rou3.addRoute)(router, "GET", "/version", "version");
112
- (0, rou3.addRoute)(router, "GET", "/fingerprint/:platform/:fingerprintHash/:channel/:minBundleId/:bundleId", "fingerprintUpdate");
113
- (0, rou3.addRoute)(router, "GET", "/app-version/:platform/:appVersion/:channel/:minBundleId/:bundleId", "appVersionUpdate");
114
- (0, rou3.addRoute)(router, "GET", "/api/bundles/channels", "getChannels");
115
- (0, rou3.addRoute)(router, "GET", "/api/bundles/:id", "getBundle");
116
- (0, rou3.addRoute)(router, "GET", "/api/bundles", "getBundles");
117
- (0, rou3.addRoute)(router, "POST", "/api/bundles", "createBundles");
118
- (0, rou3.addRoute)(router, "DELETE", "/api/bundles/:id", "deleteBundle");
119
- return async (request) => {
167
+ const updateCheckEnabled = options.routes?.updateCheck ?? true;
168
+ const bundlesEnabled = options.routes?.bundles ?? true;
169
+ const router = require_internalRouter.createRouter();
170
+ if (updateCheckEnabled) {
171
+ require_internalRouter.addRoute(router, "GET", "/fingerprint/:platform/:fingerprintHash/:channel/:minBundleId/:bundleId", "fingerprintUpdateWithCohort");
172
+ require_internalRouter.addRoute(router, "GET", "/fingerprint/:platform/:fingerprintHash/:channel/:minBundleId/:bundleId/:cohort", "fingerprintUpdateWithCohort");
173
+ require_internalRouter.addRoute(router, "GET", "/app-version/:platform/:appVersion/:channel/:minBundleId/:bundleId", "appVersionUpdateWithCohort");
174
+ require_internalRouter.addRoute(router, "GET", "/app-version/:platform/:appVersion/:channel/:minBundleId/:bundleId/:cohort", "appVersionUpdateWithCohort");
175
+ }
176
+ if (bundlesEnabled) {
177
+ require_internalRouter.addRoute(router, "GET", "/version", "version");
178
+ require_internalRouter.addRoute(router, "GET", "/api/bundles/channels", "getChannels");
179
+ require_internalRouter.addRoute(router, "GET", "/api/bundles/:id", "getBundle");
180
+ require_internalRouter.addRoute(router, "GET", "/api/bundles", "getBundles");
181
+ require_internalRouter.addRoute(router, "POST", "/api/bundles", "createBundles");
182
+ require_internalRouter.addRoute(router, "PATCH", "/api/bundles/:id", "updateBundle");
183
+ require_internalRouter.addRoute(router, "DELETE", "/api/bundles/:id", "deleteBundle");
184
+ }
185
+ return async (request, context) => {
120
186
  try {
121
187
  const path = new URL(request.url).pathname;
122
188
  const method = request.method;
123
- const match = (0, rou3.findRoute)(router, method, path.startsWith(basePath) ? path.slice(basePath.length) : path);
189
+ const match = require_internalRouter.findRoute(router, method, path.startsWith(basePath) ? path.slice(basePath.length) : path);
124
190
  if (!match) return new Response(JSON.stringify({ error: "Not found" }), {
125
191
  status: 404,
126
192
  headers: { "Content-Type": "application/json" }
@@ -130,8 +196,12 @@ function createHandler(api, options = {}) {
130
196
  status: 500,
131
197
  headers: { "Content-Type": "application/json" }
132
198
  });
133
- return await handler(match.params || {}, request, api);
199
+ return await handler(match.params || {}, request, api, context);
134
200
  } catch (error) {
201
+ if (error instanceof HandlerBadRequestError) return new Response(JSON.stringify({ error: error.message }), {
202
+ status: 400,
203
+ headers: { "Content-Type": "application/json" }
204
+ });
135
205
  console.error("Hot Updater handler error:", error);
136
206
  return new Response(JSON.stringify({
137
207
  error: "Internal server error",
@@ -143,6 +213,5 @@ function createHandler(api, options = {}) {
143
213
  }
144
214
  };
145
215
  }
146
-
147
216
  //#endregion
148
- exports.createHandler = createHandler;
217
+ exports.createHandler = createHandler;
@@ -1,24 +1,19 @@
1
1
  import { PaginationInfo } from "./types/index.cjs";
2
+ import { DatabaseBundleQueryOptions, HotUpdaterContext } from "@hot-updater/plugin-core";
2
3
  import { AppUpdateInfo, AppVersionGetBundlesArgs, Bundle, FingerprintGetBundlesArgs } from "@hot-updater/core";
3
4
 
4
5
  //#region src/handler.d.ts
5
- interface HandlerAPI {
6
- getAppUpdateInfo: (args: AppVersionGetBundlesArgs | FingerprintGetBundlesArgs) => Promise<AppUpdateInfo | null>;
7
- getBundleById: (id: string) => Promise<Bundle | null>;
8
- getBundles: (options: {
9
- where?: {
10
- channel?: string;
11
- platform?: string;
12
- };
13
- limit: number;
14
- offset: number;
15
- }) => Promise<{
6
+ interface HandlerAPI<TContext = unknown> {
7
+ getAppUpdateInfo: (args: AppVersionGetBundlesArgs | FingerprintGetBundlesArgs, context?: HotUpdaterContext<TContext>) => Promise<AppUpdateInfo | null>;
8
+ getBundleById: (id: string, context?: HotUpdaterContext<TContext>) => Promise<Bundle | null>;
9
+ getBundles: (options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>) => Promise<{
16
10
  data: Bundle[];
17
11
  pagination: PaginationInfo;
18
12
  }>;
19
- insertBundle: (bundle: Bundle) => Promise<void>;
20
- deleteBundleById: (bundleId: string) => Promise<void>;
21
- getChannels: () => Promise<string[]>;
13
+ insertBundle: (bundle: Bundle, context?: HotUpdaterContext<TContext>) => Promise<void>;
14
+ updateBundleById: (bundleId: string, bundle: Partial<Bundle>, context?: HotUpdaterContext<TContext>) => Promise<void>;
15
+ deleteBundleById: (bundleId: string, context?: HotUpdaterContext<TContext>) => Promise<void>;
16
+ getChannels: (context?: HotUpdaterContext<TContext>) => Promise<string[]>;
22
17
  }
23
18
  interface HandlerOptions {
24
19
  /**
@@ -26,12 +21,27 @@ interface HandlerOptions {
26
21
  * @default "/api"
27
22
  */
28
23
  basePath?: string;
24
+ routes?: HandlerRoutes;
25
+ }
26
+ interface HandlerRoutes {
27
+ /**
28
+ * Controls whether update-check routes are mounted.
29
+ * @default true
30
+ */
31
+ updateCheck?: boolean;
32
+ /**
33
+ * Controls whether bundle management routes are mounted.
34
+ * This includes `/version` and `/api/bundles*`, which are used by the
35
+ * CLI `standaloneRepository` plugin.
36
+ * @default true
37
+ */
38
+ bundles?: boolean;
29
39
  }
30
40
  /**
31
41
  * Creates a Web Standard Request handler for Hot Updater API
32
- * This handler is framework-agnostic and works with any framework
33
- * that supports Web Standard Request/Response (Hono, Elysia, etc.)
42
+ * This handler is framework-agnostic and works with any runtime that
43
+ * supports standard Request/Response objects.
34
44
  */
35
- declare function createHandler(api: HandlerAPI, options?: HandlerOptions): (request: Request) => Promise<Response>;
45
+ declare function createHandler<TContext = unknown>(api: HandlerAPI<TContext>, options?: HandlerOptions): (request: Request, context?: HotUpdaterContext<TContext>) => Promise<Response>;
36
46
  //#endregion
37
- export { HandlerAPI, HandlerOptions, createHandler };
47
+ export { HandlerAPI, HandlerOptions, HandlerRoutes, createHandler };
@@ -0,0 +1,47 @@
1
+ import { PaginationInfo } from "./types/index.mjs";
2
+ import { AppUpdateInfo, AppVersionGetBundlesArgs, Bundle, FingerprintGetBundlesArgs } from "@hot-updater/core";
3
+ import { DatabaseBundleQueryOptions, HotUpdaterContext } from "@hot-updater/plugin-core";
4
+
5
+ //#region src/handler.d.ts
6
+ interface HandlerAPI<TContext = unknown> {
7
+ getAppUpdateInfo: (args: AppVersionGetBundlesArgs | FingerprintGetBundlesArgs, context?: HotUpdaterContext<TContext>) => Promise<AppUpdateInfo | null>;
8
+ getBundleById: (id: string, context?: HotUpdaterContext<TContext>) => Promise<Bundle | null>;
9
+ getBundles: (options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>) => Promise<{
10
+ data: Bundle[];
11
+ pagination: PaginationInfo;
12
+ }>;
13
+ insertBundle: (bundle: Bundle, context?: HotUpdaterContext<TContext>) => Promise<void>;
14
+ updateBundleById: (bundleId: string, bundle: Partial<Bundle>, context?: HotUpdaterContext<TContext>) => Promise<void>;
15
+ deleteBundleById: (bundleId: string, context?: HotUpdaterContext<TContext>) => Promise<void>;
16
+ getChannels: (context?: HotUpdaterContext<TContext>) => Promise<string[]>;
17
+ }
18
+ interface HandlerOptions {
19
+ /**
20
+ * Base path for all routes
21
+ * @default "/api"
22
+ */
23
+ basePath?: string;
24
+ routes?: HandlerRoutes;
25
+ }
26
+ interface HandlerRoutes {
27
+ /**
28
+ * Controls whether update-check routes are mounted.
29
+ * @default true
30
+ */
31
+ updateCheck?: boolean;
32
+ /**
33
+ * Controls whether bundle management routes are mounted.
34
+ * This includes `/version` and `/api/bundles*`, which are used by the
35
+ * CLI `standaloneRepository` plugin.
36
+ * @default true
37
+ */
38
+ bundles?: boolean;
39
+ }
40
+ /**
41
+ * Creates a Web Standard Request handler for Hot Updater API
42
+ * This handler is framework-agnostic and works with any runtime that
43
+ * supports standard Request/Response objects.
44
+ */
45
+ declare function createHandler<TContext = unknown>(api: HandlerAPI<TContext>, options?: HandlerOptions): (request: Request, context?: HotUpdaterContext<TContext>) => Promise<Response>;
46
+ //#endregion
47
+ export { HandlerAPI, HandlerOptions, HandlerRoutes, createHandler };