@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,26 @@
1
+ const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
2
+ let fumadb_schema = require("fumadb/schema");
3
+ fumadb_schema = require_rolldown_runtime.__toESM(fumadb_schema);
4
+
5
+ //#region src/schema/v1.ts
6
+ const v1 = (0, fumadb_schema.schema)({
7
+ version: "1.0.0",
8
+ tables: { bundles: (0, fumadb_schema.table)("bundles", {
9
+ id: (0, fumadb_schema.idColumn)("id", "varchar(255)").defaultTo$("auto"),
10
+ platform: (0, fumadb_schema.column)("platform", "string"),
11
+ should_force_update: (0, fumadb_schema.column)("should_force_update", "bool"),
12
+ enabled: (0, fumadb_schema.column)("enabled", "bool"),
13
+ file_hash: (0, fumadb_schema.column)("file_hash", "string"),
14
+ git_commit_hash: (0, fumadb_schema.column)("git_commit_hash", "string").nullable(),
15
+ message: (0, fumadb_schema.column)("message", "string").nullable(),
16
+ channel: (0, fumadb_schema.column)("channel", "string"),
17
+ storage_uri: (0, fumadb_schema.column)("storage_uri", "string"),
18
+ target_app_version: (0, fumadb_schema.column)("target_app_version", "string").nullable(),
19
+ fingerprint_hash: (0, fumadb_schema.column)("fingerprint_hash", "string").nullable(),
20
+ metadata: (0, fumadb_schema.column)("metadata", "json")
21
+ }) },
22
+ relations: {}
23
+ });
24
+
25
+ //#endregion
26
+ exports.v1 = v1;
@@ -0,0 +1,24 @@
1
+ import { column, idColumn, schema, table } from "fumadb/schema";
2
+
3
+ //#region src/schema/v1.ts
4
+ const v1 = schema({
5
+ version: "1.0.0",
6
+ tables: { 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
+ relations: {}
21
+ });
22
+
23
+ //#endregion
24
+ export { v1 };
@@ -0,0 +1,20 @@
1
+ import { Bundle, Bundle as Bundle$1 } from "@hot-updater/core";
2
+
3
+ //#region src/types/index.d.ts
4
+ interface PaginationInfo {
5
+ total: number;
6
+ hasNextPage: boolean;
7
+ hasPreviousPage: boolean;
8
+ currentPage: number;
9
+ totalPages: number;
10
+ }
11
+ interface PaginationOptions {
12
+ limit: number;
13
+ offset: number;
14
+ }
15
+ interface PaginatedResult {
16
+ data: Bundle[];
17
+ pagination: PaginationInfo;
18
+ }
19
+ //#endregion
20
+ export { type Bundle$1 as Bundle, PaginatedResult, PaginationInfo, PaginationOptions };
@@ -0,0 +1,20 @@
1
+ import { Bundle, Bundle as Bundle$1 } from "@hot-updater/core";
2
+
3
+ //#region src/types/index.d.ts
4
+ interface PaginationInfo {
5
+ total: number;
6
+ hasNextPage: boolean;
7
+ hasPreviousPage: boolean;
8
+ currentPage: number;
9
+ totalPages: number;
10
+ }
11
+ interface PaginationOptions {
12
+ limit: number;
13
+ offset: number;
14
+ }
15
+ interface PaginatedResult {
16
+ data: Bundle[];
17
+ pagination: PaginationInfo;
18
+ }
19
+ //#endregion
20
+ export { type Bundle$1 as Bundle, PaginatedResult, PaginationInfo, PaginationOptions };
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@hot-updater/server",
3
+ "version": "0.21.0",
4
+ "type": "module",
5
+ "description": "React Native OTA solution for self-hosted",
6
+ "sideEffects": false,
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.cts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./adapters/drizzle": {
16
+ "import": "./dist/adapters/drizzle.js",
17
+ "require": "./dist/adapters/drizzle.cjs"
18
+ },
19
+ "./adapters/kysely": {
20
+ "import": "./dist/adapters/kysely.js",
21
+ "require": "./dist/adapters/kysely.cjs"
22
+ },
23
+ "./adapters/mongodb": {
24
+ "import": "./dist/adapters/mongodb.js",
25
+ "require": "./dist/adapters/mongodb.cjs"
26
+ },
27
+ "./adapters/prisma": {
28
+ "import": "./dist/adapters/prisma.js",
29
+ "require": "./dist/adapters/prisma.cjs"
30
+ },
31
+ "./adapters/typeorm": {
32
+ "import": "./dist/adapters/typeorm.js",
33
+ "require": "./dist/adapters/typeorm.cjs"
34
+ },
35
+ "./package.json": "./package.json"
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "src",
40
+ "package.json"
41
+ ],
42
+ "license": "MIT",
43
+ "repository": "https://github.com/gronxb/hot-updater",
44
+ "author": "gronxb <gron1gh1@gmail.com> (https://github.com/gronxb)",
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "fumadb": "0.1.1",
50
+ "semver": "^7.7.2",
51
+ "@hot-updater/core": "0.21.0",
52
+ "@hot-updater/plugin-core": "0.21.0"
53
+ },
54
+ "devDependencies": {
55
+ "@electric-sql/pglite": "^0.2.17",
56
+ "@types/node": "^20",
57
+ "@types/semver": "^7.5.8",
58
+ "execa": "^9.5.2",
59
+ "kysely": "^0.28.5",
60
+ "kysely-pglite-dialect": "^1.2.0",
61
+ "msw": "^2.7.0",
62
+ "@hot-updater/aws": "0.21.0",
63
+ "@hot-updater/cloudflare": "0.21.0",
64
+ "@hot-updater/firebase": "0.21.0",
65
+ "@hot-updater/standalone": "0.21.0",
66
+ "@hot-updater/supabase": "0.21.0",
67
+ "@hot-updater/test-utils": "0.21.0"
68
+ },
69
+ "scripts": {
70
+ "build": "tsdown",
71
+ "test:type": "tsc --noEmit"
72
+ }
73
+ }
@@ -0,0 +1 @@
1
+ export * from "fumadb/adapters/drizzle";
@@ -0,0 +1 @@
1
+ export * from "fumadb/adapters/kysely";
@@ -0,0 +1 @@
1
+ export * from "fumadb/adapters/mongodb";
@@ -0,0 +1 @@
1
+ export * from "fumadb/adapters/prisma";
@@ -0,0 +1 @@
1
+ export * from "fumadb/adapters/typeorm";
@@ -0,0 +1,34 @@
1
+ import type { PaginationInfo, PaginationOptions } from "./types";
2
+
3
+ /**
4
+ * Calculate pagination information based on total count, limit, and offset
5
+ */
6
+ export function calculatePagination(
7
+ total: number,
8
+ options: PaginationOptions,
9
+ ): PaginationInfo {
10
+ const { limit, offset } = options;
11
+
12
+ if (total === 0) {
13
+ return {
14
+ total: 0,
15
+ hasNextPage: false,
16
+ hasPreviousPage: false,
17
+ currentPage: 1,
18
+ totalPages: 0,
19
+ };
20
+ }
21
+
22
+ const currentPage = Math.floor(offset / limit) + 1;
23
+ const totalPages = Math.ceil(total / limit);
24
+ const hasNextPage = offset + limit < total;
25
+ const hasPreviousPage = offset > 0;
26
+
27
+ return {
28
+ total,
29
+ hasNextPage,
30
+ hasPreviousPage,
31
+ currentPage,
32
+ totalPages,
33
+ };
34
+ }
@@ -0,0 +1,231 @@
1
+ import { PGlite } from "@electric-sql/pglite";
2
+ import { s3Storage } from "@hot-updater/aws";
3
+ import { r2Storage } from "@hot-updater/cloudflare";
4
+ import type { Bundle, GetBundlesArgs, UpdateInfo } from "@hot-updater/core";
5
+ import { NIL_UUID } from "@hot-updater/core";
6
+ import { firebaseStorage } from "@hot-updater/firebase";
7
+ import { kyselyAdapter } from "@hot-updater/server/adapters/kysely";
8
+ import { supabaseStorage } from "@hot-updater/supabase";
9
+ import { setupGetUpdateInfoTestSuite } from "@hot-updater/test-utils";
10
+ import { Kysely } from "kysely";
11
+ import { PGliteDialect } from "kysely-pglite-dialect";
12
+ import {
13
+ afterAll,
14
+ afterEach,
15
+ beforeAll,
16
+ beforeEach,
17
+ describe,
18
+ expect,
19
+ it,
20
+ vi,
21
+ } from "vitest";
22
+ import { hotUpdater } from "./index";
23
+
24
+ describe("server/db hotUpdater getUpdateInfo (PGlite + Kysely)", async () => {
25
+ const db = new PGlite();
26
+
27
+ const kysely = new Kysely({ dialect: new PGliteDialect(db) });
28
+
29
+ const api = hotUpdater({
30
+ database: kyselyAdapter({
31
+ db: kysely,
32
+ provider: "postgresql",
33
+ }),
34
+ storagePlugins: [
35
+ s3Storage({
36
+ region: "us-east-1",
37
+ credentials: {
38
+ accessKeyId: "test-access-key",
39
+ secretAccessKey: "test-secret-key",
40
+ },
41
+ bucketName: "test-bucket",
42
+ }),
43
+ r2Storage({
44
+ cloudflareApiToken: "test-token",
45
+ accountId: "test-account-id",
46
+ bucketName: "test-bucket",
47
+ }),
48
+ supabaseStorage({
49
+ supabaseUrl: "https://test.supabase.co",
50
+ supabaseAnonKey: "test-anon-key",
51
+ bucketName: "test-bucket",
52
+ }),
53
+ firebaseStorage({
54
+ storageBucket: "test-bucket.appspot.com",
55
+ }),
56
+ ],
57
+ });
58
+
59
+ beforeAll(async () => {
60
+ // Initialize FumaDB schema to latest (creates tables under the hood)
61
+ const migrator = api.createMigrator();
62
+ const result = await migrator.migrateToLatest({
63
+ mode: "from-schema",
64
+ updateSettings: true,
65
+ });
66
+ await result.execute();
67
+ });
68
+
69
+ beforeEach(async () => {
70
+ await db.exec("DELETE FROM bundles");
71
+ });
72
+
73
+ afterAll(async () => {
74
+ await kysely.destroy();
75
+ await db.close();
76
+ });
77
+
78
+ const getUpdateInfo = async (
79
+ bundles: Bundle[],
80
+ options: GetBundlesArgs,
81
+ ): Promise<UpdateInfo | null> => {
82
+ // Insert fixtures via the server API to exercise its types + mapping
83
+ for (const b of bundles) {
84
+ await api.insertBundle(b);
85
+ }
86
+ return api.getUpdateInfo(options);
87
+ };
88
+
89
+ setupGetUpdateInfoTestSuite({ getUpdateInfo });
90
+
91
+ describe("getAppUpdateInfo with storage plugins", () => {
92
+ beforeEach(() => {
93
+ // Fix time for deterministic signed URLs
94
+ vi.useFakeTimers();
95
+ vi.setSystemTime(new Date("2025-10-15T12:21:00Z"));
96
+ });
97
+
98
+ afterEach(() => {
99
+ vi.useRealTimers();
100
+ });
101
+
102
+ it("resolves s3:// storage URI to signed URL via s3StoragePlugin", async () => {
103
+ const bundle: Bundle = {
104
+ id: "00000000-0000-0000-0000-000000000001",
105
+ platform: "ios",
106
+ shouldForceUpdate: false,
107
+ enabled: true,
108
+ fileHash: "hash123",
109
+ gitCommitHash: null,
110
+ message: "Test bundle",
111
+ channel: "production",
112
+ storageUri: "s3://test-bucket/bundles/bundle.zip",
113
+ targetAppVersion: "1.0.0",
114
+ fingerprintHash: null,
115
+ };
116
+
117
+ await api.insertBundle(bundle);
118
+
119
+ const updateInfo = await api.getAppUpdateInfo({
120
+ appVersion: "1.0.0",
121
+ bundleId: NIL_UUID,
122
+ platform: "ios",
123
+ _updateStrategy: "appVersion",
124
+ });
125
+
126
+ expect(updateInfo).not.toBeNull();
127
+ expect(updateInfo?.fileUrl).toBe(
128
+ "https://test-bucket.s3.us-east-1.amazonaws.com/bundles/bundle.zip?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=test-access-key%2F20251015%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20251015T122100Z&X-Amz-Expires=3600&X-Amz-Signature=4fa782e86a842ce2eacbfa6534d1f5d5145d733092959cf6ad755cc306bbe98e&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject",
129
+ );
130
+ });
131
+
132
+ it("passes through http:// URLs without plugin resolution", async () => {
133
+ const bundle: Bundle = {
134
+ id: "00000000-0000-0000-0000-000000000004",
135
+ platform: "ios",
136
+ shouldForceUpdate: false,
137
+ enabled: true,
138
+ fileHash: "hashhttp",
139
+ gitCommitHash: null,
140
+ message: "HTTP bundle",
141
+ channel: "production",
142
+ storageUri: "s3://bundle/bundle.zip",
143
+ targetAppVersion: "1.0.0",
144
+ fingerprintHash: null,
145
+ };
146
+
147
+ await api.insertBundle(bundle);
148
+
149
+ const updateInfo = await api.getAppUpdateInfo({
150
+ appVersion: "1.0.0",
151
+ bundleId: NIL_UUID,
152
+ platform: "ios",
153
+ _updateStrategy: "appVersion",
154
+ });
155
+
156
+ expect(updateInfo).not.toBeNull();
157
+ expect(updateInfo?.fileUrl).toBe(
158
+ "https://bundle.s3.us-east-1.amazonaws.com/bundle.zip?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=test-access-key%2F20251015%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20251015T122100Z&X-Amz-Expires=3600&X-Amz-Signature=b83d9cfc9bd23275e5eb3baf792776fd7b49730f3aa2f5172d067c9dfb10cd94&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject",
159
+ );
160
+ });
161
+
162
+ it("passes through https:// URLs without plugin resolution", async () => {
163
+ const bundle: Bundle = {
164
+ id: "00000000-0000-0000-0000-000000000005",
165
+ platform: "ios",
166
+ shouldForceUpdate: false,
167
+ enabled: true,
168
+ fileHash: "hashhttps",
169
+ gitCommitHash: null,
170
+ message: "HTTPS bundle",
171
+ channel: "production",
172
+ storageUri: "https://cdn.example.com/bundle.zip",
173
+ targetAppVersion: "1.0.0",
174
+ fingerprintHash: null,
175
+ };
176
+
177
+ await api.insertBundle(bundle);
178
+
179
+ const updateInfo = await api.getAppUpdateInfo({
180
+ appVersion: "1.0.0",
181
+ bundleId: NIL_UUID,
182
+ platform: "ios",
183
+ _updateStrategy: "appVersion",
184
+ });
185
+
186
+ expect(updateInfo).not.toBeNull();
187
+ expect(updateInfo?.fileUrl).toBe("https://cdn.example.com/bundle.zip");
188
+ });
189
+
190
+ it("returns null when no update is available", async () => {
191
+ const updateInfo = await api.getAppUpdateInfo({
192
+ appVersion: "99.0.0",
193
+ bundleId: NIL_UUID,
194
+ platform: "ios",
195
+ _updateStrategy: "appVersion",
196
+ });
197
+
198
+ expect(updateInfo).toBeNull();
199
+ });
200
+
201
+ it("works with fingerprint strategy", async () => {
202
+ const bundle: Bundle = {
203
+ id: "00000000-0000-0000-0000-000000000008",
204
+ platform: "ios",
205
+ shouldForceUpdate: false,
206
+ enabled: true,
207
+ fileHash: "hashfp",
208
+ gitCommitHash: null,
209
+ message: "Fingerprint bundle",
210
+ channel: "production",
211
+ storageUri: "s3://test-bucket/fp-bundle.zip",
212
+ targetAppVersion: null,
213
+ fingerprintHash: "fingerprint123",
214
+ };
215
+
216
+ await api.insertBundle(bundle);
217
+
218
+ const updateInfo = await api.getAppUpdateInfo({
219
+ fingerprintHash: "fingerprint123",
220
+ bundleId: NIL_UUID,
221
+ platform: "ios",
222
+ _updateStrategy: "fingerprint",
223
+ });
224
+
225
+ expect(updateInfo).not.toBeNull();
226
+ expect(updateInfo?.fileUrl).toBe(
227
+ "https://test-bucket.s3.us-east-1.amazonaws.com/fp-bundle.zip?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=test-access-key%2F20251015%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20251015T122100Z&X-Amz-Expires=3600&X-Amz-Signature=d70e9b699dccbb51cf32f3e5b7912f2567d38f7e508b1f30091a8fee0d0abb65&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject",
228
+ );
229
+ });
230
+ });
231
+ });