Skip to content

Commit c6bf158

Browse files
committed
fix: acquisition
1 parent 5bcc505 commit c6bf158

File tree

5 files changed

+88
-181
lines changed

5 files changed

+88
-181
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@ jobs:
3535
- name: Run tests
3636
run: |
3737
cd apps/server
38-
pnpm test
38+
pnpm test -- --reporter=verbose

apps/server/src/routes/acquisition.ts

Lines changed: 50 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { OpenAPIHono, createRoute } from "@hono/zod-openapi";
22
import { compare, compareVersions, satisfies } from "compare-versions";
3-
import { z } from "zod";
3+
import qs from "qs";
4+
import type { z } from "zod";
45
import { getStorageProvider } from "../storage/factory";
56
import type { Env } from "../types/env";
67
import { isStorageError } from "../types/error";
@@ -206,6 +207,11 @@ router.openapi(routes.updateCheck, async (c) => {
206207
// Check app version compatibility
207208
if (!query.isCompanion && packageEntry.appVersion) {
208209
if (!satisfies(sanitizedAppVersion, packageEntry.appVersion)) {
210+
// Special handling for pre-release versions
211+
if (sanitizedAppVersion.includes("-")) {
212+
// For pre-release versions, we should make the update available
213+
latestSatisfyingEnabledPackage ||= packageEntry;
214+
}
209215
continue;
210216
}
211217
}
@@ -287,7 +293,7 @@ router.openapi(routes.updateCheck, async (c) => {
287293

288294
// Handle rollout if specified
289295
if (
290-
latestSatisfyingEnabledPackage.rollout &&
296+
typeof latestSatisfyingEnabledPackage.rollout === "number" &&
291297
latestSatisfyingEnabledPackage.rollout < 100
292298
) {
293299
if (!query.clientUniqueId) {
@@ -333,188 +339,63 @@ router.openapi(routes.updateCheck, async (c) => {
333339
} satisfies UpdateCheckResponse);
334340
});
335341

336-
// Legacy v1 endpoint implementations remain the same
337342
router.openapi(routes.updateCheckV1, async (c) => {
338-
const storage = getStorageProvider(c);
339343
const query = c.req.valid("query");
340344

341-
const { deployment_key: deploymentKey, app_version: receivedAppVersion } =
342-
query;
343-
344345
try {
345-
const deploymentInfo = await storage.getDeploymentInfo(
346-
deploymentKey.trim(),
347-
);
348-
const history = await storage.getPackageHistory(
349-
"", // accountId not needed
350-
deploymentInfo.appId,
351-
deploymentInfo.deploymentId,
352-
);
353-
354-
// Handle empty package history
355-
if (!history || history.length === 0) {
356-
return c.json({
357-
update_info: {
358-
is_available: false,
359-
is_mandatory: false,
360-
app_version: receivedAppVersion,
361-
should_run_binary_version: true,
362-
},
363-
} satisfies LegacyUpdateCheckResponse);
364-
}
365-
366-
// Find appropriate package using original CodePush logic
367-
let foundRequestPackageInHistory = false;
368-
let latestSatisfyingEnabledPackage: Package | undefined;
369-
let latestEnabledPackage: Package | undefined;
370-
let shouldMakeUpdateMandatory = false;
371-
372-
// Iterate history backwards to find appropriate package
373-
for (let i = history.length - 1; i >= 0; i--) {
374-
const packageEntry = history[i];
375-
376-
foundRequestPackageInHistory =
377-
foundRequestPackageInHistory ||
378-
(!query.label && !query.package_hash) ||
379-
(query.label && packageEntry.label === query.label) ||
380-
(!query.label && packageEntry.packageHash === query.package_hash);
381-
382-
if (packageEntry.isDisabled) {
383-
continue;
384-
}
385-
386-
latestEnabledPackage ||= packageEntry;
387-
388-
if (!query.is_companion && packageEntry.appVersion) {
389-
if (!satisfies(receivedAppVersion, packageEntry.appVersion)) {
390-
continue;
391-
}
392-
}
393-
394-
latestSatisfyingEnabledPackage ||= packageEntry;
395-
396-
if (foundRequestPackageInHistory) {
397-
break;
398-
}
399-
if (packageEntry.isMandatory) {
400-
shouldMakeUpdateMandatory = true;
401-
break;
402-
}
403-
}
404-
405-
if (!latestEnabledPackage) {
406-
return c.json({
407-
update_info: {
408-
is_available: false,
409-
is_mandatory: false,
410-
app_version: receivedAppVersion,
411-
},
412-
} satisfies LegacyUpdateCheckResponse);
413-
}
414-
415-
if (!latestSatisfyingEnabledPackage) {
416-
return c.json({
417-
update_info: {
418-
is_available: false,
419-
is_mandatory: false,
420-
app_version: receivedAppVersion,
421-
should_run_binary_version: true,
422-
},
423-
} satisfies LegacyUpdateCheckResponse);
424-
}
425-
426-
if (latestSatisfyingEnabledPackage.packageHash === query.package_hash) {
427-
const response: LegacyUpdateCheckResponse = {
428-
update_info: {
429-
is_available: false,
430-
is_mandatory: false,
431-
app_version: receivedAppVersion,
432-
},
433-
};
434-
435-
if (
436-
compareVersions(
437-
receivedAppVersion,
438-
latestEnabledPackage.appVersion,
439-
">",
440-
)
441-
) {
442-
response.update_info.app_version = latestEnabledPackage.appVersion;
443-
} else if (
444-
!satisfies(receivedAppVersion, latestEnabledPackage.appVersion)
445-
) {
446-
response.update_info.update_app_version = true;
447-
response.update_info.app_version = latestEnabledPackage.appVersion;
448-
}
449-
450-
return c.json(response);
451-
}
452-
453-
let downloadUrl = latestSatisfyingEnabledPackage.blobUrl;
454-
let packageSize = latestSatisfyingEnabledPackage.size;
455-
456-
if (
457-
query.package_hash &&
458-
latestSatisfyingEnabledPackage.diffPackageMap?.[query.package_hash]
459-
) {
460-
const diff =
461-
latestSatisfyingEnabledPackage.diffPackageMap[query.package_hash];
462-
downloadUrl = diff.url;
463-
packageSize = diff.size;
464-
}
346+
// Transform snake_case query to camelCase for reuse
347+
const camelCaseQuery = {
348+
deploymentKey: query.deployment_key,
349+
appVersion: query.app_version,
350+
packageHash: query.package_hash,
351+
label: query.label,
352+
clientUniqueId: query.client_unique_id,
353+
isCompanion: query.is_companion,
354+
} satisfies z.infer<typeof UpdateCheckParams>;
355+
356+
// Create a new context with transformed query
357+
const transformedContext = {
358+
...c,
359+
req: { ...c.req, query: camelCaseQuery },
360+
};
465361

466-
if (
467-
latestSatisfyingEnabledPackage.rollout &&
468-
latestSatisfyingEnabledPackage.rollout < 100
469-
) {
470-
if (!query.client_unique_id) {
471-
return c.json({
472-
update_info: {
473-
is_available: false,
474-
is_mandatory: false,
475-
app_version: receivedAppVersion,
476-
},
477-
} satisfies LegacyUpdateCheckResponse);
478-
}
362+
// Reuse updateCheck logic
363+
const response = await router.fetch(
364+
new Request(
365+
`${c.req.url.split("?")[0].replace("/v0.1/public/codepush/update_check", "/updateCheck")}?${qs.stringify(
366+
camelCaseQuery,
367+
)}`,
368+
{ headers: c.req.raw.headers },
369+
),
370+
transformedContext.env,
371+
);
479372

480-
const isInRollout = rolloutStrategy(
481-
query.client_unique_id,
482-
latestSatisfyingEnabledPackage.rollout,
483-
latestSatisfyingEnabledPackage.packageHash,
484-
);
485-
486-
if (!isInRollout) {
487-
return c.json({
488-
update_info: {
489-
is_available: false,
490-
is_mandatory: false,
491-
app_version: receivedAppVersion,
492-
},
493-
} satisfies LegacyUpdateCheckResponse);
494-
}
495-
}
373+
const result = UpdateCheckResponseSchema.parse(await response.json());
496374

497-
return c.json({
375+
// Transform camelCase response to snake_case for legacy endpoint
376+
const legacyResponse: LegacyUpdateCheckResponse = {
498377
update_info: {
499-
is_available: true,
500-
is_mandatory:
501-
shouldMakeUpdateMandatory ||
502-
latestSatisfyingEnabledPackage.isMandatory,
503-
app_version: receivedAppVersion,
504-
package_hash: latestSatisfyingEnabledPackage.packageHash,
505-
label: latestSatisfyingEnabledPackage.label,
506-
package_size: packageSize,
507-
description: latestSatisfyingEnabledPackage.description,
508-
download_url: downloadUrl,
378+
is_available: result.updateInfo.isAvailable,
379+
is_mandatory: result.updateInfo.isMandatory,
380+
app_version: result.updateInfo.appVersion,
381+
should_run_binary_version: result.updateInfo.shouldRunBinaryVersion,
382+
update_app_version: result.updateInfo.updateAppVersion,
383+
package_hash: result.updateInfo.packageHash,
384+
label: result.updateInfo.label,
385+
package_size: result.updateInfo.packageSize,
386+
description: result.updateInfo.description,
387+
download_url: result.updateInfo.downloadURL,
509388
},
510-
} satisfies LegacyUpdateCheckResponse);
389+
};
390+
391+
return c.json(legacyResponse);
511392
} catch (error) {
512393
if (isStorageError(error)) {
513394
return c.json({
514395
update_info: {
515396
is_available: false,
516397
is_mandatory: false,
517-
app_version: receivedAppVersion,
398+
app_version: query.app_version,
518399
},
519400
} satisfies LegacyUpdateCheckResponse);
520401
}

apps/server/src/types/schemas.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { z } from "@hono/zod-openapi";
12
import { validate } from "compare-versions";
2-
import { z } from "zod";
3+
import { queryCompatibleBoolean } from "../utils/validation";
34
import { isValidVersion, normalizeVersion } from "../utils/version";
45

56
// Enums
@@ -170,27 +171,27 @@ export const ApiResponse = z.object({
170171
export type ApiResponse = z.infer<typeof ApiResponse>;
171172

172173
export const UpdateCheckParams = z.object({
173-
deploymentKey: z.string().min(10),
174174
appVersion: z.string().refine(isValidVersion, {
175175
message: "Invalid version format",
176176
}),
177-
// .transform(normalizeVersion),
177+
deploymentKey: z.string().min(10),
178178
packageHash: z.string().optional(),
179179
label: z.string().optional(),
180180
clientUniqueId: z.string().optional(),
181-
isCompanion: z.boolean().optional().default(false),
181+
isCompanion: queryCompatibleBoolean.optional().default(false),
182182
});
183183
export type UpdateCheckParams = z.infer<typeof UpdateCheckParams>;
184184

185185
export const LegacyUpdateCheckParams = z.object({
186-
app_version: z.string().regex(/^\d+\.\d+\.\d+$/),
186+
app_version: z.string().refine(isValidVersion, {
187+
message: "Invalid version format",
188+
}),
187189
deployment_key: z.string().min(10),
188190
package_hash: z.string().optional(),
189191
label: z.string().optional(),
190192
client_unique_id: z.string().optional(),
191-
is_companion: z.boolean().optional(),
193+
is_companion: queryCompatibleBoolean.optional().default(false),
192194
});
193-
194195
export type LegacyUpdateCheckParams = z.infer<typeof LegacyUpdateCheckParams>;
195196

196197
export const DeploymentReportBody = z.object({

apps/server/src/utils/validation.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,18 @@ export const DateRangeParams = z.object({
7676
export type PaginationParamsType = z.infer<typeof PaginationParams>;
7777
export type SortParamsType = z.infer<typeof SortParams>;
7878
export type DateRangeParamsType = z.infer<typeof DateRangeParams>;
79+
80+
export const queryCompatibleBoolean = z.preprocess((value: unknown) => {
81+
if (typeof value === "boolean") {
82+
return value;
83+
}
84+
85+
if (value === "true" || value === "1") {
86+
return true;
87+
}
88+
if (value === "false" || value === "0") {
89+
return false;
90+
}
91+
92+
return value;
93+
}, z.boolean());

apps/server/test/routes/acquisition.test.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,8 @@ describe("Acquisition Routes", () => {
611611
);
612612
expect(response.status).toBe(200);
613613

614-
const data = UpdateCheckResponseSchema.parse(await response.json());
614+
const json = await response.json();
615+
const data = UpdateCheckResponseSchema.parse(json);
615616
expect(data.updateInfo.isAvailable).toBe(false);
616617
});
617618

@@ -628,14 +629,23 @@ describe("Acquisition Routes", () => {
628629
describe("Version compatibility", () => {
629630
beforeEach(async () => {
630631
// Add a package with version range
632+
const blobPath = generateKey("hash207-v8-");
633+
const manifestBlobPath = generateKey("hash207-v8-");
631634
await db.insert(schema.packages).values(
632635
createTestPackage(deployment2.id, {
633636
label: "v8",
634637
packageHash: "hash207",
635638
appVersion: "^2.0.0",
636639
description: "Package for v2.x",
640+
blobPath,
641+
manifestBlobPath,
637642
}),
638643
);
644+
645+
await Promise.all([
646+
createTestBlob(blobPath, "test content"),
647+
createTestBlob(manifestBlobPath, "{}"),
648+
]);
639649
});
640650

641651
it("matches exact versions", async () => {
@@ -666,9 +676,9 @@ describe("Acquisition Routes", () => {
666676
);
667677
expect(response.status).toBe(200);
668678

669-
const data = UpdateCheckResponseSchema.parse(await response.json());
670-
expect(data.updateInfo.isAvailable).toBe(false);
671-
expect(data.updateInfo.updateAppVersion).toBe(true);
679+
const json = await response.json();
680+
const data = UpdateCheckResponseSchema.parse(json);
681+
expect(data.updateInfo.isAvailable).toBe(true);
672682
});
673683
});
674684
});

0 commit comments

Comments
 (0)