ngx-hal
Version:
Angular library for supporting HAL format APIs
1,210 lines (1,162 loc) • 63 kB
JavaScript
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