hydraNormalizer.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import * as _ from "lodash-es";
  2. import type { AnyJson, ApiResponse, HydraMetadata } from "~/types/data";
  3. import UrlUtils from "~/services/utils/urlUtils";
  4. import { METADATA_TYPE } from "~/types/enum/data";
  5. import models from "~/models/models";
  6. import ApiResource from "~/models/ApiResource";
  7. /**
  8. * Normalisation et dé-normalisation du format de données Hydra
  9. */
  10. class HydraNormalizer {
  11. static models = models;
  12. /**
  13. * Normalize the given entity into a Hydra formatted content.
  14. * @param entity
  15. */
  16. public static normalizeEntity(entity: ApiResource): AnyJson {
  17. const iriEncodedFields =
  18. Object.getPrototypeOf(entity).constructor.getIriEncodedFields();
  19. for (const field in iriEncodedFields) {
  20. const value = entity[field];
  21. const targetEntity = iriEncodedFields[field].entity;
  22. if (_.isArray(value)) {
  23. entity[field] = value.map((id: number) => {
  24. return UrlUtils.makeIRI(targetEntity, id);
  25. });
  26. } else {
  27. entity[field] =
  28. value !== null ? UrlUtils.makeIRI(targetEntity, value) : null;
  29. }
  30. }
  31. return entity.$toJson();
  32. }
  33. /**
  34. * Parse une réponse Hydra et retourne un objet ApiResponse
  35. *
  36. * @param {AnyJson} data
  37. * @param model
  38. * @return {AnyJson} réponse parsée
  39. */
  40. public static denormalize(
  41. data: AnyJson,
  42. model?: typeof ApiResource,
  43. ): ApiResponse {
  44. return {
  45. data: HydraNormalizer.getData(data, model),
  46. metadata: HydraNormalizer.getMetadata(data),
  47. };
  48. }
  49. protected static getData(
  50. hydraData: AnyJson,
  51. model?: typeof ApiResource,
  52. ): AnyJson {
  53. if (hydraData["@type"] === "hydra:Collection") {
  54. const members = hydraData["hydra:member"];
  55. return members.map((item: AnyJson) =>
  56. HydraNormalizer.denormalizeItem(item, model),
  57. );
  58. } else {
  59. return HydraNormalizer.denormalizeItem(hydraData, model);
  60. }
  61. }
  62. /**
  63. * Génère les métadonnées d'un item ou d'une collection
  64. *
  65. * @param data
  66. * @protected
  67. */
  68. protected static getMetadata(data: AnyJson): HydraMetadata {
  69. if (data["@type"] !== "hydra:Collection") {
  70. // A single item, no metadata
  71. return { type: METADATA_TYPE.ITEM };
  72. }
  73. const metadata: HydraMetadata = {
  74. totalItems: data["hydra:totalItems"],
  75. };
  76. if (data["hydra:view"]) {
  77. /**
  78. * Extract the page number from the IRIs in the hydra:view section
  79. */
  80. const extractPageNumber = (
  81. pos: string,
  82. default_: number | undefined = undefined,
  83. ): number | undefined => {
  84. const iri = data["hydra:view"]["hydra:" + pos];
  85. if (!iri) {
  86. return default_;
  87. }
  88. return UrlUtils.getParameter(
  89. data["hydra:view"]["hydra:" + pos],
  90. "page",
  91. default_,
  92. ) as number | undefined;
  93. };
  94. // TODO: utile d'ajouter la page en cours?
  95. metadata.firstPage = extractPageNumber("first", 1);
  96. metadata.lastPage = extractPageNumber("last", 1);
  97. metadata.nextPage = extractPageNumber("next");
  98. metadata.previousPage = extractPageNumber("previous");
  99. }
  100. metadata.type = METADATA_TYPE.COLLECTION;
  101. return metadata;
  102. }
  103. /**
  104. * Dénormalise un item d'une réponse hydra
  105. *
  106. * @param item
  107. * @param model
  108. * @protected
  109. */
  110. protected static denormalizeItem(
  111. item: AnyJson,
  112. model?: typeof ApiResource,
  113. ): AnyJson {
  114. if (model) {
  115. return HydraNormalizer.denormalizeEntity(model, item);
  116. }
  117. if (!item.hasOwnProperty("@id")) {
  118. // Not hydra formatted
  119. console.error(
  120. "De-normalization error : the item is not hydra formatted",
  121. item,
  122. );
  123. return item;
  124. }
  125. if (item["@id"].match(/\/api\/enum\/\w+/)) {
  126. return HydraNormalizer.denormalizeEnum(item);
  127. }
  128. let entity = null;
  129. // On essaie de déterminer la nature de l'objet à partir de son id
  130. try {
  131. const iri = HydraNormalizer.parseEntityIRI(item["@id"]);
  132. entity = iri.entity;
  133. } catch (e) {
  134. console.error("De-normalization error : could not parse the IRI", item);
  135. return item;
  136. }
  137. if (entity && HydraNormalizer.models.hasOwnProperty(entity)) {
  138. model = HydraNormalizer.models[entity];
  139. return HydraNormalizer.denormalizeEntity(model, item);
  140. }
  141. throw new Error(
  142. "De-normalization error : Could not determine the type of the entity " +
  143. item["@id"] +
  144. " (found: " +
  145. entity +
  146. ")",
  147. );
  148. }
  149. protected static denormalizeEntity(model: typeof ApiResource, item: AnyJson) {
  150. const instance = new model(item);
  151. const iriEncodedFields = model.getIriEncodedFields();
  152. for (const field in iriEncodedFields) {
  153. const value = instance[field];
  154. if (_.isEmpty(value)) {
  155. continue;
  156. }
  157. const targetEntity = iriEncodedFields[field].entity;
  158. if (_.isArray(value)) {
  159. instance[field] = value.map((iri: string) => {
  160. return HydraNormalizer.getIdFromEntityIri(iri, targetEntity);
  161. });
  162. } else {
  163. instance[field] = HydraNormalizer.getIdFromEntityIri(
  164. value,
  165. targetEntity,
  166. );
  167. }
  168. }
  169. return instance;
  170. }
  171. protected static denormalizeEnum(item: AnyJson): AnyJson {
  172. return item;
  173. }
  174. /**
  175. * Parse the given IRI
  176. * @param iri
  177. * @protected
  178. */
  179. protected static parseEntityIRI(iri: string) {
  180. const rx = /\/api\/(\w+)\/(\d+)/;
  181. const match = rx.exec(iri);
  182. if (!match) {
  183. throw new Error("could not parse the IRI : " + JSON.stringify(iri));
  184. }
  185. return {
  186. entity: match[1],
  187. id: parseInt(match[2]),
  188. };
  189. }
  190. /**
  191. * Retrieve the entitie's id from the given IRI
  192. * Throws an error if the IRI does not match the expected entity
  193. *
  194. * @param iri
  195. * @param expectedEntity
  196. * @protected
  197. */
  198. protected static getIdFromEntityIri(
  199. iri: string,
  200. expectedEntity: string,
  201. ): number | string {
  202. const { entity, id } = HydraNormalizer.parseEntityIRI(iri);
  203. if (entity !== expectedEntity) {
  204. throw new Error(
  205. "IRI entity does not match the field's target entity (" +
  206. entity +
  207. " != " +
  208. expectedEntity +
  209. ")",
  210. );
  211. }
  212. return id;
  213. }
  214. }
  215. export default HydraNormalizer;