-
Notifications
You must be signed in to change notification settings - Fork 114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove resource hooks in favor of resource middleware #259
Comments
The issue I see with middleware is that it makes it harder to use optional interfaces. We use those on storer for instance, IIRC. |
I think that's solvable with clousure defaults, however I think we can probably just drop the optional interfaces all-together and make things easier. To make writing middleware easier we can also drop the type Storer interface{
Count(ctx context.Context, q *query.Query) (int, error) // merged from Counter interface
Find(ctx context.Context, q *query.Query) (*ItemList, error)
Clear(ctx context.Context, q *query.Query) (int, error)
Insert(ctx context.Context, items []*Item) error
Replace(ctx context.Context, item *Item, original *query.Query) error // original as query
} Middleware we could now wrote:
The resource could still provide convenience methods for I am imagining we still call a method on // Type aliases for functions.
type (
CountFunc = func(ctx context.Context, q *query.Query) (int, error)
ClearFunc = CountFunc
FindFunc = func(ctx context.Context, q *query.Query) (*ItemList, error)
InsertFunc = func(ctx context.Context, items []*Item) error
)
type CountMiddleware struct {
CountFunc(parent Resource) CounterFunc
}
type FindMiddleware struct {
FindFunc(parent Resource) FindFunc
}
// ...
type Resource struct {
count CountFunc
find FindFunc
clear ClearFunc
insert InsertFunc
// ...
}
// newResource creates a new resource with provided spec, handler and config.
func newResource(name string, s schema.Schema, h Storer, c Conf) Resource {
return Resource{
count: h.Count,
find: h.Find,
// ...
}
}
func (r Resource) WithMiddleware(m interface{}) Resource {
parent := r // ensure all middleware functions on a given m get's passed the same parent.
if t, ok := m.(CountMiddleware); ok {
r.count = t.CountFunc(parent)
}
if t, ok := m.(FindMiddleware); ok {
r.find = t.FindFunc(parent)
}
// ...
return r
}
func (r Resource) Get(id interface{}) (*Item, error) {
// Call Find with limit 1, report 0 items found as error
}
func (r Resource) Delete(id interface{}) error {
// Call Clear with limit 1, report 0 deleted as error
} |
Dropping If you can't add a middleware for the In your example you can not chain middleware right? Calling |
No, it can be chained; r := newResource("r", s, h, c)
r2 := r.WithMiddleware(b).WithMiddleare(c) // normally you would do r =, not r2 := Given
The original |
Through the middleware defined for the Find method; resources where we lack access won't be found.
This is the same as what we get for hooks today: // Use attaches an event handler to the resource. This event handler must
// implement on of the resource.*EventHandler interface or this method returns
// an error.
func (r *Resource) Use(e interface{}) error {
return r.hooks.use(e)
} But agree it would be nice with a concrete interface, but then you can't allow middleware for multiple operations to be passed in the same function call. |
Maybe this is better: type MiddlewareFunc func(r Resource) Resource We don't even need to define this, it could just be by convention. Example middleware: func WithSoftDelete(r Resource) Resource {
parent := r // capture parent
r.Clear = func(ctx context.Context, q *query.Query) (int, error) {
deleteTime := time.Now()
items, err := parent.Find(ctx, q)
if err != nil {
return 0, err
}
for i, item := range items.Items {
origQ, err := query.Predicate{
query.Equal{"_etag", item.Etag},
query.Equal{"id": item.ID},
}
item.Payload["deletedAt"] = deleteTime
item.UpdateEtag() // recalculate E-tag based on item changes.
err := parent.Replace(item, origQ)
if err != nil {
return i, err
}
}
return len(items.Items), nil
}
return r
} Ideally Replace would also be defined as a bulk operation somehow to allow doing this kind of things as a transaction internally in the storage layer. Usage: r := resource.New(schema, h) // pass in only schema and Storer?
r = WithSoftDelete(r)
r = WithPermissions(r)
idx.Bind("path", r, conf) // Pass in resource config during bind?
// ...
idx.Bind("path.sub", r2, conf) When to pass in path and config (during initialization or bind) can be determined later; if middleware need access to it, it must be during New. If it's safe to put the information on the index only, it can happen during Bind. |
If we could get a simpler / safer access control, and get to implement soft-delete, I would say it's likely worth the price. Storage layers could still optimize fetching multiple items by ID, but would need to inspect the Find predicate to do so rather than implementing an additional interfaces. I don't know which storage engine you had in mind that can do a multi-get faster than a normal find with |
That's my point, it is expensive to introspect a query to see if an optimization is possible. Same for ACL, how do you put an ACL on |
The same is true for Our own permission syte, isn't actually ACL; permissions are stored on "protected fields" on the object, or on related objects that are fetched before the resource itself, but I don't get how this would be any different with an ACL permission system.
For the case in particular, it's something like this: // AsIDQuery returns an IDQuery or nil.
AsIDQuery(q *query.Query) *IDQuery {
if len(q.Predicate) != 1 {
return nil
}
in, ok := q.Predicate[0].(query.In)
if !ok || in.Field != "id" {
return nil
}
return *IDQuery{IDs: in.Values, Window: q.Window}
} I could agree it being complex, but I don't buy that it's going to be expensive (depending on weather Expressions are allowed as Either way, the best thing about it complexity wise, is that the complexity is moved from the rest-layer end-user to the rest-layer storer backend implementation, and that's a good thing. To reiterate on the last point from our own experience on writing hooks, it's really hard and limiting to work with for advanced use-cases. For the permission hooks in particular, we need to:
With the new proposed interface, our permission system in particular only need to alter queries. On replace for instance, we could alter the original query to validate that the relevant protected fields are either not-changed, or that we have the right permissions. Writing a proper Soft-delete, as explained by #225, just isn't possible through hooks. Probably we could do_ almost_ everything we want as middleware directly on top of the Storage layer, but we have so far chosen to use the documented hooks instead. |
OK I get what you mean now. Sorry for being slow. You refer to if, e.g. a user has This is a good point, and need to be supported. I want to remind you though, that the current implementation of
On that note, since PATCH needs to fetch the original resource to perform it (and would not want to trigger ACL checks when doing so) I would say ACL to allow/disallow certain operations on a resource (either for a user or globally), should perhapsto be implemented at the rest layer (e.g. as HTTP middleware), and not as resource layer checks. |
Ok, if we go that route, why not simplifying it even more and just let people wrap storer to build middleware like it is down with HTTP? This way there is no complexity to be added at the rest-layer layer. |
Yeah, Probably thats fair. |
Hooks have several problems and limitations compared to middleware:
With middleware we get more power:
I want to suggest completely removing resource hooks in favor of middleware. This may involve making
resource.Resource
an interface that can be wrapped, or we might decide it's sufficient to wrap the storage layer and document that as the way to implement hooks. It could also involve changing the methods of a resource to be settable (except for wrapper methods such as MultiGet), setting the methods to the storage layer method on initalization, and wrapping them when Resource.Use is called.The text was updated successfully, but these errors were encountered: