@hot-updater/server 0.29.5 → 0.29.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/dist/db/ormCore.cjs +105 -23
- package/dist/db/ormCore.mjs +105 -23
- package/dist/db/pluginCore.cjs +5 -4
- package/dist/db/pluginCore.mjs +5 -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 +41 -0
- package/src/db/pluginCore.ts +12 -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/types/index.ts +2 -0
package/dist/db/ormCore.cjs
CHANGED
|
@@ -18,6 +18,40 @@ const parseTargetCohorts = (value) => {
|
|
|
18
18
|
};
|
|
19
19
|
const schemas = [require_v0_21_0.v0_21_0, require_v0_29_0.v0_29_0];
|
|
20
20
|
const getLastItem = (items) => items.at(-1);
|
|
21
|
+
const DEFAULT_BUNDLE_ORDER = {
|
|
22
|
+
field: "id",
|
|
23
|
+
direction: "desc"
|
|
24
|
+
};
|
|
25
|
+
const mergeIdFilter = (base, patch) => ({
|
|
26
|
+
...base,
|
|
27
|
+
...patch
|
|
28
|
+
});
|
|
29
|
+
const mergeWhereWithIdFilter = (where, idFilter) => ({
|
|
30
|
+
...where,
|
|
31
|
+
id: mergeIdFilter(where?.id, idFilter)
|
|
32
|
+
});
|
|
33
|
+
const buildCursorPageWhere = (where, cursor, orderBy) => {
|
|
34
|
+
const direction = orderBy.direction;
|
|
35
|
+
if (cursor.after) return {
|
|
36
|
+
reverseData: false,
|
|
37
|
+
where: mergeWhereWithIdFilter(where, { [direction === "desc" ? "lt" : "gt"]: cursor.after }),
|
|
38
|
+
orderBy
|
|
39
|
+
};
|
|
40
|
+
if (cursor.before) return {
|
|
41
|
+
reverseData: true,
|
|
42
|
+
where: mergeWhereWithIdFilter(where, { [direction === "desc" ? "gt" : "lt"]: cursor.before }),
|
|
43
|
+
orderBy: {
|
|
44
|
+
field: orderBy.field,
|
|
45
|
+
direction: direction === "desc" ? "asc" : "desc"
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
reverseData: false,
|
|
50
|
+
where: where ?? {},
|
|
51
|
+
orderBy
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
const buildCountBeforeWhere = (where, firstBundleId, orderBy) => mergeWhereWithIdFilter(where, { [orderBy.direction === "desc" ? "gt" : "lt"]: firstBundleId });
|
|
21
55
|
const HotUpdaterDB = (0, fumadb.fumadb)({
|
|
22
56
|
namespace: "hot_updater",
|
|
23
57
|
schemas
|
|
@@ -264,7 +298,9 @@ function createOrmDatabaseCore({ database, resolveFileUrl }) {
|
|
|
264
298
|
},
|
|
265
299
|
async getBundles(options) {
|
|
266
300
|
const orm = await ensureORM();
|
|
267
|
-
const { where, limit
|
|
301
|
+
const { where, limit } = options;
|
|
302
|
+
const orderBy = options.orderBy ?? DEFAULT_BUNDLE_ORDER;
|
|
303
|
+
const offset = ("offset" in options ? options.offset : void 0) ?? 0;
|
|
268
304
|
const total = await orm.count("bundles", { where: buildBundleWhere(where) });
|
|
269
305
|
const selectedColumns = [
|
|
270
306
|
"id",
|
|
@@ -282,39 +318,85 @@ function createOrmDatabaseCore({ database, resolveFileUrl }) {
|
|
|
282
318
|
"rollout_cohort_count",
|
|
283
319
|
"target_cohorts"
|
|
284
320
|
];
|
|
285
|
-
|
|
286
|
-
|
|
321
|
+
const mapRowsToBundles = (rows) => rows.map((r) => ({
|
|
322
|
+
id: r.id,
|
|
323
|
+
platform: r.platform,
|
|
324
|
+
shouldForceUpdate: Boolean(r.should_force_update),
|
|
325
|
+
enabled: Boolean(r.enabled),
|
|
326
|
+
fileHash: r.file_hash,
|
|
327
|
+
gitCommitHash: r.git_commit_hash ?? null,
|
|
328
|
+
message: r.message ?? null,
|
|
329
|
+
channel: r.channel,
|
|
330
|
+
storageUri: r.storage_uri,
|
|
331
|
+
targetAppVersion: r.target_app_version ?? null,
|
|
332
|
+
fingerprintHash: r.fingerprint_hash ?? null,
|
|
333
|
+
rolloutCohortCount: r.rollout_cohort_count ?? _hot_updater_core.DEFAULT_ROLLOUT_COHORT_COUNT,
|
|
334
|
+
targetCohorts: parseTargetCohorts(r.target_cohorts)
|
|
335
|
+
}));
|
|
336
|
+
const findBundles = async ({ where, orderBy, limit, offset }) => {
|
|
337
|
+
return mapRowsToBundles(isMongoAdapter ? (await orm.findMany("bundles", {
|
|
287
338
|
select: selectedColumns,
|
|
288
339
|
where: buildBundleWhere(where)
|
|
289
340
|
})).sort((a, b) => {
|
|
290
|
-
const direction = orderBy?.direction ?? "desc";
|
|
291
341
|
const result = a.id.localeCompare(b.id);
|
|
292
|
-
return direction === "asc" ? result : -result;
|
|
342
|
+
return orderBy.direction === "asc" ? result : -result;
|
|
293
343
|
}).slice(offset, offset + limit) : await orm.findMany("bundles", {
|
|
294
344
|
select: selectedColumns,
|
|
295
345
|
where: buildBundleWhere(where),
|
|
296
|
-
orderBy: [[orderBy
|
|
346
|
+
orderBy: [[orderBy.field, orderBy.direction]],
|
|
297
347
|
limit,
|
|
298
348
|
offset
|
|
299
|
-
}))
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
gitCommitHash: r.git_commit_hash ?? null,
|
|
306
|
-
message: r.message ?? null,
|
|
307
|
-
channel: r.channel,
|
|
308
|
-
storageUri: r.storage_uri,
|
|
309
|
-
targetAppVersion: r.target_app_version ?? null,
|
|
310
|
-
fingerprintHash: r.fingerprint_hash ?? null,
|
|
311
|
-
rolloutCohortCount: r.rollout_cohort_count ?? _hot_updater_core.DEFAULT_ROLLOUT_COHORT_COUNT,
|
|
312
|
-
targetCohorts: parseTargetCohorts(r.target_cohorts)
|
|
313
|
-
})),
|
|
314
|
-
pagination: require_calculatePagination.calculatePagination(total, {
|
|
349
|
+
}));
|
|
350
|
+
};
|
|
351
|
+
if (!options.cursor?.after && !options.cursor?.before) {
|
|
352
|
+
const data = await findBundles({
|
|
353
|
+
where,
|
|
354
|
+
orderBy,
|
|
315
355
|
limit,
|
|
316
356
|
offset
|
|
317
|
-
})
|
|
357
|
+
});
|
|
358
|
+
return {
|
|
359
|
+
data,
|
|
360
|
+
pagination: {
|
|
361
|
+
...require_calculatePagination.calculatePagination(total, {
|
|
362
|
+
limit,
|
|
363
|
+
offset
|
|
364
|
+
}),
|
|
365
|
+
...data.length > 0 && offset + data.length < total ? { nextCursor: data.at(-1)?.id } : {},
|
|
366
|
+
...data.length > 0 && offset > 0 ? { previousCursor: data[0]?.id } : {}
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
const { where: cursorWhere, orderBy: cursorOrderBy, reverseData } = buildCursorPageWhere(where, options.cursor, orderBy);
|
|
371
|
+
const cursorPage = await findBundles({
|
|
372
|
+
where: cursorWhere,
|
|
373
|
+
orderBy: cursorOrderBy,
|
|
374
|
+
limit,
|
|
375
|
+
offset: 0
|
|
376
|
+
});
|
|
377
|
+
const data = reverseData ? cursorPage.slice().reverse() : cursorPage;
|
|
378
|
+
if (data.length === 0) return {
|
|
379
|
+
data,
|
|
380
|
+
pagination: {
|
|
381
|
+
...require_calculatePagination.calculatePagination(total, {
|
|
382
|
+
limit,
|
|
383
|
+
offset: options.cursor.after ? total : 0
|
|
384
|
+
}),
|
|
385
|
+
...options.cursor.after ? { previousCursor: options.cursor.after } : {},
|
|
386
|
+
...options.cursor.before ? { nextCursor: options.cursor.before } : {}
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
const startIndex = await orm.count("bundles", { where: buildBundleWhere(buildCountBeforeWhere(where, data[0].id, orderBy)) });
|
|
390
|
+
return {
|
|
391
|
+
data,
|
|
392
|
+
pagination: {
|
|
393
|
+
...require_calculatePagination.calculatePagination(total, {
|
|
394
|
+
limit,
|
|
395
|
+
offset: startIndex
|
|
396
|
+
}),
|
|
397
|
+
...startIndex + data.length < total ? { nextCursor: data.at(-1)?.id } : {},
|
|
398
|
+
...startIndex > 0 ? { previousCursor: data[0]?.id } : {}
|
|
399
|
+
}
|
|
318
400
|
};
|
|
319
401
|
},
|
|
320
402
|
async insertBundle(bundle) {
|
package/dist/db/ormCore.mjs
CHANGED
|
@@ -18,6 +18,40 @@ const parseTargetCohorts = (value) => {
|
|
|
18
18
|
};
|
|
19
19
|
const schemas = [v0_21_0, v0_29_0];
|
|
20
20
|
const getLastItem = (items) => items.at(-1);
|
|
21
|
+
const DEFAULT_BUNDLE_ORDER = {
|
|
22
|
+
field: "id",
|
|
23
|
+
direction: "desc"
|
|
24
|
+
};
|
|
25
|
+
const mergeIdFilter = (base, patch) => ({
|
|
26
|
+
...base,
|
|
27
|
+
...patch
|
|
28
|
+
});
|
|
29
|
+
const mergeWhereWithIdFilter = (where, idFilter) => ({
|
|
30
|
+
...where,
|
|
31
|
+
id: mergeIdFilter(where?.id, idFilter)
|
|
32
|
+
});
|
|
33
|
+
const buildCursorPageWhere = (where, cursor, orderBy) => {
|
|
34
|
+
const direction = orderBy.direction;
|
|
35
|
+
if (cursor.after) return {
|
|
36
|
+
reverseData: false,
|
|
37
|
+
where: mergeWhereWithIdFilter(where, { [direction === "desc" ? "lt" : "gt"]: cursor.after }),
|
|
38
|
+
orderBy
|
|
39
|
+
};
|
|
40
|
+
if (cursor.before) return {
|
|
41
|
+
reverseData: true,
|
|
42
|
+
where: mergeWhereWithIdFilter(where, { [direction === "desc" ? "gt" : "lt"]: cursor.before }),
|
|
43
|
+
orderBy: {
|
|
44
|
+
field: orderBy.field,
|
|
45
|
+
direction: direction === "desc" ? "asc" : "desc"
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
reverseData: false,
|
|
50
|
+
where: where ?? {},
|
|
51
|
+
orderBy
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
const buildCountBeforeWhere = (where, firstBundleId, orderBy) => mergeWhereWithIdFilter(where, { [orderBy.direction === "desc" ? "gt" : "lt"]: firstBundleId });
|
|
21
55
|
const HotUpdaterDB = fumadb({
|
|
22
56
|
namespace: "hot_updater",
|
|
23
57
|
schemas
|
|
@@ -264,7 +298,9 @@ function createOrmDatabaseCore({ database, resolveFileUrl }) {
|
|
|
264
298
|
},
|
|
265
299
|
async getBundles(options) {
|
|
266
300
|
const orm = await ensureORM();
|
|
267
|
-
const { where, limit
|
|
301
|
+
const { where, limit } = options;
|
|
302
|
+
const orderBy = options.orderBy ?? DEFAULT_BUNDLE_ORDER;
|
|
303
|
+
const offset = ("offset" in options ? options.offset : void 0) ?? 0;
|
|
268
304
|
const total = await orm.count("bundles", { where: buildBundleWhere(where) });
|
|
269
305
|
const selectedColumns = [
|
|
270
306
|
"id",
|
|
@@ -282,39 +318,85 @@ function createOrmDatabaseCore({ database, resolveFileUrl }) {
|
|
|
282
318
|
"rollout_cohort_count",
|
|
283
319
|
"target_cohorts"
|
|
284
320
|
];
|
|
285
|
-
|
|
286
|
-
|
|
321
|
+
const mapRowsToBundles = (rows) => rows.map((r) => ({
|
|
322
|
+
id: r.id,
|
|
323
|
+
platform: r.platform,
|
|
324
|
+
shouldForceUpdate: Boolean(r.should_force_update),
|
|
325
|
+
enabled: Boolean(r.enabled),
|
|
326
|
+
fileHash: r.file_hash,
|
|
327
|
+
gitCommitHash: r.git_commit_hash ?? null,
|
|
328
|
+
message: r.message ?? null,
|
|
329
|
+
channel: r.channel,
|
|
330
|
+
storageUri: r.storage_uri,
|
|
331
|
+
targetAppVersion: r.target_app_version ?? null,
|
|
332
|
+
fingerprintHash: r.fingerprint_hash ?? null,
|
|
333
|
+
rolloutCohortCount: r.rollout_cohort_count ?? DEFAULT_ROLLOUT_COHORT_COUNT,
|
|
334
|
+
targetCohorts: parseTargetCohorts(r.target_cohorts)
|
|
335
|
+
}));
|
|
336
|
+
const findBundles = async ({ where, orderBy, limit, offset }) => {
|
|
337
|
+
return mapRowsToBundles(isMongoAdapter ? (await orm.findMany("bundles", {
|
|
287
338
|
select: selectedColumns,
|
|
288
339
|
where: buildBundleWhere(where)
|
|
289
340
|
})).sort((a, b) => {
|
|
290
|
-
const direction = orderBy?.direction ?? "desc";
|
|
291
341
|
const result = a.id.localeCompare(b.id);
|
|
292
|
-
return direction === "asc" ? result : -result;
|
|
342
|
+
return orderBy.direction === "asc" ? result : -result;
|
|
293
343
|
}).slice(offset, offset + limit) : await orm.findMany("bundles", {
|
|
294
344
|
select: selectedColumns,
|
|
295
345
|
where: buildBundleWhere(where),
|
|
296
|
-
orderBy: [[orderBy
|
|
346
|
+
orderBy: [[orderBy.field, orderBy.direction]],
|
|
297
347
|
limit,
|
|
298
348
|
offset
|
|
299
|
-
}))
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
gitCommitHash: r.git_commit_hash ?? null,
|
|
306
|
-
message: r.message ?? null,
|
|
307
|
-
channel: r.channel,
|
|
308
|
-
storageUri: r.storage_uri,
|
|
309
|
-
targetAppVersion: r.target_app_version ?? null,
|
|
310
|
-
fingerprintHash: r.fingerprint_hash ?? null,
|
|
311
|
-
rolloutCohortCount: r.rollout_cohort_count ?? DEFAULT_ROLLOUT_COHORT_COUNT,
|
|
312
|
-
targetCohorts: parseTargetCohorts(r.target_cohorts)
|
|
313
|
-
})),
|
|
314
|
-
pagination: calculatePagination(total, {
|
|
349
|
+
}));
|
|
350
|
+
};
|
|
351
|
+
if (!options.cursor?.after && !options.cursor?.before) {
|
|
352
|
+
const data = await findBundles({
|
|
353
|
+
where,
|
|
354
|
+
orderBy,
|
|
315
355
|
limit,
|
|
316
356
|
offset
|
|
317
|
-
})
|
|
357
|
+
});
|
|
358
|
+
return {
|
|
359
|
+
data,
|
|
360
|
+
pagination: {
|
|
361
|
+
...calculatePagination(total, {
|
|
362
|
+
limit,
|
|
363
|
+
offset
|
|
364
|
+
}),
|
|
365
|
+
...data.length > 0 && offset + data.length < total ? { nextCursor: data.at(-1)?.id } : {},
|
|
366
|
+
...data.length > 0 && offset > 0 ? { previousCursor: data[0]?.id } : {}
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
const { where: cursorWhere, orderBy: cursorOrderBy, reverseData } = buildCursorPageWhere(where, options.cursor, orderBy);
|
|
371
|
+
const cursorPage = await findBundles({
|
|
372
|
+
where: cursorWhere,
|
|
373
|
+
orderBy: cursorOrderBy,
|
|
374
|
+
limit,
|
|
375
|
+
offset: 0
|
|
376
|
+
});
|
|
377
|
+
const data = reverseData ? cursorPage.slice().reverse() : cursorPage;
|
|
378
|
+
if (data.length === 0) return {
|
|
379
|
+
data,
|
|
380
|
+
pagination: {
|
|
381
|
+
...calculatePagination(total, {
|
|
382
|
+
limit,
|
|
383
|
+
offset: options.cursor.after ? total : 0
|
|
384
|
+
}),
|
|
385
|
+
...options.cursor.after ? { previousCursor: options.cursor.after } : {},
|
|
386
|
+
...options.cursor.before ? { nextCursor: options.cursor.before } : {}
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
const startIndex = await orm.count("bundles", { where: buildBundleWhere(buildCountBeforeWhere(where, data[0].id, orderBy)) });
|
|
390
|
+
return {
|
|
391
|
+
data,
|
|
392
|
+
pagination: {
|
|
393
|
+
...calculatePagination(total, {
|
|
394
|
+
limit,
|
|
395
|
+
offset: startIndex
|
|
396
|
+
}),
|
|
397
|
+
...startIndex + data.length < total ? { nextCursor: data.at(-1)?.id } : {},
|
|
398
|
+
...startIndex > 0 ? { previousCursor: data[0]?.id } : {}
|
|
399
|
+
}
|
|
318
400
|
};
|
|
319
401
|
},
|
|
320
402
|
async insertBundle(bundle) {
|
package/dist/db/pluginCore.cjs
CHANGED
|
@@ -69,13 +69,13 @@ function createPluginDatabaseCore(getPlugin, resolveFileUrl, options) {
|
|
|
69
69
|
return (0, _hot_updater_core.isCohortEligibleForUpdate)(bundle.id, cohort, bundle.rolloutCohortCount, bundle.targetCohorts);
|
|
70
70
|
};
|
|
71
71
|
const findUpdateInfoByScanning = async ({ args, queryWhere, isCandidate, context }) => {
|
|
72
|
-
let
|
|
72
|
+
let after;
|
|
73
73
|
while (true) {
|
|
74
74
|
const { data, pagination } = await getSortedBundlePage({
|
|
75
75
|
where: queryWhere,
|
|
76
76
|
limit: PAGE_SIZE,
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
orderBy: DESC_ORDER,
|
|
78
|
+
...after ? { cursor: { after } } : {}
|
|
79
79
|
}, context);
|
|
80
80
|
for (const bundle of data) {
|
|
81
81
|
if (!bundleMatchesQueryWhere(bundle, queryWhere) || !isCandidate(bundle)) continue;
|
|
@@ -95,7 +95,8 @@ function createPluginDatabaseCore(getPlugin, resolveFileUrl, options) {
|
|
|
95
95
|
return makeResponse(bundle, "ROLLBACK");
|
|
96
96
|
}
|
|
97
97
|
if (!pagination.hasNextPage) break;
|
|
98
|
-
|
|
98
|
+
after = data.at(-1)?.id;
|
|
99
|
+
if (!after) break;
|
|
99
100
|
}
|
|
100
101
|
if (args.bundleId === _hot_updater_core.NIL_UUID) return null;
|
|
101
102
|
if (args.minBundleId && args.bundleId.localeCompare(args.minBundleId) <= 0) return null;
|
package/dist/db/pluginCore.mjs
CHANGED
|
@@ -69,13 +69,13 @@ function createPluginDatabaseCore(getPlugin, resolveFileUrl, options) {
|
|
|
69
69
|
return isCohortEligibleForUpdate(bundle.id, cohort, bundle.rolloutCohortCount, bundle.targetCohorts);
|
|
70
70
|
};
|
|
71
71
|
const findUpdateInfoByScanning = async ({ args, queryWhere, isCandidate, context }) => {
|
|
72
|
-
let
|
|
72
|
+
let after;
|
|
73
73
|
while (true) {
|
|
74
74
|
const { data, pagination } = await getSortedBundlePage({
|
|
75
75
|
where: queryWhere,
|
|
76
76
|
limit: PAGE_SIZE,
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
orderBy: DESC_ORDER,
|
|
78
|
+
...after ? { cursor: { after } } : {}
|
|
79
79
|
}, context);
|
|
80
80
|
for (const bundle of data) {
|
|
81
81
|
if (!bundleMatchesQueryWhere(bundle, queryWhere) || !isCandidate(bundle)) continue;
|
|
@@ -95,7 +95,8 @@ function createPluginDatabaseCore(getPlugin, resolveFileUrl, options) {
|
|
|
95
95
|
return makeResponse(bundle, "ROLLBACK");
|
|
96
96
|
}
|
|
97
97
|
if (!pagination.hasNextPage) break;
|
|
98
|
-
|
|
98
|
+
after = data.at(-1)?.id;
|
|
99
|
+
if (!after) break;
|
|
99
100
|
}
|
|
100
101
|
if (args.bundleId === NIL_UUID) return null;
|
|
101
102
|
if (args.minBundleId && args.bundleId.localeCompare(args.minBundleId) <= 0) return null;
|
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.7" }), {
|
|
11
11
|
status: 200,
|
|
12
12
|
headers: { "Content-Type": "application/json" }
|
|
13
13
|
});
|
|
@@ -97,7 +97,13 @@ const handleGetBundles = async (_params, request, api, context) => {
|
|
|
97
97
|
const channel = url.searchParams.get("channel") ?? void 0;
|
|
98
98
|
const platform = url.searchParams.get("platform");
|
|
99
99
|
const limit = Number(url.searchParams.get("limit")) || 50;
|
|
100
|
-
const
|
|
100
|
+
const pageParam = url.searchParams.get("page");
|
|
101
|
+
const offset = url.searchParams.get("offset");
|
|
102
|
+
const after = url.searchParams.get("after") ?? void 0;
|
|
103
|
+
const before = url.searchParams.get("before") ?? void 0;
|
|
104
|
+
const page = pageParam === null ? void 0 : Number.isInteger(Number(pageParam)) && Number(pageParam) > 0 ? Number(pageParam) : null;
|
|
105
|
+
if (offset !== null) throw new HandlerBadRequestError("The 'offset' query parameter has been removed. Use 'after' or 'before' cursor pagination instead.");
|
|
106
|
+
if (page === null) throw new HandlerBadRequestError("The 'page' query parameter must be a positive integer.");
|
|
101
107
|
if (platform !== null && !isPlatform(platform)) throw new HandlerBadRequestError(`Invalid platform: ${platform}. Expected 'ios' or 'android'.`);
|
|
102
108
|
const result = await api.getBundles({
|
|
103
109
|
where: {
|
|
@@ -105,7 +111,11 @@ const handleGetBundles = async (_params, request, api, context) => {
|
|
|
105
111
|
...platform && { platform }
|
|
106
112
|
},
|
|
107
113
|
limit,
|
|
108
|
-
|
|
114
|
+
page,
|
|
115
|
+
cursor: after || before ? {
|
|
116
|
+
after,
|
|
117
|
+
before
|
|
118
|
+
} : void 0
|
|
109
119
|
}, context);
|
|
110
120
|
return new Response(JSON.stringify(result), {
|
|
111
121
|
status: 200,
|
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.7" }), {
|
|
11
11
|
status: 200,
|
|
12
12
|
headers: { "Content-Type": "application/json" }
|
|
13
13
|
});
|
|
@@ -97,7 +97,13 @@ const handleGetBundles = async (_params, request, api, context) => {
|
|
|
97
97
|
const channel = url.searchParams.get("channel") ?? void 0;
|
|
98
98
|
const platform = url.searchParams.get("platform");
|
|
99
99
|
const limit = Number(url.searchParams.get("limit")) || 50;
|
|
100
|
-
const
|
|
100
|
+
const pageParam = url.searchParams.get("page");
|
|
101
|
+
const offset = url.searchParams.get("offset");
|
|
102
|
+
const after = url.searchParams.get("after") ?? void 0;
|
|
103
|
+
const before = url.searchParams.get("before") ?? void 0;
|
|
104
|
+
const page = pageParam === null ? void 0 : Number.isInteger(Number(pageParam)) && Number(pageParam) > 0 ? Number(pageParam) : null;
|
|
105
|
+
if (offset !== null) throw new HandlerBadRequestError("The 'offset' query parameter has been removed. Use 'after' or 'before' cursor pagination instead.");
|
|
106
|
+
if (page === null) throw new HandlerBadRequestError("The 'page' query parameter must be a positive integer.");
|
|
101
107
|
if (platform !== null && !isPlatform(platform)) throw new HandlerBadRequestError(`Invalid platform: ${platform}. Expected 'ios' or 'android'.`);
|
|
102
108
|
const result = await api.getBundles({
|
|
103
109
|
where: {
|
|
@@ -105,7 +111,11 @@ const handleGetBundles = async (_params, request, api, context) => {
|
|
|
105
111
|
...platform && { platform }
|
|
106
112
|
},
|
|
107
113
|
limit,
|
|
108
|
-
|
|
114
|
+
page,
|
|
115
|
+
cursor: after || before ? {
|
|
116
|
+
after,
|
|
117
|
+
before
|
|
118
|
+
} : void 0
|
|
109
119
|
}, context);
|
|
110
120
|
return new Response(JSON.stringify(result), {
|
|
111
121
|
status: 200,
|
package/dist/types/index.d.cts
CHANGED
package/dist/types/index.d.mts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/server",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.7",
|
|
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/
|
|
58
|
-
"@hot-updater/js": "0.29.
|
|
56
|
+
"@hot-updater/plugin-core": "0.29.7",
|
|
57
|
+
"@hot-updater/core": "0.29.7",
|
|
58
|
+
"@hot-updater/js": "0.29.7"
|
|
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.7",
|
|
70
|
+
"@hot-updater/test-utils": "0.29.7"
|
|
71
71
|
},
|
|
72
72
|
"scripts": {
|
|
73
73
|
"build": "tsdown",
|
package/src/db/ormCore.ts
CHANGED
|
@@ -13,6 +13,9 @@ import {
|
|
|
13
13
|
NIL_UUID,
|
|
14
14
|
} from "@hot-updater/core";
|
|
15
15
|
import type {
|
|
16
|
+
DatabaseBundleCursor,
|
|
17
|
+
DatabaseBundleIdFilter,
|
|
18
|
+
DatabaseBundleQueryOrder,
|
|
16
19
|
DatabaseBundleQueryOptions,
|
|
17
20
|
DatabaseBundleQueryWhere,
|
|
18
21
|
HotUpdaterContext,
|
|
@@ -53,6 +56,74 @@ const getLastItem = <T extends unknown[]>(
|
|
|
53
56
|
): T extends [...infer _, infer Last] ? Last : never =>
|
|
54
57
|
items.at(-1) as T extends [...infer _, infer Last] ? Last : never;
|
|
55
58
|
|
|
59
|
+
const DEFAULT_BUNDLE_ORDER = { field: "id", direction: "desc" } as const;
|
|
60
|
+
|
|
61
|
+
const mergeIdFilter = (
|
|
62
|
+
base: DatabaseBundleIdFilter | undefined,
|
|
63
|
+
patch: DatabaseBundleIdFilter,
|
|
64
|
+
): DatabaseBundleIdFilter => ({
|
|
65
|
+
...base,
|
|
66
|
+
...patch,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const mergeWhereWithIdFilter = (
|
|
70
|
+
where: DatabaseBundleQueryWhere | undefined,
|
|
71
|
+
idFilter: DatabaseBundleIdFilter,
|
|
72
|
+
): DatabaseBundleQueryWhere => ({
|
|
73
|
+
...where,
|
|
74
|
+
id: mergeIdFilter(where?.id, idFilter),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const buildCursorPageWhere = (
|
|
78
|
+
where: DatabaseBundleQueryWhere | undefined,
|
|
79
|
+
cursor: DatabaseBundleCursor,
|
|
80
|
+
orderBy: DatabaseBundleQueryOrder,
|
|
81
|
+
): {
|
|
82
|
+
reverseData: boolean;
|
|
83
|
+
where: DatabaseBundleQueryWhere;
|
|
84
|
+
orderBy: DatabaseBundleQueryOrder;
|
|
85
|
+
} => {
|
|
86
|
+
const direction = orderBy.direction;
|
|
87
|
+
|
|
88
|
+
if (cursor.after) {
|
|
89
|
+
return {
|
|
90
|
+
reverseData: false,
|
|
91
|
+
where: mergeWhereWithIdFilter(where, {
|
|
92
|
+
[direction === "desc" ? "lt" : "gt"]: cursor.after,
|
|
93
|
+
}),
|
|
94
|
+
orderBy,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (cursor.before) {
|
|
99
|
+
return {
|
|
100
|
+
reverseData: true,
|
|
101
|
+
where: mergeWhereWithIdFilter(where, {
|
|
102
|
+
[direction === "desc" ? "gt" : "lt"]: cursor.before,
|
|
103
|
+
}),
|
|
104
|
+
orderBy: {
|
|
105
|
+
field: orderBy.field,
|
|
106
|
+
direction: direction === "desc" ? "asc" : "desc",
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
reverseData: false,
|
|
113
|
+
where: where ?? {},
|
|
114
|
+
orderBy,
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const buildCountBeforeWhere = (
|
|
119
|
+
where: DatabaseBundleQueryWhere | undefined,
|
|
120
|
+
firstBundleId: string,
|
|
121
|
+
orderBy: DatabaseBundleQueryOrder,
|
|
122
|
+
): DatabaseBundleQueryWhere =>
|
|
123
|
+
mergeWhereWithIdFilter(where, {
|
|
124
|
+
[orderBy.direction === "desc" ? "gt" : "lt"]: firstBundleId,
|
|
125
|
+
});
|
|
126
|
+
|
|
56
127
|
export const HotUpdaterDB = fumadb({
|
|
57
128
|
namespace: "hot_updater",
|
|
58
129
|
schemas,
|
|
@@ -511,7 +582,12 @@ export function createOrmDatabaseCore<TContext = unknown>({
|
|
|
511
582
|
options: DatabaseBundleQueryOptions,
|
|
512
583
|
): Promise<Paginated<Bundle[]>> {
|
|
513
584
|
const orm = await ensureORM();
|
|
514
|
-
const { where, limit
|
|
585
|
+
const { where, limit } = options;
|
|
586
|
+
const orderBy = options.orderBy ?? DEFAULT_BUNDLE_ORDER;
|
|
587
|
+
const offset =
|
|
588
|
+
(("offset" in options ? options.offset : undefined) as
|
|
589
|
+
| number
|
|
590
|
+
| undefined) ?? 0;
|
|
515
591
|
|
|
516
592
|
const total = await orm.count("bundles", {
|
|
517
593
|
where: buildBundleWhere(where),
|
|
@@ -549,49 +625,129 @@ export function createOrmDatabaseCore<TContext = unknown>({
|
|
|
549
625
|
"target_cohorts",
|
|
550
626
|
];
|
|
551
627
|
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
628
|
+
const mapRowsToBundles = (rows: any[]): Bundle[] =>
|
|
629
|
+
rows.map(
|
|
630
|
+
(r): Bundle => ({
|
|
631
|
+
id: r.id,
|
|
632
|
+
platform: r.platform as Platform,
|
|
633
|
+
shouldForceUpdate: Boolean(r.should_force_update),
|
|
634
|
+
enabled: Boolean(r.enabled),
|
|
635
|
+
fileHash: r.file_hash,
|
|
636
|
+
gitCommitHash: r.git_commit_hash ?? null,
|
|
637
|
+
message: r.message ?? null,
|
|
638
|
+
channel: r.channel,
|
|
639
|
+
storageUri: r.storage_uri,
|
|
640
|
+
targetAppVersion: r.target_app_version ?? null,
|
|
641
|
+
fingerprintHash: r.fingerprint_hash ?? null,
|
|
642
|
+
rolloutCohortCount:
|
|
643
|
+
r.rollout_cohort_count ?? DEFAULT_ROLLOUT_COHORT_COUNT,
|
|
644
|
+
targetCohorts: parseTargetCohorts(r.target_cohorts),
|
|
645
|
+
}),
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
const findBundles = async ({
|
|
649
|
+
where,
|
|
650
|
+
orderBy,
|
|
651
|
+
limit,
|
|
652
|
+
offset,
|
|
653
|
+
}: {
|
|
654
|
+
where?: DatabaseBundleQueryWhere;
|
|
655
|
+
orderBy: DatabaseBundleQueryOrder;
|
|
656
|
+
limit: number;
|
|
657
|
+
offset: number;
|
|
658
|
+
}) => {
|
|
659
|
+
const rows = isMongoAdapter
|
|
660
|
+
? (
|
|
661
|
+
await orm.findMany("bundles", {
|
|
662
|
+
select: selectedColumns,
|
|
663
|
+
where: buildBundleWhere(where),
|
|
664
|
+
})
|
|
665
|
+
)
|
|
666
|
+
.sort((a, b) => {
|
|
667
|
+
const result = a.id.localeCompare(b.id);
|
|
668
|
+
return orderBy.direction === "asc" ? result : -result;
|
|
669
|
+
})
|
|
670
|
+
.slice(offset, offset + limit)
|
|
671
|
+
: await orm.findMany("bundles", {
|
|
555
672
|
select: selectedColumns,
|
|
556
673
|
where: buildBundleWhere(where),
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
const result = a.id.localeCompare(b.id);
|
|
562
|
-
return direction === "asc" ? result : -result;
|
|
563
|
-
})
|
|
564
|
-
.slice(offset, offset + limit)
|
|
565
|
-
: await orm.findMany("bundles", {
|
|
566
|
-
select: selectedColumns,
|
|
567
|
-
where: buildBundleWhere(where),
|
|
568
|
-
orderBy: [[orderBy?.field ?? "id", orderBy?.direction ?? "desc"]],
|
|
569
|
-
limit,
|
|
570
|
-
offset,
|
|
571
|
-
});
|
|
674
|
+
orderBy: [[orderBy.field, orderBy.direction]],
|
|
675
|
+
limit,
|
|
676
|
+
offset,
|
|
677
|
+
});
|
|
572
678
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
679
|
+
return mapRowsToBundles(rows);
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
if (!options.cursor?.after && !options.cursor?.before) {
|
|
683
|
+
const data = await findBundles({
|
|
684
|
+
where,
|
|
685
|
+
orderBy,
|
|
686
|
+
limit,
|
|
687
|
+
offset,
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
return {
|
|
691
|
+
data,
|
|
692
|
+
pagination: {
|
|
693
|
+
...calculatePagination(total, { limit, offset }),
|
|
694
|
+
...(data.length > 0 && offset + data.length < total
|
|
695
|
+
? { nextCursor: data.at(-1)?.id }
|
|
696
|
+
: {}),
|
|
697
|
+
...(data.length > 0 && offset > 0
|
|
698
|
+
? { previousCursor: data[0]?.id }
|
|
699
|
+
: {}),
|
|
700
|
+
},
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const {
|
|
705
|
+
where: cursorWhere,
|
|
706
|
+
orderBy: cursorOrderBy,
|
|
707
|
+
reverseData,
|
|
708
|
+
} = buildCursorPageWhere(where, options.cursor, orderBy);
|
|
709
|
+
const cursorPage = await findBundles({
|
|
710
|
+
where: cursorWhere,
|
|
711
|
+
orderBy: cursorOrderBy,
|
|
712
|
+
limit,
|
|
713
|
+
offset: 0,
|
|
714
|
+
});
|
|
715
|
+
const data = reverseData ? cursorPage.slice().reverse() : cursorPage;
|
|
716
|
+
|
|
717
|
+
if (data.length === 0) {
|
|
718
|
+
const emptyStartIndex = options.cursor.after ? total : 0;
|
|
719
|
+
return {
|
|
720
|
+
data,
|
|
721
|
+
pagination: {
|
|
722
|
+
...calculatePagination(total, {
|
|
723
|
+
limit,
|
|
724
|
+
offset: emptyStartIndex,
|
|
725
|
+
}),
|
|
726
|
+
...(options.cursor.after
|
|
727
|
+
? { previousCursor: options.cursor.after }
|
|
728
|
+
: {}),
|
|
729
|
+
...(options.cursor.before
|
|
730
|
+
? { nextCursor: options.cursor.before }
|
|
731
|
+
: {}),
|
|
732
|
+
},
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const startIndex = await orm.count("bundles", {
|
|
737
|
+
where: buildBundleWhere(
|
|
738
|
+
buildCountBeforeWhere(where, data[0]!.id, orderBy),
|
|
739
|
+
),
|
|
740
|
+
});
|
|
591
741
|
|
|
592
742
|
return {
|
|
593
743
|
data,
|
|
594
|
-
pagination:
|
|
744
|
+
pagination: {
|
|
745
|
+
...calculatePagination(total, { limit, offset: startIndex }),
|
|
746
|
+
...(startIndex + data.length < total
|
|
747
|
+
? { nextCursor: data.at(-1)?.id }
|
|
748
|
+
: {}),
|
|
749
|
+
...(startIndex > 0 ? { previousCursor: data[0]?.id } : {}),
|
|
750
|
+
},
|
|
595
751
|
};
|
|
596
752
|
},
|
|
597
753
|
|
|
@@ -82,6 +82,47 @@ describe("createPluginDatabaseCore", () => {
|
|
|
82
82
|
expect(getBundles).not.toHaveBeenCalled();
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
+
it("does not fall back to scanning when plugin getUpdateInfo returns null", async () => {
|
|
86
|
+
const getBundles = vi.fn<DatabasePlugin["getBundles"]>(async () => ({
|
|
87
|
+
data: [baseBundle],
|
|
88
|
+
pagination: {
|
|
89
|
+
currentPage: 1,
|
|
90
|
+
hasNextPage: false,
|
|
91
|
+
hasPreviousPage: false,
|
|
92
|
+
total: 1,
|
|
93
|
+
totalPages: 1,
|
|
94
|
+
},
|
|
95
|
+
}));
|
|
96
|
+
const getUpdateInfo = vi.fn<NonNullable<DatabasePlugin["getUpdateInfo"]>>(
|
|
97
|
+
async () => null,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const plugin: DatabasePlugin = {
|
|
101
|
+
name: "null-fast-path-plugin",
|
|
102
|
+
async appendBundle() {},
|
|
103
|
+
async commitBundle() {},
|
|
104
|
+
async deleteBundle() {},
|
|
105
|
+
async getBundleById() {
|
|
106
|
+
return null;
|
|
107
|
+
},
|
|
108
|
+
getBundles,
|
|
109
|
+
getUpdateInfo,
|
|
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.toBeNull();
|
|
122
|
+
expect(getUpdateInfo).toHaveBeenCalledWith(updateArgs);
|
|
123
|
+
expect(getBundles).not.toHaveBeenCalled();
|
|
124
|
+
});
|
|
125
|
+
|
|
85
126
|
it("falls back to scanning when plugin getUpdateInfo is absent", async () => {
|
|
86
127
|
const latestBundle = {
|
|
87
128
|
...baseBundle,
|
package/src/db/pluginCore.ts
CHANGED
|
@@ -172,15 +172,21 @@ export function createPluginDatabaseCore<TContext = unknown>(
|
|
|
172
172
|
isCandidate: (bundle: Bundle) => boolean;
|
|
173
173
|
context?: HotUpdaterContext<TContext>;
|
|
174
174
|
}): Promise<UpdateInfo | null> => {
|
|
175
|
-
let
|
|
175
|
+
let after: string | undefined;
|
|
176
176
|
|
|
177
177
|
while (true) {
|
|
178
178
|
const { data, pagination } = await getSortedBundlePage(
|
|
179
179
|
{
|
|
180
180
|
where: queryWhere,
|
|
181
181
|
limit: PAGE_SIZE,
|
|
182
|
-
offset,
|
|
183
182
|
orderBy: DESC_ORDER,
|
|
183
|
+
...(after
|
|
184
|
+
? {
|
|
185
|
+
cursor: {
|
|
186
|
+
after,
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
: {}),
|
|
184
190
|
},
|
|
185
191
|
context,
|
|
186
192
|
);
|
|
@@ -223,7 +229,10 @@ export function createPluginDatabaseCore<TContext = unknown>(
|
|
|
223
229
|
break;
|
|
224
230
|
}
|
|
225
231
|
|
|
226
|
-
|
|
232
|
+
after = data.at(-1)?.id;
|
|
233
|
+
if (!after) {
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
227
236
|
}
|
|
228
237
|
|
|
229
238
|
if (args.bundleId === NIL_UUID) {
|
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
DatabasePlugin,
|
|
7
7
|
} from "../../../../plugins/plugin-core/src";
|
|
8
8
|
import {
|
|
9
|
-
|
|
9
|
+
paginateBundles,
|
|
10
10
|
semverSatisfies,
|
|
11
11
|
} from "../../../../plugins/plugin-core/src";
|
|
12
12
|
import type {
|
|
@@ -124,17 +124,19 @@ const createBenchPlugin = (bundles: Bundle[]): DatabasePlugin => {
|
|
|
124
124
|
return bundlesById.get(bundleId) ?? null;
|
|
125
125
|
},
|
|
126
126
|
async getBundles(options: DatabaseBundleQueryOptions) {
|
|
127
|
-
const { where, limit,
|
|
127
|
+
const { where, limit, cursor, orderBy } = options;
|
|
128
128
|
const source = sortByDirection(orderBy?.direction);
|
|
129
129
|
const matched = source.filter((bundle) =>
|
|
130
130
|
bundleMatchesWhere(bundle, where),
|
|
131
131
|
);
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
};
|
|
132
|
+
const paginated = paginateBundles({
|
|
133
|
+
bundles: matched.map(cloneBundle),
|
|
134
|
+
limit,
|
|
135
|
+
cursor,
|
|
136
|
+
orderBy,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return paginated;
|
|
138
140
|
},
|
|
139
141
|
async getChannels() {
|
|
140
142
|
return [...new Set(bundles.map((bundle) => bundle.channel))];
|
|
@@ -164,7 +166,6 @@ const oldPluginCoreGetUpdateInfo = async (
|
|
|
164
166
|
const { pagination } = await plugin.getBundles({
|
|
165
167
|
where,
|
|
166
168
|
limit: 1,
|
|
167
|
-
offset: 0,
|
|
168
169
|
});
|
|
169
170
|
|
|
170
171
|
if (pagination.total === 0) {
|
|
@@ -174,7 +175,6 @@ const oldPluginCoreGetUpdateInfo = async (
|
|
|
174
175
|
const { data } = await plugin.getBundles({
|
|
175
176
|
where,
|
|
176
177
|
limit: pagination.total,
|
|
177
|
-
offset: 0,
|
|
178
178
|
});
|
|
179
179
|
|
|
180
180
|
for (const bundle of data) {
|
|
@@ -234,7 +234,7 @@ describe("Handler <-> Standalone Repository Integration", () => {
|
|
|
234
234
|
})();
|
|
235
235
|
|
|
236
236
|
// Get all bundles
|
|
237
|
-
const result = await repo.getBundles({ limit: 50
|
|
237
|
+
const result = await repo.getBundles({ limit: 50 });
|
|
238
238
|
|
|
239
239
|
expect(result.data).toHaveLength(3);
|
|
240
240
|
expect(result.pagination.total).toBe(3);
|
|
@@ -243,7 +243,6 @@ describe("Handler <-> Standalone Repository Integration", () => {
|
|
|
243
243
|
const prodResult = await repo.getBundles({
|
|
244
244
|
where: { channel: "production" },
|
|
245
245
|
limit: 50,
|
|
246
|
-
offset: 0,
|
|
247
246
|
});
|
|
248
247
|
|
|
249
248
|
expect(prodResult.data).toHaveLength(2);
|
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
|
);
|