hydraNormalizer.ts 6.8 KB

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