hydraNormalizer.ts 6.8 KB

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