3

In Python Eve framework, is it possible to have a condition which checks combination of two fields to be unique?

For example the below definition restricts only firstname and lastname to be unique for items in the resource.

people = {
    # 'title' tag used in item links.
    'item_title': 'person',
    'schema': {
        'firstname': {
            'type': 'string',
            'required': True,
            'unique': True
        },
        'lastname': {
            'type': 'string',
            'required': True,
            'unique': True
        }
}

Instead, is there a way to restrict firstname and lastname combination to be unique?

Or is there a way to implement a CustomValidator for this?

Yogeswaran
  • 357
  • 1
  • 13

3 Answers3

3

You can probably achieve what you want by overloading the _validate_unique and implementing custom logic there, taking advantage of self.document in order to retrieve the other field value.

However, since _validate_unique is called for every unique field, you would end up performing your custom validation twice, once for firstname and then for lastname. Not really desirable. Of course the wasy way out is setting up fullname field, but I guess that's not an option in your case.

Have you considered going for a slighty different design? Something like:

{'name': {'first': 'John', 'last': 'Doe'}}

Then all you need is make sure that name is required and unique:

{
    'name': {
        'type':'dict', 
        'required': True, 
        'unique': True,
        'schema': {
            'first': {'type': 'string'},
            'last': {'type': 'string'}
        }
    }
}
Nicola Iarocci
  • 6,606
  • 1
  • 20
  • 33
  • Thanks Nicola. Using `self.document` I am querying internally to find if the combination is existing already. Added a new validator for this. – Yogeswaran Jun 11 '15 at 12:00
  • I tried using the alternate design for the data, but was not successful in getting the validation to work as intended. @Yogeswaran, could you post your code? – fullerja Jun 17 '16 at 21:02
1

Inspired by Nicola and _validate_unique.

from eve.io.mongo import Validator
from eve.utils import config
from flask import current_app as app

class ExtendedValidator(Validator):
    def _validate_unique_combination(self, unique_combination, field, value):
        """ {'type': 'list'} """
        self._is_combination_unique(unique_combination, field, value, {})


    def _is_combination_unique(self, unique_combination, field, value, query):
        """ Test if the value combination is unique.
        """
        if unique_combination:
            query = {k: self.document[k] for k in unique_combination}
            query[field] = value

            resource_config = config.DOMAIN[self.resource]

            # exclude soft deleted documents if applicable
            if resource_config['soft_delete']:
                query[config.DELETED] = {'$ne': True}

            if self.document_id:
                id_field = resource_config['id_field']
                query[id_field] = {'$ne': self.document_id}

            datasource, _, _, _ = app.data.datasource(self.resource)

            if app.data.driver.db[datasource].find_one(query):
                key_names = ', '.join([k for k in query])
                self._error(field, "value combination of '%s' is not unique" % key_names)
Colin Lee
  • 11
  • 3
0

The way I solved this issue is by creating a dynamic field using a combination of functions and lambdas to create a hash that will use which ever fields you provide

def unique_record(fields):
    def is_lambda(field):
        # Test if a variable is a lambda
        return callable(field) and field.__name__ == "<lambda>"

    def default_setter(doc):
        # Generate the composite list
        r = [
            str(field(doc)
                # Check is lambda
                if is_lambda(field)
                # jmespath is not required, but it enables using nested doc values
                else jmespath.search(field, doc))
            for field in fields
        ]

        # Generate MD5 has from composite string (Keep it clean)
        return hashlib.md5(''.join(r).encode()).hexdigest()

    return {
        'type': 'string',
        'unique': True,
        'default_setter': default_setter
    }

Practical Implementation

My use case was to create a collection that limits the amount of key value pairs a user can create within the collection

domain = {
    'schema': {
        'key': {
            'type': 'string',
            'minlength': 1,
            'maxlength': 25,
            'required': True,
        },
        'value': {
            'type': 'string',
            'minlength': 1,
            'required': True
        },
        'hash': unique_record([
            'key',
            lambda doc: request.USER['_id']
        ]),
        'user': {
            'type': 'objectid',
            'default_setter': lambda doc: request.USER['_id']  # User tenant ID
            }
        }
    }
}

The function will receive a list of either string or lambda function for dynamic value setting at request time, in my case the user's "_id"

The function supports the use of JSON query with the JMESPATH package, this isn't mandatory, but leave the door open for nested doc flexibility in other usecases

NOTE: This will only work with values that are set by the USER at request time or injected into the request body using the pre_GET trigger pattern, like the USER object I inject in the pre_GET trigger which represents the USER currently making the request

Illegal Operator
  • 656
  • 6
  • 14