this repo has no description
1/**
2 * Created by joel on 11/4/2018.
3 */
4import { isNothing, isSome } from "@jet/environment/types/optional";
5import * as serverData from "../json-parsing/server-data";
6import * as urls from "../network/urls";
7import * as attributes from "./attributes";
8/// this is exposed for compatibility. If you find yourself needing to use this outside of the media api module you
9/// probably have code smell. DO NOT USE.
10export function buildURLFromRequest(objectGraph, request) {
11 var _a, _b;
12 const baseURL = request.href && request.href.length > 0
13 ? baseURLForHref(request.href)
14 : baseURLForResourceType(objectGraph, request.isMixedMediaRequest, request.resourceType, request.countryCodeOverride);
15 const mediaApiURL = new urls.URL(baseURL);
16 if (serverData.isDefinedNonNullNonEmpty(request.resourceType)) {
17 for (const pathComponent of pathComponentsForRequest(request.resourceType, request.targetResourceType)) {
18 mediaApiURL.append("pathname", pathComponent);
19 }
20 }
21 if (request.isMixedMediaRequest) {
22 for (const [resourceType, ids] of request.idsByResourceType.entries()) {
23 mediaApiURL.param(`ids[${resourceType}]`, Array.from(ids).sort().join(","));
24 }
25 }
26 else if (request.ids.size > 1 || request.useIdsAsQueryParam) {
27 mediaApiURL.param("ids", Array.from(request.ids).sort().join(","));
28 }
29 else if (request.ids.size === 1) {
30 const id = request.ids.values().next().value;
31 mediaApiURL.append("pathname", id);
32 }
33 if (request.resourceType !== undefined) {
34 const trailingPathComponent = trailingPathComponentForResourceType(request.resourceType);
35 if (serverData.isDefinedNonNullNonEmpty(trailingPathComponent)) {
36 mediaApiURL.append("pathname", trailingPathComponent);
37 }
38 }
39 mediaApiURL.param("platform", (_a = request.platform) !== null && _a !== void 0 ? _a : undefined);
40 if (request.additionalPlatforms.size > 0) {
41 mediaApiURL.param("additionalPlatforms", Array.from(request.additionalPlatforms).sort().join(","));
42 }
43 /**
44 * Add `extend` attributes.
45 * Note that when `useCustomAttributes` is true, there is `customArtwork` param even when `attributeIncludes` is initially empty.
46 * This due MAPI auto-extend for `artwork`, and lack of auto-extend for `customArtwork`
47 */
48 if (request.attributeIncludes.size > 0 || request.useCustomAttributes) {
49 let extendAttributes = Array.from(request.attributeIncludes);
50 if (request.useCustomAttributes) {
51 extendAttributes = convertRequestAttributesToCustomAttributes(objectGraph, extendAttributes);
52 }
53 extendAttributes.sort();
54 mediaApiURL.param("extend", extendAttributes.join(","));
55 }
56 // Add age restriction if present.
57 if (serverData.isDefinedNonNull(request.ageRestriction) && objectGraph.bag.enableAgeRatingFilter) {
58 mediaApiURL.param("restrict[ageRestriction]", request.ageRestriction.toString());
59 }
60 // Automatically extend iOS catalog requests for apps to include appBinaryTraits.
61 if (request.includeAppBinaryTraitsAttribute) {
62 request.includingScopedAttributes("apps", ["appBinaryTraits"]);
63 }
64 if (serverData.isDefinedNonNull(request.scopedAttributeIncludes)) {
65 for (const [dataType, scopedIncludes] of request.scopedAttributeIncludes.entries()) {
66 mediaApiURL.param(`extend[${dataType}]`, Array.from(scopedIncludes).sort().join(","));
67 }
68 }
69 if (request.relationshipIncludes.size > 0) {
70 mediaApiURL.param("include", Array.from(request.relationshipIncludes).sort().join(","));
71 }
72 if (serverData.isDefinedNonNull(request.scopedRelationshipIncludes)) {
73 for (const [dataType, scopedIncludes] of request.scopedRelationshipIncludes.entries()) {
74 mediaApiURL.param(`include[${dataType}]`, Array.from(scopedIncludes).sort().join(","));
75 }
76 }
77 if (serverData.isDefinedNonNull(request.metaIncludes)) {
78 for (const [dataType, scopedMeta] of request.metaIncludes.entries()) {
79 mediaApiURL.param(`meta[${dataType}]`, Array.from(scopedMeta).sort().join(","));
80 }
81 }
82 if (serverData.isSetDefinedNonNullNonEmpty(request.viewsIncludes)) {
83 mediaApiURL.param("views", Array.from(request.viewsIncludes).sort().join(","));
84 }
85 if (serverData.isDefinedNonNull(request.kindIncludes)) {
86 for (const [dataType, scopedMeta] of request.kindIncludes.entries()) {
87 mediaApiURL.param(`kinds[${dataType}]`, Array.from(scopedMeta).sort().join(","));
88 }
89 }
90 if (serverData.isDefinedNonNull(request.associateIncludes)) {
91 for (const [dataType, scopedAssociate] of request.associateIncludes.entries()) {
92 mediaApiURL.param(`associate[${dataType}]`, Array.from(scopedAssociate).sort().join(","));
93 }
94 }
95 if (serverData.isDefinedNonNull(request.scopedAvailableInIncludes)) {
96 for (const [dataType, scopedAvailableIn] of request.scopedAvailableInIncludes.entries()) {
97 mediaApiURL.param(`availableIn[${dataType}]`, Array.from(scopedAvailableIn).sort().join(","));
98 }
99 }
100 if (serverData.isDefinedNonNullNonEmpty(request.fields)) {
101 let extendedFields = Array.from(request.fields);
102 if (request.useCustomAttributes) {
103 extendedFields = convertRequestFieldsToCustomFields(extendedFields);
104 }
105 request.fields.sort();
106 mediaApiURL.param("fields", extendedFields.join(","));
107 }
108 if (serverData.isDefinedNonNull(request.limit) && request.limit > 0) {
109 mediaApiURL.param(`limit`, `${request.limit}`);
110 }
111 if (serverData.isDefinedNonNull(request.sparseLimit)) {
112 mediaApiURL.param(`sparseLimit`, `${request.sparseLimit}`);
113 }
114 if (serverData.isDefinedNonNull(request.scopedSparseLimit)) {
115 for (const [dataType, scopedLimit] of request.scopedSparseLimit.entries()) {
116 mediaApiURL.param(`sparseLimit[${dataType}]`, String(scopedLimit));
117 }
118 }
119 if (serverData.isDefinedNonNull(request.sparseCount)) {
120 mediaApiURL.param(`sparseCount`, `${request.sparseCount}`);
121 }
122 for (const relationshipID of Object.keys(request.relationshipLimits).sort()) {
123 const limit = request.relationshipLimits[relationshipID];
124 mediaApiURL.param(`limit[${relationshipID}]`, `${limit}`);
125 }
126 if (serverData.isDefinedNonNullNonEmpty(request.additionalQuery)) {
127 mediaApiURL.append("query", request.additionalQuery);
128 }
129 if (serverData.isDefinedNonNullNonEmpty(request.searchTerm)) {
130 // Search hints shouldn't add `search` to the end of the path name as the correct final path
131 // is `v1/catalog/us/search/suggestions`, which is handled by `trailingPathComponentForResourceType()`
132 // Search hints also shouldn't have the bubble param
133 if (isNothing(request.resourceType) || request.resourceType !== "search-hints") {
134 mediaApiURL.append("pathname", "search");
135 mediaApiURL.param("bubble[search]", request.searchTypes.join(","));
136 }
137 mediaApiURL.param("term", request.searchTerm);
138 }
139 if (serverData.isDefinedNonNullNonEmpty(request.enabledFeatures)) {
140 mediaApiURL.param("with", request.enabledFeatures.join(","));
141 }
142 if (serverData.isDefinedNonNullNonEmpty(request.context)) {
143 mediaApiURL.param("contexts", request.context);
144 }
145 if (serverData.isDefinedNonNullNonEmpty(request.filterType) &&
146 serverData.isDefinedNonNullNonEmpty(request.filterValue)) {
147 mediaApiURL.param(`filter[${request.filterType}]`, request.filterValue);
148 }
149 const language = objectGraph.bag.mediaApiLanguage;
150 // Only attach the language query param if:
151 // - there is a language available in the bag, and
152 // - a language has not been manually attached to the request. This is used for special situations to override the language for particular content,
153 // so it should take precedence over the default.
154 if (serverData.isDefinedNonNull(language) && serverData.isNull(request.additionalQuery["l"])) {
155 mediaApiURL.param("l", language);
156 }
157 mediaApiURL.host = (_b = hostForUrl(objectGraph, mediaApiURL, request)) !== null && _b !== void 0 ? _b : undefined;
158 mediaApiURL.protocol = "https";
159 return mediaApiURL;
160}
161/**
162 * Get the media api base url for all requests.
163 * @param objectGraph Current object graph
164 * @param isMixedCatalogRequest Whether the request intends to use mixed catalog
165 * @param type The request resource type
166 * @param overrideCountryCode A country code to override the bag value.
167 * @returns A built base URL string
168 */
169function baseURLForResourceType(objectGraph, isMixedCatalogRequest, type, overrideCountryCode) {
170 switch (type) {
171 case "personalization-data":
172 case "reviews":
173 case "app-distribution":
174 return `/v1/${endpointTypeForResourceType(type)}/`;
175 default:
176 const countryCode = isSome(overrideCountryCode) && overrideCountryCode.length > 0
177 ? overrideCountryCode
178 : objectGraph.bag.mediaCountryCode;
179 const baseURL = `/v1/${endpointTypeForResourceType(type)}/${countryCode}`;
180 return isMixedCatalogRequest ? baseURL : `${baseURL}/`;
181 }
182}
183/**
184 * Get the media api base url for all requests that already have an href.
185 * @return {string}
186 */
187function baseURLForHref(href) {
188 return href;
189}
190function endpointTypeForResourceType(type) {
191 switch (type) {
192 case "apps":
193 case "app-events":
194 case "arcade-apps":
195 case "app-bundles":
196 case "charts":
197 case "contents":
198 case "developers":
199 case "eula":
200 case "in-apps":
201 case "multiple-system-operators":
202 case "user-reviews":
203 case "customers-also-bought-apps-with-download-intent":
204 return "catalog";
205 case "categories":
206 case "editorial-pages":
207 case "editorial-items":
208 case "editorial-item-groups":
209 case "editorial-elements":
210 case "groupings":
211 case "multiplex":
212 case "multirooms":
213 case "rooms":
214 case "today":
215 case "collections":
216 return "editorial";
217 case "ratings":
218 return "ratings";
219 case "personalization-data":
220 case "reviews":
221 return "me";
222 case "upsellMarketingItem":
223 case "landing":
224 return "engagement";
225 case "landing:new-protocol":
226 return "recommendations";
227 case "personal-recommendations":
228 return "recommendations";
229 case "engagement-data":
230 return "engagement";
231 case "app-distribution":
232 return "listing";
233 default:
234 return "catalog";
235 }
236}
237/**
238 * The path component to add for the given resource
239 */
240function pathComponentsForRequest(resourceType, targetResourceType) {
241 switch (resourceType) {
242 case "eula":
243 if (targetResourceType === undefined) {
244 return [resourceType]; // Might be modelled better as an error.
245 }
246 else {
247 return [resourceType, targetResourceType];
248 }
249 case "landing:new-protocol":
250 return [];
251 case "landing":
252 if (targetResourceType === undefined) {
253 return ["search", resourceType]; // Might be modelled better as an error.
254 }
255 else {
256 return ["search", resourceType, targetResourceType];
257 }
258 case "user-reviews":
259 return ["apps"];
260 case "reviews":
261 return ["reviews", "apps"];
262 case "multiplex":
263 return ["multiplex"];
264 case "upsellMarketingItem":
265 return ["upsell", "marketing-items"];
266 case "trending-contents":
267 return ["search", resourceType];
268 case "customers-also-bought-apps-with-download-intent":
269 return ["apps"];
270 case "searchLanding:see-all":
271 return [];
272 case "search-hints":
273 return [];
274 case "app-distribution":
275 return ["apps"];
276 default:
277 return [resourceType];
278 }
279}
280/**
281 * Add a component to the end of the path for the given resource
282 */
283function trailingPathComponentForResourceType(type) {
284 switch (type) {
285 case "user-reviews":
286 return "reviews";
287 case "customers-also-bought-apps-with-download-intent":
288 return "view/customers-also-bought-apps-with-download-intent";
289 case "collections":
290 return "contents";
291 case "searchLanding:see-all":
292 return "view/see-all";
293 case "search-hints":
294 return "search/suggestions";
295 default:
296 return null;
297 }
298}
299function hostForUrl(objectGraph, url, request) {
300 var _a;
301 const path = (_a = url.pathname) !== null && _a !== void 0 ? _a : "";
302 let host = null;
303 if (request.isStorePreviewRequest) {
304 host = objectGraph.bag.mediaPreviewHost;
305 }
306 else if (request.isMediaRealmRequest) {
307 host = objectGraph.bag.mediaRealmHost;
308 }
309 else if (path.includes("search/landing")) {
310 // Special case <rdar://problem/50185140> RFW3: Use bag key "apps-media-api-edge-end-points" for "search/landing" end-point
311 // until we figure out a better way to test the paths
312 const useEdgeForSearchLanding = objectGraph.bag.edgeEndpoints.indexOf("landing") !== -1;
313 host = useEdgeForSearchLanding ? objectGraph.bag.mediaEdgeHost(objectGraph) : objectGraph.bag.mediaHost;
314 }
315 else if (request.resourceType === "app-distribution" && isSome(objectGraph.bag.appDistributionMediaAPIHost)) {
316 host = objectGraph.bag.appDistributionMediaAPIHost;
317 }
318 else if (request.isMixedMediaRequest && objectGraph.bag.mediaAPICatalogMixedShouldUseEdge) {
319 // CatalogMixed endpoint should be routed to edge when the bag is enabled.
320 host = objectGraph.bag.mediaEdgeHost(objectGraph);
321 }
322 else if (objectGraph.bag.edgeEndpoints.map((endpoint) => path.includes(endpoint)).reduce(truthReducer, false)) {
323 if (path.includes("search") && !path.includes("view/see-all")) {
324 host = objectGraph.bag.mediaEdgeSearchHost;
325 }
326 else {
327 host = objectGraph.bag.mediaEdgeHost(objectGraph);
328 }
329 }
330 else {
331 host = objectGraph.bag.mediaHost;
332 }
333 if (serverData.isNull(host)) {
334 host = "api.apps.apple.com";
335 }
336 return host;
337}
338const truthReducer = (accumulator, current) => accumulator || current;
339/**
340 * Performs a conversion for given attribute to fetch the customAttribute variant of it.
341 * @param objectGraph The object graph
342 * @param attribute Attribute to convert if needed, e.g. `artwork`
343 * @returns `string` attribute that is custom equivalent of `attribute`, or `attribute` unmodified.
344 */
345function convertRequestAttributesToCustomAttributes(objectGraph, requestAttributes) {
346 const convertedAttributes = requestAttributes.map((attribute) => {
347 var _a;
348 return (_a = attributes.attributeKeyAsCustomAttributeKey(attribute)) !== null && _a !== void 0 ? _a : attribute;
349 });
350 /**
351 * `artwork` is an autoincluded resources, so `attributes` usually doesn't contain this explicitly :(
352 * Per MAPI contract, we "autoinclude" `customArtwork` explicitly for requests with custon attributes.
353 */
354 convertedAttributes.push("customArtwork");
355 /**
356 * `iconArtwork` is not autoincluded, but we need to ensure it is always requested even alongside its
357 * custom counterpart, `customIconArtwork`. This is because we might be viewing a macOS only app on iOS,
358 * where custom attributes are supported, but not available for macOS apps.
359 */
360 if (requestAttributes.includes("iconArtwork")) {
361 convertedAttributes.push("iconArtwork");
362 }
363 /**
364 * `customDeepLink` is always desired as an included resource in case an app decides to use a custom tap destination.
365 * Per MAPI contract, we "autoinclude" `customDeepLink` explicitly for all requests with custom attributes.
366 */
367 convertedAttributes.push("customDeepLink");
368 return convertedAttributes;
369}
370/**
371 * Performs the conversion for given field value (which may specify `attributes` keys) to customAttribute variant of it.
372 */
373function convertRequestFieldsToCustomFields(requestFields) {
374 const convertedFields = requestFields.map((fieldName) => {
375 var _a;
376 return (_a = attributes.attributeKeyAsCustomAttributeKey(fieldName)) !== null && _a !== void 0 ? _a : fieldName;
377 });
378 // DON'T include `customArtwork` for request `field` conversion. Only specify if `artwork` was initially in `requestFields`.
379 return convertedFields;
380}
381//# sourceMappingURL=url-builder.js.map