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 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"] === "hydra:Collection") { const members = hydraData["hydra: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 (!item.hasOwnProperty("@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 && HydraNormalizer.models.hasOwnProperty(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) { 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; } } export default HydraNormalizer;