UNPKG

ngx-hal

Version:

Angular library for supporting HAL format APIs

1,210 lines (1,162 loc) 63 kB
import * as i0 from '@angular/core'; import { NgModule, Injectable } from '@angular/core'; import deepmerge from 'deepmerge'; import * as i1 from '@angular/common/http'; import { HttpHeaders, HttpParams } from '@angular/common/http'; import { map, flatMap, tap, catchError } from 'rxjs/operators'; import { of, combineLatest, throwError } from 'rxjs'; import * as UriTemplates from 'uri-templates'; class NgxHalModule { } NgxHalModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.8", ngImport: i0, type: NgxHalModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); NgxHalModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "15.2.8", ngImport: i0, type: NgxHalModule }); NgxHalModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.2.8", ngImport: i0, type: NgxHalModule }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.8", ngImport: i0, type: NgxHalModule, decorators: [{ type: NgModule, args: [{ declarations: [], imports: [], exports: [], }] }] }); const ATTRIBUTE_PROPERTIES_METADATA_KEY = 'attributeProperties'; const HEADER_ATTRIBUTE_PROPERTIES_METADATA_KEY = 'headerAttributeProperties'; const HAS_MANY_PROPERTIES_METADATA_KEY = 'hasManyProperties'; const HAS_ONE_PROPERTIES_METADATA_KEY = 'hasOneProperties'; const LINK_PROPERTIES_METADATA_KEY = 'linkProperties'; const HAL_DATASTORE_DOCUMENT_CLASS_METADATA_KEY = 'halDatastoreDocumentClass'; const HAL_MODEL_DOCUMENT_CLASS_METADATA_KEY = 'halModelDocumentClass'; const DEFAULT_NETWORK_CONFIG = { baseUrl: '/', endpoint: '', globalRequestOptions: {}, }; function deepmergeWrapper(...args) { const ensuredArgs = args.map((arg) => arg || {}); return deepmerge.all(ensuredArgs); } function getObjProperty(obj, propertyKey, defaultValue = []) { const objClass = getClass(obj); if (!Object.prototype.hasOwnProperty.call(objClass, propertyKey)) { setObjProperty(objClass, propertyKey, defaultValue); } const x = objClass[propertyKey]; return x; } function setObjProperty(objClass, propertyKey, value) { Object.defineProperty(objClass, propertyKey, { configurable: true, enumerable: false, value, }); } // Returns a new array, therefore, pushing into it won't affect the class metadata function getArrayObjProperty(obj, propertyKey) { const objClass = getClass(obj); const parentClass = Object.getPrototypeOf(objClass); const isTopLevelClass = parentClass === Object.getPrototypeOf(Function); let parentMeta = []; if (!isTopLevelClass) { parentMeta = getArrayObjProperty(parentClass, propertyKey); } const meta = getObjProperty(obj, propertyKey); const finalMeta = [].concat(meta, parentMeta); return finalMeta; } function getClass(obj) { return (typeof obj === 'function' ? obj : obj.constructor); } function DatastoreConfig(config) { return function (target) { const networkConfig = deepmergeWrapper(DEFAULT_NETWORK_CONFIG, config.network || {}); Object.defineProperty(target.prototype, 'paginationClass', { value: config.paginationClass, }); Object.defineProperty(target.prototype, '_cacheStrategy', { value: config.cacheStrategy, }); Object.defineProperty(target.prototype, '_storage', { value: config.storage, }); Object.defineProperty(target.prototype, 'networkConfig', { value: networkConfig, writable: true, }); setObjProperty(target, HAL_DATASTORE_DOCUMENT_CLASS_METADATA_KEY, config.halDocumentClass); return target; }; } function ModelServiceConfig(config) { return function (target) { return target; }; } class ModelOptions { } const DEFAULT_MODEL_OPTIONS = { type: '', }; const DEFAULT_MODEL_TYPE = '__DEFAULT_MODEL_TYPE__'; function ModelConfig(config) { return function (target) { const configValue = deepmergeWrapper(DEFAULT_MODEL_OPTIONS, config); Object.defineProperty(target.prototype, 'config', { value: configValue, writable: true, }); setObjProperty(target, HAL_MODEL_DOCUMENT_CLASS_METADATA_KEY, config.halDocumentClass); return target; }; } var ModelProperty; (function (ModelProperty) { ModelProperty["Attribute"] = "Attribute"; ModelProperty["HasMany"] = "HasMany"; ModelProperty["HasOne"] = "HasOne"; ModelProperty["HeaderAttribute"] = "HeaderAttribute"; ModelProperty["Link"] = "Link"; })(ModelProperty || (ModelProperty = {})); // Modifies the original array function updatePropertyMetadata(modelProperties, newModelProperty) { const existingProperty = modelProperties.find((property) => { return property.name === newModelProperty.name; }); if (existingProperty) { const indexOfExistingProperty = modelProperties.indexOf(existingProperty); modelProperties[indexOfExistingProperty] = newModelProperty; } else { modelProperties.push(newModelProperty); } return modelProperties; } const DEFAULT_ATTRIBUTE_OPTIONS = { excludeFromPayload: false, useClass: false, }; function Attribute(options = {}) { return (model, propertyName) => { const attributeOptions = deepmergeWrapper(DEFAULT_ATTRIBUTE_OPTIONS, options); const existingAttributeProperties = getObjProperty(model, ATTRIBUTE_PROPERTIES_METADATA_KEY, []); const attributeProperty = { type: ModelProperty.Attribute, transformResponseValue: attributeOptions.transformResponseValue, transformBeforeSave: attributeOptions.transformBeforeSave, name: propertyName, externalName: options.externalName || propertyName, excludeFromPayload: options.excludeFromPayload, }; if (attributeOptions.useClass) { attributeProperty.propertyClass = attributeOptions.useClass; } updatePropertyMetadata(existingAttributeProperties, attributeProperty); }; } const DEFAULT_HEADER_ATTRIBUTE_OPTIONS = { useClass: false, }; function HeaderAttribute(options = {}) { return (model, propertyName) => { const headerAttributeOptions = deepmergeWrapper(DEFAULT_HEADER_ATTRIBUTE_OPTIONS, options); const existingHeaderAttributeProperties = getObjProperty(model, HEADER_ATTRIBUTE_PROPERTIES_METADATA_KEY, []); const attributeProperty = { type: ModelProperty.HeaderAttribute, transformResponseValue: headerAttributeOptions.transformResponseValue, transformBeforeSave: headerAttributeOptions.transformBeforeSave, name: propertyName, externalName: options.externalName || propertyName, }; if (headerAttributeOptions.useClass) { attributeProperty.propertyClass = headerAttributeOptions.useClass; } updatePropertyMetadata(existingHeaderAttributeProperties, attributeProperty); }; } const DEFAULT_HAS_MANY_OPTIONS = { includeInPayload: false, }; function HasMany(options) { return (model, propertyName) => { const hasManyOptions = deepmergeWrapper(DEFAULT_HAS_MANY_OPTIONS, options); const existingHasManyProperties = getObjProperty(model, HAS_MANY_PROPERTIES_METADATA_KEY, []); const hasManyProperty = { includeInPayload: hasManyOptions.includeInPayload, name: propertyName, propertyClass: hasManyOptions.itemsType, type: ModelProperty.HasMany, externalName: options.externalName || propertyName, }; updatePropertyMetadata(existingHasManyProperties, hasManyProperty); }; } const DEFAULT_HAS_ONE_OPTIONS = { includeInPayload: false, }; function HasOne(options) { return (model, propertyName) => { const hasOneOptions = deepmergeWrapper(DEFAULT_HAS_ONE_OPTIONS, options); const existingHasOneProperties = getObjProperty(model, HAS_ONE_PROPERTIES_METADATA_KEY, []); const hasOneProperty = { includeInPayload: hasOneOptions.includeInPayload, name: propertyName, propertyClass: hasOneOptions.propertyClass, type: ModelProperty.HasOne, externalName: options.externalName || propertyName, }; updatePropertyMetadata(existingHasOneProperties, hasOneProperty); }; } function Link(options = {}) { return (model, propertyName) => { const existingLinkProperties = getObjProperty(model, LINK_PROPERTIES_METADATA_KEY, []); const linkProperty = { name: propertyName, type: ModelProperty.Link, externalName: options.externalName || propertyName, }; updatePropertyMetadata(existingLinkProperties, linkProperty); }; } const EMBEDDED_PROPERTY_NAME = '_embedded'; const LINKS_PROPERTY_NAME = '_links'; const SELF_PROPERTY_NAME = 'self'; const LOCAL_MODEL_ID_PREFIX = 'local-MODEL-identificator'; const LOCAL_DOCUMENT_ID_PREFIX = 'local-document-identificator'; function isArray(item) { return Array.isArray(item); } function generateUUID() { return `${Math.floor(Math.random() * 1e10)}-${Date.now()}`; } class HalDocument { constructor(rawResource, rawResponse, modelClass, datastore) { this.rawResource = rawResource; this.rawResponse = rawResponse; this.modelClass = modelClass; this.datastore = datastore; this.parseRawResources(rawResource); this.generateUniqueModelIdentificator(); } get hasEmbeddedItems() { const listPropertyName = this.getListPropertyName(this.rawResource); return (this.rawResource[EMBEDDED_PROPERTY_NAME] && this.rawResource[EMBEDDED_PROPERTY_NAME][listPropertyName]); } get itemLinks() { const listPropertyName = this.getListPropertyName(this.rawResource); return this.links[listPropertyName] || []; } getPage(pageNumber, includeRelationships = [], requestOptions = {}, subsequentRequestsOptions = {}) { requestOptions.params = requestOptions.params || {}; if (pageNumber || pageNumber === 0) { requestOptions.params['page'] = pageNumber; } const relationshipUrl = this.links[SELF_PROPERTY_NAME].href; return this.datastore.find(this.modelClass, {}, true, includeRelationships, requestOptions, relationshipUrl, subsequentRequestsOptions); } parseRawResources(resources) { const items = this.getRawResourcesFromResponse(resources); this.models = this.generateModels(items); this.pagination = this.generatePagination(resources); } generateModels(resources) { return resources.map((resource) => { return new this.modelClass(resource, this.datastore, this.rawResponse); }); } generatePagination(pagination) { if (!this.datastore.paginationClass) { return null; } return new this.datastore.paginationClass(pagination); } getRawResourcesFromResponse(resources) { const listPropertyName = this.getListPropertyName(resources); if (!resources[EMBEDDED_PROPERTY_NAME]) { return []; } return resources[EMBEDDED_PROPERTY_NAME][listPropertyName] || []; } getListPropertyName(listResponse) { const links = listResponse[LINKS_PROPERTY_NAME]; const embdedded = this.rawResource[EMBEDDED_PROPERTY_NAME]; const fallbackListPropertyName = embdedded ? Object.keys(embdedded)[0] : 'noListPropertyPresent'; return (Object.keys(links || {}).find((propertyName) => { return isArray(links[propertyName]); }) || fallbackListPropertyName); } get selfLink() { return this.links && this.links[SELF_PROPERTY_NAME] ? this.links[SELF_PROPERTY_NAME].href : null; } get links() { return this.rawResource[LINKS_PROPERTY_NAME]; } generateUniqueModelIdentificator() { this.uniqueModelIdentificator = generateUUID(); } } function getResponseHeader(response, headerName) { const emptyHeaders = new HttpHeaders(); const headers = response ? response.headers || emptyHeaders : emptyHeaders; return headers.get(headerName); } function isHalModelInstance(classInstance) { if (!classInstance) { return false; } if (classInstance instanceof HalModel) { return true; } return isHalModelInstance(classInstance.prototype); } function ensureRelationshipRequestDescriptors(relationships) { return relationships.map((relationshipDescriptor) => { if (typeof relationshipDescriptor === 'string') { return { name: relationshipDescriptor }; } return relationshipDescriptor; }); } function removeQueryParams(uri) { const splittedUri = hasOnlyTemplatedQueryParameters(uri) ? uri.split('{?') : uri.split('?'); if (splittedUri.length > 1) { splittedUri.pop(); } return splittedUri.join(''); } function hasOnlyTemplatedQueryParameters(uri) { return uri.indexOf('{?') !== -1; } function setRequestHeader(initialHeaders, headerName, headerValue) { if (initialHeaders instanceof HttpHeaders) { return setHttpRequestHeader(initialHeaders, headerName, headerValue); } return setObjectRequestHeader(initialHeaders, headerName, headerValue); } function setHttpRequestHeader(initialHeaders, headerName, headerValue) { if (headerValue !== undefined && headerValue !== null) { return initialHeaders.append(headerName, headerValue); } return initialHeaders; } function setObjectRequestHeader(initialHeaders, headerName, headerValue) { const headers = {}; Object.assign(headers, initialHeaders); if (headerValue !== undefined && headerValue !== null) { headers[headerName] = headerValue; } return headers; } function isString(item) { return typeof item === 'string' || item instanceof String; } class SimpleHalModel { } function isSimpleHalModelInstance(classInstance) { if (!classInstance) { return false; } if (classInstance instanceof SimpleHalModel) { return true; } return isSimpleHalModelInstance(classInstance.prototype); } function isFunction(functionToCheck) { return (typeof functionToCheck === 'function' && !isHalModelInstance(functionToCheck) && !isSimpleHalModelInstance(functionToCheck)); } class HalModel { constructor(resource = {}, datastore, rawResponse) { this.resource = resource; this.datastore = datastore; this.rawResponse = rawResponse; this.config = this['config'] || DEFAULT_MODEL_OPTIONS; this.temporarySelfLink = null; this.internalHasManyDocumentIdentificators = {}; this.setLocalModelIdentificator(); this.parseAttributes(resource); this.parseHeaderAttributes(rawResponse); this.initializeHasOneProperties(); this.initializeHasManyProperties(); this.extractEmbeddedProperties(resource); } get uniqueModelIdentificator() { return this.getUniqueModelIdentificator(); } getUniqueModelIdentificator() { return this.selfLink || this.localModelIdentificator; } get id() { if (!this.selfLink) { return null; } const selfLink = removeQueryParams(this.selfLink); return selfLink.split('/').pop(); } get endpoint() { return this.config.endpoint || 'unknownModelEndpoint'; } get modelEndpoints() { return null; } get networkConfig() { return this.config.networkConfig; } get type() { return this.config.type; } getHalDocumentClass() { return getObjProperty(this, HAL_MODEL_DOCUMENT_CLASS_METADATA_KEY, null); } getRelationshipUrl(relationshipName) { const property = this.getPropertyData(relationshipName); if (!property) { console.warn(`Relationship with the name ${relationshipName} is not defined on the model.`); return; } const fieldName = property.externalName || relationshipName; const url = this.links[fieldName] ? this.links[fieldName].href : ''; if (!url || url.startsWith(LOCAL_MODEL_ID_PREFIX) || url.startsWith(LOCAL_DOCUMENT_ID_PREFIX)) { return null; } return url; } getPropertyData(propertyName) { const attributeProperty = this.attributeProperties.find((property) => property.name === propertyName); const hasOneProperty = this.hasOneProperties.find((property) => property.name === propertyName); const hasManyProperty = this.hasManyProperties.find((property) => property.name === propertyName); const linkProperty = this.linkProperties.find((property) => property.name === propertyName); return attributeProperty || hasOneProperty || hasManyProperty || linkProperty; } getEmbeddedResource(resourceName) { const property = this.getPropertyData(resourceName); if (this.resource[property.externalName]) { return this.resource[property.externalName]; } if (!this.resource[EMBEDDED_PROPERTY_NAME]) { return; } return this.resource[EMBEDDED_PROPERTY_NAME][property.externalName]; } save(requestOptions, options = {}) { const modelClass = Object.getPrototypeOf(this).constructor; return this.datastore.save(this, modelClass, requestOptions, options); } update(requestOptions, options = {}) { return this.datastore.update(this, requestOptions, options); } delete(requestOptions, options = {}) { return this.datastore.delete(this, requestOptions, options); } refetch(includeRelationships, requestOptions) { const modelClass = Object.getPrototypeOf(this).constructor; return this.datastore .findOne(modelClass, undefined, includeRelationships, requestOptions, this.selfLink) .pipe(map((fetchedModel) => { this.populateModelMetadata(fetchedModel); return this; })); } generatePayload(options = {}) { const attributePropertiesPayload = this.getAttributePropertiesPayload(options); const relationshipsPayload = this.generateRelationshipsPayload(options); const hasRelationshipLinks = Boolean(Object.keys(relationshipsPayload).length); const payload = Object.assign({}, attributePropertiesPayload); if (hasRelationshipLinks) { payload[LINKS_PROPERTY_NAME] = relationshipsPayload; } return payload; } // Used only when HalModels or HalDocument are passed when creating a new model extractEmbeddedProperties(rawResource) { const embeddedProperties = rawResource[EMBEDDED_PROPERTY_NAME] || {}; Object.keys(embeddedProperties).forEach((propertyName) => { const property = this.getPropertyData(propertyName); const isRelationshipProperty = property && (this.isHasOneProperty(property) || this.isHasManyProperty(property)); const propertyValue = embeddedProperties[propertyName]; const isHalModelOrDocument = isHalModelInstance(propertyValue) || propertyValue instanceof HalDocument; if (isRelationshipProperty && isHalModelOrDocument) { this[property.name] = propertyValue; } }); } getAttributePropertiesPayload(payloadOptions = {}) { const { specificFields, changedPropertiesOnly } = payloadOptions; return this.attributeProperties.reduce((payload, property) => { const propertyName = property.name; const isPropertyExcludedFromPaylaod = property.excludeFromPayload; const isSpecificFieldsSpecified = specificFields && Boolean(specificFields.length); const isSpecificFieldsConditionSatisfied = !isSpecificFieldsSpecified || specificFields.indexOf(propertyName) !== -1; if (isPropertyExcludedFromPaylaod || !isSpecificFieldsConditionSatisfied) { return payload; } const externalPropertyName = property.externalName; const propertyPayload = property.transformBeforeSave ? property.transformBeforeSave(this[propertyName]) : this[propertyName]; if (changedPropertiesOnly) { const isPropertyChanged = propertyPayload !== this.resource[propertyName]; if (isPropertyChanged) { payload[externalPropertyName] = propertyPayload; } } else { payload[externalPropertyName] = propertyPayload; } return payload; }, {}); } generateHasOnePropertyPayload(property) { const payload = {}; const propertyName = property.name; const externalPropertyName = property.externalName; if (!this[propertyName].selfLink) { return payload; } payload[externalPropertyName] = { href: this[propertyName].selfLink, }; return payload; } generateHasManyPropertyPayload(property) { const payload = {}; const hasManyPropertyLinks = []; const propertyName = property.name; const externalPropertyName = property.externalName; // TODO check if this[propertyName] is an array of models or just a HalDocument this[propertyName].forEach((model) => { if (model && model.selfLink) { hasManyPropertyLinks.push({ href: model.selfLink, }); } }); if (hasManyPropertyLinks.length) { payload[externalPropertyName] = hasManyPropertyLinks; } return payload; } generateRelationshipsPayload(payloadOptions = {}) { const { specificFields } = payloadOptions; const isSpecificFieldsSpecified = specificFields && Boolean(specificFields.length); return [...this.hasOneProperties, ...this.hasManyProperties] .filter((property) => property.includeInPayload) .filter((property) => !isSpecificFieldsSpecified || specificFields.indexOf(property.name) !== -1) .reduce((payload, property) => { const propertyName = property.name; if (!this[propertyName]) { return payload; } const isHasOneProperty = property.type === ModelProperty.HasOne; let propertyPayload; if (isHasOneProperty) { propertyPayload = this.generateHasOnePropertyPayload(property); } else { propertyPayload = this.generateHasManyPropertyPayload(property); } Object.assign(payload, propertyPayload); return payload; }, {}); } generateHeaders() { return this.headerAttributeProperties.reduce((headers, property) => { const externalPropertyName = property.externalName; const propertyName = property.name; const propertyValue = property.transformBeforeSave ? property.transformBeforeSave(this[propertyName]) : this[propertyName]; return setRequestHeader(headers, externalPropertyName, propertyValue); }, {}); } get isSaved() { return Boolean(this.id); } fetchRelationships(relationships, requestOptions = {}) { const relationshipsArray = [].concat(relationships); const relationshipDescriptors = ensureRelationshipRequestDescriptors(relationshipsArray); return this.datastore.fetchModelRelationships(this, relationshipDescriptors, requestOptions); } getRelationship(relationshipName) { const property = this.getPropertyData(relationshipName); const isHasOneProperty = property.type === ModelProperty.HasOne; if (isHasOneProperty) { return this.getHasOneRelationship(property); } else if (this.isHasManyProperty) { return this.getHasManyRelationship(property); } } get attributeProperties() { return this.getPropertiesMetadata(ATTRIBUTE_PROPERTIES_METADATA_KEY); } get headerAttributeProperties() { return this.getPropertiesMetadata(HEADER_ATTRIBUTE_PROPERTIES_METADATA_KEY); } get hasOneProperties() { return this.getPropertiesMetadata(HAS_ONE_PROPERTIES_METADATA_KEY); } get hasManyProperties() { return this.getPropertiesMetadata(HAS_MANY_PROPERTIES_METADATA_KEY); } get linkProperties() { return this.getPropertiesMetadata(LINK_PROPERTIES_METADATA_KEY); } getPropertiesMetadata(propertyKey) { const propertiesMetadata = getArrayObjProperty(this, propertyKey); const uniqueMetadata = []; propertiesMetadata.forEach((property) => { if (uniqueMetadata.map((metadata) => metadata.name).indexOf(property.name) === -1) { uniqueMetadata.push(property); } }); return uniqueMetadata; } initializeHasOneProperties() { this.hasOneProperties.forEach((property) => { Object.defineProperty(this, property.name, { configurable: true, get() { return this.getHasOneRelationship(property); }, set(value) { if (isHalModelInstance(value) || !value) { this.replaceRelationshipModel(property.externalName, value); } else { console.warn(`Only HalModel instances can be assigned to property: ${property.name}. This will become an error in the next ngx-hal release`); // throw new Error(`Only HalModel instances can be assigned to property: ${property.name}`); } }, }); }); } initializeHasManyProperties() { this.hasManyProperties.forEach((property) => { Object.defineProperty(this, property.name, { configurable: true, get() { const halDocument = this.getHasManyRelationship(property); if (!halDocument) { return; } return halDocument.models; }, set(value) { const existingHalDocument = this.getHasManyRelationship(property); if (existingHalDocument) { existingHalDocument.models = value; } else { const halDocumentRaw = { models: value, uniqueModelIdentificator: `${LOCAL_DOCUMENT_ID_PREFIX}-${generateUUID()}`, }; this.updateHasManyDocumentIdentificator(property, halDocumentRaw.uniqueModelIdentificator); this.datastore.storage.save(halDocumentRaw); } }, }); }); } setProperty(modelProperty, rawPropertyValue) { const propertyValue = modelProperty.transformResponseValue ? modelProperty.transformResponseValue(rawPropertyValue) : rawPropertyValue; if (isString(modelProperty.propertyClass)) { const propertyClass = this.datastore.findModelClassByType(modelProperty.propertyClass); this[modelProperty.name] = new propertyClass(propertyValue); } else if (isFunction(modelProperty.propertyClass)) { const propertyClass = modelProperty.propertyClass(propertyValue); this[modelProperty.name] = new propertyClass(propertyValue); } else if (modelProperty.propertyClass) { this[modelProperty.name] = new modelProperty.propertyClass(propertyValue); } else { this[modelProperty.name] = propertyValue; } } parseAttributes(resource) { this.attributeProperties.forEach((attributeProperty) => { const rawPropertyValue = resource[attributeProperty.externalName]; this.setProperty(attributeProperty, rawPropertyValue); }); } parseHeaderAttributes(response) { this.headerAttributeProperties.forEach((headerAttributeProperty) => { const rawPropertyValue = getResponseHeader(response, headerAttributeProperty.externalName); this.setProperty(headerAttributeProperty, rawPropertyValue); }); } getHasOneRelationship(property) { const relationshipLinks = this.links[property.externalName]; if (!relationshipLinks) { return; } const modelIdentificator = relationshipLinks.href; return this.datastore.storage.get(modelIdentificator); } getHasManyRelationship(property) { const uniqueRelationshipIdentificator = this.hasManyDocumentIdentificators[property.externalName]; if (!uniqueRelationshipIdentificator) { return; } const halDocument = this.datastore.storage.get(uniqueRelationshipIdentificator); if (!halDocument) { console.warn(`Has many relationship ${property.name} is not fetched.`); return; } return halDocument; } get links() { return this.resource[LINKS_PROPERTY_NAME] || {}; } get selfLink() { return this.links && this.links[SELF_PROPERTY_NAME] ? this.links[SELF_PROPERTY_NAME].href : this.temporarySelfLink; } set selfLink(link) { this.temporarySelfLink = link; } replaceRelationshipModel(relationshipName, relationshipModel) { this.resource[LINKS_PROPERTY_NAME] = this.resource[LINKS_PROPERTY_NAME] || { self: null, }; let relationshipLink = null; if (relationshipModel) { relationshipLink = { href: relationshipModel.uniqueModelIdentificator || relationshipModel.selfLink, }; } this.resource[LINKS_PROPERTY_NAME][relationshipName] = relationshipLink; // Save the model to the storage if it's not already there if (!this[relationshipName] && relationshipModel) { // TODO should the model be removed from the storage if relationshipModel does not exist? this.datastore.storage.save(relationshipModel); } } setLocalModelIdentificator() { this.localModelIdentificator = `${LOCAL_MODEL_ID_PREFIX}-${generateUUID()}`; } isHasOneProperty(property) { return property.type === ModelProperty.HasOne; } isHasManyProperty(property) { return property.type === ModelProperty.HasMany; } populateModelMetadata(sourceModel) { this.resource = sourceModel.resource; this.rawResponse = sourceModel.rawResponse; this.parseAttributes(this.resource); this.parseHeaderAttributes(this.rawResponse); this.extractEmbeddedProperties(this.resource); } updateHasManyDocumentIdentificator(property, identificator) { this.hasManyDocumentIdentificators[property.externalName] = identificator; } set hasManyDocumentIdentificators(hasManyDocumentIdentificators) { this.internalHasManyDocumentIdentificators = Object.assign({}, hasManyDocumentIdentificators); } get hasManyDocumentIdentificators() { return this.internalHasManyDocumentIdentificators; } } HalModel.modelType = DEFAULT_MODEL_TYPE; class Pagination { constructor(rawResource = {}) { this.rawResource = rawResource; } } class HalStorage { constructor() { this.internalStorage = {}; } saveAll(models, savePartialModels = false) { models.forEach((model) => { if (savePartialModels || !this.get(model.uniqueModelIdentificator)) { this.save(model); } }); } remove(model) { delete this.internalStorage[model.uniqueModelIdentificator]; } enrichRequestOptions(uniqueModelIdentificator, requestOptions) { // noop } } class EtagHalStorage extends HalStorage { save(model, response, alternateUniqueIdentificators = []) { const storedModels = []; const identificators = [].concat(alternateUniqueIdentificators); identificators.push(model.uniqueModelIdentificator); identificators.filter(Boolean).forEach((identificator) => { const storedModel = { model, etag: this.getEtagFromResponse(response), }; this.internalStorage[identificator] = storedModel; storedModels.push(storedModel); }); return storedModels; } get(uniqueModelIdentificator) { const localModel = this.getRawStorageModel(uniqueModelIdentificator); return localModel ? localModel.model : undefined; } enrichRequestOptions(uniqueModelIdentificator, requestOptions) { const storageModel = this.getRawStorageModel(uniqueModelIdentificator); if (!storageModel) { return; } if (storageModel.etag) { requestOptions.headers = setRequestHeader(requestOptions.headers, 'If-None-Match', storageModel.etag); } } getRawStorageModel(uniqueModelIdentificator) { return this.internalStorage[uniqueModelIdentificator]; } getEtagFromResponse(response) { if (!response || !response.headers || !response.headers.get) { return; } return response.headers.get('ETag'); } } class SimpleHalStorage extends HalStorage { save(model, response, alternateUniqueIdentificators = []) { const identificators = [].concat(alternateUniqueIdentificators); identificators.push(model.uniqueModelIdentificator); identificators.filter(Boolean).forEach((identificator) => { this.internalStorage[identificator] = model; }); } get(uniqueModelIdentificator) { return this.internalStorage[uniqueModelIdentificator]; } } var CacheStrategy; (function (CacheStrategy) { CacheStrategy["CUSTOM"] = "CUSTOM"; CacheStrategy["ETAG"] = "ETAG"; CacheStrategy["NONE"] = "NONE"; })(CacheStrategy || (CacheStrategy = {})); class ModelServiceOptions { } const DEFAULT_REQUEST_OPTIONS = { observe: 'response', params: {}, }; function createHalStorage(cacheStrategy = CacheStrategy.NONE, storageInstance) { let storage; switch (cacheStrategy) { case CacheStrategy.NONE: storage = new SimpleHalStorage(); break; case CacheStrategy.ETAG: storage = new EtagHalStorage(); break; case CacheStrategy.CUSTOM: if (!storageInstance) { throw new Error('When CacheStrategy.CUSTOM is specified, config.storage is required.'); } storage = storageInstance; break; default: throw new Error(`Unknown CacheStrategy: ${cacheStrategy}`); break; } return storage; } function makeQueryParamsString(params, sortAlphabetically = false) { let paramKeys = Object.keys(params); if (sortAlphabetically) { paramKeys = paramKeys.sort(); } const queryParamsString = paramKeys.reduce((paramsString, queryParamKey) => { return `${paramsString}&${queryParamKey}=${params[queryParamKey]}`; }, ''); return queryParamsString.slice(1); } function getQueryParams(url) { const queryParams = {}; const parser = document.createElement('a'); parser.href = url; const query = parser.search.substring(1); if (!query) { return {}; } const params = query.split('&'); params.forEach((param) => { const [key, value] = param.split('='); if (queryParams[key]) { queryParams[key] = [decodeURIComponentWithErrorHandling(value)].concat(queryParams[key]); } else { const items = value.split(','); if (items.length === 1) { queryParams[key] = decodeURIComponentWithErrorHandling(value); } else { queryParams[key] = items.map((urlParam) => decodeURIComponentWithErrorHandling(urlParam)); } } }); return queryParams; } function decodeURIComponentWithErrorHandling(value) { try { return decodeURIComponent(value); } catch (e) { console.error(e); return value; } } function makeHttpParams(params, httpParamsOptions) { let httpParams = new HttpParams(httpParamsOptions); Object.keys(params).forEach((paramKey) => { httpParams = httpParams.append(paramKey, params[paramKey]); }); return httpParams; } const UriTemplate = UriTemplates.default || UriTemplates; function populateTemplatedUrl(url, params) { const safeUrl = url || ''; return new UriTemplate(safeUrl).fill(params); } class DatastoreService { constructor(http) { this.http = http; this.networkConfig = this['networkConfig'] || DEFAULT_NETWORK_CONFIG; this.internalStorage = createHalStorage(this.cacheStrategy, this.halStorage); this.modelTypes = []; } getHalDocumentClass() { return getObjProperty(this, HAL_DATASTORE_DOCUMENT_CLASS_METADATA_KEY, null) || HalDocument; } buildUrl(model) { const hostUrl = this.buildHostUrl(model); const urlParts = [hostUrl, model ? model.endpoint : null]; if (model && model.id) { urlParts.push(model.id); } return urlParts.filter((urlPart) => urlPart).join('/'); } createHalDocument(rawResource, modelClass, rawResponse) { const propertyClass = isFunction(modelClass) ? modelClass(rawResource) : modelClass; const representantiveModel = new propertyClass({}, this); const halDocumentClass = representantiveModel.getHalDocumentClass() || this.getHalDocumentClass(); return new halDocumentClass(rawResource, rawResponse, propertyClass, this); } findOne(modelClass, modelId, includeRelationships = [], requestOptions = {}, customUrl, subsequentRequestsOptions = {}) { const url = customUrl || this.buildModelUrl(modelClass, modelId); const requestsOptions = { mainRequest: requestOptions, subsequentRequests: subsequentRequestsOptions, }; const relationshipDescriptors = ensureRelationshipRequestDescriptors(includeRelationships); return this.handleGetRequestWithRelationships(url, requestsOptions, modelClass, true, relationshipDescriptors); } fetchModelRelationships(model, relationshipNames, requestOptions = {}) { const ensuredRelationshipNames = [].concat(relationshipNames); const relationships$ = this.fetchRelationships(model, ensuredRelationshipNames, requestOptions); if (!relationships$.length) { return of(model); } return combineLatest(relationships$).pipe(map(() => model)); } fetchRelationships(model, relationshipDescriptors, requestOptions = {}) { const relationshipCalls = []; const relationshipMappings = this.extractCurrentLevelRelationships(relationshipDescriptors); for (const relationshipName in relationshipMappings) { const url = model.getRelationshipUrl(relationshipName); const property = model.getPropertyData(relationshipName); if (!property) { continue; } let modelClass = property.propertyClass; if (isString(modelClass)) { modelClass = this.findModelClassByType(modelClass); } const isSingleResource = property.type === ModelProperty.Attribute || property.type === ModelProperty.HasOne; // Checks if the relationship is already embdedded inside the emdedded property, or // as a part of attribute properties const embeddedRelationship = model.getEmbeddedResource(relationshipName); let fetchedModels; if (embeddedRelationship) { fetchedModels = this.processRawResource(embeddedRelationship, modelClass, isSingleResource, model.rawResponse); } if (!url) { continue; } const relationshipRequestOptions = relationshipMappings[relationshipName] .originalRelationshipDescriptor ? relationshipMappings[relationshipName].originalRelationshipDescriptor.options : null; const requestsOptions = { mainRequest: relationshipRequestOptions || requestOptions, subsequentRequests: requestOptions, }; const relationshipCall$ = this.handleGetRequestWithRelationships(url, requestsOptions, modelClass, isSingleResource, relationshipMappings[relationshipName].childrenRelationships, fetchedModels).pipe(map((fetchedRelation) => { const externalRelationshipName = property.externalName; if (isHalModelInstance(model)) { if (property.type === ModelProperty.HasOne) { // The original relationship URL on the parent model must be replaced because // the actual relationship URL may have some query parameteres attached to it model.links[externalRelationshipName].href = fetchedRelation.uniqueModelIdentificator; } else if (property.type === ModelProperty.HasMany) { model.updateHasManyDocumentIdentificator(property, fetchedRelation.uniqueModelIdentificator); // In case of a HalDocument, halDocument.models may contain model instances which are not the same as the models // saved in local storage. That happens if the same models are fetch beforehand through another API call. // In that case, hasManyDocumentIdentificators of the models from HalDocument must be updated as well. const localModel = this.storage.get(model.uniqueModelIdentificator); if (localModel && localModel !== model) { localModel.updateHasManyDocumentIdentificator(property, fetchedRelation.uniqueModelIdentificator); } } } return fetchedRelation; })); relationshipCalls.push(relationshipCall$); } return relationshipCalls; } extractCurrentLevelRelationships(relationshipDescriptors) { return relationshipDescriptors.reduce((relationships, currentRelationshipDescriptor) => { const relationshipNameParts = currentRelationshipDescriptor.name.split('.'); const currentLevelRelationship = relationshipNameParts.shift(); relationships[currentLevelRelationship] = relationships[currentLevelRelationship] || { childrenRelationships: [], }; if (relationshipNameParts.length) { relationships[currentLevelRelationship].childrenRelationships.push({ name: relationshipNameParts.join('.'), options: currentRelationshipDescriptor.options, }); } else { relationships[currentLevelRelationship].originalRelationshipDescriptor = currentRelationshipDescriptor; } return relationships; }, {}); } handleGetRequestWithRelationships(url, requestsOptions, modelClass, isSingleResource, includeRelationships = [], fetchedModels = null, storePartialModels) { let models$; if (fetchedModels) { models$ = of(fetchedModels); } else { models$ = this.makeGetRequestWrapper(url, requestsOptions, modelClass, isSingleResource, storePartialModels); } if (includeRelationships.length) { return models$.pipe(flatMap((model) => { const models = isSingleResource ? [model] : model.models; const relationshipCalls = this.triggerFetchingModelRelationships(models, includeRelationships, requestsOptions.subsequentRequests); if (!relationshipCalls.length) { return of(model); } return combineLatest(relationshipCalls).pipe(map(() => model)); })); } return models$; } makeGetRequestWrapper(url, requestsOptions, modelClass, isSingleResource, storePartialModels) { const originalGetRequest$ = this.makeGetRequest(url, requestsOptions.mainRequest, modelClass, isSingleResource, storePartialModels); if (this.storage.makeGetRequestWrapper) { const { cleanUrl, urlWithParams, requestOptions: options, } = this.extractRequestInfo(url, requestsOptions.mainRequest); const cachedResoucesFromUrl = this.storage.get(decodeURIComponentWithErrorHandling(url)) || this.storage.get(decodeURIComponentWithErrorHandling(urlWithParams)); return this.storage.makeGetRequestWrapper({ cleanUrl, urlWithParams, originalUrl: url }, cachedResoucesFromUrl, originalGetRequest$, options, modelClass, storePartialModels); } return originalGetRequest$; } triggerFetchingModelRelationships(models, includeRelationships, requestOptions) { const modelRelationshipCalls = []; models.forEach((model) => { const relationshipCalls = this.fetchRelationships(model, includeRelationships, requestOptions); modelRelationshipCalls.push(...relationshipCalls); }); return modelRelationshipCalls; } find(modelClass, params = {}, includeMeta = false, includeRelationships = [], requestOptions = {}, customUrl, subsequentRequestsOptions = {}, storePartialModels = false) { const url = customUrl || this.buildModelUrl(modelClass); const subsequentOptions = deepmergeWrapper({}, subsequentRequestsOptions); const paramsObject = this.ensureParamsObject(params || {}); requestOptions.params = this.ensureParamsObject(requestOptions.params || {}); requestOptions.params = Object.assign(requestOptions.params, paramsObject); const options = deepmergeWrapper({}, requestOptions); const requestsOptions = { mainRequest: options, subsequentRequests: subsequentOptions, }; const relationshipDescriptors = ensureRelationshipRequestDescriptors(includeRelationships); return this.handleGetRequestWithRelationships(url, requestsOptions, modelClass, false, relationshipDescriptors, null, storePartialModels).pipe(flatMap((halDocument) => { if (relationshipDescriptors.length) { return of(halDocument); } return this.fetchEmbeddedListItems(halDocument, modelClass, relationshipDescriptors, subsequentOptions).pipe(map((models) => { halDocument.models = models; return halDocument; })); }), map((halDocument) => (includeMeta ? halDocument : halDocument.models))); } save(model, modelClass, requestOptions, saveOptions = {}) { const defaultSaveOptions = { buildUrlFunction: this.defaultUrlBuildFunction, specificFields: null, transformPayloadBeforeSave: this.defaultTransformPayloadBeforeSaveFunction, }; const options = deepmergeWrapper(defaultSaveOptions, saveOptions); const url = options.buildUrlFunction(model, this.buildUrl(model)); const payload = model.generatePayload({ specificFields: options.specificFields, changedPropertiesOnly: false, }); const transformedPaylaod = options.transformPayloadBeforeSave(payload); const modelHeaders = model.generateHeaders(); const modelRequestOptions = requestOptions || {}; modelRequestOptions.headers = modelRequestOptions.headers || {}; Object.assign(modelRequestOptions.headers, modelHeaders); let request$; if (model.isSaved) { request$ = this.makePutRequest(url, transformedPaylaod, modelRequestOptions); } else { request$ = this.makePostRequest(url, transformedPaylaod, modelRequestOptions); } return request$.pipe(map((response) => { const rawResource = this.extractResourceFromResponse(response); if (rawResource) { return this.processRawResource(rawResource, modelClass, true, response); } const newLocationLink = getResponseHeader(response, 'Location'); if (newLocationLink && model.selfLink !== newLocationLink) { model.selfLink = newLocationLink; } if (!this.storage.get(model.selfLink)) { this.storage.save(model, response); } return model; })); } updateModelWithChangedProperties(model, payload) { Object.keys(payload).forEach((externalPropertyName) => { const property = model.getPropertyData(externalPropertyName); if (payload[externalPr