@itwin/ecschema-metadata
Version:
ECObjects core concepts in typescript
442 lines • 20.6 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Metadata
*/
import { DelayedPromiseWithProps } from "../DelayedPromise";
import { SchemaReadHelper } from "../Deserialization/Helper";
import { XmlSerializationUtils } from "../Deserialization/XmlSerializationUtils";
import { parseStrength, parseStrengthDirection, RelationshipEnd, SchemaItemType, StrengthDirection, strengthDirectionToString, strengthToString, StrengthType, } from "../ECObjects";
import { ECSchemaError, ECSchemaStatus } from "../Exception";
import { ECClass } from "./Class";
import { serializeCustomAttributes } from "./CustomAttribute";
import { createNavigationProperty, createNavigationPropertySync, EntityClass } from "./EntityClass";
import { Mixin } from "./Mixin";
import { SchemaItem } from "./SchemaItem";
/**
* A Typescript class representation of a ECRelationshipClass.
* @public @preview
*/
export class RelationshipClass extends ECClass {
schemaItemType = RelationshipClass.schemaItemType;
/** @internal */
static get schemaItemType() { return SchemaItemType.RelationshipClass; }
/** @internal */
_strength;
/** @internal */
_strengthDirection;
/** @internal */
_source;
/** @internal */
_target;
/** @internal */
constructor(schema, name, modifier) {
super(schema, name, modifier);
this._strengthDirection = StrengthDirection.Forward;
this._strength = StrengthType.Referencing;
this._source = new RelationshipConstraint(this, RelationshipEnd.Source);
this._target = new RelationshipConstraint(this, RelationshipEnd.Target);
}
get strength() { return this._strength; }
get strengthDirection() { return this._strengthDirection; }
get source() { return this._source; }
get target() { return this._target; }
/**
*
* @param name
* @param relationship
* @param direction
* @internal
*/
async createNavigationProperty(name, relationship, direction) {
return this.addProperty(await createNavigationProperty(this, name, relationship, direction));
}
/** @internal */
createNavigationPropertySync(name, relationship, direction) {
return this.addProperty(createNavigationPropertySync(this, name, relationship, direction));
}
/**
* @internal Used for schema editing.
*/
setStrength(strength) {
this._strength = strength;
}
/**
* @internal Used for schema editing.
*/
setStrengthDirection(direction) {
this._strengthDirection = direction;
}
/**
* @internal Used for schema editing.
*/
setSourceConstraint(source) {
this._source = source;
}
/**
* @internal Used for schema editing.
*/
setTargetConstraint(target) {
this._target = target;
}
/**
* Save this RelationshipClass's properties to an object for serializing to JSON.
* @param standalone Serialization includes only this object (as opposed to the full schema).
* @param includeSchemaVersion Include the Schema's version information in the serialized object.
*/
toJSON(standalone = false, includeSchemaVersion = false) {
const schemaJson = super.toJSON(standalone, includeSchemaVersion);
schemaJson.strength = strengthToString(this.strength);
schemaJson.strengthDirection = strengthDirectionToString(this.strengthDirection);
schemaJson.source = this.source.toJSON();
schemaJson.target = this.target.toJSON();
return schemaJson;
}
/** @internal */
async toXml(schemaXml) {
const itemElement = await super.toXml(schemaXml);
itemElement.setAttribute("strength", strengthToString(this.strength));
itemElement.setAttribute("strengthDirection", strengthDirectionToString(this.strengthDirection));
itemElement.appendChild(await this.source.toXml(schemaXml));
itemElement.appendChild(await this.target.toXml(schemaXml));
return itemElement;
}
fromJSONSync(relationshipClassProps) {
super.fromJSONSync(relationshipClassProps);
let strength = parseStrength(relationshipClassProps.strength);
if (undefined === strength) {
if (SchemaReadHelper.isECSpecVersionNewer({ readVersion: relationshipClassProps.originalECSpecMajorVersion, writeVersion: relationshipClassProps.originalECSpecMinorVersion }))
strength = StrengthType.Referencing;
else
throw new ECSchemaError(ECSchemaStatus.InvalidStrength, `The RelationshipClass ${this.fullName} has an invalid 'strength' attribute. '${relationshipClassProps.strength}' is not a valid StrengthType.`);
}
const strengthDirection = parseStrengthDirection(relationshipClassProps.strengthDirection);
if (undefined === strengthDirection)
throw new ECSchemaError(ECSchemaStatus.InvalidStrength, `The RelationshipClass ${this.fullName} has an invalid 'strengthDirection' attribute. '${relationshipClassProps.strengthDirection}' is not a valid StrengthDirection.`);
this._strength = strength;
this._strengthDirection = strengthDirection;
}
async fromJSON(relationshipClassProps) {
this.fromJSONSync(relationshipClassProps);
}
/**
* Type guard to check if the SchemaItem is of type RelationshipClass.
* @param item The SchemaItem to check.
* @returns True if the item is a RelationshipClass, false otherwise.
*/
static isRelationshipClass(item) {
if (item && item.schemaItemType === SchemaItemType.RelationshipClass)
return true;
return false;
}
/**
* Type assertion to check if the SchemaItem is of type RelationshipClass.
* @param item The SchemaItem to check.
* @returns The item cast to RelationshipClass if it is a RelationshipClass, undefined otherwise.
* @internal
*/
static assertIsRelationshipClass(item) {
if (!this.isRelationshipClass(item))
throw new ECSchemaError(ECSchemaStatus.InvalidSchemaItemType, `Expected '${SchemaItemType.RelationshipClass}' (RelationshipClass)`);
}
}
/**
* A Typescript class representation of a ECRelationshipConstraint.
* @public @preview
*/
export class RelationshipConstraint {
_abstractConstraint;
_relationshipClass;
_relationshipEnd;
_multiplicity;
_polymorphic;
_roleLabel;
_constraintClasses;
_customAttributes;
/** @internal */
constructor(relClass, relEnd, roleLabel, polymorphic) {
this._relationshipEnd = relEnd;
if (polymorphic)
this._polymorphic = polymorphic;
else
this._polymorphic = false;
this._multiplicity = RelationshipMultiplicity.zeroOne;
this._relationshipClass = relClass;
this._roleLabel = roleLabel;
}
get multiplicity() { return this._multiplicity ?? RelationshipMultiplicity.zeroOne; }
get polymorphic() { return this._polymorphic ?? false; }
get roleLabel() { return this._roleLabel; }
get constraintClasses() { return this._constraintClasses; }
get relationshipClass() { return this._relationshipClass; }
get relationshipEnd() { return this._relationshipEnd; }
get customAttributes() { return this._customAttributes; }
/** Returns the constraint name, ie. 'RelationshipName.Source/Target' */
get fullName() { return `${this._relationshipClass.name}:${this.isSource ? "Source" : "Target"}`; }
/** Returns the schema of the RelationshipClass. */
get schema() { return this._relationshipClass.schema; }
get abstractConstraint() {
if (this._abstractConstraint)
return this._abstractConstraint;
if (this.constraintClasses && this.constraintClasses.length === 1)
return this.constraintClasses[0];
return this._abstractConstraint;
}
/**
* True if this RelationshipConstraint is the Source relationship end.
*/
get isSource() { return this.relationshipEnd === RelationshipEnd.Source; }
/**
* Adds the provided class as a constraint class to this constraint.
* @param constraint The class to add as a constraint class.
* @internal
*/
addClass(constraint) {
// TODO: Ensure we don't start mixing constraint class types
// TODO: Check that this class is or subclasses abstract constraint?
if (!this._constraintClasses)
this._constraintClasses = [];
// TODO: Handle relationship constraints
this._constraintClasses.push(constraint);
}
/**
* Removes the provided class as a constraint class from this constraint.
* @param constraint The class to add as a constraint class.
*
* @internal
*/
removeClass(constraint) {
if (undefined === this._constraintClasses)
return;
this._constraintClasses.forEach((item, index) => {
const constraintName = item.fullName;
if (constraintName === constraint.fullName)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this._constraintClasses?.splice(index, 1);
});
}
/**
* Save this RelationshipConstraint's properties to an object for serializing to JSON.
*/
toJSON() {
const schemaJson = {};
schemaJson.multiplicity = this.multiplicity.toString();
schemaJson.roleLabel = this.roleLabel;
schemaJson.polymorphic = this.polymorphic;
if (undefined !== this._abstractConstraint)
schemaJson.abstractConstraint = this._abstractConstraint.fullName;
if (undefined !== this.constraintClasses && this.constraintClasses.length > 0)
schemaJson.constraintClasses = this.constraintClasses.map((constraintClass) => constraintClass.fullName);
const customAttributes = serializeCustomAttributes(this.customAttributes);
if (undefined !== customAttributes)
schemaJson.customAttributes = customAttributes;
return schemaJson;
}
/** @internal */
async toXml(schemaXml) {
const elementName = this.isSource ? "Source" : "Target";
const itemElement = schemaXml.createElement(elementName);
if (undefined !== this.polymorphic)
itemElement.setAttribute("polymorphic", this.polymorphic.toString());
if (undefined !== this.roleLabel)
itemElement.setAttribute("roleLabel", this.roleLabel);
if (undefined !== this.multiplicity)
itemElement.setAttribute("multiplicity", this.multiplicity.toString());
const abstractConstraint = await this.abstractConstraint;
if (undefined !== abstractConstraint) {
const abstractConstraintName = XmlSerializationUtils.createXmlTypedName(this.schema, abstractConstraint.schema, abstractConstraint.name);
itemElement.setAttribute("abstractConstraint", abstractConstraintName);
}
if (undefined !== this.constraintClasses) {
for (const item of this.constraintClasses) {
const constraintClass = await item;
const classElement = schemaXml.createElement("Class");
const constraintClassName = XmlSerializationUtils.createXmlTypedName(this.schema, constraintClass.schema, constraintClass.name);
classElement.setAttribute("class", constraintClassName);
itemElement.appendChild(classElement);
}
}
if (this._customAttributes) {
const caContainerElement = schemaXml.createElement("ECCustomAttributes");
for (const [name, attribute] of this._customAttributes) {
const caElement = await XmlSerializationUtils.writeCustomAttribute(name, attribute, schemaXml, this.schema);
caContainerElement.appendChild(caElement);
}
itemElement.appendChild(caContainerElement);
}
return itemElement;
}
fromJSONSync(relationshipConstraintProps) {
this._roleLabel = relationshipConstraintProps.roleLabel;
this._polymorphic = relationshipConstraintProps.polymorphic;
const parsedMultiplicity = RelationshipMultiplicity.fromString(relationshipConstraintProps.multiplicity);
if (!parsedMultiplicity)
throw new ECSchemaError(ECSchemaStatus.InvalidMultiplicity, ``);
this._multiplicity = parsedMultiplicity;
const relClassSchema = this.relationshipClass.schema;
if (undefined !== relationshipConstraintProps.abstractConstraint) {
const abstractConstraintSchemaItemKey = relClassSchema.getSchemaItemKey(relationshipConstraintProps.abstractConstraint);
if (!abstractConstraintSchemaItemKey)
throw new ECSchemaError(ECSchemaStatus.InvalidECJson, `Unable to locate the abstractConstraint ${relationshipConstraintProps.abstractConstraint}.`);
this.setAbstractConstraint(new DelayedPromiseWithProps(abstractConstraintSchemaItemKey, async () => {
const tempAbstractConstraint = await relClassSchema.lookupItem(relationshipConstraintProps.abstractConstraint);
if (undefined === tempAbstractConstraint ||
(!EntityClass.isEntityClass(tempAbstractConstraint) && !Mixin.isMixin(tempAbstractConstraint) && !RelationshipClass.isRelationshipClass(tempAbstractConstraint)))
throw new ECSchemaError(ECSchemaStatus.InvalidECJson, `Unable to locate the abstractConstraint ${relationshipConstraintProps.abstractConstraint}.`);
return tempAbstractConstraint;
}));
}
const loadEachConstraint = (constraintClassName) => {
const tempConstraintClass = relClassSchema.lookupItemSync(constraintClassName);
if (!tempConstraintClass ||
(!EntityClass.isEntityClass(tempConstraintClass) && !Mixin.isMixin(tempConstraintClass) && !RelationshipClass.isRelationshipClass(tempConstraintClass)))
throw new ECSchemaError(ECSchemaStatus.InvalidECJson, ``);
return tempConstraintClass;
};
for (const constraintClassName of relationshipConstraintProps.constraintClasses) {
const constraintClass = loadEachConstraint(constraintClassName);
this.addClass(new DelayedPromiseWithProps(constraintClass.key, async () => constraintClass));
}
}
async fromJSON(relationshipConstraintProps) {
this.fromJSONSync(relationshipConstraintProps);
}
/**
* Indicates if the provided [[ECClass]] is supported by this [[RelationshipConstraint]].
* @param ecClass The class to check.
*/
async supportsClass(ecClass) {
if (!this.constraintClasses) {
if (this.relationshipClass.baseClass) {
const baseRelationship = await this.relationshipClass.baseClass;
const baseConstraint = this.isSource ? baseRelationship.source : baseRelationship.target;
return baseConstraint.supportsClass(ecClass);
}
return false;
}
if (ecClass.schemaItemType !== SchemaItemType.EntityClass && ecClass.schemaItemType !== SchemaItemType.RelationshipClass &&
ecClass.schemaItemType !== SchemaItemType.Mixin) {
return false;
}
const abstractConstraint = await this.abstractConstraint;
if (abstractConstraint && await RelationshipConstraint.classCompatibleWithConstraint(abstractConstraint, ecClass, this.polymorphic || false))
return true;
for (const constraint of this.constraintClasses) {
if (await RelationshipConstraint.classCompatibleWithConstraint(await constraint, ecClass, this.polymorphic || false))
return true;
}
return false;
}
/**
* Indicates if an ECClass is of the type or applies to the type (if a mixin) of the ECClass specified by the constraintClass parameter.
* @param constraintClass The ECClass that is a constraint class of a relationship.
* @param testClass The ECClass to check against the constraint class.
* @param isPolymorphic Indicates if the testClass should be checked polymorphically.
*/
static async classCompatibleWithConstraint(constraintClass, testClass, isPolymorphic) {
if (SchemaItem.equalByKey(constraintClass, testClass))
return true;
if (isPolymorphic) {
if (testClass.schemaItemType === SchemaItemType.EntityClass || testClass.schemaItemType === SchemaItemType.RelationshipClass) {
return testClass.is(constraintClass);
}
if (testClass.schemaItemType === SchemaItemType.Mixin) {
if (constraintClass.schemaItemType === SchemaItemType.EntityClass)
return testClass.applicableTo(constraintClass);
else
return testClass.is(constraintClass);
}
}
return false;
}
/**
* @internal
*/
static isRelationshipConstraint(object) {
const relationshipConstraint = object;
return relationshipConstraint !== undefined && relationshipConstraint.polymorphic !== undefined && relationshipConstraint.multiplicity !== undefined
&& relationshipConstraint.relationshipEnd !== undefined && relationshipConstraint._relationshipClass !== undefined;
}
/** @internal */
addCustomAttribute(customAttribute) {
if (!this._customAttributes)
this._customAttributes = new Map();
this._customAttributes.set(customAttribute.className, customAttribute);
}
/** @internal */
setRoleLabel(roleLabel) {
this._roleLabel = roleLabel;
}
/** @internal */
setRelationshipEnd(relationshipEnd) {
this._relationshipEnd = relationshipEnd;
}
/** @internal */
setPolymorphic(polymorphic) {
this._polymorphic = polymorphic;
}
/** @internal */
setMultiplicity(multiplicity) {
this._multiplicity = multiplicity;
}
/** @internal */
setAbstractConstraint(abstractConstraint) {
this._abstractConstraint = abstractConstraint;
}
}
/**
* Hackish approach that works like a "friend class" so we can access protected members without making them public.
* @internal
*/
export class MutableRelationshipConstraint extends RelationshipConstraint {
}
const INT32_MAX = 2147483647;
/**
* @public @preview
*/
export class RelationshipMultiplicity {
static zeroOne = new RelationshipMultiplicity(0, 1);
static zeroMany = new RelationshipMultiplicity(0, INT32_MAX);
static oneOne = new RelationshipMultiplicity(1, 1);
static oneMany = new RelationshipMultiplicity(1, INT32_MAX);
lowerLimit;
upperLimit;
/** @internal */
constructor(lowerLimit, upperLimit) {
this.lowerLimit = lowerLimit;
this.upperLimit = upperLimit;
}
static fromString(str) {
const matches = /^\(([0-9]*)\.\.([0-9]*|\*)\)$/.exec(str);
if (matches === null || matches.length !== 3)
return undefined;
const lowerLimit = parseInt(matches[1], 10);
const upperLimit = matches[2] === "*" ? INT32_MAX : parseInt(matches[2], 10);
if (0 === lowerLimit && 1 === upperLimit)
return RelationshipMultiplicity.zeroOne;
else if (0 === lowerLimit && INT32_MAX === upperLimit)
return RelationshipMultiplicity.zeroMany;
else if (1 === lowerLimit && 1 === upperLimit)
return RelationshipMultiplicity.oneOne;
else if (1 === lowerLimit && INT32_MAX === upperLimit)
return RelationshipMultiplicity.oneMany;
return new RelationshipMultiplicity(lowerLimit, upperLimit);
}
equals(rhs) {
return this.lowerLimit === rhs.lowerLimit && this.upperLimit === rhs.upperLimit;
}
toString() {
return `(${this.lowerLimit}..${this.upperLimit === INT32_MAX ? "*" : this.upperLimit})`;
}
}
/**
* @internal
* An abstract class used for schema editing.
*/
export class MutableRelationshipClass extends RelationshipClass {
get source() { return this._source; }
get target() { return this._target; }
}
//# sourceMappingURL=RelationshipClass.js.map