@hot-updater/server 0.29.4 → 0.29.6
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/ormCore.cjs +105 -23
- package/dist/db/ormCore.mjs +105 -23
- package/dist/db/pluginCore.cjs +7 -4
- package/dist/db/pluginCore.mjs +7 -4
- package/dist/handler.cjs +13 -3
- package/dist/handler.mjs +13 -3
- package/dist/types/index.d.cts +2 -0
- package/dist/types/index.d.mts +2 -0
- package/package.json +6 -6
- package/src/db/ormCore.ts +194 -38
- package/src/db/pluginCore.spec.ts +172 -0
- package/src/db/pluginCore.ts +20 -3
- package/src/db/pluginUpdateCheck.bench.ts +10 -10
- package/src/handler-standalone.integration.spec.ts +1 -2
- package/src/handler.spec.ts +152 -2
- package/src/handler.ts +30 -2
- package/src/runtime.spec.ts +105 -0
- package/src/types/index.ts +2 -0
package/src/handler.spec.ts
CHANGED
|
@@ -179,7 +179,7 @@ describe("createHandler", () => {
|
|
|
179
179
|
|
|
180
180
|
const response = await handler(
|
|
181
181
|
new Request(
|
|
182
|
-
"http://localhost/hot-updater/api/bundles?channel=production&platform=ios&limit=2
|
|
182
|
+
"http://localhost/hot-updater/api/bundles?channel=production&platform=ios&limit=2",
|
|
183
183
|
),
|
|
184
184
|
);
|
|
185
185
|
|
|
@@ -202,12 +202,162 @@ describe("createHandler", () => {
|
|
|
202
202
|
platform: "ios",
|
|
203
203
|
},
|
|
204
204
|
limit: 2,
|
|
205
|
-
|
|
205
|
+
page: undefined,
|
|
206
206
|
},
|
|
207
207
|
undefined,
|
|
208
208
|
);
|
|
209
209
|
});
|
|
210
210
|
|
|
211
|
+
it("passes cursor pagination params through to getBundles", async () => {
|
|
212
|
+
const api = createApi();
|
|
213
|
+
api.getBundles.mockResolvedValue({
|
|
214
|
+
data: [testBundle],
|
|
215
|
+
pagination: {
|
|
216
|
+
total: 51,
|
|
217
|
+
hasNextPage: true,
|
|
218
|
+
hasPreviousPage: true,
|
|
219
|
+
currentPage: 2,
|
|
220
|
+
totalPages: 26,
|
|
221
|
+
nextCursor: "bundle-1",
|
|
222
|
+
previousCursor: "bundle-9",
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
const handler = createHandler(api, { basePath: "/hot-updater" });
|
|
226
|
+
|
|
227
|
+
const response = await handler(
|
|
228
|
+
new Request(
|
|
229
|
+
"http://localhost/hot-updater/api/bundles?channel=production&limit=20&after=bundle-20",
|
|
230
|
+
),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
expect(response.status).toBe(200);
|
|
234
|
+
expect(api.getBundles).toHaveBeenCalledWith(
|
|
235
|
+
{
|
|
236
|
+
where: {
|
|
237
|
+
channel: "production",
|
|
238
|
+
},
|
|
239
|
+
limit: 20,
|
|
240
|
+
page: undefined,
|
|
241
|
+
cursor: {
|
|
242
|
+
after: "bundle-20",
|
|
243
|
+
before: undefined,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
undefined,
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("supports cursor pagination without a legacy offset query param", async () => {
|
|
251
|
+
const api = createApi();
|
|
252
|
+
api.getBundles.mockResolvedValue({
|
|
253
|
+
data: [testBundle],
|
|
254
|
+
pagination: {
|
|
255
|
+
total: 1,
|
|
256
|
+
hasNextPage: false,
|
|
257
|
+
hasPreviousPage: false,
|
|
258
|
+
currentPage: 1,
|
|
259
|
+
totalPages: 1,
|
|
260
|
+
nextCursor: null,
|
|
261
|
+
previousCursor: null,
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
const handler = createHandler(api, { basePath: "/hot-updater" });
|
|
265
|
+
|
|
266
|
+
const response = await handler(
|
|
267
|
+
new Request(
|
|
268
|
+
"http://localhost/hot-updater/api/bundles?channel=production&limit=20&after=bundle-20",
|
|
269
|
+
),
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
expect(response.status).toBe(200);
|
|
273
|
+
expect(api.getBundles).toHaveBeenCalledWith(
|
|
274
|
+
{
|
|
275
|
+
where: {
|
|
276
|
+
channel: "production",
|
|
277
|
+
},
|
|
278
|
+
limit: 20,
|
|
279
|
+
page: undefined,
|
|
280
|
+
cursor: {
|
|
281
|
+
after: "bundle-20",
|
|
282
|
+
before: undefined,
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
undefined,
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("returns 400 when bundle list requests still send offset pagination", async () => {
|
|
290
|
+
const api = createApi();
|
|
291
|
+
const handler = createHandler(api, { basePath: "/hot-updater" });
|
|
292
|
+
|
|
293
|
+
const response = await handler(
|
|
294
|
+
new Request(
|
|
295
|
+
"http://localhost/hot-updater/api/bundles?limit=20&offset=40",
|
|
296
|
+
),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
expect(response.status).toBe(400);
|
|
300
|
+
await expect(response.json()).resolves.toEqual({
|
|
301
|
+
error:
|
|
302
|
+
"The 'offset' query parameter has been removed. Use 'after' or 'before' cursor pagination instead.",
|
|
303
|
+
});
|
|
304
|
+
expect(api.getBundles).not.toHaveBeenCalled();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("passes page-aligned pagination params through to getBundles", async () => {
|
|
308
|
+
const api = createApi();
|
|
309
|
+
api.getBundles.mockResolvedValue({
|
|
310
|
+
data: [testBundle],
|
|
311
|
+
pagination: {
|
|
312
|
+
total: 121,
|
|
313
|
+
hasNextPage: true,
|
|
314
|
+
hasPreviousPage: true,
|
|
315
|
+
currentPage: 2,
|
|
316
|
+
totalPages: 7,
|
|
317
|
+
nextCursor: "bundle-1",
|
|
318
|
+
previousCursor: "bundle-9",
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
const handler = createHandler(api, { basePath: "/hot-updater" });
|
|
322
|
+
|
|
323
|
+
const response = await handler(
|
|
324
|
+
new Request(
|
|
325
|
+
"http://localhost/hot-updater/api/bundles?channel=production&limit=20&page=2&after=bundle-20",
|
|
326
|
+
),
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
expect(response.status).toBe(200);
|
|
330
|
+
expect(api.getBundles).toHaveBeenCalledWith(
|
|
331
|
+
{
|
|
332
|
+
where: {
|
|
333
|
+
channel: "production",
|
|
334
|
+
},
|
|
335
|
+
limit: 20,
|
|
336
|
+
page: 2,
|
|
337
|
+
cursor: {
|
|
338
|
+
after: "bundle-20",
|
|
339
|
+
before: undefined,
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
undefined,
|
|
343
|
+
);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("returns 400 when bundle list requests send an invalid page", async () => {
|
|
347
|
+
const api = createApi();
|
|
348
|
+
const handler = createHandler(api, { basePath: "/hot-updater" });
|
|
349
|
+
|
|
350
|
+
const response = await handler(
|
|
351
|
+
new Request("http://localhost/hot-updater/api/bundles?limit=20&page=0"),
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
expect(response.status).toBe(400);
|
|
355
|
+
await expect(response.json()).resolves.toEqual({
|
|
356
|
+
error: "The 'page' query parameter must be a positive integer.",
|
|
357
|
+
});
|
|
358
|
+
expect(api.getBundles).not.toHaveBeenCalled();
|
|
359
|
+
});
|
|
360
|
+
|
|
211
361
|
it("returns 400 when the platform route parameter is invalid", async () => {
|
|
212
362
|
const api = createApi();
|
|
213
363
|
const handler = createHandler(api, { basePath: "/hot-updater" });
|
package/src/handler.ts
CHANGED
|
@@ -243,7 +243,28 @@ const handleGetBundles: RouteHandler = async (
|
|
|
243
243
|
const channel = url.searchParams.get("channel") ?? undefined;
|
|
244
244
|
const platform = url.searchParams.get("platform");
|
|
245
245
|
const limit = Number(url.searchParams.get("limit")) || 50;
|
|
246
|
-
const
|
|
246
|
+
const pageParam = url.searchParams.get("page");
|
|
247
|
+
const offset = url.searchParams.get("offset");
|
|
248
|
+
const after = url.searchParams.get("after") ?? undefined;
|
|
249
|
+
const before = url.searchParams.get("before") ?? undefined;
|
|
250
|
+
const page =
|
|
251
|
+
pageParam === null
|
|
252
|
+
? undefined
|
|
253
|
+
: Number.isInteger(Number(pageParam)) && Number(pageParam) > 0
|
|
254
|
+
? Number(pageParam)
|
|
255
|
+
: null;
|
|
256
|
+
|
|
257
|
+
if (offset !== null) {
|
|
258
|
+
throw new HandlerBadRequestError(
|
|
259
|
+
"The 'offset' query parameter has been removed. Use 'after' or 'before' cursor pagination instead.",
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (page === null) {
|
|
264
|
+
throw new HandlerBadRequestError(
|
|
265
|
+
"The 'page' query parameter must be a positive integer.",
|
|
266
|
+
);
|
|
267
|
+
}
|
|
247
268
|
|
|
248
269
|
if (platform !== null && !isPlatform(platform)) {
|
|
249
270
|
throw new HandlerBadRequestError(
|
|
@@ -258,7 +279,14 @@ const handleGetBundles: RouteHandler = async (
|
|
|
258
279
|
...(platform && { platform }),
|
|
259
280
|
},
|
|
260
281
|
limit,
|
|
261
|
-
|
|
282
|
+
page,
|
|
283
|
+
cursor:
|
|
284
|
+
after || before
|
|
285
|
+
? {
|
|
286
|
+
after,
|
|
287
|
+
before,
|
|
288
|
+
}
|
|
289
|
+
: undefined,
|
|
262
290
|
},
|
|
263
291
|
context,
|
|
264
292
|
);
|
package/src/runtime.spec.ts
CHANGED
|
@@ -31,6 +31,111 @@ type TestEnv = {
|
|
|
31
31
|
type TestContext = RequestEnvContext<TestEnv>;
|
|
32
32
|
|
|
33
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
|
+
|
|
34
139
|
it("passes the handler context to database and storage resolution", async () => {
|
|
35
140
|
const request = new Request(
|
|
36
141
|
"https://updates.example.com/api/check-update/app-version/ios/1.0.0/production/" +
|