@hot-updater/server 0.21.5 → 0.21.7

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.
package/src/handler.ts CHANGED
@@ -4,6 +4,7 @@ import type {
4
4
  Bundle,
5
5
  FingerprintGetBundlesArgs,
6
6
  } from "@hot-updater/core";
7
+ import { addRoute, createRouter, findRoute } from "rou3";
7
8
  import type { PaginationInfo } from "./types";
8
9
 
9
10
  // Narrow API surface needed by the handler to avoid circular types
@@ -30,6 +31,139 @@ export interface HandlerOptions {
30
31
  basePath?: string;
31
32
  }
32
33
 
34
+ type RouteHandler = (
35
+ params: Record<string, string>,
36
+ request: Request,
37
+ api: HandlerAPI,
38
+ ) => Promise<Response>;
39
+
40
+ // Route handlers
41
+ const handleUpdate: RouteHandler = async (_params, request, api) => {
42
+ const body = (await request.json()) as
43
+ | AppVersionGetBundlesArgs
44
+ | FingerprintGetBundlesArgs;
45
+ const updateInfo = await api.getAppUpdateInfo(body);
46
+
47
+ return new Response(JSON.stringify(updateInfo), {
48
+ status: 200,
49
+ headers: { "Content-Type": "application/json" },
50
+ });
51
+ };
52
+
53
+ const handleFingerprintUpdate: RouteHandler = async (params, _request, api) => {
54
+ const updateInfo = await api.getAppUpdateInfo({
55
+ _updateStrategy: "fingerprint",
56
+ platform: params.platform as "ios" | "android",
57
+ fingerprintHash: params.fingerprintHash,
58
+ channel: params.channel,
59
+ minBundleId: params.minBundleId,
60
+ bundleId: params.bundleId,
61
+ });
62
+
63
+ return new Response(JSON.stringify(updateInfo), {
64
+ status: 200,
65
+ headers: { "Content-Type": "application/json" },
66
+ });
67
+ };
68
+
69
+ const handleAppVersionUpdate: RouteHandler = async (params, _request, api) => {
70
+ const updateInfo = await api.getAppUpdateInfo({
71
+ _updateStrategy: "appVersion",
72
+ platform: params.platform as "ios" | "android",
73
+ appVersion: params.appVersion,
74
+ channel: params.channel,
75
+ minBundleId: params.minBundleId,
76
+ bundleId: params.bundleId,
77
+ });
78
+
79
+ return new Response(JSON.stringify(updateInfo), {
80
+ status: 200,
81
+ headers: { "Content-Type": "application/json" },
82
+ });
83
+ };
84
+
85
+ const handleGetBundle: RouteHandler = async (params, _request, api) => {
86
+ const bundle = await api.getBundleById(params.id);
87
+
88
+ if (!bundle) {
89
+ return new Response(JSON.stringify({ error: "Bundle not found" }), {
90
+ status: 404,
91
+ headers: { "Content-Type": "application/json" },
92
+ });
93
+ }
94
+
95
+ return new Response(JSON.stringify(bundle), {
96
+ status: 200,
97
+ headers: { "Content-Type": "application/json" },
98
+ });
99
+ };
100
+
101
+ const handleGetBundles: RouteHandler = async (_params, request, api) => {
102
+ const url = new URL(request.url);
103
+ const channel = url.searchParams.get("channel") ?? undefined;
104
+ const platform = url.searchParams.get("platform") ?? undefined;
105
+ const limit = Number(url.searchParams.get("limit")) || 50;
106
+ const offset = Number(url.searchParams.get("offset")) || 0;
107
+
108
+ const result = await api.getBundles({
109
+ where: {
110
+ ...(channel && { channel }),
111
+ ...(platform && { platform }),
112
+ },
113
+ limit,
114
+ offset,
115
+ });
116
+
117
+ return new Response(JSON.stringify(result.data), {
118
+ status: 200,
119
+ headers: { "Content-Type": "application/json" },
120
+ });
121
+ };
122
+
123
+ const handleCreateBundles: RouteHandler = async (_params, request, api) => {
124
+ const body = await request.json();
125
+ const bundles = Array.isArray(body) ? body : [body];
126
+
127
+ for (const bundle of bundles) {
128
+ await api.insertBundle(bundle as Bundle);
129
+ }
130
+
131
+ return new Response(JSON.stringify({ success: true }), {
132
+ status: 201,
133
+ headers: { "Content-Type": "application/json" },
134
+ });
135
+ };
136
+
137
+ const handleDeleteBundle: RouteHandler = async (params, _request, api) => {
138
+ await api.deleteBundleById(params.id);
139
+
140
+ return new Response(JSON.stringify({ success: true }), {
141
+ status: 200,
142
+ headers: { "Content-Type": "application/json" },
143
+ });
144
+ };
145
+
146
+ const handleGetChannels: RouteHandler = async (_params, _request, api) => {
147
+ const channels = await api.getChannels();
148
+
149
+ return new Response(JSON.stringify({ channels }), {
150
+ status: 200,
151
+ headers: { "Content-Type": "application/json" },
152
+ });
153
+ };
154
+
155
+ // Route handlers map
156
+ const routes: Record<string, RouteHandler> = {
157
+ update: handleUpdate,
158
+ fingerprintUpdate: handleFingerprintUpdate,
159
+ appVersionUpdate: handleAppVersionUpdate,
160
+ getBundle: handleGetBundle,
161
+ getBundles: handleGetBundles,
162
+ createBundles: handleCreateBundles,
163
+ deleteBundle: handleDeleteBundle,
164
+ getChannels: handleGetChannels,
165
+ };
166
+
33
167
  /**
34
168
  * Creates a Web Standard Request handler for Hot Updater API
35
169
  * This handler is framework-agnostic and works with any framework
@@ -41,6 +175,29 @@ export function createHandler(
41
175
  ): (request: Request) => Promise<Response> {
42
176
  const basePath = options.basePath ?? "/api";
43
177
 
178
+ // Create and configure router
179
+ const router = createRouter();
180
+
181
+ // Register routes
182
+ addRoute(router, "POST", "/update", "update");
183
+ addRoute(
184
+ router,
185
+ "GET",
186
+ "/fingerprint/:platform/:fingerprintHash/:channel/:minBundleId/:bundleId",
187
+ "fingerprintUpdate",
188
+ );
189
+ addRoute(
190
+ router,
191
+ "GET",
192
+ "/app-version/:platform/:appVersion/:channel/:minBundleId/:bundleId",
193
+ "appVersionUpdate",
194
+ );
195
+ addRoute(router, "GET", "/bundles/:id", "getBundle");
196
+ addRoute(router, "GET", "/bundles", "getBundles");
197
+ addRoute(router, "POST", "/bundles", "createBundles");
198
+ addRoute(router, "DELETE", "/bundles/:id", "deleteBundle");
199
+ addRoute(router, "GET", "/channels", "getChannels");
200
+
44
201
  return async (request: Request): Promise<Response> => {
45
202
  try {
46
203
  const url = new URL(request.url);
@@ -52,168 +209,26 @@ export function createHandler(
52
209
  ? path.slice(basePath.length)
53
210
  : path;
54
211
 
55
- // POST /api/update - Client checks for updates
56
- if (routePath === "/update" && method === "POST") {
57
- const body = (await request.json()) as
58
- | AppVersionGetBundlesArgs
59
- | FingerprintGetBundlesArgs;
60
- const updateInfo = await api.getAppUpdateInfo(body);
61
-
62
- if (!updateInfo) {
63
- return new Response(JSON.stringify(null), {
64
- status: 200,
65
- headers: { "Content-Type": "application/json" },
66
- });
67
- }
68
-
69
- return new Response(JSON.stringify(updateInfo), {
70
- status: 200,
71
- headers: { "Content-Type": "application/json" },
72
- });
73
- }
212
+ // Find matching route
213
+ const match = findRoute(router, method, routePath);
74
214
 
75
- // GET /api/fingerprint/:platform/:fingerprintHash/:channel/:minBundleId/:bundleId
76
- const fingerprintMatch = routePath.match(
77
- /^\/fingerprint\/([^/]+)\/([^/]+)\/([^/]+)\/([^/]+)\/([^/]+)$/,
78
- );
79
- if (fingerprintMatch && method === "GET") {
80
- const [, platform, fingerprintHash, channel, minBundleId, bundleId] =
81
- fingerprintMatch;
82
-
83
- const updateInfo = await api.getAppUpdateInfo({
84
- _updateStrategy: "fingerprint",
85
- platform: platform as "ios" | "android",
86
- fingerprintHash,
87
- channel,
88
- minBundleId,
89
- bundleId,
90
- });
91
-
92
- if (!updateInfo) {
93
- return new Response(JSON.stringify(null), {
94
- status: 200,
95
- headers: { "Content-Type": "application/json" },
96
- });
97
- }
98
-
99
- return new Response(JSON.stringify(updateInfo), {
100
- status: 200,
215
+ if (!match) {
216
+ return new Response(JSON.stringify({ error: "Not found" }), {
217
+ status: 404,
101
218
  headers: { "Content-Type": "application/json" },
102
219
  });
103
220
  }
104
221
 
105
- // GET /api/app-version/:platform/:appVersion/:channel/:minBundleId/:bundleId
106
- const appVersionMatch = routePath.match(
107
- /^\/app-version\/([^/]+)\/([^/]+)\/([^/]+)\/([^/]+)\/([^/]+)$/,
108
- );
109
- if (appVersionMatch && method === "GET") {
110
- const [, platform, appVersion, channel, minBundleId, bundleId] =
111
- appVersionMatch;
112
-
113
- const updateInfo = await api.getAppUpdateInfo({
114
- _updateStrategy: "appVersion",
115
- platform: platform as "ios" | "android",
116
- appVersion,
117
- channel,
118
- minBundleId,
119
- bundleId,
120
- });
121
-
122
- if (!updateInfo) {
123
- return new Response(JSON.stringify(null), {
124
- status: 200,
125
- headers: { "Content-Type": "application/json" },
126
- });
127
- }
128
-
129
- return new Response(JSON.stringify(updateInfo), {
130
- status: 200,
131
- headers: { "Content-Type": "application/json" },
132
- });
133
- }
134
-
135
- // GET /api/bundles/:id - Get single bundle
136
- const getBundleMatch = routePath.match(/^\/bundles\/([^/]+)$/);
137
- if (getBundleMatch && method === "GET") {
138
- const id = getBundleMatch[1];
139
- const bundle = await api.getBundleById(id);
140
-
141
- if (!bundle) {
142
- return new Response(JSON.stringify({ error: "Bundle not found" }), {
143
- status: 404,
144
- headers: { "Content-Type": "application/json" },
145
- });
146
- }
147
-
148
- return new Response(JSON.stringify(bundle), {
149
- status: 200,
150
- headers: { "Content-Type": "application/json" },
151
- });
152
- }
153
-
154
- // GET /api/bundles - List bundles
155
- if (routePath === "/bundles" && method === "GET") {
156
- const channel = url.searchParams.get("channel") ?? undefined;
157
- const platform = url.searchParams.get("platform") ?? undefined;
158
- const limit = Number(url.searchParams.get("limit")) || 50;
159
- const offset = Number(url.searchParams.get("offset")) || 0;
160
-
161
- const result = await api.getBundles({
162
- where: {
163
- ...(channel && { channel }),
164
- ...(platform && { platform }),
165
- },
166
- limit,
167
- offset,
168
- });
169
-
170
- return new Response(JSON.stringify(result.data), {
171
- status: 200,
172
- headers: { "Content-Type": "application/json" },
173
- });
174
- }
175
-
176
- // POST /api/bundles - Create new bundle(s)
177
- if (routePath === "/bundles" && method === "POST") {
178
- const body = await request.json();
179
- const bundles = Array.isArray(body) ? body : [body];
180
-
181
- for (const bundle of bundles) {
182
- await api.insertBundle(bundle as Bundle);
183
- }
184
-
185
- return new Response(JSON.stringify({ success: true }), {
186
- status: 201,
187
- headers: { "Content-Type": "application/json" },
188
- });
189
- }
190
-
191
- // DELETE /api/bundles/:id - Delete bundle
192
- if (routePath.startsWith("/bundles/") && method === "DELETE") {
193
- const id = routePath.slice("/bundles/".length);
194
- await api.deleteBundleById(id);
195
-
196
- return new Response(JSON.stringify({ success: true }), {
197
- status: 200,
198
- headers: { "Content-Type": "application/json" },
199
- });
200
- }
201
-
202
- // GET /api/channels - List all channels
203
- if (routePath === "/channels" && method === "GET") {
204
- const channels = await api.getChannels();
205
-
206
- return new Response(JSON.stringify({ channels }), {
207
- status: 200,
222
+ // Get handler and execute
223
+ const handler = routes[match.data as string];
224
+ if (!handler) {
225
+ return new Response(JSON.stringify({ error: "Handler not found" }), {
226
+ status: 500,
208
227
  headers: { "Content-Type": "application/json" },
209
228
  });
210
229
  }
211
230
 
212
- // 404 Not Found
213
- return new Response(JSON.stringify({ error: "Not found" }), {
214
- status: 404,
215
- headers: { "Content-Type": "application/json" },
216
- });
231
+ return await handler(match.params || {}, request, api);
217
232
  } catch (error) {
218
233
  console.error("Hot Updater handler error:", error);
219
234
  return new Response(
package/src/node.ts ADDED
@@ -0,0 +1,104 @@
1
+ import type { HotUpdaterAPI } from "./types";
2
+
3
+ /**
4
+ * Node.js request/response types (compatible with Express, Connect, etc.)
5
+ */
6
+ interface NodeRequest {
7
+ method?: string;
8
+ url?: string;
9
+ headers: Record<string, string | string[] | undefined>;
10
+ body?: unknown;
11
+ protocol?: string;
12
+ get?(name: string): string | undefined;
13
+ [key: string]: unknown;
14
+ }
15
+
16
+ interface NodeResponse {
17
+ status(code: number): NodeResponse;
18
+ setHeader(name: string, value: string | string[]): void;
19
+ send(body: string): void;
20
+ end(): void;
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ /**
25
+ * Converts a Hot Updater handler to a Node.js-compatible middleware
26
+ * Works with Express, Connect, and other frameworks using Node.js req/res
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * import { toNodeHandler } from "@hot-updater/server/node";
31
+ * import express from "express";
32
+ *
33
+ * const app = express();
34
+ *
35
+ * // Mount middleware
36
+ * app.use(express.json());
37
+ *
38
+ * // Mount hot-updater handler
39
+ * app.all("/hot-updater/*", toNodeHandler(hotUpdater));
40
+ * ```
41
+ */
42
+ export function toNodeHandler(
43
+ hotUpdater: HotUpdaterAPI,
44
+ ): (req: any, res: any, next?: any) => Promise<void> {
45
+ return async (req: NodeRequest, res: NodeResponse) => {
46
+ try {
47
+ // Build full URL
48
+ const protocol = req.protocol || "http";
49
+ const host = req.get?.("host") || "localhost";
50
+ const url = `${protocol}://${host}${req.url || "/"}`;
51
+
52
+ // Convert headers to Web Headers
53
+ const headers = new Headers();
54
+ for (const [key, value] of Object.entries(req.headers)) {
55
+ if (value) {
56
+ headers.set(key, Array.isArray(value) ? value.join(", ") : value);
57
+ }
58
+ }
59
+
60
+ // Handle request body
61
+ let body: string | undefined;
62
+ if (
63
+ req.method &&
64
+ req.method !== "GET" &&
65
+ req.method !== "HEAD" &&
66
+ req.body
67
+ ) {
68
+ // If body is already parsed (by express.json()), stringify it
69
+ body = JSON.stringify(req.body);
70
+ }
71
+
72
+ // Create Web Request
73
+ const webRequest = new globalThis.Request(url, {
74
+ method: req.method || "GET",
75
+ headers,
76
+ body,
77
+ });
78
+
79
+ // Call hot-updater handler
80
+ const response = await hotUpdater.handler(webRequest);
81
+
82
+ // Set status code
83
+ res.status(response.status);
84
+
85
+ // Set headers
86
+ response.headers.forEach((value, key) => {
87
+ res.setHeader(key, value);
88
+ });
89
+
90
+ // Send response body
91
+ const text = await response.text();
92
+ if (text) {
93
+ res.send(text);
94
+ } else {
95
+ res.end();
96
+ }
97
+ } catch (error) {
98
+ // Handle errors gracefully
99
+ console.error("Hot Updater handler error:", error);
100
+ res.status(500);
101
+ res.send("Internal Server Error");
102
+ }
103
+ };
104
+ }
@@ -1,10 +1,10 @@
1
1
  import { column, idColumn, schema, table } from "fumadb/schema";
2
2
 
3
- export const v1 = schema({
4
- version: "1.0.0",
3
+ export const v0_21_0 = schema({
4
+ version: "0.21.0",
5
5
  tables: {
6
6
  bundles: table("bundles", {
7
- id: idColumn("id", "varchar(255)").defaultTo$("auto"),
7
+ id: idColumn("id", "uuid"),
8
8
  platform: column("platform", "string"),
9
9
  should_force_update: column("should_force_update", "bool"),
10
10
  enabled: column("enabled", "bool"),
@@ -1,6 +1,7 @@
1
1
  import type { Bundle } from "@hot-updater/core";
2
2
 
3
3
  export type { Bundle } from "@hot-updater/core";
4
+ export type { HotUpdaterAPI } from "../db";
4
5
 
5
6
  export interface PaginationInfo {
6
7
  total: number;
@@ -1,9 +0,0 @@
1
-
2
-
3
- var fumadb_adapters_typeorm = require("fumadb/adapters/typeorm");
4
- Object.keys(fumadb_adapters_typeorm).forEach(function (k) {
5
- if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
6
- enumerable: true,
7
- get: function () { return fumadb_adapters_typeorm[k]; }
8
- });
9
- });
@@ -1 +0,0 @@
1
- export * from "fumadb/adapters/typeorm";
@@ -1 +0,0 @@
1
- export * from "fumadb/adapters/typeorm";
@@ -1,3 +0,0 @@
1
- export * from "fumadb/adapters/typeorm"
2
-
3
- export { };
@@ -1 +0,0 @@
1
- export * from "fumadb/adapters/typeorm";