hydraNormalizer.ts 6.6 KB

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