hydraNormalizer.ts 6.0 KB

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