attributes.ts
1 import { Opt, isNothing } from "@jet/environment/types/optional"; 2 import * as serverData from "./server-data"; 3 import * as media from "./data-structure"; 4 import { JSONValue, MapLike, JSONData } from "./json-types"; 5 import * as errors from "./errors"; 6 7 // region Generic Attribute retrieval 8 9 // region Attribute retrieval 10 11 /** 12 * Retrieve the specified attribute from the data, coercing it to a JSONData dictionary 13 * 14 * @param data The data from which to retrieve the attribute. 15 * @param attributePath The path of the attribute. 16 * @param defaultValue The object to return if the path search fails. 17 * @returns The dictionary of data 18 */ 19 export function attributeAsDictionary<Type extends JSONValue>( 20 data: media.Data, 21 attributePath?: serverData.ObjectPath, 22 defaultValue?: MapLike<Type>, 23 ): MapLike<Type> | null { 24 if (serverData.isNull(data)) { 25 return null; 26 } 27 return serverData.asDictionary(data.attributes, attributePath, defaultValue); 28 } 29 30 /** 31 * Retrieve the specified attribute from the data, coercing it to an Interface 32 * 33 * @param data The data from which to retrieve the attribute. 34 * @param attributePath The path of the attribute. 35 * @param defaultValue The object to return if the path search fails. 36 * @returns The dictionary of data as an interface 37 */ 38 export function attributeAsInterface<Interface>( 39 data: media.Data, 40 attributePath?: serverData.ObjectPath, 41 defaultValue?: JSONData, 42 ): Interface | null { 43 return attributeAsDictionary(data, attributePath, defaultValue) as unknown as Interface; 44 } 45 46 /** 47 * Retrieve the specified attribute from the data as an array, coercing to an empty array if the object is not an array. 48 * 49 * @param data The data from which to retrieve the attribute. 50 * @param attributePath The path of the attribute. 51 * @returns {any[]} The attribute value as an array. 52 */ 53 export function attributeAsArrayOrEmpty<T extends JSONValue>( 54 data: media.Data, 55 attributePath?: serverData.ObjectPath, 56 ): T[] { 57 if (serverData.isNull(data)) { 58 return []; 59 } 60 return serverData.asArrayOrEmpty(data.attributes, attributePath); 61 } 62 63 /** 64 * Retrieve the specified attribute from the data as a string. 65 * 66 * @param data The data from which to retrieve the attribute. 67 * @param attributePath The object path for the attribute. 68 * @param policy The validation policy to use when resolving this value. 69 * @returns {string} The attribute value as a string. 70 */ 71 export function attributeAsString( 72 data: media.Data, 73 attributePath?: serverData.ObjectPath, 74 policy: serverData.ValidationPolicy = "coercible", 75 ): Opt<string> { 76 if (serverData.isNull(data)) { 77 return null; 78 } 79 return serverData.asString(data.attributes, attributePath, policy); 80 } 81 82 /** 83 * Retrieve the specified meta from the data as a string. 84 * 85 * @param data The data from which to retrieve the attribute. 86 * @param metaPath The object path for the meta. 87 * @param policy The validation policy to use when resolving this value. 88 * @returns {string} The meta value as a string. 89 */ 90 export function metaAsString( 91 data: media.Data, 92 metaPath?: serverData.ObjectPath, 93 policy: serverData.ValidationPolicy = "coercible", 94 ): Opt<string> { 95 if (serverData.isNull(data)) { 96 return null; 97 } 98 return serverData.asString(data.meta, metaPath, policy); 99 } 100 101 /** 102 * Retrieve the specified attribute from the data as a date. 103 * 104 * @param data The data from which to retrieve the attribute. 105 * @param attributePath The object path for the attribute. 106 * @param policy The validation policy to use when resolving this value. 107 * @returns {Date} The attribute value as a date. 108 */ 109 export function attributeAsDate( 110 data: media.Data, 111 attributePath?: serverData.ObjectPath, 112 policy: serverData.ValidationPolicy = "coercible", 113 ): Opt<Date> { 114 if (serverData.isNull(data)) { 115 return null; 116 } 117 const dateString = serverData.asString(data.attributes, attributePath, policy); 118 if (isNothing(dateString)) { 119 return null; 120 } 121 return new Date(dateString); 122 } 123 124 /** 125 * Retrieve the specified attribute from the data as a boolean. 126 * 127 * @param data The data from which to retrieve the attribute. 128 * @param attributePath The path of the attribute. 129 * @param policy The validation policy to use when resolving this value. 130 * @returns {boolean} The attribute value as a boolean. 131 */ 132 export function attributeAsBoolean( 133 data: media.Data, 134 attributePath?: serverData.ObjectPath, 135 policy: serverData.ValidationPolicy = "coercible", 136 ): boolean | null { 137 if (serverData.isNull(data)) { 138 return null; 139 } 140 return serverData.asBoolean(data.attributes, attributePath, policy); 141 } 142 143 /** 144 * Retrieve the specified attribute from the data as a boolean, which will be `false` if the attribute does not exist. 145 * 146 * @param data The data from which to retrieve the attribute. 147 * @param attributePath The path of the attribute. 148 * @returns {boolean} The attribute value as a boolean, coercing to `false` if the value is not present.. 149 */ 150 export function attributeAsBooleanOrFalse(data: media.Data, attributePath?: serverData.ObjectPath): boolean { 151 if (serverData.isNull(data)) { 152 return false; 153 } 154 return serverData.asBooleanOrFalse(data.attributes, attributePath); 155 } 156 157 /** 158 * Retrieve the specified attribute from the data as a number. 159 * 160 * @param data The data from which to retrieve the attribute. 161 * @param attributePath The path of the attribute. 162 * @param policy The validation policy to use when resolving this value. 163 * @returns {boolean} The attribute value as a number. 164 */ 165 export function attributeAsNumber( 166 data: media.Data, 167 attributePath?: serverData.ObjectPath, 168 policy: serverData.ValidationPolicy = "coercible", 169 ): Opt<number> { 170 if (serverData.isNull(data)) { 171 return null; 172 } 173 return serverData.asNumber(data.attributes, attributePath, policy); 174 } 175 176 export function hasAttributes(data: media.Data): boolean { 177 return !serverData.isNull(serverData.asDictionary(data, "attributes")); 178 } 179 180 /** 181 * The canonical way to detect if an item from Media API is hydrated or not. 182 * 183 * @param data The data from which to retrieve the attributes. 184 */ 185 export function isNotHydrated(data: media.Data): boolean { 186 return !hasAttributes(data); 187 } 188 189 // region Custom Attributes 190 191 /** 192 * Performs conversion for a custom variant of given attribute, if any are available. 193 * @param attribute Attribute to get custom attribute key for, if any. 194 */ 195 export function attributeKeyAsCustomAttributeKey(attribute: string): string | undefined { 196 return customAttributeMapping[attribute]; 197 } 198 199 /** 200 * Whether or not given custom attributes key allows fallback to default page with AB testing treatment within a nondefault page. 201 * This is to allow AB testing to affect only icons within custom product pages. 202 */ 203 export function attributeAllowsNonDefaultTreatmentInNonDefaultPage(customAttribute: string): boolean { 204 return customAttribute === "customArtwork" || customAttribute === "customIconArtwork"; // Only the icon artwork. 205 } 206 207 /** 208 * Defines mapping of attribute to custom attribute. 209 */ 210 const customAttributeMapping: { [key: string]: string } = { 211 artwork: "customArtwork", 212 iconArtwork: "customIconArtwork", 213 screenshotsByType: "customScreenshotsByType", 214 promotionalText: "customPromotionalText", 215 videoPreviewsByType: "customVideoPreviewsByType", 216 customScreenshotsByTypeForAd: "customScreenshotsByTypeForAd", 217 customVideoPreviewsByTypeForAd: "customVideoPreviewsByTypeForAd", 218 }; 219 220 export function requiredAttributeAsString(data: media.Data, attributePath: serverData.ObjectPath): string { 221 const value = attributeAsString(data, attributePath); 222 if (isNothing(value)) { 223 throw new errors.MissingFieldError(data, concatObjectPaths("attributes", attributePath)); 224 } 225 return value; 226 } 227 228 export function requiredAttributeAsDate(data: media.Data, attributePath: serverData.ObjectPath): Date { 229 const value = attributeAsDate(data, attributePath); 230 if (isNothing(value)) { 231 throw new errors.MissingFieldError(data, concatObjectPaths("attributes", attributePath)); 232 } 233 return value; 234 } 235 236 export function requiredAttributeAsDictionary<Type extends JSONValue>( 237 data: media.Data, 238 attributePath: serverData.ObjectPath, 239 ): MapLike<Type> { 240 const value: MapLike<Type> | null = attributeAsDictionary(data, attributePath); 241 if (isNothing(value)) { 242 throw new errors.MissingFieldError(data, concatObjectPaths("attributes", attributePath)); 243 } 244 return value; 245 } 246 247 export function requiredMeta(data: media.Data): MapLike<JSONValue> { 248 const value = serverData.asDictionary(data, "meta"); 249 if (isNothing(value)) { 250 throw new errors.MissingFieldError(data, "meta"); 251 } 252 return value; 253 } 254 255 export function requiredMetaAttributeAsString(data: media.Data, attributePath: serverData.ObjectPath): string { 256 const meta = requiredMeta(data); 257 const value = serverData.asString(meta, attributePath); 258 if (isNothing(value)) { 259 throw new errors.MissingFieldError(data, concatObjectPaths("meta", attributePath)); 260 } 261 return value; 262 } 263 264 export function requiredMetaAttributeAsNumber(data: media.Data, attributePath: serverData.ObjectPath): number { 265 const meta = requiredMeta(data); 266 const value = serverData.asNumber(meta, attributePath); 267 if (isNothing(value)) { 268 throw new errors.MissingFieldError(data, concatObjectPaths("meta", attributePath)); 269 } 270 return value; 271 } 272 273 export function concatObjectPaths(prefix: serverData.ObjectPath, suffix: serverData.ObjectPath): serverData.ObjectPath { 274 let finalPath: string[]; 275 if (Array.isArray(prefix)) { 276 finalPath = prefix; 277 } else { 278 finalPath = [prefix]; 279 } 280 281 if (Array.isArray(suffix)) { 282 finalPath.push(...suffix); 283 } else { 284 finalPath.push(suffix); 285 } 286 return finalPath; 287 } 288 289 // endregion