Skip to content

Commit

Permalink
Add further support for Locator (#8)
Browse files Browse the repository at this point in the history
1. Changed SearchKey to Locator.
2. Locator used to only work for relationships with target element being ECEntity.
  • Loading branch information
ziyzhu authored Sep 7, 2021
1 parent 1313899 commit 0ee6f0b
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 47 deletions.
2 changes: 1 addition & 1 deletion core/src/BaseApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class JobArgs implements JobArgsProps {
public outputDir: string = path.join(__dirname, "output");
public logLevel: LogLevel = LogLevel.None;
public enableDelete: boolean = true;
public revisionHeader: string = "itwin-pcf";
public revisionHeader: string = "iTwin.PCF";

constructor(props: JobArgsProps) {
this.connectorPath = props.connectorPath;
Expand Down
14 changes: 7 additions & 7 deletions core/src/DMO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { IRInstance } from "./IRModel";
* The combination of this set of properties and values should uniquely identify a target element that already exists in the iModel.
* e.g. {"ECClassId": "Bis.PhysicalElement", "CodeValue": "Wall"}
*/
export type SearchKey = string;
export type Locator = string;

/*
* In one of the following formats:
Expand Down Expand Up @@ -89,25 +89,25 @@ export interface RelationshipDMO extends DMO {
/*
* References a primary/foreign key (attribute) that uniquely identifies a source entity.
*/
readonly fromAttr: string;
readonly fromAttr: Locator | string;

/*
* The type of the source entity.
* Currently it must be IR Entity.
*/
readonly fromType: "IREntity";
readonly fromType: "IREntity" | "ECEntity";

/*
* References a primary/foreign key (attribute) that uniquely identifies a source IR/EC Entity.
* toAttr must contain SearchKey if toType = "ECEntity"
* toAttr must contain Locator if toType = "ECEntity"
*/
readonly toAttr: SearchKey | string;
readonly toAttr: Locator | string;

/*
* The type of the target entity.
* toAttr must contain a SearchKey if toType = "ECEntity"
* toAttr must contain a Locator if toType = "ECEntity"
*/
readonly toType: "ECEntity" | "IREntity";
readonly toType: "IREntity" | "ECEntity";

/*
* Definition of registered relationship class that extends an existing BIS Relationship class
Expand Down
31 changes: 18 additions & 13 deletions core/src/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class RepoTree {
this.nodeMap = new Map<string, Node>();
}

public getNodes(subjectKey: string): Array<SubjectNode | ModelNode | LoaderNode | ElementNode | RelationshipNode | RelatedElementNode> {
public getNodes<T extends Node>(subjectKey: string): Array<T> {
const nodes: any[] = [];
for (const node of this.nodeMap.values()) {
if (node instanceof SubjectNode && node.key === subjectKey)
Expand Down Expand Up @@ -435,12 +435,13 @@ export interface RelationshipNodeProps extends NodeProps {

/*
* References the source element
* This is not defined if dmo points to an EC Entity with Locator
*/
source: ElementNode;
source?: ElementNode;

/*
* References the target element
* This is not defined if dmo points to an EC Entity with SearchKey
* This is not defined if dmo points to an EC Entity with Locator
*/
target?: ElementNode;
}
Expand All @@ -449,15 +450,18 @@ export class RelationshipNode extends Node {

public subject: SubjectNode;
public dmo: RelationshipDMO;
public source: ElementNode;
public target?: ElementNode | undefined;
public source?: ElementNode;
public target?: ElementNode;

constructor(pc: PConnector, props: RelationshipNodeProps) {
super(pc, props);
this.subject = props.subject;
this.dmo = props.dmo;
this.source = props.source;
this.target = props.target;
if (props.source)
this.source = props.source;
if (props.target)
this.target = props.target;

this.pc.tree.insert<RelationshipNode>(this);
}

Expand All @@ -475,7 +479,7 @@ export class RelationshipNode extends Node {
const { ecRelationship } = this.dmo;
const classFullName = typeof ecRelationship === "string" ? ecRelationship : `${this.pc.dynamicSchemaName}:${ecRelationship.name}`;

const [sourceId, targetId] = pair;
const { sourceId, targetId } = pair;
const existing = this.pc.db.relationships.tryGetInstance(classFullName, { sourceId, targetId });
if (existing) {
resList.push({ entityId: existing.id, state: ItemState.Unchanged, comment: "" })
Expand All @@ -493,7 +497,7 @@ export class RelationshipNode extends Node {
}

public toJSON(): any {
return { key: this.key, subjectNode: this.subject.key, dmo: this.dmo, sourceNode: this.source.key, targetNode: this.target ? this.target.key : "" };
return { key: this.key, subjectNode: this.subject.key, dmo: this.dmo, sourceNode: this.source ? this.source.key : "", targetNode: this.target ? this.target.key : "" };
}
}

Expand All @@ -517,7 +521,7 @@ export interface RelatedElementNodeProps extends NodeProps {

/*
* References the target element in the relationship
* This is not defined if dmo points to an EC Entity with SearchKey
* This is not defined if dmo points to an EC Entity with Locator
*/
target?: ElementNode;
}
Expand All @@ -527,14 +531,15 @@ export class RelatedElementNode extends Node {
public subject: SubjectNode;
public dmo: RelatedElementDMO;
public source: ElementNode;
public target?: ElementNode | undefined;
public target?: ElementNode;

constructor(pc: PConnector, props: RelatedElementNodeProps) {
super(pc, props);
this.subject = props.subject;
this.dmo = props.dmo;
this.source = props.source;
this.target = props.target;
if (props.target)
this.target = props.target;
this.pc.tree.insert<RelatedElementNode>(this);
}

Expand All @@ -549,7 +554,7 @@ export class RelatedElementNode extends Node {
if (!pair)
continue;

const [sourceId, targetId] = pair;
const { sourceId, targetId } = pair;
const targetElement = this.pc.db.elements.getElement(targetId);

const { ecRelationship } = this.dmo;
Expand Down
37 changes: 22 additions & 15 deletions core/src/PConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,47 +419,54 @@ export abstract class PConnector extends IModelBridge {
}


public async getSourceTargetIdPair(node: pcf.RelatedElementNode | pcf.RelationshipNode, instance: pcf.IRInstance): Promise<string[] | void> {
public async getSourceTargetIdPair(node: pcf.RelatedElementNode | pcf.RelationshipNode, instance: pcf.IRInstance): Promise<{ sourceId: string, targetId: string } | undefined> {
if (!node.dmo.fromAttr || !node.dmo.toAttr)
return;

let sourceId: Id64String | undefined;
if (node.dmo.fromType === "IREntity") {
if (node.source && node.dmo.fromType === "IREntity") {
const sourceModelId = this.modelCache[node.source.model.key];
const sourceValue = instance.get(node.dmo.fromAttr);
if (!sourceValue)
return;
return undefined;
const sourceCode = this.getCode(node.source.dmo.irEntity, sourceModelId, sourceValue);
sourceId = this.db.elements.queryElementIdByCode(sourceCode);
} else if (node.dmo.fromType === "ECEntity") {
const res = await pcf.locateElement(this.db, instance.data[node.dmo.fromAttr]) as pcf.LocateResult;
if (res.error) {
Logger.logWarning(LogCategory.PCF, `Could not find the source EC entity for relationship instance = ${instance.key}: ${res.error}`);
return undefined;
}
sourceId = res.elementId;
}

let targetId: Id64String | undefined;
if (node.dmo.toType === "IREntity") {
const targetModelId = this.modelCache[node.target!.model.key];
if (node.target && node.dmo.toType === "IREntity") {
const targetModelId = this.modelCache[node.target.model.key];
const targetValue = instance.get(node.dmo.toAttr);
if (!targetValue)
return;
const targetCode = this.getCode(node.target!.dmo.irEntity, targetModelId, targetValue);
return undefined;
const targetCode = this.getCode(node.target.dmo.irEntity, targetModelId, targetValue);
targetId = this.db.elements.queryElementIdByCode(targetCode);
} else if (node.dmo.toType === "ECEntity") {
const result = await pcf.searchElement(this.db, instance.data[node.dmo.toAttr]) as pcf.SearchResult;
if (result.error) {
Logger.logWarning(LogCategory.PCF, `Could not find the target EC entity for relationship instance = ${instance.key}: ${result.error}`);
return;
const res = await pcf.locateElement(this.db, instance.data[node.dmo.toAttr]) as pcf.LocateResult;
if (res.error) {
Logger.logWarning(LogCategory.PCF, `Could not find the target EC entity for relationship instance = ${instance.key}: ${res.error}`);
return undefined;
}
targetId = result.elementId;
targetId = res.elementId;
}

if (!sourceId) {
Logger.logWarning(LogCategory.PCF, `Could not find the source IR entity for relationship instance = ${instance.key}`);
return;
return undefined;
}
if (!targetId) {
Logger.logWarning(LogCategory.PCF, `Could not find target IR entity for relationship instance = ${instance.key}`);
return;
return undefined;
}

return [sourceId, targetId];
return { sourceId, targetId };
}

public getCode(entityKey: string, modelId: Id64String, value: string): Code {
Expand Down
12 changes: 6 additions & 6 deletions core/src/Util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ export async function verifyIModel(db: IModelDb, qtc: QueryToCount): Promise<Mis
return mismatches;
}

export interface SearchResult {
export interface LocateResult {
error?: string;
elementId?: string;
ecsql?: string;
}

export async function searchElement(db: IModelDb, searchKey: string): Promise<SearchResult> {
export async function locateElement(db: IModelDb, locator: string): Promise<LocateResult> {

function isHex(hexstr: string): boolean {
const hexnum = parseInt(hexstr, 16);
Expand All @@ -62,12 +62,12 @@ export async function searchElement(db: IModelDb, searchKey: string): Promise<Se

let searchObj: {[ecProperty: string]: string | number} = {};
try {
const obj = JSON.parse(searchKey);
const obj = JSON.parse(locator);
for (const k of Object.keys(obj)) {
searchObj[k.toLowerCase()] = obj[k];
}
} catch(err) {
return { error: `Failed to parse SearchKey. Invalid syntax.` };
return { error: `Failed to parse Locator. Invalid syntax.` };
}

const table = "ecclassid" in searchObj ? searchObj.ecclassid : "bis.element";
Expand All @@ -90,13 +90,13 @@ export async function searchElement(db: IModelDb, searchKey: string): Promise<Se
try {
rows = await getRows(db, ecsql);
} catch (err) {
return { error: `At least one of the properties defined in SearchKey is unrecognized.`, ecsql };
return { error: `At least one of the properties defined in Locator is unrecognized.`, ecsql };
}

if (rows.length === 0)
return { error: "No target EC entity found.", ecsql };
if (rows.length > 1)
return { error: "More than one entity found. You should define a stricter rule in SearchKey to uniquely identify an EC target entity.", ecsql };
return { error: "More than one entity found. You should define a stricter rule in Locator to uniquely identify an EC target entity.", ecsql };

return { elementId: rows[0].id, ecsql };
}
Expand Down
1 change: 1 addition & 0 deletions core/src/test/ExpectedTestResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const TestResults: {[fileName: string]: QueryToCount} = {
"select * from TestSchema:ExtElementGroupsMembers": 0, // -1 (from v1)
"select * from TestSchema:ExtElementRefersToElements": 2, // +1 (from v1)
"select * from TestSchema:ExtElementRefersToExistingElements": 1, // +1 (from v1)
"select * from TestSchema:ExtExistingElementRefersToElements": 1,
// Domain Class
"select * from BuildingSpatial:Space": 1,
},
Expand Down
9 changes: 8 additions & 1 deletion core/src/test/JSONConnector/JSONConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class JSONConnector extends pcf.PConnector {
loader: new pcf.JSONLoader({
format: "json",
entities: ["ExtPhysicalElement", "ExtPhysicalType", "ExtGroupInformationElement", "ExtSpace", "ExtSpatialCategory"],
relationships: ["ExtPhysicalElement", "ExtElementRefersToElements", "ExtElementRefersToExistingElements", "ExtElementGroupsMembers"],
relationships: ["ExtPhysicalElement", "ExtElementRefersToElements", "ExtElementRefersToExistingElements", "ExtExistingElementRefersToElements", "ExtElementGroupsMembers"],
defaultPrimaryKey: "id",
}),
});
Expand Down Expand Up @@ -83,6 +83,13 @@ export class JSONConnector extends pcf.PConnector {
source: extPhysicalElement,
});

new pcf.RelationshipNode(this, {
key: "ExtExistingElementRefersToElements",
subject: subject1,
dmo: relationships.ExtExistingElementRefersToElements,
target: extPhysicalElement,
});

new pcf.RelationshipNode(this, {
key: "ExtElementGroupMembers",
subject: subject1,
Expand Down
32 changes: 30 additions & 2 deletions core/src/test/JSONConnector/dmos/Relationships.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const ExtElementRefersToExistingElements: RelationshipDMO = {
irEntity: "ExtElementRefersToExistingElements",
fromAttr: "ExtPhysicalElementKey",
fromType: "IREntity",
toAttr: "ExistingElementSearchKey",
toAttr: "ExistingElementLocator",
toType: "ECEntity",
ecRelationship: {
name: "ExtElementRefersToExistingElements",
Expand All @@ -55,13 +55,41 @@ export const ExtElementRefersToExistingElements: RelationshipDMO = {
target: {
polymorphic: true,
multiplicity: "(0..*)",
roleLabel: "To ExistingElementSearchKey",
roleLabel: "To ExistingElementLocator",
abstractConstraint: PhysicalElement.classFullName,
constraintClasses: [PhysicalElement.classFullName],
},
},
};

export const ExtExistingElementRefersToElements: RelationshipDMO = {
irEntity: "ExtExistingElementRefersToElements",
fromAttr: "ExistingElementLocator",
fromType: "ECEntity",
toAttr: "ExtPhysicalElementKey",
toType: "IREntity",
ecRelationship: {
name: "ExtExistingElementRefersToElements",
baseClass: ElementRefersToElements.classFullName,
strength: strengthToString(StrengthType.Referencing),
strengthDirection: strengthDirectionToString(StrengthDirection.Forward),
source: {
polymorphic: true,
multiplicity: "(0..*)",
roleLabel: "To ExistingElementLocator",
abstractConstraint: PhysicalElement.classFullName,
constraintClasses: [PhysicalElement.classFullName],
},
target: {
polymorphic: true,
multiplicity: "(0..*)",
roleLabel: "From ExtPhysicalElementKey",
abstractConstraint: PhysicalElement.classFullName,
constraintClasses: ["TestSchema:ExtPhysicalElement"],
},
},
};

export const ExtElementGroupsMembers: RelationshipDMO = {
irEntity: "ExtElementGroupsMembers",
fromAttr: "ExtGroupInformationElementKey",
Expand Down
9 changes: 8 additions & 1 deletion core/src/test/assets/v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,14 @@
{
"id": "1",
"ExtPhysicalElementKey": "2",
"ExistingElementSearchKey": "{\"ECClassId\": \"BisCore.PhysicalElement\", \"CodeValue\": \"ExtPhysicalElement-2\"}"
"ExistingElementLocator": "{\"ECClassId\": \"BisCore.PhysicalElement\", \"CodeValue\": \"ExtPhysicalElement-2\"}"
}
],
"ExtExistingElementRefersToElements": [
{
"id": "1",
"ExtPhysicalElementKey": "2",
"ExistingElementLocator": "{\"ECClassId\": \"BisCore.PhysicalElement\", \"CodeValue\": \"ExtPhysicalElement-2\"}"
}
],
"ExtElementGroupsMembers": [
Expand Down
9 changes: 8 additions & 1 deletion core/src/test/assets/v3.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,14 @@
{
"id": "1",
"ExtPhysicalElementKey": "2",
"ExistingElementSearchKey": "{\"ECClassId\": \"BisCore.PhysicalElement\", \"CodeValue\": \"ExtPhysicalElement-2\"}"
"ExistingElementLocator": "{\"ECClassId\": \"BisCore.PhysicalElement\", \"CodeValue\": \"ExtPhysicalElement-2\"}"
}
],
"ExtExistingElementRefersToElements": [
{
"id": "1",
"ExtPhysicalElementKey": "2",
"ExistingElementLocator": "{\"ECClassId\": \"BisCore.PhysicalElement\", \"CodeValue\": \"ExtPhysicalElement-2\"}"
}
],
"ExtElementGroupsMembers": [
Expand Down

0 comments on commit 0ee6f0b

Please sign in to comment.