@hot-updater/server 0.29.3 → 0.29.5
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/dist/db/pluginCore.cjs +2 -0
- package/dist/db/pluginCore.mjs +2 -0
- package/dist/handler.cjs +1 -1
- package/dist/handler.mjs +1 -1
- package/package.json +6 -6
- package/src/db/index.spec.ts +1 -0
- package/src/db/index.ts +1 -0
- package/src/db/ormCore.ts +1 -0
- package/src/db/ormUpdateCheck.bench.ts +1 -0
- package/src/db/pluginCore.spec.ts +131 -0
- package/src/db/pluginCore.ts +9 -0
- package/src/db/pluginUpdateCheck.bench.ts +1 -0
- package/src/db/types.ts +1 -0
- package/src/handler-standalone.integration.spec.ts +1 -0
- package/src/handler.spec.ts +1 -0
- package/src/handler.ts +1 -0
- package/src/runtime.spec.ts +106 -0
- package/src/runtime.ts +1 -0
package/dist/db/pluginCore.cjs
CHANGED
|
@@ -113,6 +113,8 @@ function createPluginDatabaseCore(getPlugin, resolveFileUrl, options) {
|
|
|
113
113
|
return getPlugin().getBundleById(id, context);
|
|
114
114
|
},
|
|
115
115
|
async getUpdateInfo(args, context) {
|
|
116
|
+
const directGetUpdateInfo = getPlugin().getUpdateInfo;
|
|
117
|
+
if (directGetUpdateInfo) return context === void 0 ? await directGetUpdateInfo(args) : await directGetUpdateInfo(args, context);
|
|
116
118
|
const channel = args.channel ?? "production";
|
|
117
119
|
const minBundleId = args.minBundleId ?? _hot_updater_core.NIL_UUID;
|
|
118
120
|
const baseWhere = getBaseWhere({
|
package/dist/db/pluginCore.mjs
CHANGED
|
@@ -113,6 +113,8 @@ function createPluginDatabaseCore(getPlugin, resolveFileUrl, options) {
|
|
|
113
113
|
return getPlugin().getBundleById(id, context);
|
|
114
114
|
},
|
|
115
115
|
async getUpdateInfo(args, context) {
|
|
116
|
+
const directGetUpdateInfo = getPlugin().getUpdateInfo;
|
|
117
|
+
if (directGetUpdateInfo) return context === void 0 ? await directGetUpdateInfo(args) : await directGetUpdateInfo(args, context);
|
|
116
118
|
const channel = args.channel ?? "production";
|
|
117
119
|
const minBundleId = args.minBundleId ?? NIL_UUID;
|
|
118
120
|
const baseWhere = getBaseWhere({
|
package/dist/handler.cjs
CHANGED
|
@@ -7,7 +7,7 @@ var HandlerBadRequestError = class extends Error {
|
|
|
7
7
|
}
|
|
8
8
|
};
|
|
9
9
|
const handleVersion = async () => {
|
|
10
|
-
return new Response(JSON.stringify({ version: "0.29.
|
|
10
|
+
return new Response(JSON.stringify({ version: "0.29.5" }), {
|
|
11
11
|
status: 200,
|
|
12
12
|
headers: { "Content-Type": "application/json" }
|
|
13
13
|
});
|
package/dist/handler.mjs
CHANGED
|
@@ -7,7 +7,7 @@ var HandlerBadRequestError = class extends Error {
|
|
|
7
7
|
}
|
|
8
8
|
};
|
|
9
9
|
const handleVersion = async () => {
|
|
10
|
-
return new Response(JSON.stringify({ version: "0.29.
|
|
10
|
+
return new Response(JSON.stringify({ version: "0.29.5" }), {
|
|
11
11
|
status: 200,
|
|
12
12
|
headers: { "Content-Type": "application/json" }
|
|
13
13
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/server",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
"fumadb": "0.2.2",
|
|
54
54
|
"rou3": "0.7.9",
|
|
55
55
|
"semver": "^7.7.2",
|
|
56
|
-
"@hot-updater/core": "0.29.
|
|
57
|
-
"@hot-updater/plugin-core": "0.29.
|
|
58
|
-
"@hot-updater/js": "0.29.
|
|
56
|
+
"@hot-updater/core": "0.29.5",
|
|
57
|
+
"@hot-updater/plugin-core": "0.29.5",
|
|
58
|
+
"@hot-updater/js": "0.29.5"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@electric-sql/pglite": "^0.2.17",
|
|
@@ -66,8 +66,8 @@
|
|
|
66
66
|
"kysely-pglite-dialect": "^1.2.0",
|
|
67
67
|
"msw": "^2.7.0",
|
|
68
68
|
"uuidv7": "^1.0.2",
|
|
69
|
-
"@hot-updater/standalone": "0.29.
|
|
70
|
-
"@hot-updater/test-utils": "0.29.
|
|
69
|
+
"@hot-updater/standalone": "0.29.5",
|
|
70
|
+
"@hot-updater/test-utils": "0.29.5"
|
|
71
71
|
},
|
|
72
72
|
"scripts": {
|
|
73
73
|
"build": "tsdown",
|
package/src/db/index.spec.ts
CHANGED
package/src/db/index.ts
CHANGED
package/src/db/ormCore.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { semverSatisfies } from "@hot-updater/plugin-core";
|
|
|
21
21
|
import type { InferFumaDB } from "fumadb";
|
|
22
22
|
import { fumadb } from "fumadb";
|
|
23
23
|
import type { FumaDBAdapter } from "fumadb/adapters";
|
|
24
|
+
|
|
24
25
|
import { calculatePagination } from "../calculatePagination";
|
|
25
26
|
import { v0_21_0 } from "../schema/v0_21_0";
|
|
26
27
|
import { v0_29_0 } from "../schema/v0_29_0";
|
|
@@ -2,6 +2,7 @@ import { PGlite } from "@electric-sql/pglite";
|
|
|
2
2
|
import { Kysely } from "kysely";
|
|
3
3
|
import { PGliteDialect } from "kysely-pglite-dialect";
|
|
4
4
|
import { bench, describe } from "vitest";
|
|
5
|
+
|
|
5
6
|
import { filterCompatibleAppVersions } from "../../../../plugins/plugin-core/src";
|
|
6
7
|
import type {
|
|
7
8
|
AppVersionGetBundlesArgs,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { Bundle, GetBundlesArgs, UpdateInfo } from "@hot-updater/core";
|
|
2
|
+
import { NIL_UUID } from "@hot-updater/core";
|
|
3
|
+
import type {
|
|
4
|
+
DatabasePlugin,
|
|
5
|
+
RequestEnvContext,
|
|
6
|
+
} from "@hot-updater/plugin-core";
|
|
7
|
+
import { describe, expect, it, vi } from "vitest";
|
|
8
|
+
|
|
9
|
+
import { createPluginDatabaseCore } from "./pluginCore";
|
|
10
|
+
|
|
11
|
+
const baseBundle: Bundle = {
|
|
12
|
+
id: "00000000-0000-0000-0000-000000000001",
|
|
13
|
+
channel: "production",
|
|
14
|
+
enabled: true,
|
|
15
|
+
fileHash: "hash-1",
|
|
16
|
+
fingerprintHash: null,
|
|
17
|
+
gitCommitHash: null,
|
|
18
|
+
message: "bundle",
|
|
19
|
+
platform: "ios",
|
|
20
|
+
shouldForceUpdate: false,
|
|
21
|
+
storageUri: "s3://bucket/bundle.zip",
|
|
22
|
+
targetAppVersion: "1.0.0",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const updateArgs: GetBundlesArgs = {
|
|
26
|
+
_updateStrategy: "appVersion",
|
|
27
|
+
appVersion: "1.0.0",
|
|
28
|
+
bundleId: NIL_UUID,
|
|
29
|
+
platform: "ios",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type TestContext = RequestEnvContext<{
|
|
33
|
+
assetHost: string;
|
|
34
|
+
}>;
|
|
35
|
+
|
|
36
|
+
describe("createPluginDatabaseCore", () => {
|
|
37
|
+
it("prefers plugin getUpdateInfo fast-path when provided", async () => {
|
|
38
|
+
const getBundles = vi.fn<DatabasePlugin<TestContext>["getBundles"]>();
|
|
39
|
+
const expected: UpdateInfo = {
|
|
40
|
+
fileHash: baseBundle.fileHash,
|
|
41
|
+
id: baseBundle.id,
|
|
42
|
+
message: baseBundle.message,
|
|
43
|
+
shouldForceUpdate: baseBundle.shouldForceUpdate,
|
|
44
|
+
status: "UPDATE",
|
|
45
|
+
storageUri: baseBundle.storageUri,
|
|
46
|
+
};
|
|
47
|
+
const getUpdateInfo = vi.fn<
|
|
48
|
+
NonNullable<DatabasePlugin<TestContext>["getUpdateInfo"]>
|
|
49
|
+
>(async () => expected);
|
|
50
|
+
|
|
51
|
+
const plugin: DatabasePlugin<TestContext> = {
|
|
52
|
+
name: "fast-path-plugin",
|
|
53
|
+
async appendBundle() {},
|
|
54
|
+
async commitBundle() {},
|
|
55
|
+
async deleteBundle() {},
|
|
56
|
+
async getBundleById() {
|
|
57
|
+
return null;
|
|
58
|
+
},
|
|
59
|
+
getBundles,
|
|
60
|
+
getUpdateInfo,
|
|
61
|
+
async getChannels() {
|
|
62
|
+
return ["production"];
|
|
63
|
+
},
|
|
64
|
+
async updateBundle() {},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const core = createPluginDatabaseCore(
|
|
68
|
+
() => plugin,
|
|
69
|
+
async () => null,
|
|
70
|
+
);
|
|
71
|
+
const context: TestContext = {
|
|
72
|
+
env: {
|
|
73
|
+
assetHost: "https://assets.example.com",
|
|
74
|
+
},
|
|
75
|
+
request: new Request("https://updates.example.com"),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
await expect(core.api.getUpdateInfo(updateArgs, context)).resolves.toEqual(
|
|
79
|
+
expected,
|
|
80
|
+
);
|
|
81
|
+
expect(getUpdateInfo).toHaveBeenCalledWith(updateArgs, context);
|
|
82
|
+
expect(getBundles).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("falls back to scanning when plugin getUpdateInfo is absent", async () => {
|
|
86
|
+
const latestBundle = {
|
|
87
|
+
...baseBundle,
|
|
88
|
+
id: "00000000-0000-0000-0000-000000000002",
|
|
89
|
+
};
|
|
90
|
+
const getBundles = vi.fn<DatabasePlugin["getBundles"]>(async () => ({
|
|
91
|
+
data: [latestBundle],
|
|
92
|
+
pagination: {
|
|
93
|
+
currentPage: 1,
|
|
94
|
+
hasNextPage: false,
|
|
95
|
+
hasPreviousPage: false,
|
|
96
|
+
total: 1,
|
|
97
|
+
totalPages: 1,
|
|
98
|
+
},
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
const plugin: DatabasePlugin = {
|
|
102
|
+
name: "scan-plugin",
|
|
103
|
+
async appendBundle() {},
|
|
104
|
+
async commitBundle() {},
|
|
105
|
+
async deleteBundle() {},
|
|
106
|
+
async getBundleById() {
|
|
107
|
+
return null;
|
|
108
|
+
},
|
|
109
|
+
getBundles,
|
|
110
|
+
async getChannels() {
|
|
111
|
+
return ["production"];
|
|
112
|
+
},
|
|
113
|
+
async updateBundle() {},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const core = createPluginDatabaseCore(
|
|
117
|
+
() => plugin,
|
|
118
|
+
async () => null,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
await expect(core.api.getUpdateInfo(updateArgs)).resolves.toEqual({
|
|
122
|
+
fileHash: latestBundle.fileHash,
|
|
123
|
+
id: latestBundle.id,
|
|
124
|
+
message: latestBundle.message,
|
|
125
|
+
shouldForceUpdate: latestBundle.shouldForceUpdate,
|
|
126
|
+
status: "UPDATE",
|
|
127
|
+
storageUri: latestBundle.storageUri,
|
|
128
|
+
});
|
|
129
|
+
expect(getBundles).toHaveBeenCalledOnce();
|
|
130
|
+
});
|
|
131
|
+
});
|
package/src/db/pluginCore.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type HotUpdaterContext,
|
|
17
17
|
semverSatisfies,
|
|
18
18
|
} from "@hot-updater/plugin-core";
|
|
19
|
+
|
|
19
20
|
import type { DatabaseAPI } from "./types";
|
|
20
21
|
|
|
21
22
|
const PAGE_SIZE = 100;
|
|
@@ -268,6 +269,14 @@ export function createPluginDatabaseCore<TContext = unknown>(
|
|
|
268
269
|
args: GetBundlesArgs,
|
|
269
270
|
context?: HotUpdaterContext<TContext>,
|
|
270
271
|
): Promise<UpdateInfo | null> {
|
|
272
|
+
const plugin = getPlugin();
|
|
273
|
+
const directGetUpdateInfo = plugin.getUpdateInfo;
|
|
274
|
+
if (directGetUpdateInfo) {
|
|
275
|
+
return context === undefined
|
|
276
|
+
? await directGetUpdateInfo(args)
|
|
277
|
+
: await directGetUpdateInfo(args, context);
|
|
278
|
+
}
|
|
279
|
+
|
|
271
280
|
const channel = args.channel ?? "production";
|
|
272
281
|
const minBundleId = args.minBundleId ?? NIL_UUID;
|
|
273
282
|
const baseWhere = getBaseWhere({
|
package/src/db/types.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { HttpResponse, http } from "msw";
|
|
|
8
8
|
import { setupServer } from "msw/node";
|
|
9
9
|
import { uuidv7 } from "uuidv7";
|
|
10
10
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
11
|
+
|
|
11
12
|
import { standaloneRepository } from "../../../plugins/standalone/src";
|
|
12
13
|
import { kyselyAdapter } from "./adapters/kysely";
|
|
13
14
|
import { createHotUpdater } from "./db";
|
package/src/handler.spec.ts
CHANGED
package/src/handler.ts
CHANGED
package/src/runtime.spec.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
} from "@hot-updater/plugin-core";
|
|
8
8
|
import { createDatabasePlugin } from "@hot-updater/plugin-core";
|
|
9
9
|
import { describe, expect, expectTypeOf, it, vi } from "vitest";
|
|
10
|
+
|
|
10
11
|
import { createHotUpdater } from "./runtime";
|
|
11
12
|
|
|
12
13
|
const bundle: Bundle = {
|
|
@@ -30,6 +31,111 @@ type TestEnv = {
|
|
|
30
31
|
type TestContext = RequestEnvContext<TestEnv>;
|
|
31
32
|
|
|
32
33
|
describe("runtime createHotUpdater", () => {
|
|
34
|
+
it("resolves storage URLs with handler context when database fast-path is used", async () => {
|
|
35
|
+
const request = new Request(
|
|
36
|
+
"https://updates.example.com/api/check-update/app-version/ios/1.0.0/production/" +
|
|
37
|
+
`${NIL_UUID}/${NIL_UUID}`,
|
|
38
|
+
);
|
|
39
|
+
const getBundles = vi.fn<DatabasePlugin<TestContext>["getBundles"]>();
|
|
40
|
+
const getUpdateInfo = vi.fn<
|
|
41
|
+
NonNullable<DatabasePlugin<TestContext>["getUpdateInfo"]>
|
|
42
|
+
>(async () => ({
|
|
43
|
+
fileHash: bundle.fileHash,
|
|
44
|
+
id: bundle.id,
|
|
45
|
+
message: bundle.message,
|
|
46
|
+
shouldForceUpdate: bundle.shouldForceUpdate,
|
|
47
|
+
status: "UPDATE",
|
|
48
|
+
storageUri: bundle.storageUri,
|
|
49
|
+
}));
|
|
50
|
+
const getDownloadUrl = vi.fn<StoragePlugin<TestContext>["getDownloadUrl"]>(
|
|
51
|
+
async (_storageUri, context) => {
|
|
52
|
+
return {
|
|
53
|
+
fileUrl: new URL("/bundle.zip", context?.env?.assetHost).toString(),
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const database: DatabasePlugin<TestContext> = {
|
|
59
|
+
name: "testDatabase",
|
|
60
|
+
async appendBundle() {},
|
|
61
|
+
async commitBundle() {},
|
|
62
|
+
async deleteBundle() {},
|
|
63
|
+
async getBundleById(id) {
|
|
64
|
+
return id === bundle.id ? bundle : null;
|
|
65
|
+
},
|
|
66
|
+
getBundles,
|
|
67
|
+
getUpdateInfo,
|
|
68
|
+
async getChannels() {
|
|
69
|
+
return ["production"];
|
|
70
|
+
},
|
|
71
|
+
async onUnmount() {},
|
|
72
|
+
async updateBundle() {},
|
|
73
|
+
};
|
|
74
|
+
const storage: StoragePlugin<TestContext> = {
|
|
75
|
+
name: "testStorage",
|
|
76
|
+
supportedProtocol: "s3",
|
|
77
|
+
async upload(key) {
|
|
78
|
+
return { storageUri: `s3://test-bucket/${key}` };
|
|
79
|
+
},
|
|
80
|
+
async delete() {},
|
|
81
|
+
getDownloadUrl,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const hotUpdater = createHotUpdater({
|
|
85
|
+
database,
|
|
86
|
+
storages: [storage],
|
|
87
|
+
basePath: "/api/check-update",
|
|
88
|
+
routes: {
|
|
89
|
+
updateCheck: true,
|
|
90
|
+
bundles: false,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const response = await hotUpdater.handler(request, {
|
|
95
|
+
env: {
|
|
96
|
+
assetHost: "https://assets.example.com",
|
|
97
|
+
},
|
|
98
|
+
request,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(response.status).toBe(200);
|
|
102
|
+
await expect(response.json()).resolves.toEqual({
|
|
103
|
+
fileHash: "hash123",
|
|
104
|
+
fileUrl: "https://assets.example.com/bundle.zip",
|
|
105
|
+
id: "00000000-0000-0000-0000-000000000001",
|
|
106
|
+
message: "Test bundle",
|
|
107
|
+
shouldForceUpdate: false,
|
|
108
|
+
status: "UPDATE",
|
|
109
|
+
});
|
|
110
|
+
expect(getUpdateInfo).toHaveBeenCalledWith(
|
|
111
|
+
{
|
|
112
|
+
_updateStrategy: "appVersion",
|
|
113
|
+
appVersion: "1.0.0",
|
|
114
|
+
bundleId: NIL_UUID,
|
|
115
|
+
channel: "production",
|
|
116
|
+
cohort: undefined,
|
|
117
|
+
minBundleId: NIL_UUID,
|
|
118
|
+
platform: "ios",
|
|
119
|
+
},
|
|
120
|
+
expect.objectContaining({
|
|
121
|
+
env: {
|
|
122
|
+
assetHost: "https://assets.example.com",
|
|
123
|
+
},
|
|
124
|
+
request: expect.any(Request),
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
expect(getBundles).not.toHaveBeenCalled();
|
|
128
|
+
expect(getDownloadUrl).toHaveBeenCalledWith(
|
|
129
|
+
"s3://test-bucket/bundles/bundle.zip",
|
|
130
|
+
expect.objectContaining({
|
|
131
|
+
env: {
|
|
132
|
+
assetHost: "https://assets.example.com",
|
|
133
|
+
},
|
|
134
|
+
request: expect.any(Request),
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
33
139
|
it("passes the handler context to database and storage resolution", async () => {
|
|
34
140
|
const request = new Request(
|
|
35
141
|
"https://updates.example.com/api/check-update/app-version/ios/1.0.0/production/" +
|