From bce557cc2dc767628bed6aac87301a1be7c5431b Mon Sep 17 00:00:00 2001 From: rxliuli Date: Tue, 4 Nov 2025 05:03:50 +0800 Subject: init commit --- .../tmp/src/foundation/media/associations.js | 17 + .../tmp/src/foundation/media/attributes.js | 149 +++++ .../tmp/src/foundation/media/data-fetching.js | 631 +++++++++++++++++++++ .../tmp/src/foundation/media/data-structure.js | 84 +++ .../app-store/tmp/src/foundation/media/network.js | 304 ++++++++++ .../src/foundation/media/platform-attributes.js | 143 +++++ .../tmp/src/foundation/media/relationships.js | 43 ++ .../tmp/src/foundation/media/url-builder.js | 381 +++++++++++++ .../app-store/tmp/src/foundation/media/util.js | 185 ++++++ 9 files changed, 1937 insertions(+) create mode 100644 node_modules/@jet-app/app-store/tmp/src/foundation/media/associations.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/foundation/media/attributes.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/foundation/media/data-fetching.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/foundation/media/data-structure.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/foundation/media/network.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/foundation/media/platform-attributes.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/foundation/media/relationships.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/foundation/media/url-builder.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/foundation/media/util.js (limited to 'node_modules/@jet-app/app-store/tmp/src/foundation/media') diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/media/associations.js b/node_modules/@jet-app/app-store/tmp/src/foundation/media/associations.js new file mode 100644 index 0000000..7e2b088 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/media/associations.js @@ -0,0 +1,17 @@ +import * as serverData from "../json-parsing/server-data"; +/** + * @param data The media api data to find teh card in + * @returns The editorial-card data for this media api item, this will return the first associated card + */ +export function editorialCardsFromData(data) { + const editorialCards = serverData.asArrayOrEmpty(data, "meta.associations.editorial-cards.data"); + return editorialCards; +} +/** + * @param data The media api data to find teh card in + * @returns The editorial-card data for this media api item, this will return the first associated card + */ +export function editorialCardFromData(data) { + return serverData.asInterface(editorialCardsFromData(data)[0]); +} +//# sourceMappingURL=associations.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/media/attributes.js b/node_modules/@jet-app/app-store/tmp/src/foundation/media/attributes.js new file mode 100644 index 0000000..992a56e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/media/attributes.js @@ -0,0 +1,149 @@ +import { isNothing } from "@jet/environment/types/optional"; +import * as serverData from "../json-parsing/server-data"; +// region Generic Attribute retrieval +// region Attribute retrieval +/** + * Retrieve the specified attribute from the data, coercing it to a JSONData dictionary + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The path of the attribute. + * @param defaultValue The object to return if the path search fails. + * @returns The dictionary of data + */ +export function attributeAsDictionary(data, attributePath, defaultValue) { + if (isNothing(data)) { + return null; + } + return serverData.asDictionary(data.attributes, attributePath, defaultValue); +} +/** + * Retrieve the specified attribute from the data, coercing it to an Interface + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The path of the attribute. + * @param defaultValue The object to return if the path search fails. + * @returns The dictionary of data as an interface + */ +export function attributeAsInterface(data, attributePath, defaultValue) { + return attributeAsDictionary(data, attributePath, defaultValue); +} +/** + * Retrieve the specified attribute from the data as an array, coercing to a JSONValue array + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The path of the attribute. + * @returns {any[]} The attribute value as an array. + */ +export function attributeAsArray(data, attributePath) { + if (serverData.isNull(data)) { + return []; + } + return serverData.asArray(data.attributes, attributePath); +} +/** + * Retrieve the specified attribute from the data as an array, coercing to an empty array if the object is not an array. + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The path of the attribute. + * @returns {any[]} The attribute value as an array. + */ +export function attributeAsArrayOrEmpty(data, attributePath) { + var _a; + return (_a = attributeAsArray(data, attributePath)) !== null && _a !== void 0 ? _a : []; +} +/** + * Retrieve the specified attribute from the data as a string. + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The object path for the attribute. + * @param policy The validation policy to use when resolving this value. + * @returns {string} The attribute value as a string. + */ +export function attributeAsString(data, attributePath, policy = "coercible") { + if (serverData.isNull(data)) { + return null; + } + return serverData.asString(data.attributes, attributePath, policy); +} +/** + * Retrieve the specified attribute from the data as a boolean. + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The path of the attribute. + * @param policy The validation policy to use when resolving this value. + * @returns {boolean} The attribute value as a boolean. + */ +export function attributeAsBoolean(data, attributePath, policy = "coercible") { + if (serverData.isNull(data)) { + return null; + } + return serverData.asBoolean(data.attributes, attributePath, policy); +} +/** + * Retrieve the specified attribute from the data as a boolean, which will be `false` if the attribute does not exist. + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The path of the attribute. + * @returns {boolean} The attribute value as a boolean, coercing to `false` if the value is not present.. + */ +export function attributeAsBooleanOrFalse(data, attributePath) { + if (serverData.isNull(data)) { + return false; + } + return serverData.asBooleanOrFalse(data.attributes, attributePath); +} +/** + * Retrieve the specified attribute from the data as a number. + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The path of the attribute. + * @param policy The validation policy to use when resolving this value. + * @returns {boolean} The attribute value as a number. + */ +export function attributeAsNumber(data, attributePath, policy = "coercible") { + if (serverData.isNull(data)) { + return null; + } + return serverData.asNumber(data.attributes, attributePath, policy); +} +export function hasAttributes(data) { + return !serverData.isNull(serverData.asDictionary(data, "attributes")); +} +/** + * The canonical way to detect if an item from Media API is hydrated or not. + * + * @param data The data from which to retrieve the attributes. + */ +export function isNotHydrated(data) { + return !hasAttributes(data); +} +// region Custom Attributes +/** + * Performs conversion for a custom variant of given attribute, if any are available. + * @param attribute Attribute to get custom attribute key for, if any. + */ +export function attributeKeyAsCustomAttributeKey(attribute) { + return customAttributeMapping[attribute]; +} +/** + * Whether or not given custom attributes key allows fallback to default page with AB testing treatment within a nondefault page. + * This is to allow AB testing to affect only icons within custom product pages. + */ +export function attributeAllowsNonDefaultTreatmentInNonDefaultPage(customAttribute) { + return customAttribute === "customArtwork" || customAttribute === "customIconArtwork"; // Only the icon artwork. +} +/** + * Defines mapping of attribute to custom attribute. + */ +const customAttributeMapping = { + artwork: "customArtwork", + iconArtwork: "customIconArtwork", + screenshotsByType: "customScreenshotsByType", + promotionalText: "customPromotionalText", + videoPreviewsByType: "customVideoPreviewsByType", + customScreenshotsByTypeForAd: "customScreenshotsByTypeForAd", + customVideoPreviewsByTypeForAd: "customVideoPreviewsByTypeForAd", + customDeepLink: "customDeepLink", +}; +// endregion +//# sourceMappingURL=attributes.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/media/data-fetching.js b/node_modules/@jet-app/app-store/tmp/src/foundation/media/data-fetching.js new file mode 100644 index 0000000..445f42f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/media/data-fetching.js @@ -0,0 +1,631 @@ +import { isNothing, isSome } from "@jet/environment/types/optional"; +import * as serverData from "../json-parsing/server-data"; +import * as client from "../wrappers/client"; +export function allPlatforms(objectGraph, platformsToExclude) { + const platforms = new Set(); + platforms.add("iphone"); + platforms.add("ipad"); + platforms.add("appletv"); + platforms.add("mac"); + platforms.add("watch"); + if (objectGraph.client.isVision || objectGraph.bag.enableVisionPlatform) { + platforms.add("realityDevice"); + } + if (isSome(platformsToExclude)) { + for (const platform of platformsToExclude) { + platforms.delete(platform); + } + } + return Array.from(platforms); +} +export function defaultPlatformForClient(objectGraph) { + if (objectGraph.client.isCompanionVisionApp) { + // The Vision companion app always prefers visionOS assets + return "realityDevice"; + } + switch (objectGraph.client.deviceType) { + case "phone": + return "iphone"; + case "pad": + return "ipad"; + case "tv": + return "appletv"; + case "mac": + return "mac"; + case "watch": + return "watch"; + case "vision": + return "realityDevice"; + case "web": + return "web"; + default: + return null; + } +} +/** + * Returns the layout size controlling the number of rows to display. + * @param {AppStoreObjectGraph} objectGraph Object graph to get the client device type. + * @returns {number} Layout size. + */ +export function defaultLayoutSize(objectGraph) { + return objectGraph.client.isPhone ? 2 : 1; +} +/** + * Returns the sparse count controlling the number of shelves to hydrate. + * @param {AppStoreObjectGraph} objectGraph Object graph to get the client device type. + * @returns {number} Sparse count. + */ +export function defaultSparseCountForClient(objectGraph) { + switch (objectGraph.client.deviceType) { + case "phone": + return 4; + case "pad": + return 5; + case "tv": + return 6; + case "mac": + return 5; + case "watch": + return 10; + default: + return null; + } +} +/** + * Returns the sparse limit controlling the number of items to hydrate per shelve. + * @param {AppStoreObjectGraph} objectGraph Object graph to get the client device type. + * @returns {number} Sparse limit. + */ +export function defaultSparseLimitForClient(objectGraph) { + switch (objectGraph.client.deviceType) { + case "phone": + return 9; + case "pad": + return 12; + case "tv": + return 15; + case "mac": + return 15; + case "watch": + return 3; + case "web": + return 12; + default: + return null; + } +} +/** + * Get the list of all platforms excluding the current platform + * @returns {Platform[]} + */ +export function defaultAdditionalPlatformsForClient(objectGraph) { + switch (objectGraph.host.clientIdentifier) { + case "com.apple.TVAppStore.AppStoreTopShelfExtension": + case "com.apple.Arcade.ArcadeTopShelfExtension": + case "com.apple.AppStore.Widgets": + // Skip additional platforms for top shelf extension due to memory constraint. + return []; + default: { + const currentPlatform = defaultPlatformForClient(objectGraph); + return allPlatforms(objectGraph, isSome(currentPlatform) ? new Set([currentPlatform]) : undefined); + } + } +} +/** + * Get the product page reviews limit for a client. + * @returns {number} The limit to use. + */ +export function defaultProductPageReviewsLimitForClient(objectGraph) { + switch (objectGraph.client.deviceType) { + case "phone": + return 6; + case "pad": + return 10; + case "mac": + return 12; + case "vision": + return 10; + default: + return 8; + } +} +/// Returns the context param to use for a given grouping request. +/// This is leveraged for switching grouping tab root in specific configurations. +export function defaultGroupingContextForClient(objectGraph) { + let context = null; + if (objectGraph.host.clientIdentifier === client.messagesIdentifier) { + // Messages Grouping (iOS) + context = "messages"; + } + else if (objectGraph.host.clientIdentifier === client.watchIdentifier) { + // Bridge App Store (iOS) + context = "watch"; + } + else if (objectGraph.client.isWatch && objectGraph.client.isTinkerWatch) { + // Tinker App Store (watchOS) + context = "tinker"; + } + return context; +} +// Nothing on the App Store is rated 1000+, so a value this high essentially +// indicates that content restrictions are disabled. +export const ageRestrictionsDisabledThreshold = 1000; +export class Request { + constructor(objectGraph, param, enableMixedCatalog, supplementaryMetadataAssociations) { + var _a; + /// The resource types used in a mixed contents call + this.contentsResourceTypes = new Set(); + /// The ID(s) associated with the resource type + this.ids = new Set(); + /// IDs associated with specific resource types, used for mixed catalog requests + this.idsByResourceType = new Map(); + /// The original ordering of the ids in this reqeust if there is a mixed catalog request + this.originalOrdering = []; + this.relationshipIncludes = new Set(); + // Contents of `extend` param + this.attributeIncludes = new Set(); + this.platform = null; + /// The paths to additional metadata that need to be fetched for mixed media requests + this.supplementaryMetadataAssociations = []; + this.additionalPlatforms = new Set(); + this.additionalQuery = {}; + this.relationshipLimits = {}; + this.searchTerm = null; + this.searchTypes = []; + /// Whether we are searching watch or messages store. + this.context = null; + /// Whether or not to use custom extend attributes, instead of using `attributeIncludes` as-is. + this.useCustomAttributes = false; + /// An optional country code override for constructing the URL. Used in very specific regulatory scenarios where the bag country code cannot be relied upon. + this.countryCodeOverride = undefined; + this.objectGraph = objectGraph; + this.platform = defaultPlatformForClient(objectGraph); + this.isMixedMediaRequest = enableMixedCatalog !== null && enableMixedCatalog !== void 0 ? enableMixedCatalog : false; + this.supplementaryMetadataAssociations = supplementaryMetadataAssociations !== null && supplementaryMetadataAssociations !== void 0 ? supplementaryMetadataAssociations : []; + this.includeAppBinaryTraitsAttribute = objectGraph.client.isiOS; + // By default, the `platform` for web defaults to whatever the `activeIntent` is (e.g. "mac"), + // but this can be overridden when calling `setPreviewPlatform` with the request, which + // will set the `platform` to `web` and `previewPlatform` will be the `activeIntent` platform. + if ((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.platform) { + this.platform = objectGraph.activeIntent.platform; + } + if (serverData.isNullOrEmpty(param)) { + return; + } + if (typeof param === "string") { + this.href = param; + } + else if (Array.isArray(param)) { + this.withDataItems(param, supplementaryMetadataAssociations, enableMixedCatalog); + } + } + /** + * Adds a data object to an idsByResourceType mapping. + * @param data The data object + * @returns An updated {resource-type: IDs} map, which includes the data object + */ + addDataToIDsByResourceType(data) { + const resourceType = data.type; + const id = data.id; + let ids = this.idsByResourceType.get(resourceType); + if (isNothing(ids)) { + ids = new Set(); + } + ids.add(id); + this.idsByResourceType.set(resourceType, ids); + } + forType(type) { + this.resourceType = type; + return this; + } + /** + * This method is used to add data items to the request. It is used to build the request for the mixed catalog / catalog + * + * @param dataItems The data items to add to the request + * @param supplementaryMetadataAssociations The metadata paths to add to the request + * @param enableMixedCatalog Whether to allow mixed catalog requests + * @returns The modified Request + */ + withDataItems(dataItems, supplementaryMetadataAssociations, enableMixedCatalog) { + if (dataItems.length === 0) { + return this; + } + this.isMixedMediaRequest = this.isMixedMediaRequest || (enableMixedCatalog !== null && enableMixedCatalog !== void 0 ? enableMixedCatalog : false); + for (const data of dataItems) { + // Track all IDs in a single set, for the case where we use the contents endpoint. + this.ids.add(data.id); + // Track all IDs by resource type, for either the case where we ultimately have just + // one resource type, or where we used the mixed catalog endpoint. + this.addDataToIDsByResourceType(data); + // If we have any additional metadata paths that need fetching, we now add them + // as additional resource types for the mixed catalog endpoint. + if (isSome(enableMixedCatalog) && + enableMixedCatalog && + isSome(supplementaryMetadataAssociations) && + supplementaryMetadataAssociations.length > 0) { + for (const association of supplementaryMetadataAssociations) { + const metadataItems = extractMetaAssociationFromData(association, data); + if (isSome(metadataItems) && metadataItems.length > 0) { + metadataItems.forEach((metadataItem) => { + this.addDataToIDsByResourceType(metadataItem); + }); + } + } + } + } + // Now that we have collated all IDs and resource types, we can assign them to different + // properties depending on the number of resource types we need. + if (this.idsByResourceType.size === 1) { + // We only have one list of IDs for a single resource type, so we just take the first one + this.resourceType = this.idsByResourceType.keys().next().value; + this.isMixedMediaRequest = false; + } + else if (this.idsByResourceType.size > 1 && !this.isMixedMediaRequest) { + this.resourceType = "contents"; + this.contentsResourceTypes = new Set(Array.from(this.idsByResourceType.keys())); + } + this.originalOrdering.push(...[...dataItems]); + return this; + } + /** + * Add a single id to this request, this should only be used when fetching a single resource + * @param id The single ID to add to the request + * @param type The type of the resource + * @returns The modified Request + */ + withIdOfType(id, type) { + return this.withDataItems([{ id, type }]); + } + /** + * Add a list of ids to this request, this will add these ids to the mapping of resource types + * @param ids The IDs to add to the request + * @param type The type of the resource + * @returns The modified Request + */ + withIdsOfType(ids, type) { + return this.withDataItems(ids.map((id) => ({ id, type }))); + } + includingRelationships(relationships) { + for (const relationship of relationships) { + this.relationshipIncludes.add(relationship); + } + return this; + } + includingScopedRelationships(scopedDataType, relationshipsToAdd) { + // Lazy init top-level property + if (!this.scopedRelationshipIncludes) { + this.scopedRelationshipIncludes = new Map(); + } + // Retrieve existing scoped relationship. Lazy init if needed. + let scopedRelationship = this.scopedRelationshipIncludes.get(scopedDataType); + if (!scopedRelationship) { + scopedRelationship = new Set(); + } + // Update + for (const newRelationship of relationshipsToAdd) { + scopedRelationship.add(newRelationship); + } + this.scopedRelationshipIncludes.set(scopedDataType, scopedRelationship); + return this; + } + includingMetaKeys(scopedDataType, keysToAdd) { + // Lazy init top-level property + if (!this.metaIncludes) { + this.metaIncludes = new Map(); + } + // Retrieve existing inclusion. Lazy init if needed. + let scopedMeta = this.metaIncludes.get(scopedDataType); + if (!scopedMeta) { + scopedMeta = new Set(); + } + // Update + for (const newKey of keysToAdd) { + scopedMeta.add(newKey); + } + this.metaIncludes.set(scopedDataType, scopedMeta); + return this; + } + includingViews(viewsToAdd) { + // Lazy init top-level property + if (!this.viewsIncludes) { + this.viewsIncludes = new Set(); + } + for (const view of viewsToAdd) { + this.viewsIncludes.add(view); + } + return this; + } + includingKindsKeys(scopedDataType, keysToAdd) { + // Lazy init top-level property + if (!this.kindIncludes) { + this.kindIncludes = new Map(); + } + // Retrieve existing inclusion. Lazy init if needed. + let scopedMeta = this.kindIncludes.get(scopedDataType); + if (!scopedMeta) { + scopedMeta = new Set(); + } + // Update + for (const newKey of keysToAdd) { + scopedMeta.add(newKey); + } + this.kindIncludes.set(scopedDataType, scopedMeta); + return this; + } + includingAssociateKeys(scopedDataType, keysToAdd) { + // Lazy init top-level property + if (!this.associateIncludes) { + this.associateIncludes = new Map(); + } + // Retrieve existing inclusion. Lazy init if needed. + let scopedAssociate = this.associateIncludes.get(scopedDataType); + if (!scopedAssociate) { + scopedAssociate = new Set(); + } + // Update + for (const newKey of keysToAdd) { + scopedAssociate.add(newKey); + } + this.associateIncludes.set(scopedDataType, scopedAssociate); + return this; + } + /** + * Include the relationships needed for an upsell. + * @param requiresScopedInclude A flag indicating whether the include should be "scoped". This will need to be the + * case when the relationships we are including is on a request for a separate primary resource type. For example, + * if we are looking to have the upsell relationship included in an EI (Today card, for example), this needs to be + * scoped. However, if we are fetching a grouping directly and need the upsell included, this should *not* be scoped. + */ + includingRelationshipsForUpsell(requiresScopedRelationshipInclude) { + const relationship = "marketing-items"; + if (requiresScopedRelationshipInclude) { + // Lazy init top-level property + if (!this.scopedRelationshipIncludes) { + this.scopedRelationshipIncludes = new Map(); + } + // Retrieve existing scoped relationship. Lazy init if needed. + let scopedRelationship = this.scopedRelationshipIncludes.get("editorial-items"); + if (!scopedRelationship) { + scopedRelationship = new Set(); + } + // Update + scopedRelationship.add(relationship); + this.scopedRelationshipIncludes.set("editorial-items", scopedRelationship); + } + else { + this.relationshipIncludes.add(relationship); + } + // In order to get metrics metadata stiched in to the marketing item relationship, we have to include it in our + // request. + if (relationship === "marketing-items") { + if (!this.metaIncludes) { + this.metaIncludes = new Map(); + } + // Retrieve existing inclusion. Lazy init if needed. + let scopedMeta = this.metaIncludes.get("marketing-items"); + if (!scopedMeta) { + scopedMeta = new Set(); + } + scopedMeta.add("metrics"); + this.metaIncludes.set("marketing-items", scopedMeta); + } + return this; + } + includingAttributes(attributes) { + for (const attribute of attributes) { + this.attributeIncludes.add(attribute); + } + return this; + } + includingScopedAttributes(resourceType, attributesToAdd) { + // Lazy init top-level property + if (!this.scopedAttributeIncludes) { + this.scopedAttributeIncludes = new Map(); + } + // Retrieve existing scoped relationship. Lazy init if needed. + let attributesForResourceType = this.scopedAttributeIncludes.get(resourceType); + if (!attributesForResourceType) { + attributesForResourceType = new Set(); + } + // Update + for (const attribute of attributesToAdd) { + attributesForResourceType.add(attribute); + } + this.scopedAttributeIncludes.set(resourceType, attributesForResourceType); + return this; + } + /** + * Adds an age restriction to the request. ManagedConfiguration provides + * this value on the client, which maps to JRPurpleRating.clientIdentifier. + * + * @returns {Request} The updated request + */ + includingAgeRestrictions() { + const maxAppContentRating = this.objectGraph.client.maxAppContentRating; + if (maxAppContentRating < ageRestrictionsDisabledThreshold) { + this.ageRestriction = maxAppContentRating; + } + return this; + } + includingAdditionalPlatforms(additionalPlatforms) { + for (const platform of additionalPlatforms) { + this.additionalPlatforms.add(platform); + } + return this; + } + includingScopedAvailableIn(resourceType, valuesToAdd) { + // Lazy init top-level property + if (!this.scopedAvailableInIncludes) { + this.scopedAvailableInIncludes = new Map(); + } + // Retrieve existing scoped relationship. Lazy init if needed. + let valuesForResourceType = this.scopedAvailableInIncludes.get(resourceType); + if (!valuesForResourceType) { + valuesForResourceType = new Set(); + } + // Update + for (const value of valuesToAdd) { + valuesForResourceType.add(value); + } + this.scopedAvailableInIncludes.set(resourceType, valuesForResourceType); + return this; + } + /** + * Include the sparse limit needed for a specific resource type. + * @param {media.Type} resourceType Resource type to use for the sparse limit. + * @param {number} value Value to set as the sparse limit. + * @returns {Request} Updated request. + */ + includingScopedSparseLimit(resourceType, value) { + // Lazy init top-level property + if (!this.scopedSparseLimit) { + this.scopedSparseLimit = new Map(); + } + this.scopedSparseLimit.set(resourceType, value); + return this; + } + addingQuery(key, value) { + if (isSome(value)) { + this.additionalQuery[key] = value; + } + else { + delete this.addingQuery[key]; + } + return this; + } + /** + * @param query The additional query values to add + * @returns The modified Reqeust + */ + addingQueryValues(query) { + this.additionalQuery = { + ...this.addingQuery, + ...query, + }; + return this; + } + addingRelationshipLimit(relationship, limit) { + this.relationshipLimits[relationship] = limit; + return this; + } + withSearchTerm(term) { + this.searchTerm = term; + return this; + } + searchingOverTypes(types) { + for (const type of types) { + this.searchTypes.push(type); + } + return this; + } + addingContext(context) { + this.context = context; + return this; + } + includingMacOSCompatibleIOSAppsWhenSupported(verifiedBadgeOnly = false) { + if (this.objectGraph.appleSilicon.isSupportEnabled) { + if (!verifiedBadgeOnly) { + this.enablingFeature("macOSCompatibleIOSApps"); + } + this.includingScopedAttributes("apps", ["isVerifiedForAppleSiliconMac"]); + } + return this; + } + /// Mark a request to include (or exclude) appBinaryTraits attributes. + includingAppBinaryTraitsAttribute(includeAppBinaryTraitsAttribute = true) { + this.includeAppBinaryTraitsAttribute = includeAppBinaryTraitsAttribute; + return this; + } + /** + * Mark a request to use custom attributes. + * This triggers usage of `customXYZ` extend attributes in url-builder for specific extend params + * e.g. `artwork`, `customArtwork`. + */ + usingCustomAttributes(useCustomAttributes) { + this.useCustomAttributes = useCustomAttributes; + return this; + } + alwaysUseIdsAsQueryParam(value) { + this.useIdsAsQueryParam = value; + return this; + } + attributingTo(canonicalUrl) { + this.canonicalUrl = canonicalUrl; + return this; + } + withFilter(type, value) { + this.filterType = type; + this.filterValue = value; + return this; + } + withLimit(limit) { + this.limit = limit; + return this; + } + withSparseLimit(sparseLimit) { + if (sparseLimit !== null) { + this.sparseLimit = sparseLimit; + } + return this; + } + withSparseCount(sparseCount) { + if (sparseCount !== null) { + this.sparseCount = sparseCount; + } + return this; + } + enablingFeature(feature) { + if (!this.enabledFeatures) { + this.enabledFeatures = []; + } + this.enabledFeatures.push(feature); + return this; + } + enablingFeatures(features) { + if (!this.enabledFeatures) { + this.enabledFeatures = []; + } + this.enabledFeatures.push(...features); + return this; + } + /** + * Limit the request to include only certain fields. + * This should only be used in very select use-cases, where: + * - There is no possibility of data being used for other purposes, e.g. sidpack + * - All consumer of this data use it in the same narrow scope, e.g. requesting a set of icons for displaying without metadata only. + * @param fields Set of fields to limit to + */ + asPartialResponseLimitedToFields(fields) { + this.fields = fields; + return this; + } + includesResourceType(resourceType) { + if (this.resourceType === resourceType) { + return true; + } + if (serverData.isDefinedNonNull(this.contentsResourceTypes)) { + return this.contentsResourceTypes.has(resourceType); + } + return false; + } + withCountryCodeOverride(countryCodeOverride) { + this.countryCodeOverride = countryCodeOverride; + return this; + } +} +/** + * Extracts the metadata association from the data. + * @param association The association to extract + * @param data The data to extract from + */ +export function extractMetaAssociationFromData(association, data) { + if (serverData.isNullOrEmpty(data)) { + return null; + } + const associationData = serverData.asArrayOrEmpty(data, `meta.associations.${association}.data`); + if (isNothing(associationData)) { + return null; + } + return [...associationData]; +} +//# sourceMappingURL=data-fetching.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/media/data-structure.js b/node_modules/@jet-app/app-store/tmp/src/foundation/media/data-structure.js new file mode 100644 index 0000000..cafcce8 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/media/data-structure.js @@ -0,0 +1,84 @@ +/** + * Created by joel on 1/25/18. + */ +import * as serverData from "../json-parsing/server-data"; +import { ResponseMetadata } from "../network/network"; +export function dataFromDataContainer(objectGraph, dataContainer) { + const dataArray = serverData.asArrayOrEmpty(dataContainer, "data"); + if (dataArray.length > 1) { + objectGraph.console.warn("tried to extract data from container but more than one member present"); + } + if (dataArray.length !== 1) { + return null; + } + return dataArray[0]; +} +export function dataCollectionFromDataContainer(dataContainer) { + return serverData.asArrayOrEmpty(dataContainer, "data"); +} +/** + * Check whether or not a server vended `Data` object is hydrated or not. + * @param {Data} data to check if hydrated. + */ +export function isDataHydrated(data) { + return serverData.isDefinedNonNull(data.attributes); +} +/** + * Check whether or not a entire data collection contains elements that are fully hydrated. + * @param {Data[]} dataArray Data array to check. + */ +export function isDataCollectionHydrated(dataCollection) { + // Iterate from the back to determine if fully hydrated faster - unhydrated elements tend to be latter elements. + const lastIndex = dataCollection.length - 1; + for (let index = lastIndex; index >= 0; index--) { + const data = dataCollection[index]; + if (!isDataHydrated(data)) { + return false; + } + } + return true; +} +/** + * Check whether or not a entire data collection at least has 1 element that is fully hydrated. + * @param {Data[]} dataArray Data array to check. + */ +export function isDataCollectionPartiallyHydrated(dataCollection) { + for (const data of dataCollection) { + if (isDataHydrated(data)) { + return true; + } + } + return false; +} +/** + * Check whether or not a today data module represents a today page on the heuristic that: + * - The "Today" modules have labels with value 'TodayForApps' as opposed to `WhatYouMissed` + * - Date marker "Today" modules have a 'date' field. + */ +export function isModuleTodayForApps(todayModule) { + const todayModuleId = "TodayForApps"; + const date = serverData.traverse(todayModule, "date"); + return todayModule.label === todayModuleId || serverData.isDefinedNonNull(date); +} +/** + * Get chart results from a server response. Always returns chart segments with valid data + */ +export function chartResultsFromServerResponse(response) { + const resultsArray = serverData.asArrayOrEmpty(response, "results.apps"); + return resultsArray.filter((segment) => { + return !serverData.isNull(segment.data); + }); +} +export function dataCollectionFromResultsListContainer(resultsListContainer) { + return serverData.asArrayOrEmpty(resultsListContainer, "results.contents"); +} +/** + * Get the metrics dictionary included in the meta data of a mediaApi Data object. + * @param {MetaDataProviding} metaDataProvidingObject + * @returns {MapLike | null} + */ +export function metricsFromMediaApiObject(metaDataProvidingObject) { + return serverData.asDictionary(metaDataProvidingObject, "meta.metrics"); +} +// endregion +//# sourceMappingURL=data-structure.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/media/network.js b/node_modules/@jet-app/app-store/tmp/src/foundation/media/network.js new file mode 100644 index 0000000..2b01d40 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/media/network.js @@ -0,0 +1,304 @@ +/** + * Created by joel on 20/2/2018. + * + * This `network.ts` is the Media API arm of the netwok fetch requests. + * It is built on `Network` object and provides standard functionality specific for MAPI. + * + * @see `src/network.ts` for fetching from non-Media API endpoints + */ +import { isNothing, isSome } from "@jet/environment/types/optional"; +import * as serverData from "../json-parsing/server-data"; +import { MetricsIdentifierType } from "../metrics/metrics-identifiers-cache"; +import { ResponseMetadata } from "../network/network"; +import * as urls from "../network/urls"; +import { extractMetaAssociationFromData } from "./data-fetching"; +import * as urlBuilder from "./url-builder"; +const secondaryUserIdHeaderName = "X-Apple-AppStore-UserId-Secondary"; +/** + * Implements the MAPI fetch, building URL from MAPI Request and opaquely managing initial token request and refreshes. + * + * @param {Request} request MAPI Request to fetch with. + * @param {FetchOptions} options? FetchOptions for the MAPI request. + * @returns {Promise} Promise resolving to some type for given MAPI request. + */ +export async function fetchData(objectGraph, request, options) { + var _a; + const startTime = Date.now(); + const token = await objectGraph.mediaToken.refreshToken(); + const fetchTimingMetricsBuilder = objectGraph.fetchTimingMetricsBuilder; + const requestOptions = options !== null && options !== void 0 ? options : {}; + const personalizationIdentifier = (_a = objectGraph.personalizationMetricsIdentifiersCache) === null || _a === void 0 ? void 0 : _a.getMetricsIdForType(MetricsIdentifierType.user); + if (isSome(personalizationIdentifier)) { + if (isSome(requestOptions === null || requestOptions === void 0 ? void 0 : requestOptions.headers)) { + requestOptions.headers[secondaryUserIdHeaderName] = personalizationIdentifier; + } + else { + requestOptions.headers = { + [secondaryUserIdHeaderName]: personalizationIdentifier, + }; + } + } + const response = await fetchWithToken(objectGraph, request, token, requestOptions, false, fetchTimingMetricsBuilder); + const endTime = Date.now(); + if (request.canonicalUrl) { + response[ResponseMetadata.requestedUrl] = request.canonicalUrl; + } + const roundTripTimeIncludingWaiting = endTime - startTime; + if (roundTripTimeIncludingWaiting > 500) { + const longFetchUrl = urlBuilder.buildURLFromRequest(objectGraph, request).toString(); + objectGraph.console.warn("Fetch took too long (" + roundTripTimeIncludingWaiting.toString() + "ms) " + longFetchUrl); + } + return response; +} +export function redirectParametersInUrl(objectGraph, url) { + const redirectURLParams = objectGraph.bag.redirectUrlWhitelistedQueryParams; + return redirectURLParams.filter((param) => { var _a; return serverData.isDefinedNonNull((_a = url.query) === null || _a === void 0 ? void 0 : _a[param]); }); +} +/** + * Given a built URL, token, and options, calls into native networking APIs to fetch content. + * + * @param {string} url URL to fetch data from. + * @param {string} request The original request used to build this url. + * @param {string} token MAPI token key. + * @param {FetchOptions} options Fetch options for MAPI requests. + * @param {boolean} isRetry flag indicating whether this is a fetch retry following a 401 request, and media token was refreshed. + * @returns {Promise} Promise resolving to some type for given MAPI request. + */ +async function fetchWithToken(objectGraph, request, token, options = {}, isRetry = false, fetchTimingMetricsBuilder) { + var _a, _b; + const originalUrl = urlBuilder.buildURLFromRequest(objectGraph, request).toString(); + // Removes all affiliate/redirect params for caching (https://connectme.apple.com/docs/DOC-577671) + const filteredURL = new urls.URL(originalUrl); + const redirectParameters = redirectParametersInUrl(objectGraph, filteredURL); + for (const param of redirectParameters) { + filteredURL.removeParam(param); + } + const filteredUrlString = filteredURL.toString(); + let headers = options.headers; + if (!headers) { + headers = {}; + } + headers["Authorization"] = "Bearer " + token; + const response = await objectGraph.network.fetch({ + url: filteredUrlString, + headers: headers, + method: options.method, + body: options.requestBodyString, + timeout: options.timeout, + }); + try { + if (response.status === 401 || response.status === 403) { + if (isRetry) { + throw Error("We refreshed the token but we still get 401 from the API"); + } + objectGraph.mediaToken.resetToken(); + return await objectGraph.mediaToken.refreshToken().then(async (newToken) => { + // Explicitly re-fetch with the original request so logging and metrics are correct + return await fetchWithToken(objectGraph, request, newToken, options, true, fetchTimingMetricsBuilder); + }); + } + else if (response.status === 404) { + // item is not available in this storefront or perhaps not at all + throw noContentError(); + } + else if (!response.ok) { + const error = new NetworkError(`Bad Status code ${response.status} for ${filteredUrlString}, original ${originalUrl}`); + error.statusCode = response.status; + throw error; + } + const parser = (resp) => { + var _a; + const parseStartTime = Date.now(); + let result; + if (serverData.isNull(resp.body) || resp.body === "") { + if (resp.status === 204) { + // 204 indicates a success, but the response will typically be empty + // Create a fake result so that we don't throw an error when JSON parsing + const emptyData = {}; + result = emptyData; + } + else { + throw noContentError(); + } + } + else { + try { + result = JSON.parse(resp.body); + } + catch (e) { + let errorMessage = e.message; + if (["debug", "internal"].includes(objectGraph.client.buildType)) { + errorMessage = `${e.message}, body: ${resp.body}`; + } + throw new JSONParseError(errorMessage); + } + } + const parseEndTime = Date.now(); + if (result) { + result[ResponseMetadata.pageInformation] = serverData.asJSONData(getPageInformationFromResponse(objectGraph, resp)); + if (resp.metrics.length > 0) { + const metrics = { + ...resp.metrics[0], + parseStartTime: parseStartTime, + parseEndTime: parseEndTime, + }; + result[ResponseMetadata.timingValues] = metrics; + } + else { + const fallbackMetrics = { + pageURL: resp.url, + parseStartTime, + parseEndTime, + }; + result[ResponseMetadata.timingValues] = fallbackMetrics; + } + result[ResponseMetadata.contentMaxAge] = getContentTimeToLiveFromResponse(objectGraph, resp); + // If we have an empty data object, throw a 204 (No Content). + if (Array.isArray(result.data) && + serverData.isArrayDefinedNonNullAndEmpty(result.data) && + !serverData.asBooleanOrFalse(options.allowEmptyDataResponse)) { + throw noContentError(); + } + if (Array.isArray(result.data) && request.originalOrdering.length > 1) { + result.data = buildHydratedDataListResponse(objectGraph, request.originalOrdering, (_a = result.data) !== null && _a !== void 0 ? _a : [], request.supplementaryMetadataAssociations); + } + result[ResponseMetadata.requestedUrl] = originalUrl; + } + return result; + }; + if (isSome(fetchTimingMetricsBuilder)) { + return fetchTimingMetricsBuilder.measureParsing(response, parser); + } + else { + return parser(response); + } + } + catch (e) { + if (e instanceof NetworkError) { + throw e; + } + const correlationKey = (_a = response.headers["x-apple-jingle-correlation-key"]) !== null && _a !== void 0 ? _a : (_b = response.metrics[0]) === null || _b === void 0 ? void 0 : _b.clientCorrelationKey; + throw new Error(`Error Fetching - filtered: ${filteredUrlString}, original: ${originalUrl}, correlationKey: ${correlationKey !== null && correlationKey !== void 0 ? correlationKey : "N/A"}, ${e.name}, ${e.message}`); + } +} +export class NetworkError extends Error { +} +class JSONParseError extends Error { +} +export function noContentError() { + const error = new NetworkError(`No content`); + error.statusCode = 204; + return error; +} +export function notFoundError() { + const error = new NetworkError("Not found"); + error.statusCode = 404; + return error; +} +const serverInstanceHeader = "x-apple-application-instance"; +const environmentDataCenterHeader = "x-apple-application-site"; +function getPageInformationFromResponse(objectGraph, response) { + const storeFrontHeader = objectGraph.client.storefrontIdentifier; + let storeFront = null; + if ((storeFrontHeader === null || storeFrontHeader === void 0 ? void 0 : storeFrontHeader.length) > 0) { + const storeFrontHeaderComponents = storeFrontHeader.split("-"); + if (serverData.isDefinedNonNullNonEmpty(storeFrontHeaderComponents)) { + storeFront = storeFrontHeaderComponents[0]; + } + } + return { + serverInstance: response.headers[serverInstanceHeader], + storeFrontHeader: storeFrontHeader, + language: objectGraph.bag.language, + storeFront: storeFront, + environmentDataCenter: response.headers[environmentDataCenterHeader], + }; +} +function getContentTimeToLiveFromResponse(objectGraph, response) { + const cacheControlHeaderKey = Object.keys(response.headers).find((key) => key.toLowerCase() === "cache-control"); + if (serverData.isNull(cacheControlHeaderKey) || cacheControlHeaderKey === "") { + return null; + } + const headerValue = response.headers[cacheControlHeaderKey]; + if (serverData.isNullOrEmpty(headerValue)) { + return null; + } + const matches = headerValue.match(/max-age=(\d+)/); + if (serverData.isNull(matches) || matches.length < 2) { + return null; + } + return serverData.asNumber(matches[1]); +} +/** + * Builds the hydrated list of shelf items, based on the data in the response, and the original unhydated items. + */ +function buildHydratedDataListResponse(objectGraph, unhydratedData, hydratedDataCollection, associations = []) { + // Create a map of all the hydrated data items from the response, using the resource type and ID as the key + const hydratedDataMap = {}; + for (const dataItem of hydratedDataCollection) { + const key = dataMapKey(objectGraph, dataItem.type, dataItem.id); + hydratedDataMap[key] = dataItem; + } + // Next iterate through the unhyrated items in their original order, and swap in the hydrated item. + const hydratedItems = []; + for (const unhydratedItem of unhydratedData) { + const key = dataMapKey(objectGraph, unhydratedItem.type, unhydratedItem.id); + const hydratedItem = hydratedDataMap[key]; + if (isSome(hydratedItem)) { + /// Start with a base of what was there already, this is to ensure we don't lose any existing meta data + /// if some of the requested associations are not present in the response. + if (serverData.isDefinedNonNullNonEmpty(associations)) { + hydratedItem.meta = { + ...unhydratedItem.meta, + }; + for (const association of associations) { + hydrateMetaAssociationIfNecessary(objectGraph, association, hydratedItem, unhydratedItem, hydratedDataMap); + } + } + hydratedItems.push(hydratedItem); + } + } + return hydratedItems; +} +/** + * Creates a key to use in a mapping of data items. + * @param objectGraph Current object graph + * @param data The data item + * @returns A string composed from the data item ID and resource type + */ +function dataMapKey(objectGraph, resourceType, id) { + return `${resourceType}_${id}`; +} +/** + * @param objectGraph The current object graph + * @param association The association we'd like to hydrate in the meta object + * @param unhydratedItem The original MAPI data item to look for editorial card, and fill in with a fetched card if there is one + * @param hydratedDataMap The data map of all the fetched items keyed by id / type + */ +function hydrateMetaAssociationIfNecessary(objectGraph, association, hydratedItem, unhydratedItem, hydratedDataMap) { + var _a; + if (isNothing(hydratedItem.meta)) { + hydratedItem.meta = { + associations: {}, + }; + } + else if (isNothing(hydratedItem.meta.associations)) { + hydratedItem.meta.associations = {}; + } + const unhydratedAssociationsData = extractMetaAssociationFromData(association, unhydratedItem); + if (serverData.isDefinedNonNullNonEmpty(unhydratedAssociationsData)) { + const hydratedAssociationData = []; + for (const unhydratedAssociation of unhydratedAssociationsData) { + const associationDataKey = dataMapKey(objectGraph, unhydratedAssociation.type, unhydratedAssociation.id); + const associationData = hydratedDataMap[associationDataKey]; + if (isSome(associationData)) { + hydratedAssociationData.push(associationData); + } + } + const hydratedAssociations = (_a = serverData.asDictionary(hydratedItem.meta.associations)) !== null && _a !== void 0 ? _a : {}; + hydratedAssociations[association] = { + data: hydratedAssociationData, + }; + } +} +//# sourceMappingURL=network.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/media/platform-attributes.js b/node_modules/@jet-app/app-store/tmp/src/foundation/media/platform-attributes.js new file mode 100644 index 0000000..3849624 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/media/platform-attributes.js @@ -0,0 +1,143 @@ +import { isNothing } from "@jet/environment/types/optional"; +import * as serverData from "../json-parsing/server-data"; +import * as attributes from "./attributes"; +/** + * There are nested attributes that are platform-specific. If provided with a platform for which the response data has + * platform-specific attributes, this function will return those attributes. + * @param {Data} data The data for which to determine attributes. + * @param {AttributePlatform} platform The platform to fetch the attributes for + * @returns {any} If a platform is provided, returns `true` exactly when platform-specific attributes exist for that + * platform. If no platform is provided, it simply returns the data's top-level attributes. + */ +function attributesForPlatform(data, platform) { + const allPlatformAttributes = attributes.attributeAsDictionary(data, "platformAttributes"); + return serverData.traverse(allPlatformAttributes, platform !== null && platform !== void 0 ? platform : undefined); +} +/** + * Determines if attributes exist for a given platform + * @param {Data} data The data to check + * @param {AttributePlatform} platform The platform to check + * @returns {boolean} True if the platform exists in the data's platform attributes. False if not. + */ +export function hasPlatformAttribute(data, platform) { + const platformAttributes = attributesForPlatform(data, platform); + return serverData.isDefinedNonNullNonEmpty(platformAttributes); +} +/** + * Retrieve the specified attribute from the data as a dictionary + * + * @param data The data from which to retrieve the attribute. + * @param platform The platform to look up + * @param attributePath The path of the attribute. + * @returns The value for the requested attribute + */ +export function platformAttributeAsDictionary(data, platform, attributePath) { + const platformAttributes = attributesForPlatform(data, platform); + if (serverData.isNull(platformAttributes)) { + return null; + } + return serverData.asDictionary(platformAttributes, attributePath); +} +/** + * Retrieve the specified attribute from the data as an array. + * + * @param data The data from which to retrieve the attribute. + * @param platform The platform to look up + * @param attributePath The path of the attribute. + * @returns {any[]} The attribute value as an array. + */ +export function platformAttributeAsArray(data, platform, attributePath) { + const platformAttributes = attributesForPlatform(data, platform); + if (isNothing(platformAttributes)) { + return null; + } + return serverData.asArray(platformAttributes, attributePath); +} +/** + * Retrieve the specified attribute from the data as an array, coercing to an empty array if the object is not an array. + * + * @param data The data from which to retrieve the attribute. + * @param platform The platform to look up + * @param attributePath The path of the attribute. + * @returns {any[]} The attribute value as an array. + */ +export function platformAttributeAsArrayOrEmpty(data, platform, attributePath) { + const platformAttributes = attributesForPlatform(data, platform); + if (serverData.isNull(platformAttributes)) { + return []; + } + return serverData.asArrayOrEmpty(platformAttributes, attributePath); +} +/** + * Retrieve the specified attribute from the data as a string. + * + * If the attribute lives under the platform-specific attributes, then a platform may be provided to properly call in to + * the nested structure. + * @param data The data from which to retrieve the attribute. + * @param platform The platform to look up + * @param attributePath The object path for the attribute. + * @param policy The validation policy to use when resolving this value. + * @returns {string} The attribute value as a string. + */ +export function platformAttributeAsString(data, platform, attributePath, policy = "coercible") { + const platformAttributes = attributesForPlatform(data, platform); + if (serverData.isNull(platformAttributes)) { + return null; + } + return serverData.asString(platformAttributes, attributePath, policy); +} +/** + * Retrieve the specified attribute from the data as a boolean. + * + * If the attribute lives under the platform-specific attributes, then a platform may be provided to properly call in to + * the nested structure. + * @param data The data from which to retrieve the attribute. + * @param platform The platform to look up + * @param attributePath The path of the attribute. + * @param policy The validation policy to use when resolving this value. + * @returns {boolean} The attribute value as a boolean. + */ +export function platformAttributeAsBoolean(data, platform, attributePath, policy = "coercible") { + const platformAttributes = attributesForPlatform(data, platform); + if (serverData.isNull(platformAttributes)) { + return null; + } + return serverData.asBoolean(platformAttributes, attributePath, policy); +} +/** + * Retrieve the specified attribute from the data as a boolean, which will be `false` if the attribute does not exist. + * + * If the attribute lives under the platform-specific attributes, then a platform may be provided to properly call in to + * the nested structure. + * @param data The data from which to retrieve the attribute. + * @param platform The platform to look up + * @param attributePath The path of the attribute. + * @returns {boolean} The attribute value as a boolean, coercing to `false` if the value is not present.. + */ +export function platformAttributeAsBooleanOrFalse(data, platform, attributePath) { + const platformAttributes = attributesForPlatform(data, platform); + if (serverData.isNull(platformAttributes)) { + return false; + } + return serverData.asBooleanOrFalse(platformAttributes, attributePath); +} +/** + * Retrieve the specified attribute from the data as a number. + * + * If the attribute lives under the platform-specific attributes, then a platform may be provided to properly call in to + * the nested structure. + * @param data The data from which to retrieve the attribute. + * @param platform The platform to look up + * @param attributePath The path of the attribute. + * @param policy The validation policy to use when resolving this value. + * @returns {boolean} The attribute value as a number. + */ +export function platformAttributeAsNumber(data, platform, attributePath, policy = "coercible") { + const platformAttributes = attributesForPlatform(data, platform); + if (serverData.isNull(platformAttributes)) { + return null; + } + return serverData.asNumber(platformAttributes, attributePath, policy); +} +// endregion +//# sourceMappingURL=platform-attributes.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/media/relationships.js b/node_modules/@jet-app/app-store/tmp/src/foundation/media/relationships.js new file mode 100644 index 0000000..0f96574 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/media/relationships.js @@ -0,0 +1,43 @@ +import * as serverData from "../json-parsing/server-data"; +export function hasRelationship(data, relationshipType, checkForContent = true) { + const relationshipDataContainer = relationship(data, relationshipType); + if (!relationshipDataContainer) { + return false; + } + if (!relationshipDataContainer.data || (checkForContent && relationshipDataContainer.data.length === 0)) { + return false; + } + return true; +} +export function relationship(data, relationshipType) { + if (serverData.isDefinedNonNull(data)) { + return serverData.asInterface(data.relationships, relationshipType); + } + return null; +} +export function relationshipViewsContainer(data, relationshipType) { + return serverData.asInterface(data.views, relationshipType); +} +export function relationshipData(objectGraph, data, relationshipType) { + const relationshipDataArray = serverData.asArrayOrEmpty(data.relationships, [ + relationshipType, + "data", + ]); + if (relationshipDataArray.length === 0) { + return null; + } + if (relationshipDataArray.length > 1) { + objectGraph.console.warn(`there was an array of relationships when only the first was asked for in relationship ${relationshipType}`); + } + return relationshipDataArray[0]; +} +export function relationshipCollection(data, relationshipType, allowNulls = false) { + if (!hasRelationship(data, relationshipType, false) && allowNulls) { + return null; + } + return serverData.asArrayOrEmpty(data.relationships, [relationshipType, "data"]); +} +export function relationshipViewsCollection(data, relationshipType) { + return serverData.asArrayOrEmpty(data.views, [relationshipType, "data"]); +} +//# sourceMappingURL=relationships.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/media/url-builder.js b/node_modules/@jet-app/app-store/tmp/src/foundation/media/url-builder.js new file mode 100644 index 0000000..955529c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/media/url-builder.js @@ -0,0 +1,381 @@ +/** + * 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 \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/media/util.js b/node_modules/@jet-app/app-store/tmp/src/foundation/media/util.js new file mode 100644 index 0000000..77ed5c2 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/media/util.js @@ -0,0 +1,185 @@ +import { noContentError, notFoundError } from "./network"; +/** + * Validate an untrusted adam ID without contacting the server. + * + * This allows avoiding the work of calling the server with completely bogus + * IDs. It's scoped to web since ID formats can change and web can be updated + * easily (whereas backporting to older native clients is trickier). + * + * @param {AppStoreObjectGraph} objectGraph + * @param {string} id - the Adam ID to validate + * @returns {void} undefined if valid, throws a `NetworkError` (204) otherwise + */ +export function validateAdamId(objectGraph, id) { + if (objectGraph.client.isWeb && !isAdamId(id)) { + throw noContentError(); + } +} +/** + * Check if an ID is a valid Adam ID. + * + * @param {string} id - string to validate + * @return {boolean} true if valid, false otherwise + */ +function isAdamId(id) { + // Media API actually validates if the number <= 2^63-1. But doubles in + // JavaScript cannot precisely represent this (the closest number is >2^63) + // so checking this requires BigInt, which might not be available. At the + // end of the day, checking this is likely not worth the complexity. 2^63-1 + // is 9223372036854775807 so we just restrict the number of digits. + // + // See: https://github.pie.apple.com/its/amp-enums/blob/f75500b44f871f35ba3ce459a5ff4c9f225e71b0/src/main/java/com/apple/jingle/store/IdSpace.java#L131-L140 + // See: https://github.com/google/guava/blob/869a75a1e3ff85d36672a3cd154772dc90c7b3d2/guava/src/com/google/common/primitives/Longs.java#L400-L440 + // See: https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/lang/Long.html#MAX_VALUE + return /^\d{1,19}$/.test(id); +} +/** + * Validate an untrusted Featured Content ID without contacting the server. + * + * This allows avoiding the work of calling the server with completely bogus + * IDs. It's scoped to web since ID formats can change and web can be updated + * easily (whereas backporting to older native clients is trickier). + * + * @param {AppStoreObjectGraph} objectGraph + * @param {string} id the FC ID to validate + * @returns {void} undefined if valid, throws a `NetworkError` (204) otherwise + */ +export function validateFcId(objectGraph, id) { + if (objectGraph.client.isWeb && !isFcId(id)) { + throw noContentError(); + } +} +/** + * Check if an ID is a valid Featured Content (FC) ID. + * + * @param {string} id - string to validate + * @return {boolean} true if valid, false otherwise + */ +function isFcId(id) { + // FcIds are actually Adam IDs under the hood. + // See: https://github.pie.apple.com/its/Jingle/blob/d2d051c9ef2891f72d5f02f5bbbf2d7748afa7b9/MZStoreComponents/src/main/java/com/apple/jingle/app/store/editorial/SFEditorialHelper.java#L293 + return isAdamId(id); +} +/** + * Validate an untrusted (Chart) Genre ID without contacting the server. + * + * This allows avoiding the work of calling the server with completely bogus + * IDs. It's scoped to web since ID formats can change and web can be updated + * easily (whereas backporting to older native clients is trickier). + * + * @param {AppStoreObjectGraph} objectGraph + * @param {string} id the genre ID to validate + * @returns {void} undefined if valid, throws a `NetworkError` (204) otherwise + */ +export function validateGenreId(objectGraph, id) { + if (objectGraph.client.isWeb && !isGenreId(id)) { + throw noContentError(); + } +} +/** + * Check if an ID is a valid (Chart) Genre ID. + * + * @param {string} id - string to validate + * @return {boolean} true if valid, false otherwise + */ +function isGenreId(id) { + // Genre IDs are Java int (signed 32-bit). + // + // Technically negative is not excluded, but all charts are positive, so + // filter out negative. + // + // See: https://github.pie.apple.com/its/Jingle/blob/main/shared/reference-data-logic/MZReferenceDataLogic/src/main/java/com/apple/jingle/eo/MZGenreService.java#L555 + return isPositiveJavaInt(id); +} +/** + * Validate an untrusted Grouping ID without contacting the server. + * + * This allows avoiding the work of calling the server with completely bogus + * IDs. It's scoped to web since ID formats can change and web can be updated + * easily (whereas backporting to older native clients is trickier). + * + * @param {AppStoreObjectGraph} objectGraph + * @param {string} id the grouping ID to validate + * @returns {void} undefined if valid, throws a `NetworkError` (204) otherwise + */ +export function validateGroupingId(objectGraph, id) { + if (objectGraph.client.isWeb && !isGroupingId(id)) { + throw noContentError(); + } +} +/** + * Check if an ID is a valid Grouping ID. + * + * @param {string} id - string to validate + * @return {boolean} true if valid, false otherwise + */ +function isGroupingId(id) { + // Grouping IDs are Java int (signed 32-bit) + // + // Technically negative does not fail to parse, but they're all positive in + // practice. + // + // See: https://github.pie.apple.com/its/Jingle/blob/aaccec936f1feed227fd171ae66bb160cf38e497/MZStorePlatform/src/main/java/com/apple/jingle/store/mediaapi/util/SFMediaAPIEditorialUtil.java#L634 + return isPositiveJavaInt(id); +} +/** + * Test if a string contains a Java int. + * + * @param {string} s - the string to test + * @returns {boolean} true if the string is a stringified Java int, false otherwise + */ +function isPositiveJavaInt(s) { + // Java int is 32-bit signed. We can check bounds since signed integers are + // exactly representable as doubles (actually up to 2^53 is representable + // exactly). + // + // See: https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/lang/Integer.html#MAX_VALUE + return /^\d+$/.test(s) && parseInt(s, 10) <= 2147483647; +} +/** + * Validate an untrusted Editorial Shelf Collection ID without contacting the server. + * + * This allows avoiding the work of calling the server with completely bogus + * IDs. It's scoped to web since ID formats can change and web can be updated + * easily (whereas backporting to older native clients is trickier). + * + * @param {AppStoreObjectGraph} objectGraph + * @param {string} id the editorial shelf collection ID to validate + * @returns {void} undefined if valid, throws a `NetworkError` (204) otherwise + */ +export function validateEditorialShelfCollectionId(objectGraph, id) { + if (objectGraph.client.isWeb && !isEditorialShelfCollectionId(id)) { + throw noContentError(); + } +} +/** + * Check if an ID is a valid editorial shelf collection ID. + * + * @param {string} id - string to validate + * @return {boolean} true if valid, false otherwise + */ +function isEditorialShelfCollectionId(id) { + // Ids are prefixed with "eds.". Beyond that they do have a UUID-type + // identifier that follows, but that seems more liable to change. + // + // See: https://github.pie.apple.com/its/amp-enums/blob/f75500b44f871f35ba3ce459a5ff4c9f225e71b0/src/main/java/com/apple/jingle/store/IdSpace.java#L43C5-L43C20 + // See: https://github.pie.apple.com/its/amp-enums/blob/f75500b44f871f35ba3ce459a5ff4c9f225e71b0/src/main/java/com/apple/jingle/store/IdSpace.java#L250-L252 + return id.startsWith("eds."); +} +/** + * Validates if a request can be performed for `vision` platform content, based on + * the feature flag is disabled. + * + * @param {AppStoreObjectGraph} objectGraph The application's state graph. + * @throws {NetworkError} Throws a 404 error if access is restricted. + * @returns {void} + */ +export function validateNeedsVisionRestriction(objectGraph) { + var _a; + if (objectGraph.client.isWeb && + ((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.previewPlatform) === "vision" && + !objectGraph.bag.enableVisionPlatform) { + throw notFoundError(); + } +} +//# sourceMappingURL=util.js.map \ No newline at end of file -- cgit v1.2.3