rest-layer icon indicating copy to clipboard operation
rest-layer copied to clipboard

Allow soft delete (that rely on Delete rather than Update permissions)

Open apuigsech opened this issue 5 years ago • 15 comments

I am going to explain my use case to see if you can help me find the best way to implement it using rest layer.

There are certain type of objects that I need to deprecate based on a TTL (time to live), so I created a field named 'ttl' on the schema with that configuration:

"ttl": {
    Default: 0,
    Validator: &schema.Integer{},
    Filterable: true,
},

Having 0 as 'ttl' means that the object is never deprecated. On the case it's not 0, the deprecation time must be updated + ttl.

Now I need to be able to sort and filter based on that deprecation time, but there isn't a specific field with that data, so i need to create it. The characteristics I need for that field are:

  • It must be ReadOnly, so nobody can write it directly but only through the 'ttl' value.
  • It must be Sortable and Filtrable.
  • If the 'ttl' is 0, It should have no value.

I cannot use OnInit or OnUpdate to set the value, because on the init and update function I don't have access to the value of the 'ttl' field, so I tried to use Hooks for OnInsert and OnUpdate; It works when creating the object, but not when updating it (because it's read only).

apuigsech avatar Jan 03 '19 17:01 apuigsech

Commenting although you are still editing the post.

I wold recommend solving the TTL on a database level based on your TTL field. For MongoDB, check out https://docs.mongodb.com/manual/core/index-ttl/

smyrman avatar Jan 03 '19 18:01 smyrman

The problem is that postgres (the db i am using) is not implementing any kind of ttl.

apuigsech avatar Jan 04 '19 10:01 apuigsech

Assuming a Nullable timestamp field in the DB named deleteAt in the schema, you cold have a goroutune that does a Clear with filter {deleteAt:{$lt: now} at a reasonable interval.

smyrman avatar Jan 06 '19 18:01 smyrman

Pseudo code for the filter...

smyrman avatar Jan 06 '19 18:01 smyrman

This is exactly the solution I am working on. The only problem is that I cannot meet the requirement of forbidding the direct edit of 'DeleteAt' (as I want it can be only modified through the 'ttl' field).

Maybe makes sense to create another attribute on which is "Computed". Computed fields are those that cannot be edited directly but they are computed on INSERT/UPDATE/REPLACE time.

Opinions?

apuigsech avatar Jan 08 '19 21:01 apuigsech

Is it really necessary to this to go through rest-layer? I have a use case to atomically increase an number in Mongo with $inc, so I will do it with direct DB connection, from rest-layer hook. Behind the hands of rest-layer,but I guess it will work, because rest-layer doesn't cache any DB data.

Dragomir-Ivanov avatar Jan 08 '19 21:01 Dragomir-Ivanov

I am not sure which is the best way to approach that, but I think that having Computed fields may be very powerful in some cases.

apuigsech avatar Jan 08 '19 21:01 apuigsech

Well, depending on what data they use to compute, I guess doing the computation in a hook is a way to go. I would prefer rest-layer to stay relatively simple, so it has lower entry barrier, but can be extended if needed for more complex scenarios. Hooks are one good extension point.

Dragomir-Ivanov avatar Jan 08 '19 21:01 Dragomir-Ivanov

I am using Hooks right now to implement what I want, and it works. But I want that nobody is able to edit the field directly through the REST interface :(

apuigsech avatar Jan 08 '19 21:01 apuigsech

Well, there are before and after hooks. So if you use before hook to catch any modification attempt, you may strip this field, or raise an error and block the REST call.

Dragomir-Ivanov avatar Jan 08 '19 22:01 Dragomir-Ivanov

Doesn't setting the field ReadOnly: true work? The resource hook can still update the field as it happens post schema validation.

smyrman avatar Jan 08 '19 23:01 smyrman

It doesn't work. It returns an error message because the field is ReadOnly and with the Hook I need to change it.

My hook is quite simple;

func (dh DeprecateHandler) OnInsert(ctx context.Context, items []*resource.Item) error {
	for _, i := range items {
		if i.Payload["ttl"].(int) > 0 {
			i.Payload["deleteAt"] = time.Now().Local().Add(time.Hour * time.Duration(i.Payload["ttl"].(int)))
		 }
	}	
	return nil
}




func (dh DeprecateHandler) OnUpdate(ctx context.Context, item *resource.Item, original *resource.Item) error {
	ttl_item, ok_item := item.Payload["ttl"].(int)
	ttl_original, ok_original := item.Payload["ttl"].(int)

	if ok_item && ttl_item > 0 {
		item.Payload["deleteAt"] = time.Now().Local().Add(time.Hour * time.Duration(ttl_item))
	} else {
		if ok_original &&  ttl_original > 0 {
			item.Payload["deleteAt"] = time.Now().Local().Add(time.Hour * time.Duration(ttl_original))
		}
	}

	return nil
}

apuigsech avatar Jan 09 '19 00:01 apuigsech

You are right 🤦‍♂️ My mistake.

There are some work-around for setting read-only fields but it's not available to the hook. It's used e.g. when setting fields from the path:

  • https://github.com/rs/rest-layer/blob/0163c1396adfb8b06e61dc74371a2f5b96c11d55/rest/method_item_patch.go#L84

It wold be nice if there was an easier way to bypass Read-Only fields; we certainly have a use-case for it, e.g. when we are using RPC to update resources directly. But I don't have a good idea for how it cold be designed.

smyrman avatar Jan 10 '19 10:01 smyrman

Longer term, I plan to have a look at this here: https://github.com/searis/schema. However I cannot commit to any particular timeline for this now.

Probably nice to come up with a solution that works for the current schema package.

smyrman avatar Jan 10 '19 10:01 smyrman

Possible work-around for now:

If you always set the field in your hook on insert/update (or remove it from the payload if present when ttl is 0), any value the user may set will be overwritten so it doesn't hurt that the field is not read-only.

The only thing you won't get from this is the validation error when users are trying to set it. It's not ideal, but perhaps it's workable.

Possible fix in the schema package:

Here is one possible thought. We add a wrapper type that can be embedded in the payload programatically to skip validation.

package schema

type SkipValidation struct{
    Value interface{}
}

When a Field validation method encounters a value of type SkipValidation, it returns the value.(SkipValidation).Value instead of passing the value to the field.Validator.

So in this case:

func (dh DeprecateHandler) OnInsert(ctx context.Context, items []*resource.Item) error {
	for _, i := range items {
		if i.Payload["ttl"].(int) > 0 {
			i.Payload["deleteAt"] = schema.SkipValidation{
				Value: time.Now().Local().Add(time.Hour * time.Duration(i.Payload["ttl"].(int))),
			}
		 }
	}	
	return nil
}

Perhaps could even type SkipValidtion interface{} work, but that needs to be tested. Not sure how to resolve a "nested" interface{} reference in go.

smyrman avatar Jan 10 '19 12:01 smyrman