@hot-updater/server 0.21.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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/dist/_virtual/rolldown_runtime.cjs +25 -0
  3. package/dist/adapters/drizzle.cjs +9 -0
  4. package/dist/adapters/drizzle.d.cts +1 -0
  5. package/dist/adapters/drizzle.d.ts +1 -0
  6. package/dist/adapters/drizzle.js +3 -0
  7. package/dist/adapters/kysely.cjs +9 -0
  8. package/dist/adapters/kysely.d.cts +1 -0
  9. package/dist/adapters/kysely.d.ts +1 -0
  10. package/dist/adapters/kysely.js +3 -0
  11. package/dist/adapters/mongodb.cjs +9 -0
  12. package/dist/adapters/mongodb.d.cts +1 -0
  13. package/dist/adapters/mongodb.d.ts +1 -0
  14. package/dist/adapters/mongodb.js +3 -0
  15. package/dist/adapters/prisma.cjs +9 -0
  16. package/dist/adapters/prisma.d.cts +1 -0
  17. package/dist/adapters/prisma.d.ts +1 -0
  18. package/dist/adapters/prisma.js +3 -0
  19. package/dist/adapters/typeorm.cjs +9 -0
  20. package/dist/adapters/typeorm.d.cts +1 -0
  21. package/dist/adapters/typeorm.d.ts +1 -0
  22. package/dist/adapters/typeorm.js +3 -0
  23. package/dist/calculatePagination.cjs +27 -0
  24. package/dist/calculatePagination.js +26 -0
  25. package/dist/db/index.cjs +289 -0
  26. package/dist/db/index.d.cts +62 -0
  27. package/dist/db/index.d.ts +62 -0
  28. package/dist/db/index.js +284 -0
  29. package/dist/handler.cjs +141 -0
  30. package/dist/handler.d.cts +37 -0
  31. package/dist/handler.d.ts +37 -0
  32. package/dist/handler.js +140 -0
  33. package/dist/index.cjs +6 -0
  34. package/dist/index.d.cts +4 -0
  35. package/dist/index.d.ts +4 -0
  36. package/dist/index.js +4 -0
  37. package/dist/schema/v1.cjs +26 -0
  38. package/dist/schema/v1.js +24 -0
  39. package/dist/types/index.d.cts +20 -0
  40. package/dist/types/index.d.ts +20 -0
  41. package/package.json +73 -0
  42. package/src/adapters/drizzle.ts +1 -0
  43. package/src/adapters/kysely.ts +1 -0
  44. package/src/adapters/mongodb.ts +1 -0
  45. package/src/adapters/prisma.ts +1 -0
  46. package/src/adapters/typeorm.ts +1 -0
  47. package/src/calculatePagination.ts +34 -0
  48. package/src/db/index.spec.ts +231 -0
  49. package/src/db/index.ts +490 -0
  50. package/src/handler-standalone-integration.spec.ts +341 -0
  51. package/src/handler.ts +231 -0
  52. package/src/index.ts +3 -0
  53. package/src/schema/v1.ts +22 -0
  54. package/src/types/index.ts +21 -0
@@ -0,0 +1,341 @@
1
+ import { PGlite } from "@electric-sql/pglite";
2
+ import type { Bundle } from "@hot-updater/core";
3
+ import { NIL_UUID } from "@hot-updater/core";
4
+ import { kyselyAdapter } from "@hot-updater/server/adapters/kysely";
5
+ import { standaloneRepository } from "@hot-updater/standalone";
6
+ import { Kysely } from "kysely";
7
+ import { PGliteDialect } from "kysely-pglite-dialect";
8
+ import { HttpResponse, http } from "msw";
9
+ import { setupServer } from "msw/node";
10
+ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
11
+ import { hotUpdater } from "./db";
12
+
13
+ /**
14
+ * Integration tests between @hot-updater/server handler and @hot-updater/standalone repository
15
+ *
16
+ * This test suite verifies real-world compatibility by:
17
+ * 1. Using actual standaloneRepository (not mocks)
18
+ * 2. Using actual handler (not mocks)
19
+ * 3. Simulating HTTP communication via MSW
20
+ * 4. Testing end-to-end flows: standalone → HTTP → handler → database
21
+ */
22
+
23
+ // Create in-memory database for testing
24
+ const db = new PGlite();
25
+ const kysely = new Kysely({ dialect: new PGliteDialect(db) });
26
+
27
+ // Create handler API with in-memory DB
28
+ const api = hotUpdater({
29
+ database: kyselyAdapter({
30
+ db: kysely,
31
+ provider: "postgresql",
32
+ }),
33
+ basePath: "/hot-updater",
34
+ });
35
+
36
+ // Setup MSW server to intercept HTTP requests
37
+ const baseUrl = "http://localhost:3000";
38
+ const server = setupServer();
39
+
40
+ beforeAll(async () => {
41
+ // Initialize database
42
+ const migrator = api.createMigrator();
43
+ const result = await migrator.migrateToLatest({
44
+ mode: "from-schema",
45
+ updateSettings: true,
46
+ });
47
+ await result.execute();
48
+
49
+ // Start MSW server
50
+ server.listen({ onUnhandledRequest: "error" });
51
+
52
+ // Route all requests to our handler
53
+ const handleRequest = async (request: Request) => {
54
+ const response = await api.handler(request);
55
+ const data = (await response.json()) as Record<string, unknown>;
56
+ return HttpResponse.json(data, {
57
+ status: response.status,
58
+ headers: response.headers,
59
+ });
60
+ };
61
+
62
+ server.use(
63
+ // Specific routes
64
+ http.get(`${baseUrl}/hot-updater/bundles`, ({ request }) =>
65
+ handleRequest(request),
66
+ ),
67
+ http.get(`${baseUrl}/hot-updater/bundles/:id`, ({ request }) =>
68
+ handleRequest(request),
69
+ ),
70
+ http.post(`${baseUrl}/hot-updater/bundles`, ({ request }) =>
71
+ handleRequest(request),
72
+ ),
73
+ http.delete(`${baseUrl}/hot-updater/bundles/:id`, ({ request }) =>
74
+ handleRequest(request),
75
+ ),
76
+ );
77
+ });
78
+
79
+ afterEach(async () => {
80
+ // Clean up database after each test
81
+ await db.exec("DELETE FROM bundles");
82
+ });
83
+
84
+ afterAll(async () => {
85
+ server.close();
86
+ await kysely.destroy();
87
+ await db.close();
88
+ });
89
+
90
+ const createTestBundle = (overrides?: Partial<Bundle>): Bundle => ({
91
+ id: NIL_UUID,
92
+ platform: "ios",
93
+ channel: "production",
94
+ enabled: true,
95
+ shouldForceUpdate: false,
96
+ fileHash: "test-hash",
97
+ gitCommitHash: null,
98
+ message: null,
99
+ targetAppVersion: "*",
100
+ storageUri: "test://storage",
101
+ fingerprintHash: null,
102
+ ...overrides,
103
+ });
104
+
105
+ describe("Handler <-> Standalone Repository Integration", () => {
106
+ it("Real integration: appendBundle + commitBundle → handler POST /bundles", async () => {
107
+ // Create standalone repository pointing to our test server
108
+ const repo = standaloneRepository({
109
+ baseUrl: `${baseUrl}/hot-updater`,
110
+ })({ cwd: process.cwd() });
111
+
112
+ const bundle = createTestBundle({
113
+ id: "integration-test-1",
114
+ fileHash: "integration-hash-1",
115
+ });
116
+
117
+ // Standalone repository operations
118
+ await repo.appendBundle(bundle);
119
+ await repo.commitBundle(); // Triggers actual commit
120
+
121
+ // Verify via handler that bundle was created
122
+ const request = new Request(
123
+ `${baseUrl}/hot-updater/bundles/integration-test-1`,
124
+ {
125
+ method: "GET",
126
+ },
127
+ );
128
+
129
+ const response = await api.handler(request);
130
+ expect(response.status).toBe(200);
131
+
132
+ const retrieved = (await response.json()) as Bundle;
133
+ expect(retrieved.id).toBe("integration-test-1");
134
+ expect(retrieved.fileHash).toBe("integration-hash-1");
135
+ });
136
+
137
+ it("Real integration: getBundleById → handler GET /bundles/:id", async () => {
138
+ // First, create a bundle directly via handler
139
+ const bundle = createTestBundle({
140
+ id: "get-test-1",
141
+ fileHash: "get-hash-1",
142
+ });
143
+
144
+ await api.insertBundle(bundle);
145
+
146
+ // Create standalone repository
147
+ const repo = standaloneRepository({
148
+ baseUrl: `${baseUrl}/hot-updater`,
149
+ })({ cwd: process.cwd() });
150
+
151
+ // Use standalone repository to retrieve
152
+ const retrieved = await repo.getBundleById("get-test-1");
153
+
154
+ expect(retrieved).toBeTruthy();
155
+ expect(retrieved?.id).toBe("get-test-1");
156
+ expect(retrieved?.fileHash).toBe("get-hash-1");
157
+ });
158
+
159
+ it("Real integration: deleteBundle + commitBundle → handler DELETE /bundles/:id", async () => {
160
+ // Create a bundle via handler
161
+ const bundle = createTestBundle({
162
+ id: "delete-test-1",
163
+ fileHash: "delete-hash-1",
164
+ });
165
+
166
+ await api.insertBundle(bundle);
167
+
168
+ // Verify it exists
169
+ const beforeDelete = await api.getBundleById("delete-test-1");
170
+ expect(beforeDelete).toBeTruthy();
171
+
172
+ // Create standalone repository
173
+ const repo = standaloneRepository({
174
+ baseUrl: `${baseUrl}/hot-updater`,
175
+ })({ cwd: process.cwd() });
176
+
177
+ // Delete via standalone repository
178
+ await repo.deleteBundle(bundle);
179
+ await repo.commitBundle();
180
+
181
+ // Verify it was deleted
182
+ const afterDelete = await api.getBundleById("delete-test-1");
183
+ expect(afterDelete).toBeNull();
184
+ });
185
+
186
+ it("Real integration: getBundles → handler GET /bundles", async () => {
187
+ // Create multiple bundles
188
+ await api.insertBundle(
189
+ createTestBundle({ id: "list-1", channel: "production" }),
190
+ );
191
+ await api.insertBundle(
192
+ createTestBundle({ id: "list-2", channel: "production" }),
193
+ );
194
+ await api.insertBundle(
195
+ createTestBundle({ id: "list-3", channel: "staging" }),
196
+ );
197
+
198
+ // Create standalone repository
199
+ const repo = standaloneRepository({
200
+ baseUrl: `${baseUrl}/hot-updater`,
201
+ })({ cwd: process.cwd() });
202
+
203
+ // Get all bundles
204
+ const result = await repo.getBundles({ limit: 50, offset: 0 });
205
+
206
+ expect(result.data).toHaveLength(3);
207
+ expect(result.pagination.total).toBe(3);
208
+
209
+ // Filter by channel
210
+ const prodResult = await repo.getBundles({
211
+ where: { channel: "production" },
212
+ limit: 50,
213
+ offset: 0,
214
+ });
215
+
216
+ expect(prodResult.data).toHaveLength(2);
217
+ });
218
+
219
+ it("Full E2E: create → retrieve → update → delete via standalone", async () => {
220
+ const repo = standaloneRepository({
221
+ baseUrl: `${baseUrl}/hot-updater`,
222
+ })({ cwd: process.cwd() });
223
+
224
+ // Step 1: Create bundle via standalone
225
+ const bundle = createTestBundle({
226
+ id: "e2e-bundle",
227
+ fileHash: "e2e-hash",
228
+ enabled: true,
229
+ });
230
+
231
+ await repo.appendBundle(bundle);
232
+ await repo.commitBundle();
233
+
234
+ // Step 2: Retrieve via standalone
235
+ const retrieved = await repo.getBundleById("e2e-bundle");
236
+ expect(retrieved).toBeTruthy();
237
+ expect(retrieved?.enabled).toBe(true);
238
+
239
+ // Step 3: Update via standalone
240
+ await repo.updateBundle("e2e-bundle", { enabled: false });
241
+ await repo.commitBundle();
242
+
243
+ // Verify update
244
+ const updated = await repo.getBundleById("e2e-bundle");
245
+ expect(updated?.enabled).toBe(false);
246
+
247
+ // Step 4: Delete via standalone
248
+ await repo.deleteBundle(bundle);
249
+ await repo.commitBundle();
250
+
251
+ // Verify deletion
252
+ const deleted = await repo.getBundleById("e2e-bundle");
253
+ expect(deleted).toBeNull();
254
+ });
255
+
256
+ it("Multiple bundles in single commit (standalone sends array)", async () => {
257
+ const repo = standaloneRepository({
258
+ baseUrl: `${baseUrl}/hot-updater`,
259
+ })({ cwd: process.cwd() });
260
+
261
+ // Append multiple bundles
262
+ await repo.appendBundle(createTestBundle({ id: "batch-1" }));
263
+ await repo.appendBundle(createTestBundle({ id: "batch-2" }));
264
+ await repo.appendBundle(createTestBundle({ id: "batch-3" }));
265
+
266
+ // Commit all at once (standalone sends array in POST)
267
+ await repo.commitBundle();
268
+
269
+ // Verify all were created
270
+ const bundle1 = await api.getBundleById("batch-1");
271
+ const bundle2 = await api.getBundleById("batch-2");
272
+ const bundle3 = await api.getBundleById("batch-3");
273
+
274
+ expect(bundle1).toBeTruthy();
275
+ expect(bundle2).toBeTruthy();
276
+ expect(bundle3).toBeTruthy();
277
+ });
278
+
279
+ it("Works with custom basePath configuration", async () => {
280
+ // Create handler with custom basePath
281
+ const customApi = hotUpdater({
282
+ database: kyselyAdapter({
283
+ db: kysely,
284
+ provider: "postgresql",
285
+ }),
286
+ basePath: "/api/v2",
287
+ });
288
+
289
+ // Setup MSW for custom basePath
290
+ server.use(
291
+ http.get(`${baseUrl}/api/v2/*`, async ({ request }) => {
292
+ const response = await customApi.handler(request);
293
+ return HttpResponse.json(
294
+ (await response.json()) as Record<string, unknown>,
295
+ {
296
+ status: response.status,
297
+ },
298
+ );
299
+ }),
300
+ http.post(`${baseUrl}/api/v2/*`, async ({ request }) => {
301
+ const response = await customApi.handler(request);
302
+ return HttpResponse.json(
303
+ (await response.json()) as Record<string, unknown>,
304
+ {
305
+ status: response.status,
306
+ },
307
+ );
308
+ }),
309
+ );
310
+
311
+ // Create standalone repository with matching basePath
312
+ const repo = standaloneRepository({
313
+ baseUrl: `${baseUrl}/api/v2`,
314
+ })({ cwd: process.cwd() });
315
+
316
+ // Test create and retrieve
317
+ const bundle = createTestBundle({
318
+ id: "custom-path-test",
319
+ fileHash: "custom-hash",
320
+ });
321
+
322
+ await repo.appendBundle(bundle);
323
+ await repo.commitBundle();
324
+
325
+ const retrieved = await repo.getBundleById("custom-path-test");
326
+ expect(retrieved).toBeTruthy();
327
+ expect(retrieved?.fileHash).toBe("custom-hash");
328
+ });
329
+
330
+ it("Handler returns 404 when bundle not found (standalone handles gracefully)", async () => {
331
+ const repo = standaloneRepository({
332
+ baseUrl: `${baseUrl}/hot-updater`,
333
+ })({ cwd: process.cwd() });
334
+
335
+ // Try to get non-existent bundle
336
+ const result = await repo.getBundleById("non-existent-bundle");
337
+
338
+ // Standalone should return null gracefully
339
+ expect(result).toBeNull();
340
+ });
341
+ });
package/src/handler.ts ADDED
@@ -0,0 +1,231 @@
1
+ import type {
2
+ AppUpdateInfo,
3
+ AppVersionGetBundlesArgs,
4
+ Bundle,
5
+ FingerprintGetBundlesArgs,
6
+ } from "@hot-updater/core";
7
+ import type { PaginationInfo } from "./types";
8
+
9
+ // Narrow API surface needed by the handler to avoid circular types
10
+ export interface HandlerAPI {
11
+ getAppUpdateInfo: (
12
+ args: AppVersionGetBundlesArgs | FingerprintGetBundlesArgs,
13
+ ) => Promise<AppUpdateInfo | null>;
14
+ getBundleById: (id: string) => Promise<Bundle | null>;
15
+ getBundles: (options: {
16
+ where?: { channel?: string; platform?: string };
17
+ limit: number;
18
+ offset: number;
19
+ }) => Promise<{ data: Bundle[]; pagination: PaginationInfo }>;
20
+ insertBundle: (bundle: Bundle) => Promise<void>;
21
+ deleteBundleById: (bundleId: string) => Promise<void>;
22
+ getChannels: () => Promise<string[]>;
23
+ }
24
+
25
+ export interface HandlerOptions {
26
+ /**
27
+ * Base path for all routes
28
+ * @default "/api"
29
+ */
30
+ basePath?: string;
31
+ }
32
+
33
+ /**
34
+ * Creates a Web Standard Request handler for Hot Updater API
35
+ * This handler is framework-agnostic and works with any framework
36
+ * that supports Web Standard Request/Response (Hono, Elysia, etc.)
37
+ */
38
+ export function createHandler(
39
+ api: HandlerAPI,
40
+ options: HandlerOptions = {},
41
+ ): (request: Request) => Promise<Response> {
42
+ const basePath = options.basePath ?? "/api";
43
+
44
+ return async (request: Request): Promise<Response> => {
45
+ try {
46
+ const url = new URL(request.url);
47
+ const path = url.pathname;
48
+ const method = request.method;
49
+
50
+ // Remove base path from pathname
51
+ const routePath = path.startsWith(basePath)
52
+ ? path.slice(basePath.length)
53
+ : path;
54
+
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
+ }
74
+
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,
101
+ headers: { "Content-Type": "application/json" },
102
+ });
103
+ }
104
+
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,
208
+ headers: { "Content-Type": "application/json" },
209
+ });
210
+ }
211
+
212
+ // 404 Not Found
213
+ return new Response(JSON.stringify({ error: "Not found" }), {
214
+ status: 404,
215
+ headers: { "Content-Type": "application/json" },
216
+ });
217
+ } catch (error) {
218
+ console.error("Hot Updater handler error:", error);
219
+ return new Response(
220
+ JSON.stringify({
221
+ error: "Internal server error",
222
+ message: error instanceof Error ? error.message : "Unknown error",
223
+ }),
224
+ {
225
+ status: 500,
226
+ headers: { "Content-Type": "application/json" },
227
+ },
228
+ );
229
+ }
230
+ };
231
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./db";
2
+ export * from "./handler";
3
+ export * from "./types";
@@ -0,0 +1,22 @@
1
+ import { column, idColumn, schema, table } from "fumadb/schema";
2
+
3
+ export const v1 = schema({
4
+ version: "1.0.0",
5
+ tables: {
6
+ bundles: table("bundles", {
7
+ id: idColumn("id", "varchar(255)").defaultTo$("auto"),
8
+ platform: column("platform", "string"),
9
+ should_force_update: column("should_force_update", "bool"),
10
+ enabled: column("enabled", "bool"),
11
+ file_hash: column("file_hash", "string"),
12
+ git_commit_hash: column("git_commit_hash", "string").nullable(),
13
+ message: column("message", "string").nullable(),
14
+ channel: column("channel", "string"),
15
+ storage_uri: column("storage_uri", "string"),
16
+ target_app_version: column("target_app_version", "string").nullable(),
17
+ fingerprint_hash: column("fingerprint_hash", "string").nullable(),
18
+ metadata: column("metadata", "json"),
19
+ }),
20
+ },
21
+ relations: {},
22
+ });
@@ -0,0 +1,21 @@
1
+ import type { Bundle } from "@hot-updater/core";
2
+
3
+ export type { Bundle } from "@hot-updater/core";
4
+
5
+ export interface PaginationInfo {
6
+ total: number;
7
+ hasNextPage: boolean;
8
+ hasPreviousPage: boolean;
9
+ currentPage: number;
10
+ totalPages: number;
11
+ }
12
+
13
+ export interface PaginationOptions {
14
+ limit: number;
15
+ offset: number;
16
+ }
17
+
18
+ export interface PaginatedResult {
19
+ data: Bundle[];
20
+ pagination: PaginationInfo;
21
+ }