cubicweb logo

Table Of Contents

Previous topic

3. Data model

Next topic

3.3. Metadata

This Page

3.1. Yams schema

The schema is the core piece of a CubicWeb instance as it defines and handles the data model. It is based on entity types that are either already defined in Yams and the CubicWeb standard library; or more specific types defined in cubes. The schema for a cube is defined in a schema python module or package.

3.1.1. Overview

The core idea of the yams schema is not far from the classical Entity-relationship model. But while an E/R model (or logical model) traditionally has to be manually translated to a lower-level data description language (such as the SQL create table sublanguage), also often described as the physical model, no such step is required with Yams and CubicWeb.

This is because in addition to high-level, logical Yams models, one uses the RQL data manipulation language to query, insert, update and delete data. RQL abstracts as much of the underlying SQL database as a Yams schema abstracts from the physical layout. The vagaries of SQL are avoided.

As a bonus point, such abstraction make it quite comfortable to build or use different backends to which RQL queries apply.

So, as in the E/R formalism, the building blocks are entities (Entity type), relationships (Relation type, Relation definition) and attributes (handled like relation with Yams).

Let us detail a little the divergences between E/R and Yams:

  • all relationship are binary which means that to represent a non-binary relationship, one has to use an entity,
  • relationships do not support attributes (yet, see: http://www.cubicweb.org/ticket/341318), hence the need to reify it as an entity if need arises,
  • all entities have an eid attribute (an integer) that is its primary key (but it is possible to declare uniqueness on other attributes)

Also Yams supports the notions of:

  • entity inheritance (quite experimental yet, and completely undocumented),
  • relation type: that is, relationships can be established over a set of couple of entity types (henre the distinction made between RelationType and RelationDefinition below)

Finally Yams has a few concepts of its own:

  • relationships being oriented and binary, we call the left hand entity type the subject and the right hand entity type the object

Note

The Yams schema is available at run time through the .schema attribute of the vregistry. It’s an instance of cubicweb.schema.Schema, which extends yams.schema.Schema.

3.1.2. Entity type

An entity type is an instance of yams.schema.EntitySchema. Each entity type has a set of attributes and relations, and some permissions which define who can add, read, update or delete entities of this type.

The following built-in types are available: String, Int, Float, Decimal, Boolean, Date, Datetime, Time, Interval, Byte and Password. They can only be used as attributes of an other entity type.

There is also a RichString kindof type:

class yams.buildobjs.RichString(default_format='text/plain', format_constraints=None, **kwargs)

Convenience RichString attribute type The following declaration:

class Card(EntityType):
    content = RichString(fulltextindexed=True, default_format='text/rest')

is equivalent to:

class Card(EntityType):
    content_format = String(internationalizable=True,
                            default='text/rest', constraints=[format_constraint])
    content  = String(fulltextindexed=True)

You can find more base entity types in Pre-defined entities in the library.

3.1.3. Relation type

A relation type is an instance of yams.schema.RelationSchema. A relation type is simply a semantic definition of a kind of relationship that may occur in an application.

It may be referenced by zero, one or more relation definitions.

It is important to choose a good name, at least to avoid conflicts with some semantically different relation defined in other cubes (since there’s only a shared name space for these names).

A relation type holds the following properties (which are hence shared between all relation definitions of that type):

  • inlined: boolean handling the physical optimization for archiving the relation in the subject entity table, instead of creating a specific table for the relation. This applies to relations where cardinality of subject->relation->object is 0..1 (?) or 1..1 (1) for all its relation definitions.
  • symmetric: boolean indicating that the relation is symmetrical, which means that X relation Y implies Y relation X.

3.1.4. Relation definition

A relation definition is an instance of yams.schema.RelationDefinition. It is a complete triplet “<subject entity type> <relation type> <object entity type>”.

When creating a new instance of that class, the corresponding RelationType instance is created on the fly if necessary.

3.1.4.1. Properties

The available properties for relation definitions are enumerated here. There are several kind of properties, as some relation definitions are actually attribute definitions, and other are not.

Some properties may be completely optional, other may have a default value.

Common properties for attributes and relations:

  • description: an unicode string describing an attribute or a relation. By default this string will be used in the editing form of the entity, which means that it is supposed to help the end-user and should be flagged by the function _ to be properly internationalized.

  • constraints: a list of conditions/constraints that the relation has to satisfy (c.f. Constraints)

  • cardinality: a two character string specifying the cardinality of the relation. The first character defines the cardinality of the relation on the subject, and the second on the object. When a relation can have multiple subjects or objects, the cardinality applies to all, not on a one-to-one basis (so it must be consistent...). Default value is ‘**’. The possible values are inspired from regular expression syntax:

    • 1: 1..1
    • ?: 0..1
    • +: 1..n
    • *: 0..n

Attributes properties:

  • unique: boolean indicating if the value of the attribute has to be unique or not within all entities of the same type (false by default)
  • indexed: boolean indicating if an index needs to be created for this attribute in the database (false by default). This is useful only if you know that you will have to run numerous searches on the value of this attribute.
  • default: default value of the attribute. In case of date types, the values which could be used correspond to the RQL keywords TODAY and NOW.

Properties for String attributes:

  • fulltextindexed: boolean indicating if the attribute is part of the full text index (false by default) (applicable on the type `Byte` as well)
  • internationalizable: boolean indicating if the value of the attribute is internationalizable (false by default)

Relation properties:

  • composite: string indicating that the subject (composite == ‘subject’) is composed of the objects of the relations. For the opposite case (when the object is composed of the subjects of the relation), we just set ‘object’ as value. The composition implies that when the relation is deleted (so when the composite is deleted, at least), the composed are also deleted.
  • fulltext_container: string indicating if the value if the full text indexation of the entity on one end of the relation should be used to find the entity on the other end. The possible values are ‘subject’ or ‘object’. For instance the use_email relation has that property set to ‘subject’, since when performing a full text search people want to find the entity using an email address, and not the entity representing the email address.

3.1.4.2. Constraints

By default, the available constraint types are:

3.1.4.2.1. General Constraints

  • SizeConstraint: allows to specify a minimum and/or maximum size on string (generic case of maxsize)
  • BoundConstraint: allows to specify a minimum and/or maximum value on numeric types and date
from yams.constraints import BoundConstraint, TODAY
BoundConstraint('<=', TODAY())
  • IntervalBoundConstraint: allows to specify an interval with included values
class Node(EntityType):
    latitude = Float(constraints=[IntervalBoundConstraint(-90, +90)])
  • UniqueConstraint: identical to “unique=True”
  • StaticVocabularyConstraint: identical to “vocabulary=(...)”

3.1.4.2.2. RQL Based Constraints

RQL based constraints may take three arguments. The first one is the WHERE clause of a RQL query used by the constraint. The second argument mainvars is the Any clause of the query. By default this include S reserved for the subject of the relation and O for the object. Additional variables could be specified using mainvars. The argument expects a single string with all variable’s name separated by spaces. The last one, msg, is the error message displayed when the constraint fails. As RQLVocabularyConstraint never fails the third argument is not available.

  • RQLConstraint: allows to specify a RQL query that has to be satisfied by the subject and/or the object of relation. In this query the variables S and O are reserved for the relation subject and object entities.
  • RQLVocabularyConstraint: similar to the previous type of constraint except that it does not express a “strong” constraint, which means it is only used to restrict the values listed in the drop-down menu of editing form, but it does not prevent another entity to be selected.
  • RQLUniqueConstraint: allows to the specify a RQL query that ensure that an attribute is unique in a specific context. The Query must never return more than a single result to be satisfied. In this query the variables S is reserved for the relation subject entity. The other variables should be specified with the second constructor argument (mainvars). This constraints should be used when UniqueConstraint doesn’t fit. Here is a simple example.
# Check that in the same Workflow each state's name is unique.  Using
# UniqueConstraint (or unique=True) here would prevent states in different
# workflows to have the same name.

# With: State S, Workflow W, String N ; S state_of W, S name N

RQLUniqueConstraint('S name N, S state_of WF, Y state_of WF, Y name N',
                    mainvars='Y',
                    msg=_('workflow already has a state of that name'))

3.1.5. The security model

The security model of CubicWeb is based on Access Control List. The main principles are:

  • users and groups of users
  • a user belongs to at least one group of user
  • permissions (read, update, create, delete)
  • permissions are assigned to groups (and not to users)

For CubicWeb in particular:

  • we associate rights at the entities/relations schema level
  • the default groups are: administrators, users and guests
  • users belong to the users group
  • there is a virtual group called owners to which we can associate only delete and update permissions
    • we can not add users to the owners group, they are implicitly added to it according to the context of the objects they own
    • the permissions of this group are only checked on update/delete actions if all the other groups the user belongs to do not provide those permissions

Setting permissions is done with the attribute __permissions__ of entities and relation definition. The value of this attribute is a dictionary where the keys are the access types (action), and the values are the authorized groups or expressions.

For an entity type, the possible actions are read, add, update and delete.

For a relation, the possible actions are read, add, and delete.

For an attribute, the possible actions are read, and update.

For each access type, a tuple indicates the name of the authorized groups and/or one or multiple RQL expressions to satisfy to grant access. The access is provided if the user is in one of the listed groups or if one of the RQL condition is satisfied.

3.1.5.1. The standard user groups

  • guests
  • users
  • managers
  • owners: virtual group corresponding to the entity’s owner. This can only be used for the actions update and delete of an entity type.

It is also possible to use specific groups if they are defined in the precreate script of the cube (migration/precreate.py). Defining groups in postcreate script or later makes them unavailable for security purposes (in this case, an sync_schema_props_perms command has to be issued in a CubicWeb shell).

3.1.5.2. Use of RQL expression for write permissions

It is possible to define RQL expression to provide update permission (add, delete and update) on entity type / relation definitions. An rql expression is a piece of query (corresponds to the WHERE statement of an RQL query), and the expression will be considered as satisfied if it returns some results. They can not be used in read permission.

To use RQL expression in entity type permission:

  • you have to use the class ERQLExpression
  • in this expression, the variables X and U are pre-defined references respectively on the current entity (on which the action is verified) and on the user who send the request

For RQL expressions on a relation type, the principles are the same except for the following:

  • you have to use the class RRQLExpression instead of ERQLExpression
  • in the expression, the variables S, O and U are pre-defined references to respectively the subject and the object of the current relation (on which the action is being verified) and the user who executed the query

To define security for attributes of an entity (non-final relation), you have to use the class ERQLExpression in which X represents the entity the attribute belongs to.

It is possible to use in those expression a special relation has_<ACTION>_permission where the subject is the user (eg ‘U’) and the object is any variable representing an entity (usually ‘X’ in ERQLExpression, ‘S’ or ‘O’ in RRQLExpression), meaning that the user needs to have permission to execute the action <ACTION> on the entities represented by this variable. It’s recommanded to use this feature whenever possible since it simplify greatly complex security definition and upgrade.

class my_relation(RelationDefinition):
  __permissions__ = {'read': ('managers', 'users'),
                     'add': ('managers', RRQLExpression('U has_update_permission S')),
                     'delete': ('managers', RRQLExpression('U has_update_permission S'))
                     }

In the above example, user will be allowed to add/delete my_relation if he has the update permission on the subject of the relation.

Note

Potentially, the use of an RQL expression to add an entity or a relation can cause problems for the user interface, because if the expression uses the entity or the relation to create, we are not able to verify the permissions before we actually added the entity (please note that this is not a problem for the RQL server at all, because the permissions checks are done after the creation). In such case, the permission check methods (CubicWebEntitySchema.check_perm and has_perm) can indicate that the user is not allowed to create this entity while it would obtain the permission. To compensate this problem, it is usually necessary in such case to use an action that reflects the schema permissions but which check properly the permissions so that it would show up only if possible.

3.1.5.3. Use of RQL expression for reading rights

The principles are the same but with the following restrictions:

  • you can not use rql expression for the read permission of relations and attributes,
  • you can not use special has_<ACTION>_permission relation in the rql expression.

3.1.5.4. Important notes about write permissions checking

Write permissions (e.g. ‘add’, ‘update’, ‘delete’) are checked in core hooks.

When a permission is checked slightly vary according to if it’s an entity or relation, and if the relation is an attribute relation or not). It’s important to understand that since according to when a permission is checked, values returned by rql expressions may changes, hence the permission being granted or not.

Here are the current rules:

  1. permission to add/update entity and its attributes are checked:
    • on commit if the entity has been added
    • in an ‘after_update_entity’ hook if the entity has been updated. If it fails at this time, it will be retried on commit (hence you get the permission if you have it just after the modification or at commit time)
  2. permission to delete an entity is checked in ‘before_delete_entity’ hook
  3. permission to add a relation is checked either:
    • in ‘before_add_relation’ hook if the relation type is in the BEFORE_ADD_RELATIONS set
    • else at commit time if the relation type is in the ON_COMMIT_ADD_RELATIONS set
    • else in ‘after_add_relation’ hook (the default)
  4. permission to delete a relation is checked in ‘before_delete_relation’ hook

Last but not least, remember queries issued from hooks and operation are by default ‘unsafe’, eg there are no read or write security checks.

See cubicweb.hooks.security for more details.

3.2. Defining your schema using yams

3.2.1. Entity type definition

An entity type is defined by a Python class which inherits from yams.buildobjs.EntityType. The class definition contains the description of attributes and relations for the defined entity type. The class name corresponds to the entity type name. It is expected to be defined in the module mycube.schema.

Note on schema definition:
 The code in mycube.schema is not meant to be executed. The class EntityType mentioned above is different from the EntitySchema class described in the previous chapter. EntityType is a helper class to make Entity definition easier. Yams will process EntityType classes and create EntitySchema instances from these class definitions. Similar manipulation happen for relations.

When defining a schema using python files, you may use the following shortcuts:

  • required: boolean indicating if the attribute is required, ed subject cardinality is ‘1’
  • vocabulary: specify static possible values of an attribute
  • maxsize: integer providing the maximum size of a string (no limit by default)

For example:

class Person(EntityType):
  """A person with the properties and the relations necessary for my
  application"""

  last_name = String(required=True, fulltextindexed=True)
  first_name = String(required=True, fulltextindexed=True)
  title = String(vocabulary=('Mr', 'Mrs', 'Miss'))
  date_of_birth = Date()
  works_for = SubjectRelation('Company', cardinality='?*')

The entity described above defines three attributes of type String, last_name, first_name and title, an attribute of type Date for the date of birth and a relation that connects a Person to another entity of type Company through the semantic works_for.

Naming convention:
 

Entity class names must start with an uppercase letter. The common usage is to use CamelCase names.

Attribute and relation names must start with a lowercase letter. The common usage is to use underscore_separated_words. Attribute and relation names starting with a single underscore are permitted, to denote a somewhat “protected” or “private” attribute.

In any case, identifiers starting with “CW” or “cw” are reserved for internal use by the framework.

The name of the Python attribute corresponds to the name of the attribute or the relation in CubicWeb application.

An attribute is defined in the schema as follows:

attr_name = attr_type(properties)

where attr_type is one of the type listed above and properties is a list of the attribute needs to satisfy (see Properties for more details).

  • it is possible to use the attribute meta to flag an entity type as a meta (e.g. used to describe/categorize other entities)

Note: if you end up with an if in the definition of your entity, this probably means that you need two separate entities that implement the ITree interface and get the result from .children() which ever entity is concerned.

3.2.2. Definition of relations

A relation is defined by a Python class heriting RelationType. The name of the class corresponds to the name of the type. The class then contains a description of the properties of this type of relation, and could as well contain a string for the subject and a string for the object. This allows to create new definition of associated relations, (so that the class can have the definition properties from the relation) for example

class locked_by(RelationType):
  """relation on all entities indicating that they are locked"""
  inlined = True
  cardinality = '?*'
  subject = '*'
  object = 'CWUser'

If provided, the subject and object attributes denote the subject and object of the various relation definitions related to the relation type. Allowed values for these attributes are:

  • a string corresponding to an entity type
  • a tuple of string corresponding to multiple entity types
  • the ‘*’ special string, meaning all types of entities

When a relation is not inlined and not symmetrical, and it does not require specific permissions, it can be defined using a SubjectRelation attribute in the EntityType class. The first argument of SubjectRelation gives the entity type for the object of the relation.

Naming convention:
 Although this way of defining relations uses a Python class, the naming convention defined earlier prevails over the PEP8 conventions used in the framework: relation type class names use underscore_separated_words.
Historical note:
 It has been historically possible to use ObjectRelation which defines a relation in the opposite direction. This feature is deprecated and therefore should not be used in newly written code.
Future deprecation note:
 In an even more remote future, it is quite possible that the SubjectRelation shortcut will become deprecated, in favor of the RelationType declaration which offers some advantages in the context of reusable cubes.

3.2.3. Definition of permissions

The entity type CWPermission from the standard library allows to build very complex and dynamic security architectures. The schema of this entity type is as follow:

class CWPermission(EntityType):
    """entity type that may be used to construct some advanced security configuration
    """
    name = String(required=True, indexed=True, internationalizable=True, maxsize=100)
    require_group = SubjectRelation('CWGroup', cardinality='+*',
                                    description=_('groups to which the permission is granted'))
    require_state = SubjectRelation('State',
                                    description=_("entity's state in which the permission is applicable"))
    # can be used on any entity
    require_permission = ObjectRelation('**', cardinality='*1', composite='subject',
                                        description=_("link a permission to the entity. This "
                                                      "permission should be used in the security "
                                                      "definition of the entity's type to be useful."))

Example of configuration:

class Version(EntityType):
    """a version is defining the content of a particular project's release"""

    __permissions__ = {'read':   ('managers', 'users', 'guests',),
                       'update': ('managers', 'logilab', 'owners',),
                       'delete': ('managers', ),
                       'add':    ('managers', 'logilab',
                                   ERQLExpression('X version_of PROJ, U in_group G,'
                                             'PROJ require_permission P, P name "add_version",'
                                             'P require_group G'),)}


class version_of(RelationType):
    """link a version to its project. A version is necessarily linked to one and only one project.
    """
    __permissions__ = {'read':   ('managers', 'users', 'guests',),
                       'delete': ('managers', ),
                       'add':    ('managers', 'logilab',
                              RRQLExpression('O require_permission P, P name "add_version",'
                                             'U in_group G, P require_group G'),)
                   }
    inlined = True

This configuration indicates that an entity CWPermission named “add_version” can be associated to a project and provides rights to create new versions on this project to specific groups. It is important to notice that:

  • in such case, we have to protect both the entity type “Version” and the relation associating a version to a project (“version_of”)
  • because of the genericity of the entity type CWPermission, we have to execute a unification with the groups and/or the states if necessary in the expression (“U in_group G, P require_group G” in the above example)

3.2.4. Handling schema changes

Also, it should be clear that to properly handle data migration, an instance’s schema is stored in the database, so the python schema file used to defined it is only read when the instance is created or upgraded.