-
Notifications
You must be signed in to change notification settings - Fork 2
Nodes in detail
This page documents the different node types in PCF. It contains everything you need to build an iModel.
The universe of an iModel has four fundamental components: elements, models, aspects, and the relationships that connect them.
Each node is responsible for exactly one IR entity in PCF and transforming its IR instances into EC instances. There are a few exceptions. Some nodes, like ModelNode
, stand for exactly one EC instance in the iModel, and aren't responsible for any IR entities. You'll know if a node is responsible for an IR entity if PCF makes you attach a DMO to it. The IR entity is tucked away in the DMO.
Each node also has a unique identifier that you need to fill in, called a key
. Usually it's a good idea to make this the same as the node's IR entity if it's responsible for one.
If you're intimately familiar with BIS and EC, you may notice that PCF has taken to calling all EC classes EC entities, even though an EC relationship is an EC class that is not an EC entity. This distinction isn't really important. For the scope of PCF, EC refers to the iModel thing and IR refers to the corresponding PCF thing. EC or Engineering Content is the underlying SQL language that powers BIS and other Bentley technologies.
Node type | DMOs |
---|---|
SubjectNode |
None |
ModelNode |
None |
ElementNode |
ElementDMO or ElementWithParentDMO
|
ModeledElementNode |
ElementDMO or ElementWithParentDMO
|
RelationshipNode |
RelationshipDMO |
RelatedElementNode |
RelatedElementDMO |
ElementAspectNode |
ElementAspectDMO |
LoaderNode |
None |
A subject node represents exactly one subject in an iModel, so you can't attach a DMO to it. You'll see this pattern throughout the rest of this page. If the node represents only one thing in the iModel, it doesn't accept DMOs.
Subjects are more constrained than subjects in BIS. This means that PCF can represent a subset of the iModels that BIS allows. Subjects in PCF cannot nest.
A model node represents exactly one top-level model in an iModel. A top-level model is one that forms a modeling perspective for the elements in a subject. In detail, it has a partition as its modeled element, and a subject as the partition's parent.
If you're brand new to BIS, these are the only models you need to know about.
Further reading:
- Top of the world by Casey Mullen
An element is something in the real world.
In detail, element nodes represent EC entities. Although we've used EC entities to mean iModel things in PCF's documentation, here I'm using the exact definition in EC.
PCF element nodes are different from BIS elements to keep the API simple for newcomers to digital twins. If you're familiar with BIS the mixed vocabulary may be confusing at first.
Every element node in PCF must have exactly one of two properties: either model
or parent
.
- The
model
property must point to a model node. Elements created from this element node will nest inside of the top-level model. - The
parent
property can point to either another element node or a modeled element node.- If the property points to another element node, elements created from the child element node will become a child of some specified element formed by the parent node. Which parent element is specified in the IR instances of the child element node using the
parentAttr
property in the DMO. You can specify the class of the embedding relationship in the element node. - If the property points to a modeled element node, elements created from the child element node will nest inside of the sub-model of some specified element formed by the modeled element node. Which modeled element is specified in the IR instances of the child element node using the
parentAttr
property in the DMO.
- If the property points to another element node, elements created from the child element node will become a child of some specified element formed by the parent node. Which parent element is specified in the IR instances of the child element node using the
If this sounds terribly confusing, it's because it's not fully explained and an example is coming! As long as you remember that a relationship between two nodes is actually many relationships between two classes of objects, you'll be good.
Sub-models in PCF behave like parent-child relationships. PCF assumes that a child element lives in the same model as its parent, so you can give an element a sub-model and pretend it's just a regular element. PCF imagines an iModel as a forest of trees dangling from each of the top-level models.
flowchart TD
19([model node]) --- 10 & 1(( ))
1 --- 2(( )) & 3(( ))
3 --- 4(( ))
10(( )) --- 11(( )) --- 12(( )) & 13(( ))
12 --- 15(( ))
13 --- 16(( ))
15 --- 17(( )) & 18(( ))
5([model node]) --- 6(( ))
6 --- 7(( )) & 8(( ))
9([subject node]) --- 5 & 19
To decide what DMO to attach to your element, ask yourself, does my element need a parent or a model? If it's the former, make an object of type ElementWithParentDMO
, otherwise make one of type ElementDMO
. The only difference between the two is ElementWithParentDMO
requires a parentAttr
property, which specifies the attribute of the IR instances in the source data that holds the key of the parent IR instance.
Let's do an example. Say you're using the JSON loader, so your IR instances look like this, divided into two IR entities: tree
and apple
. We've set the defaultPrimaryKey
in our loader to key
.
{
"tree": [
{
"key": "orchard-avenue",
"season": "fall"
},
{
"key": "pastoral-drive",
"season": "winter"
}
],
"apple": [
{
"key": "yellow apple",
"tree": "orchard-avenue",
"ripeness": "firm"
},
{
"key": "going-into-crosshatch-pie apple",
"tree": "orchard-avenue",
"ripeness": "just right"
},
{
"key": "green apple",
"tree": "orchard-avenue",
"ripeness": "brick"
}
]
}
We'll have two element nodes for these IR entities, because each element node is responsible for an IR entity and acts as the mold into which we pour its IR instances. We want our apple elements to be children of their trees. They are not redundant with the tree, but give the model more detail.
const tree = new pcf.ElementNode(this, {
key: "tree-node",
model: treeModel,
dmo: treeDMO,
});
const apple = new pcf.ElementNode(this, {
key: "apple-node",
parent: tree,
dmo: appleDMO,
});
const appleDMO: pcf.ElementDMO = {
irEntity: "apple",
ecElement: PhysicalObject.classFullName,
parentAttr: "tree",
// ...
};
The most confusing part of this endeavor is remembering that a parent relationship between the tree node and apple node is actually a relationship between these classes of objects (two sets of objects) in the source data. A node in PCF may stand for multiple objects! Note that all three of our apples become a child of the tree on Orchard Avenue, and the tree on Pastoral Drive has no apples because it's winter.
If you want to give an element a sub-model, use a modeled element node. It's the same as a regular element except that you have to give it the class of the model you want it to own.
In the diagram below, the tree on the left is a subtree of an iModel seen through the eyes of BIS. On the right is the same subtree as seen by PCF. The rectangles are models, and the circles elements. In the left subtree, the dashed element represents a modeled element. In the right subtree, that same element looks like it only contains children. The sub-model is abstracted away even though the effect is the same.
Please read the section on elements if this doesn't make sense yet.
flowchart LR
subgraph model [" "]
1(( )) --- 2(( )) --- 3(( )) & 4(( ))
3 --- sub-model
4 --- 7(( ))
subgraph sub-model [" "]
direction TB
6(( )) --- 8(( )) & 9(( ))
end
end
subgraph pcf [" "]
19([model node]) --- 10
10(( )) --- 11(( )) --- 12(( )) & 13(( ))
12 --- 15(( ))
13 --- 16(( ))
15 --- 17(( )) & 18(( ))
end
model --> pcf
classDef hide visibility:hidden;
classDef dashed stroke-dasharray:1;
class pcf hide;
class 3,12 dashed;
Relationship nodes in PCF map to link-table relationships in BIS. These are distinct from navigation properties, or foreign-key relationships. The iTwin libraries have an API for each. Link-table relationships are handled by the Relationship
class, while foreign-key relationships are handled by the RelatedElement
class.
In PCF, link-table relationships can't be moved once they're inserted. They also can't be updated if they have additional properties like those specified in a custom schema because PCF does not supply a modifyProps
for link-table relationships. The latter is an easy fix, the former not so much. If you're interested why, see the section on PCF's design below.
A relationship node takes an element node to act as the source of the relationship and an element node to act as the target. Because element nodes are each responsible for an IR entity, a relationship node defines a relationship between two IR entities, or more precisely, relationships between their IR instances. PCF is also capable of hanging relationships between EC instances that already exist in the iModel, but you should rarely need that capability.
So don't forget your source
and target
properties in the node! Otherwise PCF will think you're trying to relate EC instances.
For most purposes, to make your RelationshipDMO
, set the fromType
and toType
properties to 'IREntity'
. Then fill in the fromAttr
and toAttr
properties which allow PCF to find the keys of the IR instances you're relating in your source data. If you're using JSON data, say, fromAttr
and toAttr
are the property names that store these keys.
Limitations and design: A limitation of PCF is that link-table relationships can't be moved or updated once they're inserted into the iModel, unless the source or target elements are destroyed during the iModel pruning phase of synchronization. iTwin considers a link-table relationship to consist of three pieces: the class of the relationship, the source element, and the target element. If any part of this triple changes, like when you edit your RelationshipNode
, PCF will consider it a new relationship despite the relationship IR instance having a unique key stuck to it.
In my opinion, this behavior is counterintuitive and may be worth addressing in an update to PCF. The issue is implementation: fir
is a declarative synchronizer that supports moving relationships, but it does so by storing a map from each unique source identifier to its link-table relationship and misusing JSONProperties
so that it can identify when the relationship "nodes" (nodes in fir
are instances, not entities) change and find the old relationship in the iModel. This is neat, but may not be a worthwhile tradeoff. Relationships don't have provenance in the BIS spec like elements do, so when the source changes, there's no way to retrieve the stale relationship in the iModel without some trickery.
A better implementation is to use the speed of SQL to maintain this mapping, although I haven't yet figured out how to model metadata like this in BIS so I thought JSONProperties
is a better, if much slower, fit because it's designed for application data.
Casey on the difference between JSONProperties
and BIS:
In this manner the JsonProperties member can hold any form and complexity of externally-defined (i.e. outside of BIS) data on any element.
In the future, because it's tedious to specify 'IREntity'
twice for each relationship you want to define, fromType
and toType
should be optional.
The good news is that if you read the section on relationship nodes navigation properties are nearly the same.
Again don't forget your source
and target
properties in the node! Otherwise PCF will think you're trying to relate EC instances. Unless that's what you want to do.
The toAttr
refers to the attribute of the relationship IR instances in your source data that holds the key of the IR instance that will be receiving the foreign key.
Never mind, that's confusing. Let's try that again.
Take the relationship bis:ExternalSourceIsInRepository
. The source multiplicity is zero or more, and the target multiplicity is zero or one, from external sources to repository. If you think about it for a second this means that the external sources must eat the foreign key, because there are potentially infinitely many of them and only one repository. They must point at the repository, so our toAttr
attribute in the source data is going to hold keys of IR instances from an IR entity representing external sources.
There's one more thing we have to stick in our RelatedElementDMO
. That's the obtuse property ecProperty
. This means: What does the TypeScript API call this navigation property when we're trying to construct our target type, in our case, an ExternalSourceAspect
?
To answer this, we pull up the iTwin reference and search for ExternalSourceAspectProps
.
Sure enough, the navigation property that points at a repository is called source
, so we use that for ecProperty
.
Alternatively, you can consult the the BIS reference and look at the property name of the navigation property you want, but make sure it's compatible with the iTwin "props" type. We need source
and not Source
. JavaScript property names are case-sensitive.
Limitations and design: I still haven't figured out why the source
property of RelatedElementNode
isn't optional. I think you should also be able to use a Loader
for it.
PCF does not yet support bis:ElementMultiAspect
. We're sorry! If you're interested why, skim the section that follows on PCF's design.
bis:ElementUniqueAspect
are supported using ElementAspectNode
. This node accepts a DMO.
PCF 0.2
supported unique aspects by requiring the DMO to specify the ECInstanceId
of the element to attach to. This is still supported in 0.4
. There's an easier way now: specify the optional elementAttr
property in your DMO and the optional element
in your node.
The former is the attribute name in your IR instances that holds the source identifier of the element the aspect will attach to, and the latter points to an element node that specifies the IR entity that aspects created from your aspect node can attach to.
You may be wondering why these properties are optional. Every aspect must attach to some element. The only reason is to maintain compatibility with 0.2
. This is error-prone, however, because TypeScript won't save you from not specifying both elementAttr
and element
. PCF will give you an error at runtime.
Limitations and design: PCF does not currently support bis:ElementMultiAspect
's due to limitations of the iTwin libraries. Like relationships, aspects do not have provenance and there is no way to locate them with an identifier in the source data. However, unlike relationships, the iTwin APIs as of September 5, 2022 does not return the ECInstanceId
of the apsect you insert, so there's no way to implement an alternative to provenance like there is in link-table relationships, discussed above. Once you insert the aspect, the aspect is gone forever and you can't maintain a map from source identifiers to ECInstanceId
.
Optional properties in the node and DMO types lead to runtime errors when the configuration of the node isn't expected. The alternative was to make a separate DMO that mandated the element
property, and make ElementAspectNodeProps
an exclusive union type like ElementNode
, but unlike ElementWithParentDMO
, a DMO with an element
property does not warrant its own type because it's only different from ElementAspectDMO
in its API. I went with the optional property, but dropping support for the old API is best for type safety.
The loader node in PCF stores all the information about your IR model that PCF needs to know to hydrate it, including a loader object. The IR model consists of IR entities and IR instances, found in your source data. When you provide a job to your connector, you specify the key of the loader node you want to use as well as the key of the subject node you want to operate on, called the job subject in BIS lingo.
The loader object itself lists the keys of the IR entities to synchronize specified as string[]
in its entities
and relationships
properties, as well as the default primary key attribute of your IR instances if no mapping is given to the loader of entities to primary key attributes.
In the iModel, the loader node is transformed into a repository link, so loaders must always be placed in link models. Actually, BIS doesn't mind if you put them in the repository model, but PCF has no concept of a repository model. PCF writes information about the job to this repository, including the time and the loader's format and entities.
A word of caution: if enableDelete
is asserted in the connector's JobArgs
and you run your connector twice under the same subject, first with one loader, and then with another, PCF will forget about the elements it saw during the first pass and delete them on the second. This is not desirable, and we should support providing a sequence of loaders for these types of operations.