viur-framework/viur-core

How can I create a unique constraint on a skeleton over multiple bones?

Opened this issue · 1 comments

I have these bones in a Skeleton:

name = StringBone(
)

module = SelectBone(
    values=lambda: {name: name for name in ModuleConf.MODULES},
)

Now I want to ensure there's at least one skeleton per permutation of the values:

for example

{name: "foo", module: "file", other_bone:"abc"}  # id: 1
{name: "bar", module: "file", other_bone:"456"}  # id: 2
{name: "foo", module: "user", other_bone:"abc"}  # id: 3
{name: "bar", module: "user", other_bone:"abc"}  # id: 4

can exist together.
But you should not able to add

{name: "foo", module: "file", other_bone: "def"}  # id: 5

because there's already an entry (ìd: 1) with {name: "foo", module: "file"}.

I tried to create a composition of these bones with a compute bone.

    unique_lock = StringBone(
        compute=Compute(lambda skel: f'{skel["module"]}_{skel["name"]}',
                        interval=ComputeInterval(ComputeMethod.OnWrite)),
        visible=False,
        unique=UniqueValue(UniqueLockMethod.SameValue, False, "Value already taken"),
    )

But this doesn't cause a client-error in fromClient and fails only in the skeleton toDB transaction:

  File "/.../lib/python3.11/site-packages/viur/core/skeleton.py", line 1029, in __txn_update
    raise ValueError(
ValueError: The unique value 'file_foo' of bone 'unique_lock' has been recently claimed!

Does anyone has an idea how I can simplify this / enforce a client error?

Maybe we can check this in getUniquePropertyIndexValues like:

    def getUniquePropertyIndexValues(self, skel: 'viur.core.skeleton.SkeletonInstance', name: str) -> list[str]:
        val = skel[name]
        if self.compute:
            match self.compute.interval.method:
                case ComputeMethod.OnWrite:
                    val = self._compute(skel, name)

                case ComputeMethod.Lifetime:
                    now = utils.utcNow()

                    last_update = \
                        skel.accessedValues.get(f"_viur_compute_{name}_") \
                        or skel.dbEntity.get(f"_viur_compute_{name}_")

                    if not last_update or last_update + self.compute.interval.lifetime < now:
                        val = self._compute(skel, name)

                case ComputeMethod.Once:
                    if name not in skel.dbEntity:
                        val = self._compute(skel, name)

        if val is None:
            return []
        return self._hashValueForUniquePropertyIndex(val)