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