Skip to content

Commit e1625e2

Browse files
authored
feat: Add support for getting edge extensions by CRX ID (#7)
1 parent b5b9a07 commit e1625e2

9 files changed

Lines changed: 221 additions & 11 deletions

File tree

cspell.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,8 @@ words:
1111
- importmap
1212
- ocfdgncpifmegplaglcnglhioflaimkd
1313
- grpahql
14+
- libstdc
15+
- libgcc
16+
- adduser
17+
- microsoftedge
18+
- crxid

src/assets/schema.gql

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ type Query {
1111
Returns information about a list of Chrome extension ids.
1212
"""
1313
chromeExtensions(ids: [String!]!): [ChromeExtension]!
14+
1415
"""
1516
Returns information about a Firefox addon based on it's ID/GUID/slug.
1617
"""
@@ -19,6 +20,15 @@ type Query {
1920
Returns information about a list of Firefox addon ID/GUID/slugs.
2021
"""
2122
firefoxAddons(ids: [String!]!): [FirefoxAddon]!
23+
24+
"""
25+
Returns information about an Edge addon based on it's CRX ID.
26+
"""
27+
edgeAddon(id: String!): EdgeAddon
28+
"""
29+
Returns information about a list of Edge addon IDs.
30+
"""
31+
edgeAddons(ids: [String!]!): [EdgeAddon]!
2232
}
2333

2434
interface Extension {
@@ -85,6 +95,29 @@ type FirefoxAddon implements Extension {
8595
dailyActiveUsers: Int!
8696
}
8797

98+
type EdgeAddon implements Extension {
99+
# Extension fields
100+
101+
id: String!
102+
name: String!
103+
iconUrl: String!
104+
storeUrl: String!
105+
shortDescription: String!
106+
longDescription: String!
107+
"Same as activeInstallCount"
108+
users: Int!
109+
version: String!
110+
lastUpdated: String!
111+
rating: Float
112+
reviewCount: Int
113+
screenshots: [Screenshot!]!
114+
115+
# Additional fields
116+
117+
"Number of users shown on `microsoftedge.microsoft.com`"
118+
activeInstallCount: Int!
119+
}
120+
88121
type Screenshot {
89122
"""
90123
The screenshot's order.

src/dependencies.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { createIocContainer } from "@aklinker1/zero-ioc";
22
import { createChromeService } from "./utils/chrome/chrome-service";
33
import { createFirefoxService } from "./utils/firefox/firefox-service";
4+
import { createEdgeService } from "./utils/edge/edge-service";
45

5-
export const dependencies = createIocContainer().register({
6-
chrome: createChromeService,
7-
firefox: createFirefoxService,
8-
});
6+
export const dependencies = createIocContainer()
7+
.register("chrome", createChromeService)
8+
.register("firefox", createFirefoxService)
9+
.register("edge", createEdgeService);

src/graphql/resolvers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@ export const rootResolver: Gql.RootResolver = {
33
chromeExtensions: ({ ids }, ctx) => ctx.chrome.getExtensions(ids),
44
firefoxAddon: ({ id }, ctx) => ctx.firefox.getAddon(id),
55
firefoxAddons: ({ ids }, ctx) => ctx.firefox.getAddons(ids),
6+
edgeAddon: ({ id }, ctx) => ctx.edge.getAddon(id),
7+
edgeAddons: ({ ids }, ctx) => ctx.edge.getAddons(ids),
68
};

src/routes/rest-routes.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const restRoutes = createApp()
99
.get(
1010
"/api/rest/chrome-extensions/:id/screenshots/:index",
1111
{
12-
summary: "Redirect to Screenshot",
12+
operationId: "chromeScreenshotRedirect",
1313
tags: ["Chrome Extensions"],
1414
description:
1515
"Redirect to a screenshot's URL from the Chrome Web Store listing",
@@ -33,7 +33,7 @@ export const restRoutes = createApp()
3333
.get(
3434
"/api/rest/firefox-addons/:addonId/screenshots/:index",
3535
{
36-
summary: "Redirect to Screenshot",
36+
operationId: "firefoxScreenshotRedirect",
3737
tags: ["Firefox Addons"],
3838
description:
3939
"Redirect to a screenshot's URL from the Firefox Addons listing.",
@@ -50,6 +50,30 @@ export const restRoutes = createApp()
5050
if (!screenshotUrl)
5151
throw new NotFoundHttpError("Extension or screenshot not found");
5252

53+
set.status = HttpStatus.Found;
54+
set.headers["Location"] = screenshotUrl;
55+
},
56+
)
57+
.get(
58+
"/api/rest/edge-addons/:addonId/screenshots/:index",
59+
{
60+
operationId: "edgeScreenshotRedirect",
61+
tags: ["Firefox Addons"],
62+
description:
63+
"Redirect to a screenshot's URL from the Edge Addons listing.",
64+
params: z.object({
65+
addonId: z.string(),
66+
index: z.coerce.number().int().min(0),
67+
}),
68+
},
69+
async ({ params, edge, set }) => {
70+
const screenshotUrl = await edge.getScreenshotUrl(
71+
params.addonId,
72+
params.index,
73+
);
74+
if (!screenshotUrl)
75+
throw new NotFoundHttpError("Extension or screenshot not found");
76+
5377
set.status = HttpStatus.Found;
5478
set.headers["Location"] = screenshotUrl;
5579
},

src/utils/edge/edge-api.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { buildScreenshotUrl } from "../urls";
2+
3+
export interface EdgeApi {
4+
getAddon(crxid: string): Promise<Gql.EdgeAddon>;
5+
}
6+
7+
export function createEdgeApi(): EdgeApi {
8+
const toGqlEdgeAddon = (
9+
res: GetProductDetailsByCrxId200Response,
10+
): Gql.EdgeAddon => ({
11+
id: res.crxId,
12+
iconUrl: `https:${res.logoUrl}`, // URL without the schema (ex: "//store-images.s-microsoft.com/image/...")
13+
lastUpdated: new Date(res.lastUpdateDate * 1000).toISOString(),
14+
longDescription: res.description,
15+
shortDescription: res.shortDescription,
16+
name: res.name,
17+
rating: res.averageRating,
18+
reviewCount: res.ratingCount,
19+
version: res.version,
20+
users: res.activeInstallCount,
21+
activeInstallCount: res.activeInstallCount,
22+
storeUrl: `https://microsoftedge.microsoft.com/addons/detail/${res.crxId}`,
23+
screenshots: res.screenshots.map((ss, i) => ({
24+
index: i,
25+
indexUrl: buildScreenshotUrl("edge-addons", res.crxId, i),
26+
rawUrl: `https:${ss.uri}`, // URL without the schema (ex: "//store-images.s-microsoft.com/image/...")
27+
})),
28+
});
29+
30+
const getAddon: EdgeApi["getAddon"] = async (crxid) => {
31+
const res = await fetch(
32+
`https://microsoftedge.microsoft.com/addons/getproductdetailsbycrxid/${crxid}`,
33+
);
34+
if (res.status !== 200) {
35+
throw Error("Edge API request failed", { cause: res });
36+
}
37+
38+
const json = (await res.json()) as GetProductDetailsByCrxId200Response;
39+
40+
return toGqlEdgeAddon(json);
41+
};
42+
43+
return {
44+
getAddon,
45+
};
46+
}
47+
48+
type GetProductDetailsByCrxId200Response = {
49+
availability: string[];
50+
activeInstallCount: number;
51+
storeProductId: string;
52+
name: string;
53+
logoUrl: string;
54+
thumbnailUrl: string;
55+
description: string;
56+
developer: string;
57+
category: string;
58+
isInstalled: boolean;
59+
crxId: string;
60+
manifest: string;
61+
isHavingMatureContent: boolean;
62+
version: string;
63+
lastUpdateDate: number;
64+
privacyUrl: string;
65+
availabilityId: string;
66+
skuId: string;
67+
locale: string;
68+
market: string;
69+
averageRating: number;
70+
ratingCount: number;
71+
availableLanguages: string[];
72+
metadata: {
73+
publisherId: string;
74+
};
75+
shortDescription: string;
76+
searchKeywords: string;
77+
screenshots: Array<{
78+
caption: string;
79+
imagePurpose: string;
80+
uri: string;
81+
}>;
82+
videos: unknown[];
83+
largePromotionImage: {
84+
caption: string;
85+
imagePurpose: string;
86+
uri: string;
87+
};
88+
publisherWebsiteUri: string;
89+
isBadgedAsFeatured: boolean;
90+
privacyData: {
91+
privacyPolicyRequired: boolean;
92+
};
93+
};

src/utils/edge/edge-service.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { createCachedDataLoader } from "../cache";
2+
import { HOUR_MS } from "../time";
3+
import { createEdgeApi } from "./edge-api";
4+
5+
export interface EdgeService {
6+
getAddon: (id: string) => Promise<Gql.EdgeAddon | undefined>;
7+
getAddons: (ids: string[]) => Promise<Array<Gql.EdgeAddon | undefined>>;
8+
getScreenshotUrl: (
9+
addonId: string,
10+
screenshotIndex: number,
11+
) => Promise<string | undefined>;
12+
}
13+
14+
export function createEdgeService(): EdgeService {
15+
const api = createEdgeApi();
16+
17+
const loader = createCachedDataLoader<string, Gql.EdgeAddon | undefined>(
18+
HOUR_MS,
19+
(ids) => Promise.all(ids.map((id) => api.getAddon(id))),
20+
);
21+
22+
const getAddon: EdgeService["getAddon"] = (id) => loader.load(id);
23+
24+
const getAddons: EdgeService["getAddons"] = async (ids) => {
25+
const result = await loader.loadMany(ids);
26+
return result.map((item) => {
27+
if (item == null) return undefined;
28+
if (item instanceof Error) {
29+
console.warn("Error fetching multiple addons:", item);
30+
return undefined;
31+
}
32+
return item;
33+
});
34+
};
35+
36+
const getScreenshotUrl: EdgeService["getScreenshotUrl"] = async (
37+
addonId,
38+
screenshotIndex,
39+
) => {
40+
const addon = await getAddon(addonId);
41+
const screenshot = addon?.screenshots.find(
42+
(screenshot) => screenshot.index == screenshotIndex,
43+
);
44+
return screenshot?.rawUrl;
45+
};
46+
47+
return {
48+
getAddon,
49+
getAddons,
50+
getScreenshotUrl,
51+
};
52+
}

src/utils/firefox/firefox-service.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ export interface FirefoxService {
1818
export function createFirefoxService(): FirefoxService {
1919
const firefox = createFirefoxApiClient();
2020

21-
const loader = createCachedDataLoader<
22-
string | number,
23-
Gql.FirefoxAddon | undefined
24-
>(HOUR_MS, (ids) => Promise.all(ids.map((id) => firefox.getAddon(id))));
21+
const loader = createCachedDataLoader<AddonId, Gql.FirefoxAddon | undefined>(
22+
HOUR_MS,
23+
(ids) => Promise.all(ids.map((id) => firefox.getAddon(id))),
24+
);
2525

2626
const getAddon: FirefoxService["getAddon"] = (addonId) =>
2727
loader.load(addonId);

src/utils/urls.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export const SERVER_ORIGIN =
22
process.env.SERVER_ORIGIN ?? "http://localhost:3000";
33

44
export function buildScreenshotUrl(
5-
base: "chrome-extensions" | "firefox-addons",
5+
base: "chrome-extensions" | "firefox-addons" | "edge-addons",
66
id: string,
77
index: number,
88
) {

0 commit comments

Comments
 (0)