hydraNormalizer.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  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'] === 'Collection') {
  54. const members = hydraData.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'] !== 'Collection') {
  70. // A single item, no metadata
  71. return { type: METADATA_TYPE.ITEM }
  72. }
  73. const metadata: HydraMetadata = {
  74. totalItems: data.totalItems,
  75. }
  76. if (data.view) {
  77. /**
  78. * Extract the page number from the IRIs in the view section
  79. */
  80. const extractPageNumber = (
  81. pos: string,
  82. default_: number | undefined = undefined
  83. ): number | undefined => {
  84. const iri = data.view['' + pos]
  85. if (!iri) {
  86. return default_
  87. }
  88. return UrlUtils.getParameter(data.view['' + pos], 'page', default_) as
  89. | number
  90. | undefined
  91. }
  92. // TODO: utile d'ajouter la page en cours?
  93. metadata.firstPage = extractPageNumber('first', 1)
  94. metadata.lastPage = extractPageNumber('last', 1)
  95. metadata.nextPage = extractPageNumber('next')
  96. metadata.previousPage = extractPageNumber('previous')
  97. }
  98. metadata.type = METADATA_TYPE.COLLECTION
  99. return metadata
  100. }
  101. /**
  102. * Dénormalise un item d'une réponse hydra
  103. *
  104. * @param item
  105. * @param model
  106. * @protected
  107. */
  108. protected static denormalizeItem(
  109. item: AnyJson,
  110. model?: typeof ApiResource
  111. ): AnyJson {
  112. if (model) {
  113. return HydraNormalizer.denormalizeEntity(model, item)
  114. }
  115. if (!Object.prototype.hasOwnProperty.call(item, '@id')) {
  116. // Not hydra formatted
  117. console.error(
  118. 'De-normalization error : the item is not hydra formatted',
  119. item
  120. )
  121. return item
  122. }
  123. if (item['@id'].match(/\/api\/enum\/\w+/)) {
  124. return HydraNormalizer.denormalizeEnum(item)
  125. }
  126. let entity = null
  127. // On essaie de déterminer la nature de l'objet à partir de son id
  128. try {
  129. const iri = HydraNormalizer.parseEntityIRI(item['@id'])
  130. entity = iri.entity
  131. } catch (e) {
  132. console.error('De-normalization error : could not parse the IRI', item)
  133. return item
  134. }
  135. if (
  136. entity &&
  137. Object.prototype.hasOwnProperty.call(HydraNormalizer.models, entity)
  138. ) {
  139. model = HydraNormalizer.models[entity]
  140. return HydraNormalizer.denormalizeEntity(model, item)
  141. }
  142. throw new Error(
  143. 'De-normalization error : Could not determine the type of the entity ' +
  144. item['@id'] +
  145. ' (found: ' +
  146. entity +
  147. ')'
  148. )
  149. }
  150. protected static denormalizeEntity(model: typeof ApiResource, item: AnyJson) {
  151. // eslint-disable-next-line new-cap
  152. const instance = new model(item)
  153. const iriEncodedFields = model.getIriEncodedFields()
  154. for (const field in iriEncodedFields) {
  155. const value = instance[field]
  156. if (_.isEmpty(value)) {
  157. continue
  158. }
  159. const targetEntity = iriEncodedFields[field].entity
  160. if (_.isArray(value)) {
  161. instance[field] = value.map((iri: string) => {
  162. return HydraNormalizer.getIdFromEntityIri(iri, targetEntity)
  163. })
  164. } else {
  165. instance[field] = HydraNormalizer.getIdFromEntityIri(
  166. value,
  167. targetEntity
  168. )
  169. }
  170. }
  171. return instance
  172. }
  173. protected static denormalizeEnum(item: AnyJson): AnyJson {
  174. return item
  175. }
  176. /**
  177. * Parse the given IRI
  178. * @param iri
  179. * @protected
  180. */
  181. protected static parseEntityIRI(iri: string) {
  182. const rx = /\/api\/(\w+)\/(\d+)/
  183. const match = rx.exec(iri)
  184. if (!match) {
  185. throw new Error('could not parse the IRI : ' + JSON.stringify(iri))
  186. }
  187. return {
  188. entity: match[1],
  189. id: parseInt(match[2]),
  190. }
  191. }
  192. /**
  193. * Retrieve the entitie's id from the given IRI
  194. * Throws an error if the IRI does not match the expected entity
  195. *
  196. * @param iri
  197. * @param expectedEntity
  198. * @protected
  199. */
  200. protected static getIdFromEntityIri(
  201. iri: string,
  202. expectedEntity: string
  203. ): number | string {
  204. const { entity, id } = HydraNormalizer.parseEntityIRI(iri)
  205. if (entity !== expectedEntity) {
  206. throw new Error(
  207. "IRI entity does not match the field's target entity (" +
  208. entity +
  209. ' != ' +
  210. expectedEntity +
  211. ')'
  212. )
  213. }
  214. return id
  215. }
  216. }
  217. export default HydraNormalizer