/** * Created by joel on 11/4/2018. */ import { isNothing, isSome } from "@jet/environment/types/optional"; import * as serverData from "../json-parsing/server-data"; import * as urls from "../network/urls"; import * as attributes from "./attributes"; /// this is exposed for compatibility. If you find yourself needing to use this outside of the media api module you /// probably have code smell. DO NOT USE. export function buildURLFromRequest(objectGraph, request) { var _a, _b; const baseURL = request.href && request.href.length > 0 ? baseURLForHref(request.href) : baseURLForResourceType(objectGraph, request.isMixedMediaRequest, request.resourceType, request.countryCodeOverride); const mediaApiURL = new urls.URL(baseURL); if (serverData.isDefinedNonNullNonEmpty(request.resourceType)) { for (const pathComponent of pathComponentsForRequest(request.resourceType, request.targetResourceType)) { mediaApiURL.append("pathname", pathComponent); } } if (request.isMixedMediaRequest) { for (const [resourceType, ids] of request.idsByResourceType.entries()) { mediaApiURL.param(`ids[${resourceType}]`, Array.from(ids).sort().join(",")); } } else if (request.ids.size > 1 || request.useIdsAsQueryParam) { mediaApiURL.param("ids", Array.from(request.ids).sort().join(",")); } else if (request.ids.size === 1) { const id = request.ids.values().next().value; mediaApiURL.append("pathname", id); } if (request.resourceType !== undefined) { const trailingPathComponent = trailingPathComponentForResourceType(request.resourceType); if (serverData.isDefinedNonNullNonEmpty(trailingPathComponent)) { mediaApiURL.append("pathname", trailingPathComponent); } } mediaApiURL.param("platform", (_a = request.platform) !== null && _a !== void 0 ? _a : undefined); if (request.additionalPlatforms.size > 0) { mediaApiURL.param("additionalPlatforms", Array.from(request.additionalPlatforms).sort().join(",")); } /** * Add `extend` attributes. * Note that when `useCustomAttributes` is true, there is `customArtwork` param even when `attributeIncludes` is initially empty. * This due MAPI auto-extend for `artwork`, and lack of auto-extend for `customArtwork` */ if (request.attributeIncludes.size > 0 || request.useCustomAttributes) { let extendAttributes = Array.from(request.attributeIncludes); if (request.useCustomAttributes) { extendAttributes = convertRequestAttributesToCustomAttributes(objectGraph, extendAttributes); } extendAttributes.sort(); mediaApiURL.param("extend", extendAttributes.join(",")); } // Add age restriction if present. if (serverData.isDefinedNonNull(request.ageRestriction) && objectGraph.bag.enableAgeRatingFilter) { mediaApiURL.param("restrict[ageRestriction]", request.ageRestriction.toString()); } // Automatically extend iOS catalog requests for apps to include appBinaryTraits. if (request.includeAppBinaryTraitsAttribute) { request.includingScopedAttributes("apps", ["appBinaryTraits"]); } if (serverData.isDefinedNonNull(request.scopedAttributeIncludes)) { for (const [dataType, scopedIncludes] of request.scopedAttributeIncludes.entries()) { mediaApiURL.param(`extend[${dataType}]`, Array.from(scopedIncludes).sort().join(",")); } } if (request.relationshipIncludes.size > 0) { mediaApiURL.param("include", Array.from(request.relationshipIncludes).sort().join(",")); } if (serverData.isDefinedNonNull(request.scopedRelationshipIncludes)) { for (const [dataType, scopedIncludes] of request.scopedRelationshipIncludes.entries()) { mediaApiURL.param(`include[${dataType}]`, Array.from(scopedIncludes).sort().join(",")); } } if (serverData.isDefinedNonNull(request.metaIncludes)) { for (const [dataType, scopedMeta] of request.metaIncludes.entries()) { mediaApiURL.param(`meta[${dataType}]`, Array.from(scopedMeta).sort().join(",")); } } if (serverData.isSetDefinedNonNullNonEmpty(request.viewsIncludes)) { mediaApiURL.param("views", Array.from(request.viewsIncludes).sort().join(",")); } if (serverData.isDefinedNonNull(request.kindIncludes)) { for (const [dataType, scopedMeta] of request.kindIncludes.entries()) { mediaApiURL.param(`kinds[${dataType}]`, Array.from(scopedMeta).sort().join(",")); } } if (serverData.isDefinedNonNull(request.associateIncludes)) { for (const [dataType, scopedAssociate] of request.associateIncludes.entries()) { mediaApiURL.param(`associate[${dataType}]`, Array.from(scopedAssociate).sort().join(",")); } } if (serverData.isDefinedNonNull(request.scopedAvailableInIncludes)) { for (const [dataType, scopedAvailableIn] of request.scopedAvailableInIncludes.entries()) { mediaApiURL.param(`availableIn[${dataType}]`, Array.from(scopedAvailableIn).sort().join(",")); } } if (serverData.isDefinedNonNullNonEmpty(request.fields)) { let extendedFields = Array.from(request.fields); if (request.useCustomAttributes) { extendedFields = convertRequestFieldsToCustomFields(extendedFields); } request.fields.sort(); mediaApiURL.param("fields", extendedFields.join(",")); } if (serverData.isDefinedNonNull(request.limit) && request.limit > 0) { mediaApiURL.param(`limit`, `${request.limit}`); } if (serverData.isDefinedNonNull(request.sparseLimit)) { mediaApiURL.param(`sparseLimit`, `${request.sparseLimit}`); } if (serverData.isDefinedNonNull(request.scopedSparseLimit)) { for (const [dataType, scopedLimit] of request.scopedSparseLimit.entries()) { mediaApiURL.param(`sparseLimit[${dataType}]`, String(scopedLimit)); } } if (serverData.isDefinedNonNull(request.sparseCount)) { mediaApiURL.param(`sparseCount`, `${request.sparseCount}`); } for (const relationshipID of Object.keys(request.relationshipLimits).sort()) { const limit = request.relationshipLimits[relationshipID]; mediaApiURL.param(`limit[${relationshipID}]`, `${limit}`); } if (serverData.isDefinedNonNullNonEmpty(request.additionalQuery)) { mediaApiURL.append("query", request.additionalQuery); } if (serverData.isDefinedNonNullNonEmpty(request.searchTerm)) { // Search hints shouldn't add `search` to the end of the path name as the correct final path // is `v1/catalog/us/search/suggestions`, which is handled by `trailingPathComponentForResourceType()` // Search hints also shouldn't have the bubble param if (isNothing(request.resourceType) || request.resourceType !== "search-hints") { mediaApiURL.append("pathname", "search"); mediaApiURL.param("bubble[search]", request.searchTypes.join(",")); } mediaApiURL.param("term", request.searchTerm); } if (serverData.isDefinedNonNullNonEmpty(request.enabledFeatures)) { mediaApiURL.param("with", request.enabledFeatures.join(",")); } if (serverData.isDefinedNonNullNonEmpty(request.context)) { mediaApiURL.param("contexts", request.context); } if (serverData.isDefinedNonNullNonEmpty(request.filterType) && serverData.isDefinedNonNullNonEmpty(request.filterValue)) { mediaApiURL.param(`filter[${request.filterType}]`, request.filterValue); } const language = objectGraph.bag.mediaApiLanguage; // Only attach the language query param if: // - there is a language available in the bag, and // - a language has not been manually attached to the request. This is used for special situations to override the language for particular content, // so it should take precedence over the default. if (serverData.isDefinedNonNull(language) && serverData.isNull(request.additionalQuery["l"])) { mediaApiURL.param("l", language); } mediaApiURL.host = (_b = hostForUrl(objectGraph, mediaApiURL, request)) !== null && _b !== void 0 ? _b : undefined; mediaApiURL.protocol = "https"; return mediaApiURL; } /** * Get the media api base url for all requests. * @param objectGraph Current object graph * @param isMixedCatalogRequest Whether the request intends to use mixed catalog * @param type The request resource type * @param overrideCountryCode A country code to override the bag value. * @returns A built base URL string */ function baseURLForResourceType(objectGraph, isMixedCatalogRequest, type, overrideCountryCode) { switch (type) { case "personalization-data": case "reviews": case "app-distribution": return `/v1/${endpointTypeForResourceType(type)}/`; default: const countryCode = isSome(overrideCountryCode) && overrideCountryCode.length > 0 ? overrideCountryCode : objectGraph.bag.mediaCountryCode; const baseURL = `/v1/${endpointTypeForResourceType(type)}/${countryCode}`; return isMixedCatalogRequest ? baseURL : `${baseURL}/`; } } /** * Get the media api base url for all requests that already have an href. * @return {string} */ function baseURLForHref(href) { return href; } function endpointTypeForResourceType(type) { switch (type) { case "apps": case "app-events": case "arcade-apps": case "app-bundles": case "charts": case "contents": case "developers": case "eula": case "in-apps": case "multiple-system-operators": case "user-reviews": case "customers-also-bought-apps-with-download-intent": return "catalog"; case "categories": case "editorial-pages": case "editorial-items": case "editorial-item-groups": case "editorial-elements": case "groupings": case "multiplex": case "multirooms": case "rooms": case "today": case "collections": return "editorial"; case "ratings": return "ratings"; case "personalization-data": case "reviews": return "me"; case "upsellMarketingItem": case "landing": return "engagement"; case "landing:new-protocol": return "recommendations"; case "personal-recommendations": return "recommendations"; case "engagement-data": return "engagement"; case "app-distribution": return "listing"; default: return "catalog"; } } /** * The path component to add for the given resource */ function pathComponentsForRequest(resourceType, targetResourceType) { switch (resourceType) { case "eula": if (targetResourceType === undefined) { return [resourceType]; // Might be modelled better as an error. } else { return [resourceType, targetResourceType]; } case "landing:new-protocol": return []; case "landing": if (targetResourceType === undefined) { return ["search", resourceType]; // Might be modelled better as an error. } else { return ["search", resourceType, targetResourceType]; } case "user-reviews": return ["apps"]; case "reviews": return ["reviews", "apps"]; case "multiplex": return ["multiplex"]; case "upsellMarketingItem": return ["upsell", "marketing-items"]; case "trending-contents": return ["search", resourceType]; case "customers-also-bought-apps-with-download-intent": return ["apps"]; case "searchLanding:see-all": return []; case "search-hints": return []; case "app-distribution": return ["apps"]; default: return [resourceType]; } } /** * Add a component to the end of the path for the given resource */ function trailingPathComponentForResourceType(type) { switch (type) { case "user-reviews": return "reviews"; case "customers-also-bought-apps-with-download-intent": return "view/customers-also-bought-apps-with-download-intent"; case "collections": return "contents"; case "searchLanding:see-all": return "view/see-all"; case "search-hints": return "search/suggestions"; default: return null; } } function hostForUrl(objectGraph, url, request) { var _a; const path = (_a = url.pathname) !== null && _a !== void 0 ? _a : ""; let host = null; if (request.isStorePreviewRequest) { host = objectGraph.bag.mediaPreviewHost; } else if (request.isMediaRealmRequest) { host = objectGraph.bag.mediaRealmHost; } else if (path.includes("search/landing")) { // Special case RFW3: Use bag key "apps-media-api-edge-end-points" for "search/landing" end-point // until we figure out a better way to test the paths const useEdgeForSearchLanding = objectGraph.bag.edgeEndpoints.indexOf("landing") !== -1; host = useEdgeForSearchLanding ? objectGraph.bag.mediaEdgeHost(objectGraph) : objectGraph.bag.mediaHost; } else if (request.resourceType === "app-distribution" && isSome(objectGraph.bag.appDistributionMediaAPIHost)) { host = objectGraph.bag.appDistributionMediaAPIHost; } else if (request.isMixedMediaRequest && objectGraph.bag.mediaAPICatalogMixedShouldUseEdge) { // CatalogMixed endpoint should be routed to edge when the bag is enabled. host = objectGraph.bag.mediaEdgeHost(objectGraph); } else if (objectGraph.bag.edgeEndpoints.map((endpoint) => path.includes(endpoint)).reduce(truthReducer, false)) { if (path.includes("search") && !path.includes("view/see-all")) { host = objectGraph.bag.mediaEdgeSearchHost; } else { host = objectGraph.bag.mediaEdgeHost(objectGraph); } } else { host = objectGraph.bag.mediaHost; } if (serverData.isNull(host)) { host = "api.apps.apple.com"; } return host; } const truthReducer = (accumulator, current) => accumulator || current; /** * Performs a conversion for given attribute to fetch the customAttribute variant of it. * @param objectGraph The object graph * @param attribute Attribute to convert if needed, e.g. `artwork` * @returns `string` attribute that is custom equivalent of `attribute`, or `attribute` unmodified. */ function convertRequestAttributesToCustomAttributes(objectGraph, requestAttributes) { const convertedAttributes = requestAttributes.map((attribute) => { var _a; return (_a = attributes.attributeKeyAsCustomAttributeKey(attribute)) !== null && _a !== void 0 ? _a : attribute; }); /** * `artwork` is an autoincluded resources, so `attributes` usually doesn't contain this explicitly :( * Per MAPI contract, we "autoinclude" `customArtwork` explicitly for requests with custon attributes. */ convertedAttributes.push("customArtwork"); /** * `iconArtwork` is not autoincluded, but we need to ensure it is always requested even alongside its * custom counterpart, `customIconArtwork`. This is because we might be viewing a macOS only app on iOS, * where custom attributes are supported, but not available for macOS apps. */ if (requestAttributes.includes("iconArtwork")) { convertedAttributes.push("iconArtwork"); } /** * `customDeepLink` is always desired as an included resource in case an app decides to use a custom tap destination. * Per MAPI contract, we "autoinclude" `customDeepLink` explicitly for all requests with custom attributes. */ convertedAttributes.push("customDeepLink"); return convertedAttributes; } /** * Performs the conversion for given field value (which may specify `attributes` keys) to customAttribute variant of it. */ function convertRequestFieldsToCustomFields(requestFields) { const convertedFields = requestFields.map((fieldName) => { var _a; return (_a = attributes.attributeKeyAsCustomAttributeKey(fieldName)) !== null && _a !== void 0 ? _a : fieldName; }); // DON'T include `customArtwork` for request `field` conversion. Only specify if `artwork` was initially in `requestFields`. return convertedFields; } //# sourceMappingURL=url-builder.js.map