ariatosca-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From emblempar...@apache.org
Subject [3/3] incubator-ariatosca git commit: Add types to modeling, plus many fixes to models
Date Fri, 03 Mar 2017 02:15:44 GMT
Add types to modeling, plus many fixes to models


Project: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/commit/daa2d538
Tree: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/tree/daa2d538
Diff: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/diff/daa2d538

Branch: refs/heads/ARIA-105-integrate-modeling
Commit: daa2d538b590e38ff97dcec2e721a011c37b3e15
Parents: 58aac8d
Author: Tal Liron <tal.liron@gmail.com>
Authored: Thu Mar 2 20:15:12 2017 -0600
Committer: Tal Liron <tal.liron@gmail.com>
Committed: Thu Mar 2 20:15:12 2017 -0600

----------------------------------------------------------------------
 aria/VERSION.py                                 |   4 +-
 aria/__init__.py                                |  40 +-
 aria/cli/commands.py                            |   3 +-
 aria/exceptions.py                              |   4 +-
 aria/modeling/bases.py                          | 309 +++++----
 aria/modeling/misc.py                           | 208 ++++++
 aria/modeling/models.py                         | 163 +++--
 aria/modeling/orchestration.py                  | 166 ++---
 aria/modeling/service.py                        | 467 ++++++-------
 aria/modeling/service_template.py               | 664 ++++++++-----------
 aria/modeling/types.py                          |   6 +
 aria/orchestrator/__init__.py                   |   2 +-
 aria/orchestrator/workflows/api/task.py         |   4 +-
 aria/orchestrator/workflows/builtin/utils.py    |   1 -
 .../workflows/core/events_handler.py            |   2 +-
 aria/orchestrator/workflows/dry.py              |   2 +-
 aria/orchestrator/workflows/events_logging.py   |   2 +-
 aria/parser/consumption/style.py                |   2 +-
 aria/parser/modeling/__init__.py                |   8 +-
 aria/parser/modeling/context.py                 |  31 +-
 aria/storage/__init__.py                        |  10 +-
 aria/storage/core.py                            |  10 +-
 docs/requirements.txt                           |   4 +-
 .../simple_v1_0/modeling/__init__.py            | 418 ++++++------
 24 files changed, 1392 insertions(+), 1138 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/daa2d538/aria/VERSION.py
----------------------------------------------------------------------
diff --git a/aria/VERSION.py b/aria/VERSION.py
index 7e11072..9ce332c 100644
--- a/aria/VERSION.py
+++ b/aria/VERSION.py
@@ -14,8 +14,8 @@
 # limitations under the License.
 
 """
-Aria Version module:
-    * version: Aria Package version
+ARIA Version module:
+    * version: ARIA Package version
 """
 
 version = '0.1.0'  # pylint: disable=C0103

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/daa2d538/aria/__init__.py
----------------------------------------------------------------------
diff --git a/aria/__init__.py b/aria/__init__.py
index 7e5da07..4675fc8 100644
--- a/aria/__init__.py
+++ b/aria/__init__.py
@@ -14,7 +14,7 @@
 # limitations under the License.
 
 """
-Aria top level package
+ARIA top level package
 """
 
 import pkgutil
@@ -62,45 +62,9 @@ def application_model_storage(api, api_kwargs=None, initiator=None, initiator_kw
     """
     Initiate model storage
     """
-    models_to_register = [
-        modeling.models.ServiceTemplate,
-        modeling.models.NodeTemplate,
-        modeling.models.GroupTemplate,
-        modeling.models.PolicyTemplate,
-        modeling.models.SubstitutionTemplate,
-        modeling.models.SubstitutionTemplateMapping,
-        modeling.models.RequirementTemplate,
-        modeling.models.RelationshipTemplate,
-        modeling.models.CapabilityTemplate,
-        modeling.models.InterfaceTemplate,
-        modeling.models.OperationTemplate,
-        modeling.models.ArtifactTemplate,
-
-        modeling.models.Parameter,
-        modeling.models.Metadata,
-
-        modeling.models.Service,
-        modeling.models.Node,
-        modeling.models.Group,
-        modeling.models.Policy,
-        modeling.models.SubstitutionMapping,
-        modeling.models.Substitution,
-        modeling.models.Relationship,
-        modeling.models.Capability,
-        modeling.models.Interface,
-        modeling.models.Operation,
-        modeling.models.Artifact,
-
-        modeling.models.Execution,
-        modeling.models.ServiceUpdate,
-        modeling.models.ServiceUpdateStep,
-        modeling.models.ServiceModification,
-        modeling.models.Plugin,
-        modeling.models.Task
-    ]
     return storage.ModelStorage(api_cls=api,
                                 api_kwargs=api_kwargs,
-                                items=models_to_register,
+                                items=modeling.models.models_to_register,
                                 initiator=initiator,
                                 initiator_kwargs=initiator_kwargs or {})
 

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/daa2d538/aria/cli/commands.py
----------------------------------------------------------------------
diff --git a/aria/cli/commands.py b/aria/cli/commands.py
index 7ad85af..25a4e0d 100644
--- a/aria/cli/commands.py
+++ b/aria/cli/commands.py
@@ -260,8 +260,7 @@ class WorkflowCommand(BaseCommand):
     def _run(self, context, workflow_name, workflow_fn, inputs):
         # Storage
         def _initialize_storage(model_storage):
-            model_storage.service_template.put(context.modeling.template)
-            model_storage.service.put(context.modeling.instance)
+            context.modeling.store(model_storage)
 
         # Create runner
         runner = Runner(workflow_name, workflow_fn, inputs, _initialize_storage,

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/daa2d538/aria/exceptions.py
----------------------------------------------------------------------
diff --git a/aria/exceptions.py b/aria/exceptions.py
index 28f8be9..0a60f01 100644
--- a/aria/exceptions.py
+++ b/aria/exceptions.py
@@ -14,8 +14,8 @@
 # limitations under the License.
 
 """
-Aria exceptions module
-Every sub-package in Aria has a module with its exceptions.
+ARIA exceptions module
+Every sub-package in ARIA has a module with its exceptions.
 aria.exceptions module conveniently collects all these exceptions for easier imports.
 """
 

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/daa2d538/aria/modeling/bases.py
----------------------------------------------------------------------
diff --git a/aria/modeling/bases.py b/aria/modeling/bases.py
index 0e583cf..c1b2f7a 100644
--- a/aria/modeling/bases.py
+++ b/aria/modeling/bases.py
@@ -53,15 +53,15 @@ class ModelMixin(object):
         raise NotImplementedError
 
     @classmethod
-    def foreign_key(cls, parent_table_name, nullable=False):
+    def foreign_key(cls, parent_table, nullable=False):
         """
         Return a ForeignKey object.
 
-        :param parent_table_name: Parent table name
+        :param parent_table: Parent table name
         :param nullable: Should the column be allowed to remain empty
         """
         return Column(Integer,
-                      ForeignKey('{table}.id'.format(table=parent_table_name),
+                      ForeignKey('{table}.id'.format(table=parent_table),
                                  ondelete='CASCADE'),
                       nullable=nullable)
 
@@ -71,68 +71,75 @@ class ModelMixin(object):
                              relationship_kwargs=None):
         relationship_kwargs = relationship_kwargs or {}
 
-        remote_side_str = '{cls}.{remote_column}'.format(
+        remote_side = '{cls}.{remote_column}'.format(
             cls=cls.__name__,
             remote_column=cls.id_column_name()
         )
 
-        primaryjoin_str = '{remote_side_str} == {cls}.{column}'.format(
-            remote_side_str=remote_side_str,
+        primaryjoin = '{remote_side} == {cls}.{column}'.format(
+            remote_side=remote_side,
             cls=cls.__name__,
             column=column_name
         )
 
         return relationship(
             cls._get_cls_by_tablename(cls.__tablename__).__name__,
-            primaryjoin=primaryjoin_str,
-            remote_side=remote_side_str,
+            primaryjoin=primaryjoin,
+            remote_side=remote_side,
             post_update=True,
             **relationship_kwargs
         )
 
     @classmethod
+    def one_to_many_relationship_to_self(cls,
+                                         key,
+                                         dict_key=None,
+                                         relationship_kwargs=None):
+        relationship_kwargs = relationship_kwargs or {}
+
+        relationship_kwargs.setdefault('remote_side', '{cls}.{remote_column}'.format(
+            cls=cls.__name__,
+            remote_column=key
+        ))
+
+        return cls._create_relationship(cls.__tablename__, None, relationship_kwargs,
+                                        backreference='', dict_key=dict_key)
+
+    @classmethod
     def one_to_one_relationship(cls,
-                                other_table_name,
+                                other_table,
+                                key=None,
+                                foreign_key=None,
                                 backreference=None,
+                                backref_kwargs=None,
                                 relationship_kwargs=None):
-        relationship_kwargs = relationship_kwargs or {}
-
-        return relationship(
-            lambda: cls._get_cls_by_tablename(other_table_name),
-            backref=backref(backreference or cls.__tablename__, uselist=False),
-            **relationship_kwargs
-        )
+        backref_kwargs = backref_kwargs or {}
+        backref_kwargs.setdefault('uselist', False)
 
+        return cls._create_relationship(other_table, backref_kwargs, relationship_kwargs,
+                                        backreference, key=key, foreign_key=foreign_key)
+    
     @classmethod
     def one_to_many_relationship(cls,
-                                 child_table_name,
-                                 foreign_key_name=None,
+                                 child_table,
+                                 key=None,
+                                 foreign_key=None,
+                                 dict_key=None,
                                  backreference=None,
-                                 key_column_name=None,
+                                 backref_kwargs=None,
                                  relationship_kwargs=None):
-        relationship_kwargs = relationship_kwargs or {}
-
-        foreign_keys = lambda: getattr(cls._get_cls_by_tablename(child_table_name),
-                                       foreign_key_name) \
-            if foreign_key_name \
-            else None
-
-        collection_class = attribute_mapped_collection(key_column_name) \
-            if key_column_name \
-            else list
+        backref_kwargs = backref_kwargs or {}
+        backref_kwargs.setdefault('uselist', False)
 
-        return relationship(
-            lambda: cls._get_cls_by_tablename(child_table_name),
-            backref=backref(backreference or cls.__tablename__, uselist=False),
-            foreign_keys=foreign_keys,
-            collection_class=collection_class,
-            **relationship_kwargs
-        )
+        return cls._create_relationship(child_table, backref_kwargs, relationship_kwargs,
+                                        backreference, key=key, foreign_key=foreign_key,
+                                        dict_key=dict_key)
 
     @classmethod
     def many_to_one_relationship(cls,
-                                 parent_table_name,
-                                 foreign_key_column=None,
+                                 parent_table,
+                                 key=None,
+                                 foreign_key=None,
                                  backreference=None,
                                  backref_kwargs=None,
                                  relationship_kwargs=None):
@@ -140,33 +147,32 @@ class ModelMixin(object):
         Return a one-to-many SQL relationship object
         Meant to be used from inside the *child* object
 
-        :param parent_class: Class of the parent table
-        :param cls: Class of the child table
-        :param foreign_key_column: The column of the foreign key (from the child table)
+        :param parent_table: Name of the parent table
+        :param foreign_key: The column of the foreign key (from the child table)
         :param backreference: The name to give to the reference to the child (on the parent table)
         """
-        relationship_kwargs = relationship_kwargs or {}
 
-        if foreign_key_column:
-            relationship_kwargs.setdefault('foreign_keys', getattr(cls, foreign_key_column))
+        if backreference is None:
+            backreference = utils.pluralize(cls.__tablename__)
 
         backref_kwargs = backref_kwargs or {}
+        backref_kwargs.setdefault('uselist', True)
         backref_kwargs.setdefault('lazy', 'dynamic')
         # The following line make sure that when the *parent* is deleted, all its connected children
         # are deleted as well
         backref_kwargs.setdefault('cascade', 'all')
 
-        return relationship(
-            lambda: cls._get_cls_by_tablename(parent_table_name),
-            backref=backref(backreference or utils.pluralize(cls.__tablename__), **backref_kwargs),
-            **relationship_kwargs
-        )
+        return cls._create_relationship(parent_table, backref_kwargs, relationship_kwargs,
+                                        backreference, key=key, foreign_key=foreign_key)
 
     @classmethod
     def many_to_many_relationship(cls,
-                                  other_table_name,
+                                  other_table,
                                   table_prefix=None,
-                                  key_column_name=None,
+                                  key=None,
+                                  dict_key=None,
+                                  backreference=None,
+                                  backref_kwargs=None,
                                   relationship_kwargs=None):
         """
         Return a many-to-many SQL relationship object
@@ -177,80 +183,41 @@ class ModelMixin(object):
         2. This method creates a new helper table in the DB
 
         :param cls: The class of the table we're connecting from
-        :param other_table_name: The class of the table we're connecting to
-        :param table_prefix: Custom prefix for the helper table name and the
-                             backreference name
-        :param key_column_name: If provided, will use a dict class with this column as the key
+        :param other_table: The class of the table we're connecting to
+        :param table_prefix: Custom prefix for the helper table name and the backreference name
+        :param dict_key: If provided, will use a dict class with this column as the key
         """
-        relationship_kwargs = relationship_kwargs or {}
 
-        current_table_name = cls.__tablename__
-        current_column_name = '{0}_id'.format(current_table_name)
-        current_foreign_key = '{0}.id'.format(current_table_name)
+        this_table = cls.__tablename__
+        this_column_name = '{0}_id'.format(this_table)
+        this_foreign_key = '{0}.id'.format(this_table)
+
+        other_column_name = '{0}_id'.format(other_table)
+        other_foreign_key = '{0}.id'.format(other_table)
 
-        other_column_name = '{0}_id'.format(other_table_name)
-        other_foreign_key = '{0}.id'.format(other_table_name)
+        helper_table = '{0}_{1}'.format(this_table, other_table)
 
-        helper_table_name = '{0}_{1}'.format(current_table_name, other_table_name)
+        if backreference is None:
+            backreference = this_table
+            if table_prefix:
+                helper_table = '{0}_{1}'.format(table_prefix, helper_table)
+                backreference = '{0}_{1}'.format(table_prefix, backreference)
 
-        backref_name = current_table_name
-        if table_prefix:
-            helper_table_name = '{0}_{1}'.format(table_prefix, helper_table_name)
-            backref_name = '{0}_{1}'.format(table_prefix, backref_name)
+        backref_kwargs = backref_kwargs or {}
+        backref_kwargs.setdefault('uselist', True)
 
-        secondary_table = cls.get_secondary_table(
+        relationship_kwargs = relationship_kwargs or {}
+        relationship_kwargs.setdefault('secondary', cls._get_secondary_table(
             cls.metadata,
-            helper_table_name,
-            current_column_name,
+            helper_table,
+            this_column_name,
             other_column_name,
-            current_foreign_key,
+            this_foreign_key,
             other_foreign_key
-        )
-
-        collection_class = attribute_mapped_collection(key_column_name) \
-            if key_column_name \
-            else list
+        ))
 
-        return relationship(
-            lambda: cls._get_cls_by_tablename(other_table_name),
-            secondary=secondary_table,
-            backref=backref(backref_name),
-            collection_class=collection_class,
-            **relationship_kwargs
-        )
-
-    @staticmethod
-    def get_secondary_table(metadata,
-                            helper_table_name,
-                            first_column_name,
-                            second_column_name,
-                            first_foreign_key,
-                            second_foreign_key):
-        """
-        Create a helper table for a many-to-many relationship
-
-        :param helper_table_name: The name of the table
-        :param first_column_name: The name of the first column in the table
-        :param second_column_name: The name of the second column in the table
-        :param first_foreign_key: The string representing the first foreign key,
-               for example `blueprint.storage_id`, or `tenants.id`
-        :param second_foreign_key: The string representing the second foreign key
-        :return: A Table object
-        """
-        return Table(
-            helper_table_name,
-            metadata,
-            Column(
-                first_column_name,
-                Integer,
-                ForeignKey(first_foreign_key)
-            ),
-            Column(
-                second_column_name,
-                Integer,
-                ForeignKey(second_foreign_key)
-            )
-        )
+        return cls._create_relationship(other_table, backref_kwargs, relationship_kwargs,
+                                        backreference, key=key, dict_key=dict_key)
 
     def to_dict(self, fields=None, suppress_error=False):
         """
@@ -260,6 +227,7 @@ class ModelMixin(object):
                                it's unable to retrieve (e.g., if a relationship wasn't established
                                yet, and so it's impossible to access a property through it)
         """
+
         res = dict()
         fields = fields or self.fields()
         for field in fields:
@@ -281,6 +249,76 @@ class ModelMixin(object):
         return res
 
     @classmethod
+    def _create_relationship(cls, table, backref_kwargs, relationship_kwargs, backreference,
+                             key=None, foreign_key=None, dict_key=None):
+        relationship_kwargs = relationship_kwargs or {}
+
+        if key:
+            relationship_kwargs.setdefault('foreign_keys',
+                                           lambda: getattr(
+                                               cls._get_cls_by_tablename(cls.__tablename__),
+                                               key))
+
+        elif foreign_key:
+            relationship_kwargs.setdefault('foreign_keys',
+                                           lambda: getattr(
+                                               cls._get_cls_by_tablename(table),
+                                               foreign_key))
+
+        if dict_key:
+            relationship_kwargs.setdefault('collection_class',
+                                           attribute_mapped_collection(dict_key))
+
+        if backreference == '':
+            return relationship(
+                lambda: cls._get_cls_by_tablename(table),
+                **relationship_kwargs
+            )
+        else:
+            if backreference is None:
+                backreference = cls.__tablename__
+            backref_kwargs = backref_kwargs or {}
+            return relationship(
+                lambda: cls._get_cls_by_tablename(table),
+                backref=backref(backreference, **backref_kwargs),
+                **relationship_kwargs
+            )
+
+    @staticmethod
+    def _get_secondary_table(metadata,
+                             helper_table,
+                             first_column,
+                             second_column,
+                             first_foreign_key,
+                             second_foreign_key):
+        """
+        Create a helper table for a many-to-many relationship
+
+        :param helper_table: The name of the table
+        :param first_column_name: The name of the first column in the table
+        :param second_column_name: The name of the second column in the table
+        :param first_foreign_key: The string representing the first foreign key,
+               for example `blueprint.storage_id`, or `tenants.id`
+        :param second_foreign_key: The string representing the second foreign key
+        :return: A Table object
+        """
+
+        return Table(
+            helper_table,
+            metadata,
+            Column(
+                first_column,
+                Integer,
+                ForeignKey(first_foreign_key)
+            ),
+            Column(
+                second_column,
+                Integer,
+                ForeignKey(second_foreign_key)
+            )
+        )
+
+    @classmethod
     def _association_proxies(cls):
         for col, value in vars(cls).items():
             if isinstance(value, associationproxy.AssociationProxy):
@@ -293,6 +331,7 @@ class ModelMixin(object):
 
         Mostly for backwards compatibility in the code (that uses `fields`)
         """
+
         fields = set(cls._association_proxies())
         fields.update(cls.__table__.columns.keys())
         return fields - set(getattr(cls, '__private_fields__', []))
@@ -302,9 +341,10 @@ class ModelMixin(object):
         """
         Return class reference mapped to table.
 
-         :param tablename: String with name of table.
-         :return: Class reference or None.
-         """
+        :param tablename: String with name of table.
+        :return: Class reference or None.
+        """
+
         if tablename in (cls.__name__, cls.__tablename__):
             return cls
 
@@ -329,3 +369,36 @@ class ModelIDMixin(object):
     @classmethod
     def name_column_name(cls):
         return 'name'
+
+
+class InstanceModelMixin(ModelMixin):
+    """
+    Mixin for :class:`ServiceInstance` models.
+
+    All models support validation, diagnostic dumping, and representation as
+    raw data (which can be translated into JSON or YAML) via :code:`as_raw`.
+    """
+
+    @property
+    def as_raw(self):
+        raise NotImplementedError
+
+    def validate(self, context):
+        pass
+
+    def coerce_values(self, context, container, report_issues):
+        pass
+
+    def dump(self, context):
+        pass
+
+
+class TemplateModelMixin(InstanceModelMixin):
+    """
+    Mixin for :class:`ServiceTemplate` models.
+
+    All model models can be instantiated into :class:`ServiceInstance` models.
+    """
+
+    def instantiate(self, context, container):
+        raise NotImplementedError

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/daa2d538/aria/modeling/misc.py
----------------------------------------------------------------------
diff --git a/aria/modeling/misc.py b/aria/modeling/misc.py
new file mode 100644
index 0000000..0b50f9a
--- /dev/null
+++ b/aria/modeling/misc.py
@@ -0,0 +1,208 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import cPickle as pickle
+
+from sqlalchemy import (
+    Column,
+    Text,
+    Binary
+)
+from sqlalchemy.ext.declarative import declared_attr
+
+from ..storage import exceptions
+from ..parser.modeling import utils as parser_utils
+from ..utils import collections, formatting, console
+from .bases import InstanceModelMixin, TemplateModelMixin
+
+
+class ParameterBase(TemplateModelMixin):
+    """
+    Represents a typed value.
+
+    This class is used by both service template and service instance elements.
+
+    :ivar name: Name
+    :ivar type_name: Type name
+    :ivar value: Value
+    :ivar description: Description
+    """
+
+    __tablename__ = 'parameter'
+
+    name = Column(Text, nullable=False)
+    type_name = Column(Text, nullable=False)
+
+    # Check: value type
+    _value = Column(Binary, nullable=True)
+    description = Column(Text)
+
+    @property
+    def as_raw(self):
+        return collections.OrderedDict((
+            ('name', self.name),
+            ('type_name', self.type_name),
+            ('value', self.value),
+            ('description', self.description)))
+
+    @property
+    def value(self):
+        if self._value is None:
+            return None
+        try:
+            return pickle.loads(self._value)
+        except BaseException:
+            raise exceptions.StorageError('Bad format for parameter of type "{0}": {1}'.format(
+                self.type_name, self._value))
+
+    @value.setter
+    def value(self, value):
+        if value is None:
+            self._value = None
+        else:
+            try:
+                self._value = pickle.dumps(value)
+            except pickle.PicklingError:
+                # TODO debug log
+                self._value = pickle.dumps(str(value))
+ 
+    def instantiate(self, context, container):
+        from . import models
+        return models.Parameter(name=self.name,
+                                type_name=self.type_name,
+                                _value=self._value,
+                                description=self.description)
+
+    def coerce_values(self, context, container, report_issues):
+        if self.value is not None:
+            self.value = parser_utils.coerce_value(context, container, self.value,
+                                                   report_issues)
+
+
+class TypeBase(InstanceModelMixin):
+    """
+    Represents a type and its children.
+    """
+
+    __tablename__ = 'type'
+
+    variant = Column(Text) 
+    description = Column(Text)
+    role = Column(Text)
+
+    @declared_attr
+    def parent(cls):
+        return cls.relationship_to_self('parent_type_fk')
+
+    @declared_attr
+    def children(cls):
+        return cls.one_to_many_relationship_to_self('parent_type_fk')
+
+    # region foreign keys
+
+    __private_fields__ = ['parent_type_fk']
+
+    # Type one-to-many to Type
+    @declared_attr
+    def parent_type_fk(cls):
+        return cls.foreign_key('type', nullable=True)
+
+    # endregion
+
+    def is_descendant(self, base_name, name):
+        base = self.get_descendant(base_name)
+        if base is not None:
+            if base.get_descendant(name) is not None:
+                return True
+        return False
+
+    def get_descendant(self, name):
+        if self.name == name:
+            return self
+        for child in self.children:
+            found = child.get_descendant(name)
+            if found is not None:
+                return found
+        return None
+
+    def iter_descendants(self):
+        for child in self.children:
+            yield child
+            for descendant in child.iter_descendants():
+                yield descendant
+
+    def get_role(self, name):
+        def _get_role(the_type):
+            if the_type is None:
+                return None
+            elif the_type.role is None:
+                return _get_role(self.parent)
+            return the_type.role
+
+        return _get_role(self.get_descendant(name))
+
+    @property
+    def as_raw(self):
+        return collections.OrderedDict((
+            ('name', self.name),
+            ('description', self.description),
+            ('role', self.role)))
+
+    @property
+    def as_raw_all(self):
+        types = []
+        self._append_raw_children(types)
+        return types
+
+    def dump(self, context):
+        if self.name:
+            console.puts(context.style.type(self.name))
+        with context.style.indent:
+            for child in self.children:
+                child.dump(context)
+
+    def _append_raw_children(self, types):
+        for child in self.children:
+            raw_child = formatting.as_raw(child)
+            raw_child['parent'] = self.name
+            types.append(raw_child)
+            child._append_raw_children(types)
+
+
+class MetadataBase(TemplateModelMixin):
+    """
+    Custom values associated with the service.
+
+    This class is used by both service template and service instance elements.
+
+    :ivar name: Name
+    :ivar value: Value
+    """
+
+    __tablename__ = 'metadata'
+
+    name = Column(Text, nullable=False)
+    value = Column(Text)
+
+    @property
+    def as_raw(self):
+        return collections.OrderedDict((
+            ('name', self.name),
+            ('value', self.value)))
+
+    def instantiate(self, context, container):
+        from . import models
+        return models.Metadata(name=self.name,
+                               value=self.value)

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/daa2d538/aria/modeling/models.py
----------------------------------------------------------------------
diff --git a/aria/modeling/models.py b/aria/modeling/models.py
index b424dea..fc2c669 100644
--- a/aria/modeling/models.py
+++ b/aria/modeling/models.py
@@ -13,62 +13,21 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+# pylint: disable=abstract-method
+
 from sqlalchemy.ext.declarative import declarative_base
 
 from . import (
     service_template,
     service,
     orchestration,
+    misc,
     bases,
 )
 
-__all__ = (
-    'aria_declarative_base',
-
-    # Service template models
-    'ServiceTemplate',
-    'NodeTemplate',
-    'GroupTemplate',
-    'PolicyTemplate',
-    'SubstitutionTemplate',
-    'SubstitutionTemplateMapping',
-    'RequirementTemplate',
-    'RelationshipTemplate',
-    'CapabilityTemplate',
-    'InterfaceTemplate',
-    'OperationTemplate',
-    'ArtifactTemplate',
-
-    # Service template and instance models
-    'Parameter',
-    'Metadata',
-
-    # Service instance models
-    'Service',
-    'Node',
-    'Group',
-    'Policy',
-    'Substitution',
-    'SubstitutionMapping',
-    'Relationship',
-    'Capability',
-    'Interface',
-    'Operation',
-    'Artifact',
-
-    # Orchestrator models
-    'Execution',
-    'ServiceUpdate',
-    'ServiceUpdateStep',
-    'ServiceModification',
-    'Plugin',
-    'Task'
-)
 
 aria_declarative_base = declarative_base(cls=bases.ModelIDMixin)
 
-# pylint: disable=abstract-method
-
 
 # region service template models
 
@@ -123,18 +82,6 @@ class ArtifactTemplate(aria_declarative_base, service_template.ArtifactTemplateB
 # endregion
 
 
-# region service template and instance models
-
-class Parameter(aria_declarative_base, service_template.ParameterBase):
-    pass
-
-
-class Metadata(aria_declarative_base, service_template.MetadataBase):
-    pass
-
-# endregion
-
-
 # region service instance models
 
 class Service(aria_declarative_base, service.ServiceBase):
@@ -209,3 +156,107 @@ class Task(aria_declarative_base, orchestration.TaskBase):
     pass
 
 # endregion
+
+
+# region misc models
+
+class Parameter(aria_declarative_base, misc.ParameterBase):
+    pass
+
+
+class Type(aria_declarative_base, misc.TypeBase):
+    pass
+
+
+class Metadata(aria_declarative_base, misc.MetadataBase):
+    pass
+
+# endregion
+
+
+models_to_register = [
+    # Service template models
+    ServiceTemplate,
+    NodeTemplate,
+    GroupTemplate,
+    PolicyTemplate,
+    SubstitutionTemplate,
+    SubstitutionTemplateMapping,
+    RequirementTemplate,
+    RelationshipTemplate,
+    CapabilityTemplate,
+    InterfaceTemplate,
+    OperationTemplate,
+    ArtifactTemplate,
+
+    # Service instance models
+    Service,
+    Node,
+    Group,
+    Policy,
+    SubstitutionMapping,
+    Substitution,
+    Relationship,
+    Capability,
+    Interface,
+    Operation,
+    Artifact,
+
+    # Orchestration models
+    Execution,
+    ServiceUpdate,
+    ServiceUpdateStep,
+    ServiceModification,
+    Plugin,
+    Task,
+
+    # Misc models
+    Parameter,
+    Type,
+    Metadata
+]
+
+__all__ = (
+    'aria_declarative_base',
+    'models_to_register',
+
+    # Service template models
+    'ServiceTemplate',
+    'NodeTemplate',
+    'GroupTemplate',
+    'PolicyTemplate',
+    'SubstitutionTemplate',
+    'SubstitutionTemplateMapping',
+    'RequirementTemplate',
+    'RelationshipTemplate',
+    'CapabilityTemplate',
+    'InterfaceTemplate',
+    'OperationTemplate',
+    'ArtifactTemplate',
+
+    # Service instance models
+    'Service',
+    'Node',
+    'Group',
+    'Policy',
+    'Substitution',
+    'SubstitutionMapping',
+    'Relationship',
+    'Capability',
+    'Interface',
+    'Operation',
+    'Artifact',
+
+    # Orchestration models
+    'Execution',
+    'ServiceUpdate',
+    'ServiceUpdateStep',
+    'ServiceModification',
+    'Plugin',
+    'Task',
+
+    # Misc models
+    'Parameter',
+    'Type',
+    'Metadata'
+)

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/daa2d538/aria/modeling/orchestration.py
----------------------------------------------------------------------
diff --git a/aria/modeling/orchestration.py b/aria/modeling/orchestration.py
index 6b5609f..9cbb3cf 100644
--- a/aria/modeling/orchestration.py
+++ b/aria/modeling/orchestration.py
@@ -27,6 +27,9 @@ classes:
     * Plugin - plugin implementation model.
     * Task - a task
 """
+
+# pylint: disable=no-self-argument, no-member, abstract-method
+
 from collections import namedtuple
 from datetime import datetime
 
@@ -57,16 +60,13 @@ __all__ = (
     'TaskBase'
 )
 
-# pylint: disable=no-self-argument, no-member, abstract-method
-
 
 class Execution(ModelMixin):
     """
     Execution model representation.
     """
-    __tablename__ = 'execution' # redundancy for PyLint: SqlAlchemy injects this
 
-    __private_fields__ = ['service_fk']
+    __tablename__ = 'execution'
 
     TERMINATED = 'terminated'
     FAILED = 'failed'
@@ -129,6 +129,8 @@ class Execution(ModelMixin):
 
     # region foreign keys
 
+    __private_fields__ = ['service_fk']
+
     @declared_attr
     def service_fk(cls):
         return cls.foreign_key('service')
@@ -147,13 +149,10 @@ class ServiceUpdateBase(ModelMixin):
     """
     Deployment update model representation.
     """
-    # Needed only for pylint. the id will be populated by sqlalcehmy and the proper column.
+
     steps = None
 
-    __tablename__ = 'service_update'  # redundancy for PyLint: SqlAlchemy injects this
-
-    __private_fields__ = ['service_fk',
-                          'execution_fk']
+    __tablename__ = 'service_update' 
 
     _private_fields = ['execution_fk', 'deployment_fk']
 
@@ -183,6 +182,9 @@ class ServiceUpdateBase(ModelMixin):
 
     # region foreign keys
 
+    __private_fields__ = ['service_fk',
+                          'execution_fk']
+
     @declared_attr
     def execution_fk(cls):
         return cls.foreign_key('execution', nullable=True)
@@ -204,9 +206,8 @@ class ServiceUpdateStepBase(ModelMixin):
     """
     Deployment update step model representation.
     """
-    __tablename__ = 'service_update_step' # redundancy for PyLint: SqlAlchemy injects this
 
-    __private_fields__ = ['service_update_fk']
+    __tablename__ = 'service_update_step'
 
     _action_types = namedtuple('ACTION_TYPES', 'ADD, REMOVE, MODIFY')
     ACTION_TYPES = _action_types(ADD='add', REMOVE='remove', MODIFY='modify')
@@ -243,6 +244,8 @@ class ServiceUpdateStepBase(ModelMixin):
 
     # region foreign keys
 
+    __private_fields__ = ['service_update_fk']
+
     @declared_attr
     def service_update_fk(cls):
         return cls.foreign_key('service_update')
@@ -281,9 +284,8 @@ class ServiceModificationBase(ModelMixin):
     """
     Deployment modification model representation.
     """
-    __tablename__ = 'service_modification' # redundancy for PyLint: SqlAlchemy injects this
 
-    __private_fields__ = ['service_fk']
+    __tablename__ = 'service_modification'
 
     STARTED = 'started'
     FINISHED = 'finished'
@@ -300,10 +302,6 @@ class ServiceModificationBase(ModelMixin):
     status = Column(Enum(*STATES, name='deployment_modification_status'))
 
     @declared_attr
-    def service_fk(cls):
-        return cls.foreign_key('service')
-
-    @declared_attr
     def service(cls):
         return cls.many_to_one_relationship('service',
                                             backreference='modifications')
@@ -312,14 +310,23 @@ class ServiceModificationBase(ModelMixin):
     def service_name(cls):
         return association_proxy('service', cls.name_column_name())
 
+    # region foreign keys
+
+    __private_fields__ = ['service_fk']
+
+    @declared_attr
+    def service_fk(cls):
+        return cls.foreign_key('service')
+
+    # endregion
+
 
 class PluginBase(ModelMixin):
     """
     Plugin model representation.
     """
-    __tablename__ = 'plugin' # redundancy for PyLint: SqlAlchemy injects this
 
-    __private_fields__ = ['service_template_fk']
+    __tablename__ = 'plugin'
 
     archive_name = Column(Text, nullable=False, index=True)
     distribution = Column(Text)
@@ -335,6 +342,8 @@ class PluginBase(ModelMixin):
 
     # region foreign keys
 
+    __private_fields__ = ['service_template_fk']
+
     @declared_attr
     def service_template_fk(cls):
         return cls.foreign_key('service_template', nullable=True)
@@ -346,12 +355,33 @@ class TaskBase(ModelMixin):
     """
     A Model which represents an task
     """
-    __tablename__ = 'task' # redundancy for PyLint: SqlAlchemy injects this
 
-    __private_fields__ = ['node_fk',
-                          'relationship_fk',
-                          'plugin_fk',
-                          'execution_fk']
+    __tablename__ = 'task'
+
+    PENDING = 'pending'
+    RETRYING = 'retrying'
+    SENT = 'sent'
+    STARTED = 'started'
+    SUCCESS = 'success'
+    FAILED = 'failed'
+    STATES = (
+        PENDING,
+        RETRYING,
+        SENT,
+        STARTED,
+        SUCCESS,
+        FAILED,
+    )
+
+    WAIT_STATES = [PENDING, RETRYING]
+    END_STATES = [SUCCESS, FAILED]
+
+    RUNS_ON_SOURCE = 'source'
+    RUNS_ON_TARGET = 'target'
+    RUNS_ON_NODE = 'node'
+    RUNS_ON = (RUNS_ON_NODE, RUNS_ON_SOURCE, RUNS_ON_TARGET)
+
+    INFINITE_RETRIES = -1
 
     @declared_attr
     def node_name(cls):
@@ -384,60 +414,7 @@ class TaskBase(ModelMixin):
     @declared_attr
     def inputs(cls):
         return cls.many_to_many_relationship('parameter', table_prefix='inputs',
-                                             key_column_name='name')
-
-    # region foreign keys
-
-    @declared_attr
-    def node_fk(cls):
-        return cls.foreign_key('node', nullable=True)
-
-    @declared_attr
-    def relationship_fk(cls):
-        return cls.foreign_key('relationship', nullable=True)
-
-    @declared_attr
-    def plugin_fk(cls):
-        return cls.foreign_key('plugin', nullable=True)
-
-    @declared_attr
-    def execution_fk(cls):
-        return cls.foreign_key('execution', nullable=True)
-
-    # endregion
-
-    PENDING = 'pending'
-    RETRYING = 'retrying'
-    SENT = 'sent'
-    STARTED = 'started'
-    SUCCESS = 'success'
-    FAILED = 'failed'
-    STATES = (
-        PENDING,
-        RETRYING,
-        SENT,
-        STARTED,
-        SUCCESS,
-        FAILED,
-    )
-
-    WAIT_STATES = [PENDING, RETRYING]
-    END_STATES = [SUCCESS, FAILED]
-
-    RUNS_ON_SOURCE = 'source'
-    RUNS_ON_TARGET = 'target'
-    RUNS_ON_NODE = 'node'
-    RUNS_ON = (RUNS_ON_NODE, RUNS_ON_SOURCE, RUNS_ON_TARGET)
-
-    @orm.validates('max_attempts')
-    def validate_max_attempts(self, _, value):                                  # pylint: disable=no-self-use
-        """Validates that max attempts is either -1 or a positive number"""
-        if value < 1 and value != TaskBase.INFINITE_RETRIES:
-            raise ValueError('Max attempts can be either -1 (infinite) or any positive number. '
-                             'Got {value}'.format(value=value))
-        return value
-
-    INFINITE_RETRIES = -1
+                                             dict_key='name')
 
     status = Column(Enum(*STATES, name='status'), default=PENDING)
 
@@ -474,6 +451,39 @@ class TaskBase(ModelMixin):
         """
         return self.node or self.relationship
 
+    @orm.validates('max_attempts')
+    def validate_max_attempts(self, _, value):                                  # pylint: disable=no-self-use
+        """Validates that max attempts is either -1 or a positive number"""
+        if value < 1 and value != TaskBase.INFINITE_RETRIES:
+            raise ValueError('Max attempts can be either -1 (infinite) or any positive number. '
+                             'Got {value}'.format(value=value))
+        return value
+
+    # region foreign keys
+
+    __private_fields__ = ['node_fk',
+                          'relationship_fk',
+                          'plugin_fk',
+                          'execution_fk']
+
+    @declared_attr
+    def node_fk(cls):
+        return cls.foreign_key('node', nullable=True)
+
+    @declared_attr
+    def relationship_fk(cls):
+        return cls.foreign_key('relationship', nullable=True)
+
+    @declared_attr
+    def plugin_fk(cls):
+        return cls.foreign_key('plugin', nullable=True)
+
+    @declared_attr
+    def execution_fk(cls):
+        return cls.foreign_key('execution', nullable=True)
+
+    # endregion
+
     @classmethod
     def as_node_task(cls, instance, runs_on, **kwargs):
         return cls(node=instance, _runs_on=runs_on, **kwargs)

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/daa2d538/aria/modeling/service.py
----------------------------------------------------------------------
diff --git a/aria/modeling/service.py b/aria/modeling/service.py
index 78cd920..1309472 100644
--- a/aria/modeling/service.py
+++ b/aria/modeling/service.py
@@ -13,6 +13,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+# pylint: disable=no-self-argument, no-member, abstract-method
+
 from sqlalchemy import (
     Column,
     Text,
@@ -22,7 +24,7 @@ from sqlalchemy import DateTime
 from sqlalchemy.ext.associationproxy import association_proxy
 from sqlalchemy.ext.declarative import declared_attr
 
-from .bases import ModelMixin
+from .bases import InstanceModelMixin
 from ..parser import validation
 from ..utils import collections, formatting, console
 
@@ -31,34 +33,10 @@ from . import (
     types as modeling_types
 )
 
-# pylint: disable=no-self-argument, no-member, abstract-method
-
-
-class _InstanceModelMixin(ModelMixin):
-    """
-    Mixin for :class:`ServiceInstance` models.
 
-    All models support validation, diagnostic dumping, and representation as
-    raw data (which can be translated into JSON or YAML) via :code:`as_raw`.
+class ServiceBase(InstanceModelMixin): # pylint: disable=too-many-public-methods
     """
-
-    @property
-    def as_raw(self):
-        raise NotImplementedError
-
-    def validate(self, context):
-        pass
-
-    def coerce_values(self, context, container, report_issues):
-        pass
-
-    def dump(self, context):
-        pass
-
-
-class ServiceBase(_InstanceModelMixin): # pylint: disable=too-many-public-methods
-    """
-    A service instance is an instance of a :class:`ServiceTemplate`.
+    A service instance is usually an instance of a :class:`ServiceTemplate`.
 
     You will usually not create it programmatically, but instead instantiate it from the template.
 
@@ -73,17 +51,14 @@ class ServiceBase(_InstanceModelMixin): # pylint: disable=too-many-public-method
     :ivar operations: Dict of :class:`Operation`
     """
 
-    __tablename__ = 'service' # redundancy for PyLint: SqlAlchemy injects this
-
-    __private_fields__ = ['substituion_fk',
-                          'service_template_fk']
+    __tablename__ = 'service'
 
     description = Column(Text)
 
     @declared_attr
     def meta_data(cls):
         # Warning! We cannot use the attr name "metadata" because it's used by SqlAlchemy!
-        return cls.many_to_many_relationship('metadata', key_column_name='name')
+        return cls.many_to_many_relationship('metadata', dict_key='name')
 
     @declared_attr
     def nodes(cls):
@@ -104,22 +79,22 @@ class ServiceBase(_InstanceModelMixin): # pylint: disable=too-many-public-method
     @declared_attr
     def inputs(cls):
         return cls.many_to_many_relationship('parameter', table_prefix='inputs',
-                                             key_column_name='name')
+                                             dict_key='name')
 
     @declared_attr
     def outputs(cls):
         return cls.many_to_many_relationship('parameter', table_prefix='outputs',
-                                             key_column_name='name')
+                                             dict_key='name')
 
     @declared_attr
     def operations(cls):
-        return cls.one_to_many_relationship('operation', key_column_name='name')
+        return cls.one_to_many_relationship('operation', dict_key='name')
 
     @declared_attr
     def service_template(cls):
         return cls.many_to_one_relationship('service_template')
 
-    # region orchestrator required columns
+    # region orchestration
 
     created_at = Column(DateTime, nullable=False, index=True)
     updated_at = Column(DateTime)
@@ -135,6 +110,9 @@ class ServiceBase(_InstanceModelMixin): # pylint: disable=too-many-public-method
 
     # region foreign keys
 
+    __private_fields__ = ['substituion_fk',
+                          'service_template_fk']
+
     # Service one-to-one to Substitution
     @declared_attr
     def substitution_fk(cls):
@@ -143,7 +121,7 @@ class ServiceBase(_InstanceModelMixin): # pylint: disable=too-many-public-method
     # Service many-to-one to ServiceTemplate
     @declared_attr
     def service_template_fk(cls):
-        return cls.foreign_key('service_template')
+        return cls.foreign_key('service_template', nullable=True)
 
     # endregion
 
@@ -276,14 +254,13 @@ class ServiceBase(_InstanceModelMixin): # pylint: disable=too-many-public-method
                         self._dump_graph_node(context, target_node)
 
 
-class NodeBase(_InstanceModelMixin):
+class NodeBase(InstanceModelMixin):
     """
-    An instance of a :class:`NodeTemplate`.
+    Usually an instance of a :class:`NodeTemplate`.
 
     Nodes may have zero or more :class:`Relationship` instances to other nodes.
 
-    :ivar name: Unique ID (prefixed with the template name)
-    :ivar type_name: Must be represented in the :class:`ModelingContext`
+    :ivar name: Unique ID (often prefixed with the template name)
     :ivar properties: Dict of :class:`Parameter`
     :ivar interfaces: Dict of :class:`Interface`
     :ivar artifacts: Dict of :class:`Artifact`
@@ -291,41 +268,39 @@ class NodeBase(_InstanceModelMixin):
     :ivar relationships: List of :class:`Relationship`
     """
 
-    __tablename__ = 'node' # redundancy for PyLint: SqlAlchemy injects this
+    __tablename__ = 'node'
 
-    __private_fields__ = ['host_fk',
-                          'service_fk',
-                          'node_template_fk']
-
-    type_name = Column(Text)
+    @declared_attr
+    def type(cls):
+        return cls.many_to_one_relationship('type')
 
     @declared_attr
     def properties(cls):
         return cls.many_to_many_relationship('parameter', table_prefix='properties',
-                                             key_column_name='name')
+                                             dict_key='name')
 
     @declared_attr
     def interfaces(cls):
-        return cls.one_to_many_relationship('interface', key_column_name='name')
+        return cls.one_to_many_relationship('interface', dict_key='name')
 
     @declared_attr
     def artifacts(cls):
-        return cls.one_to_many_relationship('artifact', key_column_name='name')
+        return cls.one_to_many_relationship('artifact', dict_key='name')
 
     @declared_attr
     def capabilities(cls):
-        return cls.one_to_many_relationship('capability', key_column_name='name')
+        return cls.one_to_many_relationship('capability', dict_key='name')
 
     @declared_attr
     def outbound_relationships(cls):
         return cls.one_to_many_relationship('relationship',
-                                            foreign_key_name='source_node_fk',
+                                            foreign_key='source_node_fk',
                                             backreference='source_node')
 
     @declared_attr
     def inbound_relationships(cls):
         return cls.one_to_many_relationship('relationship',
-                                            foreign_key_name='target_node_fk',
+                                            foreign_key='target_node_fk',
                                             backreference='target_node')
 
     @declared_attr
@@ -336,7 +311,7 @@ class NodeBase(_InstanceModelMixin):
     def node_template(cls):
         return cls.many_to_one_relationship('node_template')
 
-    # region orchestrator required columns
+    # region orchestration
 
     runtime_properties = Column(modeling_types.Dict)
     scaling_groups = Column(modeling_types.List)
@@ -369,6 +344,16 @@ class NodeBase(_InstanceModelMixin):
 
     # region foreign_keys
 
+    __private_fields__ = ['type_fk',
+                          'host_fk',
+                          'service_fk',
+                          'node_template_fk']
+
+    # Node many-to-one to Type
+    @declared_attr
+    def type_fk(cls):
+        return cls.foreign_key('type')
+
     # Node one-to-one to Node
     @declared_attr
     def host_fk(cls):
@@ -382,7 +367,7 @@ class NodeBase(_InstanceModelMixin):
     # Node many-to-one to NodeTemplate
     @declared_attr
     def node_template_fk(cls):
-        return cls.foreign_key('node_template')
+        return cls.foreign_key('node_template', nullable=True)
 
     # endregion
 
@@ -426,13 +411,16 @@ class NodeBase(_InstanceModelMixin):
                 target_node = target_nodes[0]
 
             if target_node is not None:
-                relationship = models.Relationship(
-                    name=requirement_template.name,
-                    requirement_template=requirement_template,
-                    target_node=target_node,
-                    capability=target_capability
-                )
+                if requirement_template.relationship_template is not None:
+                    relationship = \
+                        requirement_template.relationship_template.instantiate(context, self)
+                else:
+                    relationship = models.Relationship(capability=target_capability)
+                relationship.name = requirement_template.name
+                relationship.requirement_template = requirement_template
+                relationship.target_node = target_node
                 self.outbound_relationships.append(relationship)
+                return True
             else:
                 context.validation.report('requirement "{0}" of node "{1}" targets node '
                                           'template "{2}" but its instantiated nodes do not '
@@ -503,8 +491,8 @@ class NodeBase(_InstanceModelMixin):
     def dump(self, context):
         console.puts('Node: {0}'.format(context.style.node(self.name)))
         with context.style.indent:
+            console.puts('Type: {0}'.format(context.style.type(self.type.name)))
             console.puts('Template: {0}'.format(context.style.node(self.node_template.name)))
-            console.puts('Type: {0}'.format(context.style.type(self.type_name)))
             utils.dump_parameters(context, self.properties)
             utils.dump_interfaces(context, self.interfaces)
             utils.dump_dict_values(context, self.artifacts, 'Artifacts')
@@ -512,38 +500,33 @@ class NodeBase(_InstanceModelMixin):
             utils.dump_list_values(context, self.outbound_relationships, 'Relationships')
 
 
-class GroupBase(_InstanceModelMixin):
+class GroupBase(InstanceModelMixin):
     """
-    An instance of a :class:`GroupTemplate`.
+    Usually an instance of a :class:`GroupTemplate`.
 
-    :ivar name: Unique ID (prefixed with the template name)
-    :ivar type_name: Must be represented in the :class:`ModelingContext`
-    :ivar template_name: Must be represented in the :class:`ServiceTemplate`
+    :ivar name: Unique ID (often equal to the template name)
     :ivar properties: Dict of :class:`Parameter`
     :ivar interfaces: Dict of :class:`Interface`
-    :ivar member_node_ids: Must be represented in the :class:`ServiceInstance`
-    :ivar member_group_ids: Must be represented in the :class:`ServiceInstance`
     """
 
-    __tablename__ = 'group' # redundancy for PyLint: SqlAlchemy injects this
-
-    __private_fields__ = ['service_fk',
-                          'group_template_fk']
+    __tablename__ = 'group'
 
-    type_name = Column(Text)
-    template_name = Column(Text)
+    @declared_attr
+    def type(cls):
+        return cls.many_to_one_relationship('type')
 
     @declared_attr
     def properties(cls):
         return cls.many_to_many_relationship('parameter', table_prefix='properties',
-                                             key_column_name='name')
+                                             dict_key='name')
 
     @declared_attr
     def interfaces(cls):
-        return cls.one_to_many_relationship('interface', key_column_name='name')
+        return cls.one_to_many_relationship('interface', dict_key='name')
 
-    member_node_ids = Column(modeling_types.StrictList(basestring))
-    member_group_ids = Column(modeling_types.StrictList(basestring))
+    @declared_attr
+    def nodes(cls):
+        return cls.many_to_many_relationship('node')
 
     @declared_attr
     def group_template(cls):
@@ -551,6 +534,15 @@ class GroupBase(_InstanceModelMixin):
 
     # region foreign_keys
 
+    __private_fields__ = ['type_fk',
+                          'service_fk',
+                          'group_template_fk']
+
+    # Group many-to-one to Type
+    @declared_attr
+    def type_fk(cls):
+        return cls.foreign_key('type')
+
     # Service one-to-many to Group
     @declared_attr
     def service_fk(cls):
@@ -559,7 +551,7 @@ class GroupBase(_InstanceModelMixin):
     # Group many-to-one to GroupTemplate
     @declared_attr
     def group_template_fk(cls):
-        return cls.foreign_key('group_template')
+        return cls.foreign_key('group_template', nullable=True)
 
     # endregion
 
@@ -567,21 +559,10 @@ class GroupBase(_InstanceModelMixin):
     def as_raw(self):
         return collections.OrderedDict((
             ('name', self.name),
-            ('type_name', self.type_name),
-            ('template_name', self.template_name),
             ('properties', formatting.as_raw_dict(self.properties)),
-            ('interfaces', formatting.as_raw_list(self.interfaces)),
-            ('member_node_ids', self.member_node_ids),
-            ('member_group_ids', self.member_group_ids)))
+            ('interfaces', formatting.as_raw_list(self.interfaces))))
 
     def validate(self, context):
-        if context.modeling.group_types.get_descendant(self.type_name) is None:
-            context.validation.report('group "{0}" has an unknown type: {1}'.format(
-                self.name,  # pylint: disable=no-member
-                # TODO fix self.name reference
-                formatting.safe_repr(self.type_name)),
-                                      level=validation.Issue.BETWEEN_TYPES)
-
         utils.validate_dict_values(context, self.properties)
         utils.validate_dict_values(context, self.interfaces)
 
@@ -592,42 +573,42 @@ class GroupBase(_InstanceModelMixin):
     def dump(self, context):
         console.puts('Group: {0}'.format(context.style.node(self.name)))
         with context.style.indent:
-            console.puts('Type: {0}'.format(context.style.type(self.type_name)))
-            console.puts('Template: {0}'.format(context.style.type(self.template_name)))
+            console.puts('Type: {0}'.format(context.style.type(self.type.name)))
             utils.dump_parameters(context, self.properties)
             utils.dump_interfaces(context, self.interfaces)
-            if self.member_node_ids:
+            if self.nodes:
                 console.puts('Member nodes:')
                 with context.style.indent:
-                    for node_id in self.member_node_ids:
-                        console.puts(context.style.node(node_id))
+                    for node in self.nodes:
+                        console.puts(context.style.node(node.name))
 
 
-class PolicyBase(_InstanceModelMixin):
+class PolicyBase(InstanceModelMixin):
     """
-    An instance of a :class:`PolicyTemplate`.
+    Usually an instance of a :class:`PolicyTemplate`.
 
     :ivar name: Name
-    :ivar type_name: Must be represented in the :class:`ModelingContext`
     :ivar properties: Dict of :class:`Parameter`
-    :ivar target_node_ids: Must be represented in the :class:`ServiceInstance`
-    :ivar target_group_ids: Must be represented in the :class:`ServiceInstance`
     """
 
-    __tablename__ = 'policy' # redundancy for PyLint: SqlAlchemy injects this
+    __tablename__ = 'policy'
 
-    __private_fields__ = ['service_fk',
-                          'policy_template_fk']
-
-    type_name = Column(Text)
+    @declared_attr
+    def type(cls):
+        return cls.many_to_one_relationship('type')
 
     @declared_attr
     def properties(cls):
         return cls.many_to_many_relationship('parameter', table_prefix='properties',
-                                             key_column_name='name')
+                                             dict_key='name')
 
-    target_node_ids = Column(modeling_types.StrictList(basestring))
-    target_group_ids = Column(modeling_types.StrictList(basestring))
+    @declared_attr
+    def nodes(cls):
+        return cls.many_to_many_relationship('node')
+
+    @declared_attr
+    def groups(cls):
+        return cls.many_to_many_relationship('group')
 
     @declared_attr
     def policy_template(cls):
@@ -635,6 +616,15 @@ class PolicyBase(_InstanceModelMixin):
 
     # region foreign_keys
 
+    __private_fields__ = ['type_fk',
+                          'service_fk',
+                          'policy_template_fk']
+
+    # Policy many-to-one to Type
+    @declared_attr
+    def type_fk(cls):
+        return cls.foreign_key('type')
+
     # Service one-to-many to Policy
     @declared_attr
     def service_fk(cls):
@@ -643,7 +633,7 @@ class PolicyBase(_InstanceModelMixin):
     # Policy many-to-one to PolicyTemplate
     @declared_attr
     def policy_template_fk(cls):
-        return cls.foreign_key('policy_template')
+        return cls.foreign_key('policy_template', nullable=True)
 
     # endregion
 
@@ -652,16 +642,9 @@ class PolicyBase(_InstanceModelMixin):
         return collections.OrderedDict((
             ('name', self.name),
             ('type_name', self.type_name),
-            ('properties', formatting.as_raw_dict(self.properties)),
-            ('target_node_ids', self.target_node_ids),
-            ('target_group_ids', self.target_group_ids)))
+            ('properties', formatting.as_raw_dict(self.properties))))
 
     def validate(self, context):
-        if context.modeling.policy_types.get_descendant(self.type_name) is None:
-            context.validation.report('policy "{0}" has an unknown type: {1}'.format(
-                self.name, formatting.safe_repr(self.type_name)),
-                                      level=validation.Issue.BETWEEN_TYPES)
-
         utils.validate_dict_values(context, self.properties)
 
     def coerce_values(self, context, container, report_issues):
@@ -670,37 +653,36 @@ class PolicyBase(_InstanceModelMixin):
     def dump(self, context):
         console.puts('Policy: {0}'.format(context.style.node(self.name)))
         with context.style.indent:
-            console.puts('Type: {0}'.format(context.style.type(self.type_name)))
+            console.puts('Type: {0}'.format(context.style.type(self.type.name)))
             utils.dump_parameters(context, self.properties)
-            if self.target_node_ids:
+            if self.nodes:
                 console.puts('Target nodes:')
                 with context.style.indent:
-                    for node_id in self.target_node_ids:
-                        console.puts(context.style.node(node_id))
-            if self.target_group_ids:
+                    for node in self.nodes:
+                        console.puts(context.style.node(node.name))
+            if self.groups:
                 console.puts('Target groups:')
                 with context.style.indent:
-                    for group_id in self.target_group_ids:
-                        console.puts(context.style.node(group_id))
+                    for group in self.groups:
+                        console.puts(context.style.node(group.name))
 
 
-class SubstitutionBase(_InstanceModelMixin):
+class SubstitutionBase(InstanceModelMixin):
     """
-    An instance of a :class:`SubstitutionTemplate`.
+    Usually an instance of a :class:`SubstitutionTemplate`.
 
-    :ivar node_type_name: Must be represented in the :class:`ModelingContext`
     :ivar mappings: Dict of :class:` SubstitutionMapping`
     """
 
-    __tablename__ = 'substitution' # redundancy for PyLint: SqlAlchemy injects this
-
-    __private_fields__ = ['substitution_template_fk']
+    __tablename__ = 'substitution'
 
-    node_type_name = Column(Text)
+    @declared_attr
+    def node_type(cls):
+        return cls.many_to_one_relationship('type')
 
     @declared_attr
     def mappings(cls):
-        return cls.one_to_many_relationship('substitution_mapping', key_column_name='name')
+        return cls.one_to_many_relationship('substitution_mapping', dict_key='name')
 
     @declared_attr
     def substitution_template(cls):
@@ -708,12 +690,21 @@ class SubstitutionBase(_InstanceModelMixin):
 
     # region foreign_keys
 
+    __private_fields__ = ['node_type_fk',
+                          'substitution_template_fk']
+
+    # Substitution many-to-one to Type
+    @declared_attr
+    def node_type_fk(cls):
+        return cls.foreign_key('type')
+
     # Substitution many-to-one to SubstitutionTemplate
     @declared_attr
     def substitution_template_fk(cls):
-        return cls.foreign_key('substitution_template')
+        return cls.foreign_key('substitution_template', nullable=True)
 
     # endregion
+
     @property
     def as_raw(self):
         return collections.OrderedDict((
@@ -721,13 +712,6 @@ class SubstitutionBase(_InstanceModelMixin):
             ('mappings', formatting.as_raw_dict(self.mappings))))
 
     def validate(self, context):
-        if context.modeling.node_types.get_descendant(self.node_type_name) is None:
-            context.validation.report('substitution "{0}" has an unknown type: {1}'.format(
-                self.name,  # pylint: disable=no-member
-                # TODO fix self.name reference
-                formatting.safe_repr(self.node_type_name)),
-                                      level=validation.Issue.BETWEEN_TYPES)
-
         utils.validate_dict_values(context, self.mappings)
 
     def coerce_values(self, context, container, report_issues):
@@ -736,23 +720,18 @@ class SubstitutionBase(_InstanceModelMixin):
     def dump(self, context):
         console.puts('Substitution:')
         with context.style.indent:
-            console.puts('Node type: {0}'.format(context.style.type(self.node_type_name)))
+            console.puts('Node type: {0}'.format(context.style.type(self.node_type.name)))
             utils.dump_dict_values(context, self.mappings, 'Mappings')
 
 
-class SubstitutionMappingBase(_InstanceModelMixin):
+class SubstitutionMappingBase(InstanceModelMixin):
     """
     An instance of a :class:`SubstitutionMappingTemplate`.
 
     :ivar name: Exposed capability or requirement name
     """
 
-    __tablename__ = 'substitution_mapping' # redundancy for PyLint: SqlAlchemy injects this
-
-    __private_fields__ = ['substitution_fk',
-                          'node_fk',
-                          'capability_fk',
-                          'requirement_template_fk']
+    __tablename__ = 'substitution_mapping'
 
     @declared_attr
     def node(cls):
@@ -768,6 +747,11 @@ class SubstitutionMappingBase(_InstanceModelMixin):
 
     # region foreign keys
 
+    __private_fields__ = ['substitution_fk',
+                          'node_fk',
+                          'capability_fk',
+                          'requirement_template_fk']
+
     # Substitution one-to-many to SubstitutionMapping
     @declared_attr
     def substitution_fk(cls):
@@ -812,36 +796,31 @@ class SubstitutionMappingBase(_InstanceModelMixin):
                                else self.requirement_template.name)))
 
 
-class RelationshipBase(_InstanceModelMixin):
+class RelationshipBase(InstanceModelMixin):
     """
     Connects :class:`Node` to another node.
 
-    An instance of a :class:`RelationshipTemplate`.
+    Might be an instance of a :class:`RelationshipTemplate`.
 
     :ivar name: Name (usually the name of the requirement at the source node template)
-    :ivar type_name: Must be represented in the :class:`ModelingContext`
     :ivar properties: Dict of :class:`Parameter`
     :ivar interfaces: Dict of :class:`Interface`
     """
 
-    __tablename__ = 'relationship' # redundancy for PyLint: SqlAlchemy injects this
+    __tablename__ = 'relationship'
 
-    __private_fields__ = ['source_node_fk',
-                          'target_node_fk',
-                          'capability_fk',
-                          'requirement_template_fk',
-                          'relationship_template_fk']
-
-    type_name = Column(Text)
+    @declared_attr
+    def type(cls):
+        return cls.many_to_one_relationship('type')
 
     @declared_attr
     def properties(cls):
         return cls.many_to_many_relationship('parameter', table_prefix='properties',
-                                             key_column_name='name')
+                                             dict_key='name')
 
     @declared_attr
     def interfaces(cls):
-        return cls.one_to_many_relationship('interface', key_column_name='name')
+        return cls.one_to_many_relationship('interface', dict_key='name')
 
     @declared_attr
     def capability(cls):
@@ -855,7 +834,7 @@ class RelationshipBase(_InstanceModelMixin):
     def relationship_template(cls):
         return cls.many_to_one_relationship('relationship_template')
 
-    # region orchestrator required columns
+    # region orchestration
 
     source_position = Column(Integer) # ???
     target_position = Column(Integer) # ???
@@ -864,6 +843,18 @@ class RelationshipBase(_InstanceModelMixin):
 
     # region foreign keys
 
+    __private_fields__ = ['type_fk',
+                          'source_node_fk',
+                          'target_node_fk',
+                          'capability_fk',
+                          'requirement_template_fk',
+                          'relationship_template_fk']
+
+    # Relationship many-to-one to Type
+    @declared_attr
+    def type_fk(cls):
+        return cls.foreign_key('type', nullable=True)
+
     # Node one-to-many to Relationship
     @declared_attr
     def source_node_fk(cls):
@@ -882,7 +873,7 @@ class RelationshipBase(_InstanceModelMixin):
     # Relationship many-to-one to RequirementTemplate
     @declared_attr
     def requirement_template_fk(cls):
-        return cls.foreign_key('requirement_template')
+        return cls.foreign_key('requirement_template', nullable=True)
 
     # Relationship many-to-one to RelationshipTemplate
     @declared_attr
@@ -902,12 +893,6 @@ class RelationshipBase(_InstanceModelMixin):
             ('interfaces', formatting.as_raw_list(self.interfaces))))
 
     def validate(self, context):
-        if self.type_name:
-            if context.modeling.relationship_types.get_descendant(self.type_name) is None:
-                context.validation.report('relationship "{0}" has an unknown type: {1}'.format(
-                    self.name,
-                    formatting.safe_repr(self.type_name)),
-                                          level=validation.Issue.BETWEEN_TYPES)
         utils.validate_dict_values(context, self.properties)
         utils.validate_dict_values(context, self.interfaces)
 
@@ -922,34 +907,35 @@ class RelationshipBase(_InstanceModelMixin):
             console.puts('->')
         with context.style.indent:
             console.puts('Node: {0}'.format(context.style.node(self.target_node.name)))
-            if self.type_name is not None:
-                console.puts('Relationship type: {0}'.format(context.style.type(self.type_name)))
-            #if self.template_name is not None:
-            #    console.puts('Relationship template: {0}'.format(
-            #        context.style.node(self.template_name)))
+            if self.capability:
+                console.puts('Capability: {0}'.format(context.style.node(self.capability.name)))
+            if self.type is not None:
+                console.puts('Relationship type: {0}'.format(context.style.type(self.type.name)))
+            if (self.relationship_template is not None) and self.relationship_template.name:
+                console.puts('Relationship template: {0}'.format(
+                    context.style.node(self.relationship_template.name)))
             utils.dump_parameters(context, self.properties)
             utils.dump_interfaces(context, self.interfaces, 'Interfaces')
 
 
-class CapabilityBase(_InstanceModelMixin):
+class CapabilityBase(InstanceModelMixin):
     """
     A capability of a :class:`Node`.
 
-    An instance of a :class:`CapabilityTemplate`.
+    Usually an instance of a :class:`CapabilityTemplate`.
 
     :ivar name: Name
-    :ivar type_name: Must be represented in the :class:`ModelingContext`
     :ivar min_occurrences: Minimum number of requirement matches required
     :ivar max_occurrences: Maximum number of requirement matches allowed
     :ivar properties: Dict of :class:`Parameter`
     """
 
-    __tablename__ = 'capability' # redundancy for PyLint: SqlAlchemy injects this
+    __tablename__ = 'capability'
 
-    __private_fields__ = ['node_fk',
-                          'capability_template_fk']
+    @declared_attr
+    def type(cls):
+        return cls.many_to_one_relationship('type')
 
-    type_name = Column(Text)
     min_occurrences = Column(Integer, default=None) # optional
     max_occurrences = Column(Integer, default=None) # optional
     occurrences = Column(Integer, default=0)
@@ -957,7 +943,7 @@ class CapabilityBase(_InstanceModelMixin):
     @declared_attr
     def properties(cls):
         return cls.many_to_many_relationship('parameter', table_prefix='properties',
-                                             key_column_name='name')
+                                             dict_key='name')
 
     @declared_attr
     def capability_template(cls):
@@ -965,6 +951,15 @@ class CapabilityBase(_InstanceModelMixin):
 
     # region foreign_keys
 
+    __private_fields__ = ['capability_fk',
+                          'node_fk',
+                          'capability_template_fk']
+
+    # Capability many-to-one to Type
+    @declared_attr
+    def type_fk(cls):
+        return cls.foreign_key('type')
+
     # Node one-to-many to Capability
     @declared_attr
     def node_fk(cls):
@@ -973,7 +968,7 @@ class CapabilityBase(_InstanceModelMixin):
     # Capability many-to-one to CapabilityTemplate
     @declared_attr
     def capability_template_fk(cls):
-        return cls.foreign_key('capability_template')
+        return cls.foreign_key('capability_template', nullable=True)
 
     # endregion
 
@@ -998,12 +993,6 @@ class CapabilityBase(_InstanceModelMixin):
             ('properties', formatting.as_raw_dict(self.properties))))
 
     def validate(self, context):
-        if context.modeling.capability_types.get_descendant(self.type_name) is None:
-            context.validation.report('capability "{0}" has an unknown type: {1}'.format(
-                self.name,
-                formatting.safe_repr(self.type_name)),
-                                      level=validation.Issue.BETWEEN_TYPES)
-
         utils.validate_dict_values(context, self.properties)
 
     def coerce_values(self, context, container, report_issues):
@@ -1012,7 +1001,7 @@ class CapabilityBase(_InstanceModelMixin):
     def dump(self, context):
         console.puts(context.style.node(self.name))
         with context.style.indent:
-            console.puts('Type: {0}'.format(context.style.type(self.type_name)))
+            console.puts('Type: {0}'.format(context.style.type(self.type.name)))
             console.puts('Occurrences: {0:d} ({1:d}{2})'.format(
                 self.occurrences,
                 self.min_occurrences or 0,
@@ -1022,35 +1011,34 @@ class CapabilityBase(_InstanceModelMixin):
             utils.dump_parameters(context, self.properties)
 
 
-class InterfaceBase(_InstanceModelMixin):
+class InterfaceBase(InstanceModelMixin):
     """
     A typed set of :class:`Operation`.
+    
+    Usually an instance of :class:`InterfaceTemplate`.
 
     :ivar name: Name
     :ivar description: Description
-    :ivar type_name: Must be represented in the :class:`ModelingContext`
     :ivar inputs: Dict of :class:`Parameter`
     :ivar operations: Dict of :class:`Operation`
     """
 
-    __tablename__ = 'interface' # redundancy for PyLint: SqlAlchemy injects this
+    __tablename__ = 'interface'
 
-    __private_fields__ = ['node_fk',
-                          'group_fk',
-                          'relationship_fk',
-                          'interface_template_fk']
+    @declared_attr
+    def type(cls):
+        return cls.many_to_one_relationship('type')
 
     description = Column(Text)
-    type_name = Column(Text)
 
     @declared_attr
     def inputs(cls):
         return cls.many_to_many_relationship('parameter', table_prefix='inputs',
-                                             key_column_name='name')
+                                             dict_key='name')
 
     @declared_attr
     def operations(cls):
-        return cls.one_to_many_relationship('operation', key_column_name='name')
+        return cls.one_to_many_relationship('operation', dict_key='name')
 
     @declared_attr
     def interface_template(cls):
@@ -1058,6 +1046,17 @@ class InterfaceBase(_InstanceModelMixin):
 
     # region foreign_keys
 
+    __private_fields__ = ['type_fk',
+                          'node_fk',
+                          'group_fk',
+                          'relationship_fk',
+                          'interface_template_fk']
+
+    # Interface many-to-one to Type
+    @declared_attr
+    def type_fk(cls):
+        return cls.foreign_key('type')
+
     # Node one-to-many to Interface
     @declared_attr
     def node_fk(cls):
@@ -1076,7 +1075,7 @@ class InterfaceBase(_InstanceModelMixin):
     # Interface many-to-one to InterfaceTemplate
     @declared_attr
     def interface_template_fk(cls):
-        return cls.foreign_key('interface_template')
+        return cls.foreign_key('interface_template', nullable=True)
 
     # endregion
 
@@ -1090,13 +1089,6 @@ class InterfaceBase(_InstanceModelMixin):
             ('operations', formatting.as_raw_list(self.operations))))
 
     def validate(self, context):
-        if self.type_name:
-            if context.modeling.interface_types.get_descendant(self.type_name) is None:
-                context.validation.report('interface "{0}" has an unknown type: {1}'.format(
-                    self.name,
-                    formatting.safe_repr(self.type_name)),
-                                          level=validation.Issue.BETWEEN_TYPES)
-
         utils.validate_dict_values(context, self.inputs)
         utils.validate_dict_values(context, self.operations)
 
@@ -1109,14 +1101,16 @@ class InterfaceBase(_InstanceModelMixin):
         if self.description:
             console.puts(context.style.meta(self.description))
         with context.style.indent:
-            console.puts('Interface type: {0}'.format(context.style.type(self.type_name)))
+            console.puts('Interface type: {0}'.format(context.style.type(self.type.name)))
             utils.dump_parameters(context, self.inputs, 'Inputs')
             utils.dump_dict_values(context, self.operations, 'Operations')
 
 
-class OperationBase(_InstanceModelMixin):
+class OperationBase(InstanceModelMixin):
     """
     An operation in a :class:`Interface`.
+    
+    Might be an instance of :class:`OperationTemplate`.
 
     :ivar name: Name
     :ivar description: Description
@@ -1128,12 +1122,7 @@ class OperationBase(_InstanceModelMixin):
     :ivar inputs: Dict of :class:`Parameter`
     """
 
-    __tablename__ = 'operation' # redundancy for PyLint: SqlAlchemy injects this
-
-    __private_fields__ = ['service_fk',
-                          'interface_fk',
-                          'plugin_fk',
-                          'operation_template_fk']
+    __tablename__ = 'operation'
 
     description = Column(Text)
     implementation = Column(Text)
@@ -1145,7 +1134,7 @@ class OperationBase(_InstanceModelMixin):
     @declared_attr
     def inputs(cls):
         return cls.many_to_many_relationship('parameter', table_prefix='inputs',
-                                             key_column_name='name')
+                                             dict_key='name')
 
     @declared_attr
     def plugin(cls):
@@ -1157,6 +1146,11 @@ class OperationBase(_InstanceModelMixin):
 
     # region foreign_keys
 
+    __private_fields__ = ['service_fk',
+                          'interface_fk',
+                          'plugin_fk',
+                          'operation_template_fk']
+
     # Service one-to-many to Operation
     @declared_attr
     def service_fk(cls):
@@ -1175,7 +1169,7 @@ class OperationBase(_InstanceModelMixin):
     # Operation many-to-one to OperationTemplate
     @declared_attr
     def operation_template_fk(cls):
-        return cls.foreign_key('operation_template')
+        return cls.foreign_key('operation_template', nullable=True)
 
     # endregion
 
@@ -1192,6 +1186,7 @@ class OperationBase(_InstanceModelMixin):
             ('inputs', formatting.as_raw_dict(self.inputs))))
 
     def validate(self, context):
+        # TODO must be associated with interface or service
         utils.validate_dict_values(context, self.inputs)
 
     def coerce_values(self, context, container, report_issues):
@@ -1219,13 +1214,14 @@ class OperationBase(_InstanceModelMixin):
             utils.dump_parameters(context, self.inputs, 'Inputs')
 
 
-class ArtifactBase(_InstanceModelMixin):
+class ArtifactBase(InstanceModelMixin):
     """
     A file associated with a :class:`Node`.
+    
+    Usually an instance of :class:`ArtifactTemplate`.
 
     :ivar name: Name
     :ivar description: Description
-    :ivar type_name: Must be represented in the :class:`ModelingContext`
     :ivar source_path: Source path (CSAR or repository)
     :ivar target_path: Path at destination machine
     :ivar repository_url: Repository URL
@@ -1233,10 +1229,11 @@ class ArtifactBase(_InstanceModelMixin):
     :ivar properties: Dict of :class:`Parameter`
     """
 
-    __tablename__ = 'artifact' # redundancy for PyLint: SqlAlchemy injects this
+    __tablename__ = 'artifact'
 
-    __private_fields__ = ['node_fk',
-                          'artifact_template_fk']
+    @declared_attr
+    def type(cls):
+        return cls.many_to_one_relationship('type')
 
     description = Column(Text)
     type_name = Column(Text)
@@ -1248,7 +1245,7 @@ class ArtifactBase(_InstanceModelMixin):
     @declared_attr
     def properties(cls):
         return cls.many_to_many_relationship('parameter', table_prefix='properties',
-                                             key_column_name='name')
+                                             dict_key='name')
 
     @declared_attr
     def artifact_template(cls):
@@ -1256,6 +1253,15 @@ class ArtifactBase(_InstanceModelMixin):
 
     # region foreign_keys
 
+    __private_fields__ = ['type_fk',
+                          'node_fk',
+                          'artifact_template_fk']
+
+    # Artifact many-to-one to Type
+    @declared_attr
+    def type_fk(cls):
+        return cls.foreign_key('type')
+
     # Node one-to-many to Artifact
     @declared_attr
     def node_fk(cls):
@@ -1264,7 +1270,7 @@ class ArtifactBase(_InstanceModelMixin):
     # Artifact many-to-one to ArtifactTemplate
     @declared_attr
     def artifact_template_fk(cls):
-        return cls.foreign_key('artifact_template')
+        return cls.foreign_key('artifact_template', nullable=True)
 
     # endregion
 
@@ -1281,11 +1287,6 @@ class ArtifactBase(_InstanceModelMixin):
             ('properties', formatting.as_raw_dict(self.properties))))
 
     def validate(self, context):
-        if context.modeling.artifact_types.get_descendant(self.type_name) is None:
-            context.validation.report('artifact "{0}" has an unknown type: {1}'.format(
-                self.name,
-                formatting.safe_repr(self.type_name)),
-                                      level=validation.Issue.BETWEEN_TYPES)
         utils.validate_dict_values(context, self.properties)
 
     def coerce_values(self, context, container, report_issues):
@@ -1296,7 +1297,7 @@ class ArtifactBase(_InstanceModelMixin):
         if self.description:
             console.puts(context.style.meta(self.description))
         with context.style.indent:
-            console.puts('Artifact type: {0}'.format(context.style.type(self.type_name)))
+            console.puts('Artifact type: {0}'.format(context.style.type(self.type.name)))
             console.puts('Source path: {0}'.format(context.style.literal(self.source_path)))
             if self.target_path is not None:
                 console.puts('Target path: {0}'.format(context.style.literal(self.target_path)))


Mime
View raw message