semantic-network
Version:
A utility library for manipulating a list of links that form a semantic interface to a network of resources.
362 lines • 17.5 kB
JavaScript
import { __awaiter } from "tslib";
import { instanceOfLinkedRepresentation, LinkUtil } from 'semantic-link';
import { FieldType } from '../types/formTypes';
import { LinkRelation } from '../linkRelation';
import anylogger from 'anylogger';
import { FormUtil } from './formUtil';
import { noopResolver } from '../representation/resourceMergeFactory';
import { instanceOfCollection } from './instanceOf/instanceOfCollection';
import { instanceOfDocumentRepresentationGroup } from './instanceOf/instanceOfDocumentRepresentationGroup';
import { instanceOfDocumentRepresentation } from './instanceOf/instanceOfDocumentRepresentation';
import { instanceOfUriListValue } from './instanceOf/instanceOfUriListValue';
import { instanceOfSimpleValue } from './instanceOf/instanceOfSimpleValue';
const log = anylogger('FieldResolverUtil');
/**
* An enumeration across the return type from a form field
*/
var FieldValueType;
(function (FieldValueType) {
/**
* The value is single
*/
FieldValueType[FieldValueType["single"] = 0] = "single";
/**
* The value is contained in an array
*
* Note: this closely aligns with the {@link FormItem.multiple} flag when set of the {@link FieldType.Select}
*/
FieldValueType[FieldValueType["multiple"] = 1] = "multiple";
/**
* The value is contained in an object
*/
FieldValueType[FieldValueType["group"] = 2] = "group";
})(FieldValueType || (FieldValueType = {}));
/**
* Mapping strategy between the across-the-wire field types and internal types
*
* TODO: this needs to be injectable/extendable
*/
function formType(formItem) {
switch (formItem.type) {
case FieldType.Select:
return FieldValueType.multiple;
case FieldType.Group:
case FieldType.Collection:
return FieldValueType.group;
case FieldType.Text:
case FieldType.TextArea:
case FieldType.Password:
case FieldType.Address:
case FieldType.Email:
case FieldType.Uri:
case FieldType.Currency:
case FieldType.Number:
case FieldType.Height:
case FieldType.Checkbox:
case FieldType.Date:
case FieldType.DateTime:
case FieldType.Tel:
case FieldType.EmailList:
case FieldType.Signature:
case FieldType.AddressPostal:
default:
return FieldValueType.single;
}
}
export class FieldResolverUtil {
/**
* Recursive, resolving merger that adds fields to a document based on a (create or edit) form
* and the allowed fields with their type.
*
* Note: field names are converted to tracked names
*/
static resolveFields(document, form, options) {
return __awaiter(this, void 0, void 0, function* () {
const { defaultFields } = Object.assign({}, options);
log.debug('resolving fields on \'%s\'', LinkUtil.getUri(document, LinkRelation.Self));
// pick all the fields as specified from the form
const fieldsToResolve = FormUtil.fieldsToResolve(document, form, defaultFields);
for (const field of fieldsToResolve) {
// find out whether there is a matching field in the form to the link relation
const formItem = FormUtil.findByField(form, field);
if (formItem) {
const fieldValue = yield this.resolve(document[field], formItem, options);
if (fieldValue) {
const { fieldResolver } = Object.assign({}, options);
log.debug('resolving field \'%s\'', field);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TS2538: Type 'Omit ' cannot be used as an index type.
(document)[field] = fieldResolver ?
fieldResolver(field, fieldValue, options) :
fieldValue;
}
else {
// an undefined result adds 'undefined' to field rather than remove
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TS2538: Type 'Omit ' cannot be used as an index type.
document[field] = fieldValue;
// catering 'false' which would log this error (probably do !! on if)
if (fieldValue === undefined || fieldValue === null) {
log.warn('Field \'%s\' is not resolved', field);
}
}
}
else {
log.info('Field \'%s\' is not matched in form', field);
}
}
return document;
});
}
/**
* Takes an incoming polymorphic field ({@link SimpleValue}, {@link UriListValue} or {@link ResourceValue}) and
* checks if its value is valid against the form. To do this, it will also resolve fields if necessary.
*
* Resolutions are performed via injected {@link MergeOptions.resolver} and {@link MergeOptions.resourceResolver}
*
* @param fieldValue
* @param item
* @param options?
*/
static resolve(fieldValue, item, options) {
return __awaiter(this, void 0, void 0, function* () {
const { resolver = noopResolver } = Object.assign({}, options);
const field = yield this.resolveByType(fieldValue, item, options);
if (field && instanceOfSimpleValue(field)) {
return resolver.resolve(field);
}
else if (instanceOfUriListValue(field)) {
return field.map(uri => resolver.resolve(uri));
}
// else field was not resolved by type because it is resource
if (instanceOfLinkedRepresentation(fieldValue)) {
const resource = yield this.resolveResource(fieldValue, item, options);
if (resource) {
// resource was found in a pooled collection and return uri (resolved)
const uri = LinkUtil.getUri(resource, LinkRelation.Self);
if (uri) {
return resolver.resolve(uri);
}
}
}
log.debug('Field type unknown - returning default value');
return fieldValue;
});
}
/**
* Resolves any {@link FormItem.id} as a {@link LinkedRepresentation} when there is a {@link MergeOptions.resourceResolver}.
*
* Note:
* - current strategy to resolve on the title of the Canonical on the field value.
* - there is no placement of the resolution onto the item
*
* @param formItem form item to process for resolution
* @param fieldValue
* @param options
*/
static resolveResource(fieldValue, formItem, options) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const { resourceResolverRelNameStrategy = this.resourceResolverRelNameStrategy, resourceResolver, } = Object.assign({}, options);
const relName = resourceResolverRelNameStrategy(fieldValue);
if (relName) {
const item = (_a = formItem.items) === null || _a === void 0 ? void 0 : _a.find(x => x.name === relName);
if (item) {
if (item.id) {
if (resourceResolver) {
log.debug('matching items collection with resolver relName \'%s\' on %s', relName, formItem.id);
return yield resourceResolver(relName)(fieldValue, options);
} // else no resolver
}
else {
log.debug('form item might expected \id\ attribute of resource');
}
} // else no matching relName in form item
}
else {
log.bind('rel name strategy did not return name on %s', LinkUtil.getUri(fieldValue, LinkRelation.Self));
}
return fieldValue;
});
}
/**
*
* Strategy is to resolve the lookup key as the title from the Canonical link relation
* @param resource field resource being resolved
*/
static resourceResolverRelNameStrategy(resource) {
return LinkUtil.getTitle(resource, LinkRelation.Canonical);
}
/**
* If a {@link MergeOptions.resolver} has been provided then there is potentially a pooled resource available
* requiring resolution
*
* @param fieldValue
* @param item
* @param options contains the resolver to be used
*/
static resolveByPooled(fieldValue, item, options) {
return __awaiter(this, void 0, void 0, function* () {
const { resolver = noopResolver } = Object.assign({}, options);
if (instanceOfSimpleValue(fieldValue)) {
return resolver.resolve(fieldValue);
}
else if (instanceOfUriListValue(fieldValue)) {
return fieldValue.map(uri => resolver.resolve(uri));
}
else if (instanceOfLinkedRepresentation(fieldValue)) {
const resource = yield this.resolveResource(fieldValue, item, options);
if (resource) {
// resource was found in a pooled collection and return uri (resolved)
const uri = LinkUtil.getUri(resource, LinkRelation.Self);
if (uri) {
return yield this.resolveByPooled(uri, item, options);
}
}
}
log.debug('Field type unknown - returning default value');
return fieldValue;
});
}
/**
*
* @param fieldValue
* @param formItem
* @param options
*/
static resolveByType(fieldValue, formItem, options) {
return __awaiter(this, void 0, void 0, function* () {
switch (formType(formItem)) {
case FieldValueType.single: // could have checks to ensure this is a 'text' or 'uri' or object (but not array)
if (fieldValue && !instanceOfSimpleValue(fieldValue) && !(typeof fieldValue === 'object')) {
log.warn('Unexpected type \'%s\' on form type %s with \'%s\'', typeof fieldValue, formItem.type, formItem.name);
return undefined;
}
if (formItem.multiple) {
log.warn('Unexpected attribute \'multiple\' on form type %s with \'%s\'', formItem.type, formItem.name);
}
return fieldValue;
/**
* Type: //types/form/select, moves through the structure
*/
case FieldValueType.multiple:
yield FormUtil.resolveItemsFromCollection(formItem, options);
// TODO: all resolutions must be on the outside
let value = yield this.findValueInItems(formItem, fieldValue, options);
if (!value && instanceOfDocumentRepresentation(fieldValue)) {
const { resourceResolver } = Object.assign({}, options);
if (resourceResolver) {
value = yield resourceResolver(formItem.name)(fieldValue, options);
}
}
// check the value returned needs to be an enumeration.
if (formItem.multiple) {
// return back an Uri[] removing all undefined to meet {@link UriListValue}
return (instanceOfSimpleValue(value) ? [value] : value)
.filter(x => !!x);
// check that the value is part of the provided items enumeration in the form
}
else {
// undefined is acceptable as it is {@link SimpleValue}
return value;
}
/**
* Type: //types/form/group, recursively moves through the structure
*
* note: groups have an items (but unlike //types/form/select) it is dealt with as part
* of the recursion structure of a form item
*/
case FieldValueType.group:
if (formItem.multiple) {
if (instanceOfDocumentRepresentationGroup(fieldValue)) {
return (yield Promise.all(fieldValue.map((value) => __awaiter(this, void 0, void 0, function* () {
if (value) {
return yield this.resolveFields(value, formItem, options);
} // else return undefined
}))))
// remove any undefined
.filter(x => !!x);
}
else {
log.error('Group with multiple must be implemented as an array of Document Representation');
}
}
else {
if (!instanceOfDocumentRepresentation(fieldValue)) {
log.warn('Group must be implemented a DocumentRepresentation');
}
// otherwise it is a single group and resolve treating the object as a LinkedRepresentation
// throw new Error('Group not implemented');
return yield this.resolveFields(fieldValue, formItem, options);
}
break;
default:
log.warn('Unknown form type \'%s\' on \'%s\'', formItem.type, formItem.name);
return undefined;
}
});
}
/**
* Matches a field value against the form items switching across {@link FieldValue} type.
*
* - {@link SimpleValue} - look through the list by value
* - {@link UriListValue} - look across the list by value
* - {@link ResourceValue} - look into the resource's {@link LinkRelation.Canonical} to locate the 'title'
*
* @param formItem a form item (with items eager loaded)
* @param fieldValue
* @param options
*/
static findValueInItems(formItem, fieldValue, options) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
// design here to load items outside of this function
if (fieldValue && (formItem === null || formItem === void 0 ? void 0 : formItem.items)) {
if (instanceOfSimpleValue(fieldValue)) {
if (formItem.items.some(item => item.value === fieldValue)) {
return fieldValue;
}
}
else if (instanceOfUriListValue(fieldValue)) {
return yield Promise.all(fieldValue.map((val) => __awaiter(this, void 0, void 0, function* () { return yield this.findValueInItems(formItem, val, options); })));
}
else if (instanceOfDocumentRepresentation(fieldValue)) {
// a lazy-loaded set of items is currently loaded from a collection
if (instanceOfCollection(formItem.items)) {
if (instanceOfLinkedRepresentation(fieldValue)) {
const uri = LinkUtil.getUri(fieldValue, LinkRelation.Self);
if (uri) {
if (formItem.items.items.some(item => LinkUtil.getUri(item, LinkRelation.Self) === uri)) {
return uri; // return type Uri
} // else fall through to return undefined
}
else {
log.warn('Field value not a linked representation');
}
} // else fall through to return undefined
}
else {
if (Array.isArray(formItem.items)) {
log.debug('Form items already loaded or recursively nothing to load: count \'%s\'', (_a = formItem.items) === null || _a === void 0 ? void 0 : _a.length);
}
else {
log.warn('Form items type not found \'%s\'', typeof fieldValue);
}
// else fall through to return undefined
}
}
else {
log.error('Field value type not found \'%s\'', typeof fieldValue);
// else fall through to return undefined
}
}
else {
log.warn('No items on form \'%s\'', formItem.name);
// else fall through to return undefined
}
log.debug('Value \'%s\' is not found on \'%s\' - still allowing value', fieldValue, formItem.name);
return undefined;
});
}
}
//# sourceMappingURL=fieldResolverUtil.js.map