import * as _ from 'lodash-es' import type { AnyJson, ApiResponse, HydraMetadata } from '~/types/data' import UrlUtils from '~/services/utils/urlUtils' import { METADATA_TYPE } from '~/types/enum/data' import models from '~/models/models' import type ApiResource from '~/models/ApiResource' /** * Normalisation et dé-normalisation du format de données Hydra */ class HydraNormalizer { static models = models /** * Normalize the given entity into a Hydra formatted content. * @param entity */ public static normalizeEntity(entity: ApiResource): AnyJson { const iriEncodedFields = Object.getPrototypeOf(entity).constructor.getIriEncodedFields() for (const field in iriEncodedFields) { const value = entity[field] const targetEntity = iriEncodedFields[field].entity if (_.isArray(value)) { entity[field] = value.map((id: number) => { return UrlUtils.makeIRI(targetEntity, id) }) } else { entity[field] = value !== null ? UrlUtils.makeIRI(targetEntity, value) : null } } return entity.$toJson() } /** * Parse une réponse Hydra et retourne un objet ApiResponse * * @param {AnyJson} data * @param model * @return {AnyJson} réponse parsée */ public static denormalize( data: AnyJson, model?: typeof ApiResource, ): ApiResponse { return { data: HydraNormalizer.getData(data, model), metadata: HydraNormalizer.getMetadata(data), } } protected static getData( hydraData: AnyJson, model?: typeof ApiResource, ): AnyJson { if (hydraData['@type'] === 'Collection') { const members = hydraData.member return members.map((item: AnyJson) => HydraNormalizer.denormalizeItem(item, model), ) } else { return HydraNormalizer.denormalizeItem(hydraData, model) } } /** * Génère les métadonnées d'un item ou d'une collection * * @param data * @protected */ protected static getMetadata(data: AnyJson): HydraMetadata { if (data['@type'] !== 'hydra:Collection') { // A single item, no metadata return { type: METADATA_TYPE.ITEM } } const metadata: HydraMetadata = { totalItems: data['hydra:totalItems'], } if (data['hydra:view']) { /** * Extract the page number from the IRIs in the hydra:view section */ const extractPageNumber = ( pos: string, default_: number | undefined = undefined, ): number | undefined => { const iri = data['hydra:view']['hydra:' + pos] if (!iri) { return default_ } return UrlUtils.getParameter( data['hydra:view']['hydra:' + pos], 'page', default_, ) as number | undefined } // TODO: utile d'ajouter la page en cours? metadata.firstPage = extractPageNumber('first', 1) metadata.lastPage = extractPageNumber('last', 1) metadata.nextPage = extractPageNumber('next') metadata.previousPage = extractPageNumber('previous') } metadata.type = METADATA_TYPE.COLLECTION return metadata } /** * Dénormalise un item d'une réponse hydra * * @param item * @param model * @protected */ protected static denormalizeItem( item: AnyJson, model?: typeof ApiResource, ): AnyJson { if (model) { return HydraNormalizer.denormalizeEntity(model, item) } if (!Object.prototype.hasOwnProperty.call(item, '@id')) { // Not hydra formatted console.error( 'De-normalization error : the item is not hydra formatted', item, ) return item } if (item['@id'].match(/\/api\/enum\/\w+/)) { return HydraNormalizer.denormalizeEnum(item) } let entity = null // On essaie de déterminer la nature de l'objet à partir de son id try { const iri = HydraNormalizer.parseEntityIRI(item['@id']) entity = iri.entity } catch (e) { console.error('De-normalization error : could not parse the IRI', item) return item } if ( entity && Object.prototype.hasOwnProperty.call(HydraNormalizer.models, entity) ) { model = HydraNormalizer.models[entity] return HydraNormalizer.denormalizeEntity(model, item) } throw new Error( 'De-normalization error : Could not determine the type of the entity ' + item['@id'] + ' (found: ' + entity + ')', ) } protected static denormalizeEntity(model: typeof ApiResource, item: AnyJson) { item.id = this.getItemIdValue(model, item) const instance = new model(item) const iriEncodedFields = model.getIriEncodedFields() for (const field in iriEncodedFields) { const value = instance[field] if (_.isEmpty(value)) { continue } const targetEntity = iriEncodedFields[field].entity if (_.isArray(value)) { instance[field] = value.map((iri: string) => { return HydraNormalizer.getIdFromEntityIri(iri, targetEntity) }) } else { instance[field] = HydraNormalizer.getIdFromEntityIri( value, targetEntity, ) } } return instance } protected static denormalizeEnum(item: AnyJson): AnyJson { return item } /** * Parse the given IRI * @param iri * @protected */ protected static parseEntityIRI(iri: string) { const rx = /\/api\/(\w+)\/(\d+)/ const match = rx.exec(iri) if (!match) { throw new Error('could not parse the IRI : ' + JSON.stringify(iri)) } return { entity: match[1], id: parseInt(match[2]), } } /** * Retrieve the entitie's id from the given IRI * Throws an error if the IRI does not match the expected entity * * @param iri * @param expectedEntity * @protected */ protected static getIdFromEntityIri( iri: string, expectedEntity: string, ): number | string { const { entity, id } = HydraNormalizer.parseEntityIRI(iri) if (entity !== expectedEntity) { throw new Error( "IRI entity does not match the field's target entity (" + entity + ' != ' + expectedEntity + ')', ) } return id } /** * Get the id value of an item * @param model * @param item * @protected */ protected static getItemIdValue(model: typeof ApiResource, item: AnyJson) { if (item.id !== undefined) { return item.id } if ( model.getIdField() !== undefined && item[model.getIdField()] !== undefined ) { return item[model.getIdField()] } throw new Error('Missing id field or @IdField decorator for ' + model) } } export default HydraNormalizer