|
|
@@ -1,37 +1,193 @@
|
|
|
-import {AnyJson} from "~/types/data";
|
|
|
+import {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";
|
|
|
-import {isArray, isNumber} from "lodash";
|
|
|
-import UrlUtils from "~/services/utils/urlUtils";
|
|
|
+import {isArray} from "lodash";
|
|
|
|
|
|
/**
|
|
|
- * Normalisation du format de données Hydra
|
|
|
+ * Normalisation et dé-normalisation du format de données Hydra
|
|
|
*/
|
|
|
class HydraNormalizer {
|
|
|
- public static normalizeEntity(entity: ApiResource): AnyJson {
|
|
|
|
|
|
- const prototype = Object.getPrototypeOf(entity)
|
|
|
- const relations = prototype.constructor.relations
|
|
|
+ /**
|
|
|
+ * Normalize the given entity into an Hydra formatted content.
|
|
|
+ * @param entity
|
|
|
+ */
|
|
|
+ public static normalizeEntity(entity: ApiResource): AnyJson {
|
|
|
+ const iriEncodedFields = HydraNormalizer.getIriEncodedFields(entity)
|
|
|
|
|
|
- for (const field in relations) {
|
|
|
- const value = entity[field]
|
|
|
+ for (const field in iriEncodedFields) {
|
|
|
+ const value = entity[field]
|
|
|
+ const targetEntity = iriEncodedFields[field].entity
|
|
|
|
|
|
- if (!isArray(value)) {
|
|
|
- console.warn("A model's relation is not an array")
|
|
|
- continue
|
|
|
- }
|
|
|
+ if (isArray(value)) {
|
|
|
+ entity[field] = value.map((id: number) => {
|
|
|
+ return UrlUtils.makeIRI(targetEntity, id)
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ entity[field] = UrlUtils.makeIRI(targetEntity, value)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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') {
|
|
|
+ let members = hydraData['hydra:member']
|
|
|
+ let results = []
|
|
|
+
|
|
|
+ for (let item of members) {
|
|
|
+ results.push(HydraNormalizer.denormalizeItem(item, model))
|
|
|
+ }
|
|
|
+
|
|
|
+ return results
|
|
|
+ } else {
|
|
|
+ return HydraNormalizer.denormalizeItem(hydraData, model)
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- const targetEntity = relations[field].entity
|
|
|
+ /**
|
|
|
+ * 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) {
|
|
|
+ const {entity, id} = HydraNormalizer.parseIRI(item['@id'])
|
|
|
+ if (!id) {
|
|
|
+ throw Error('De-normalization error : Could not determine the model of the entity')
|
|
|
+ }
|
|
|
+ model = models[entity]
|
|
|
+ }
|
|
|
+
|
|
|
+ const instance = new model(item)
|
|
|
+
|
|
|
+ const iriEncodedFields = HydraNormalizer.getIriEncodedFields(instance)
|
|
|
+
|
|
|
+ for (const field in iriEncodedFields) {
|
|
|
+ const value = instance[field]
|
|
|
+ const targetEntity = iriEncodedFields[field].entity
|
|
|
+
|
|
|
+ if (isArray(value)) {
|
|
|
+ instance[field] = value.map((iri: string) => {
|
|
|
+ return HydraNormalizer.getIdFromIri(iri, targetEntity)
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ instance[field] = HydraNormalizer.getIdFromIri(value, targetEntity)
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- entity[field] = value.map((id: number | string) => {
|
|
|
- if (!isNumber(id)) {
|
|
|
- throw Error(field + ' : invalid id')
|
|
|
- }
|
|
|
- return UrlUtils.makeIRI(targetEntity, id)
|
|
|
- })
|
|
|
+ return instance
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 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
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Parse the given IRI
|
|
|
+ * @param iri
|
|
|
+ * @protected
|
|
|
+ */
|
|
|
+ protected static parseIRI(iri: string) {
|
|
|
+ const rx = /\/api\/(\w+)\/(\d+)/
|
|
|
+ const match = rx.exec(iri)
|
|
|
+ if (!match) {
|
|
|
+ throw Error('could not parse the IRI : ' + iri)
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ entity: match[1],
|
|
|
+ id: parseInt(match[2])
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get the array of the entity's fields marked as IRIEncoded
|
|
|
+ * @see models/decorators.ts
|
|
|
+ *
|
|
|
+ * @param entity
|
|
|
+ * @protected
|
|
|
+ */
|
|
|
+ protected static getIriEncodedFields(entity: ApiResource): Record<string, ApiResource> {
|
|
|
+ const prototype = Object.getPrototypeOf(entity)
|
|
|
+ return prototype.constructor.relations
|
|
|
+ }
|
|
|
|
|
|
- return entity.$toJson()
|
|
|
+ /**
|
|
|
+ * 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 getIdFromIri(iri: string, expectedEntity: string): number {
|
|
|
+ const { entity, id } = HydraNormalizer.parseIRI(iri)
|
|
|
+ if (entity !== expectedEntity) {
|
|
|
+ throw Error("IRI entity does not match the field's target entity (" + entity + ' != ' + expectedEntity + ")")
|
|
|
}
|
|
|
+ return id
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
export default HydraNormalizer
|