func TestHandlerDeleteItemEtag(t *testing.T) { s := mem.NewHandler() s.Insert(context.TODO(), []*resource.Item{ {ID: "1", ETag: "a", Payload: map[string]interface{}{"id": "1"}}, }) index := resource.NewIndex() test := index.Bind("test", schema.Schema{ Fields: schema.Fields{"foo": {Filterable: true}}, }, s, resource.DefaultConf) r, _ := http.NewRequest("DELETE", "/test/2", nil) r.Header.Set("If-Match", "a") rm := &RouteMatch{ ResourcePath: []*ResourcePathComponent{ &ResourcePathComponent{ Name: "test", Field: "id", Value: "1", Resource: test, }, }, } status, headers, body := itemDelete(context.TODO(), r, rm) assert.Equal(t, http.StatusNoContent, status) assert.Nil(t, headers) assert.Nil(t, body) l, err := s.Find(context.TODO(), resource.NewLookup(), 1, -1) assert.NoError(t, err) assert.Len(t, l.Items, 0) }
// Lookup builds a Lookup object from the matched route func (r RouteMatch) Lookup() (*resource.Lookup, *Error) { l := resource.NewLookup() // Append route fields to the query for _, rp := range r.ResourcePath { if rp.Value != nil { l.AddQuery(schema.Query{schema.Equal{Field: rp.Field, Value: rp.Value}}) } } // Parse query string params if sort := r.Params.Get("sort"); sort != "" { if err := l.SetSort(sort, r.Resource().Validator()); err != nil { return nil, &Error{422, fmt.Sprintf("Invalid `sort` paramter: %s", err), nil} } } if filters, found := r.Params["filter"]; found { // If several filter parameters are present, merge them using $and (see lookup.addFilter) for _, filter := range filters { if err := l.AddFilter(filter, r.Resource().Validator()); err != nil { return nil, &Error{422, fmt.Sprintf("Invalid `filter` parameter: %s", err), nil} } } } if fields := r.Params.Get("fields"); fields != "" { if err := l.SetSelector(fields, r.Resource()); err != nil { return nil, &Error{422, fmt.Sprintf("Invalid `fields` paramter: %s", err), nil} } } return l, nil }
func TestHandlerDeleteList(t *testing.T) { s := mem.NewHandler() s.Insert(context.TODO(), []*resource.Item{ {ID: "1", Payload: map[string]interface{}{}}, {ID: "2", Payload: map[string]interface{}{}}, {ID: "3", Payload: map[string]interface{}{}}, {ID: "4", Payload: map[string]interface{}{}}, {ID: "5", Payload: map[string]interface{}{}}, }) index := resource.NewIndex() test := index.Bind("test", schema.Schema{}, s, resource.DefaultConf) r, _ := http.NewRequest("DELETE", "/test", bytes.NewBufferString("{}")) rm := &RouteMatch{ ResourcePath: []*ResourcePathComponent{ &ResourcePathComponent{ Name: "test", Resource: test, }, }, } status, headers, body := listDelete(context.TODO(), r, rm) assert.Equal(t, http.StatusNoContent, status) assert.Equal(t, http.Header{"X-Total": []string{"5"}}, headers) assert.Nil(t, body) l, err := s.Find(context.TODO(), resource.NewLookup(), 1, -1) assert.NoError(t, err) assert.Len(t, l.Items, 0) }
// ParentsExist checks if the each intermediate parents in the path exist and // return either a ErrNotFound or an error returned by on of the intermediate // resource. func (p ResourcePath) ParentsExist(ctx context.Context) error { // First we check that we have no field conflict on the path (i.e.: two path // components defining the same field with a different value) fields := map[string]interface{}{} for _, rp := range p { if val, found := fields[rp.Field]; found && val != rp.Value { return &Error{404, "Resource Path Conflict", nil} } fields[rp.Field] = rp.Value } // Check parents existence parents := len(p) - 1 if parents <= 0 { return nil } q := schema.Query{} wait := sync.WaitGroup{} defer wait.Wait() c := make(chan error, parents) for i := 0; i < parents; i++ { if p[i].Value == nil { continue } // Create a lookup with the parent path fields + the current path id l := resource.NewLookup() lq := append(q[:], schema.Equal{Field: "id", Value: p[i].Value}) l.AddQuery(lq) // Execute all intermediate checkes in concurence wait.Add(1) go func(index int) { defer wait.Done() // Check if the resource exists list, err := p[index].Resource.Find(ctx, l, 1, 1) if err != nil { c <- err } else if len(list.Items) == 0 { c <- &Error{404, "Parent Resource Not Found", nil} } else { c <- nil } }(i) // Push the resource field=value for the next hops q = append(q, schema.Equal{Field: p[i].Field, Value: p[i].Value}) } // Fail on first error for i := 0; i < parents; i++ { if err := <-c; err != nil { return err } } return nil }
// itemGet handles GET and HEAD resquests on an item URL func (r *request) itemGet(ctx context.Context, route *RouteMatch) (status int, headers http.Header, body interface{}) { lookup, e := route.Lookup() if e != nil { return e.Code, nil, e } list, err := route.Resource().Find(ctx, lookup, 1, 1) if err != nil { e = NewError(err) return e.Code, nil, e } else if len(list.Items) == 0 { return ErrNotFound.Code, nil, ErrNotFound } item := list.Items[0] // Handle conditional request: If-None-Match if compareEtag(r.req.Header.Get("If-None-Match"), item.ETag) { return 304, nil, nil } // Handle conditional request: If-Modified-Since if r.req.Header.Get("If-Modified-Since") != "" { if ifModTime, err := time.Parse(time.RFC1123, r.req.Header.Get("If-Modified-Since")); err != nil { return 400, nil, &Error{400, "Invalid If-Modified-Since header", nil} } else if item.Updated.Equal(ifModTime) || item.Updated.Before(ifModTime) { return 304, nil, nil } } item.Payload, err = lookup.ApplySelector(route.Resource(), item.Payload, func(path string, value interface{}) (*resource.Resource, map[string]interface{}, error) { router, ok := IndexFromContext(ctx) if !ok { return nil, nil, errors.New("router not available in context") } rsrc, _, found := router.GetResource(path) if !found { return nil, nil, fmt.Errorf("invalid resource reference: %s", path) } l := resource.NewLookup() l.AddQuery(schema.Query{schema.Equal{Field: "id", Value: value}}) list, _ := rsrc.Find(ctx, l, 1, 1) if len(list.Items) == 1 { item := list.Items[0] return rsrc, item.Payload, nil } // If no item found, just return an empty dict so we don't error the main request return rsrc, map[string]interface{}{}, nil }) if err != nil { e = NewError(err) return e.Code, nil, e } return 200, nil, item }
func listParamResolver(r *resource.Resource, p graphql.ResolveParams, params url.Values) (lookup *resource.Lookup, page int, perPage int, err error) { page = 1 // Default value on non HEAD request for perPage is -1 (pagination disabled) perPage = -1 if l := r.Conf().PaginationDefaultLimit; l > 0 { perPage = l } if p, ok := p.Args["page"].(string); ok && p != "" { i, err := strconv.ParseUint(p, 10, 32) if err != nil { return nil, 0, 0, errors.New("invalid `limit` parameter") } page = int(i) } if l, ok := p.Args["limit"].(string); ok && l != "" { i, err := strconv.ParseUint(l, 10, 32) if err != nil { return nil, 0, 0, errors.New("invalid `limit` parameter") } perPage = int(i) } if perPage == -1 && page != 1 { return nil, 0, 0, errors.New("cannot use `page' parameter with no `limit' paramter on a resource with no default pagination size") } lookup = resource.NewLookup() if sort, ok := p.Args["sort"].(string); ok && sort != "" { if err := lookup.SetSort(sort, r.Validator()); err != nil { return nil, 0, 0, fmt.Errorf("invalid `sort` parameter: %v", err) } } if filter, ok := p.Args["filter"].(string); ok && filter != "" { if err := lookup.AddFilter(filter, r.Validator()); err != nil { return nil, 0, 0, fmt.Errorf("invalid `filter` parameter: %v", err) } } if params != nil { if filter := params.Get("filter"); filter != "" { if err := lookup.AddFilter(filter, r.Validator()); err != nil { return nil, 0, 0, fmt.Errorf("invalid `filter` parameter: %v", err) } } } return }
func TestHandlerDeleteItemFilterCondition(t *testing.T) { s := mem.NewHandler() s.Insert(context.TODO(), []*resource.Item{ {ID: "1", Payload: map[string]interface{}{"id": "1", "foo": "bar"}}, {ID: "2", Payload: map[string]interface{}{"id": "2", "foo": "bar"}}, {ID: "3", Payload: map[string]interface{}{"id": "3", "foo": "bar"}}, }) index := resource.NewIndex() test := index.Bind("test", schema.Schema{ Fields: schema.Fields{"foo": {Filterable: true}}, }, s, resource.DefaultConf) r, _ := http.NewRequest("DELETE", "/test/2", nil) rm := &RouteMatch{ ResourcePath: []*ResourcePathComponent{ &ResourcePathComponent{ Name: "test", Field: "id", Value: "2", Resource: test, }, }, Params: url.Values{ "filter": []string{`{"foo": "baz"}`}, }, } status, headers, body := itemDelete(context.TODO(), r, rm) assert.Equal(t, http.StatusNotFound, status) assert.Nil(t, headers) if assert.IsType(t, body, &Error{}) { err := body.(*Error) assert.Equal(t, http.StatusNotFound, err.Code) assert.Equal(t, "Not Found", err.Message) } l, err := s.Find(context.TODO(), resource.NewLookup(), 1, -1) assert.NoError(t, err) assert.Len(t, l.Items, 3) }
// checkReferences checks that fields with the Reference validator reference an existing object func (r *request) checkReferences(ctx context.Context, payload map[string]interface{}, s schema.Validator) *Error { for name, value := range payload { field := s.GetField(name) if field == nil { continue } // Check reference if validator is of type Reference if field.Validator != nil { if ref, ok := field.Validator.(*schema.Reference); ok { router, ok := IndexFromContext(ctx) if !ok { return &Error{500, "Router not available in context", nil} } rsrc, _, found := router.GetResource(ref.Path) if !found { return &Error{500, fmt.Sprintf("Invalid resource reference for field `%s': %s", name, ref.Path), nil} } l := resource.NewLookup() l.AddQuery(schema.Query{schema.Equal{Field: "id", Value: value}}) list, _ := rsrc.Find(ctx, l, 1, 1) if len(list.Items) == 0 { return &Error{404, fmt.Sprintf("Resource reference not found for field `%s'", name), nil} } } } // Check sub-schema if any if field.Schema != nil && value != nil { if subPayload, ok := value.(map[string]interface{}); ok { if err := r.checkReferences(ctx, subPayload, field.Schema); err != nil { return err } } } } return nil }
// listGet handles GET resquests on a resource URL func (r *request) listGet(ctx context.Context, route *RouteMatch) (status int, headers http.Header, body interface{}) { page := 1 perPage := 0 if route.Method != "HEAD" { if l := route.Resource().Conf().PaginationDefaultLimit; l > 0 { perPage = l } else { // Default value on non HEAD request for perPage is -1 (pagination disabled) perPage = -1 } if p := r.req.URL.Query().Get("page"); p != "" { i, err := strconv.ParseUint(p, 10, 32) if err != nil { return 422, nil, &Error{422, "Invalid `page` paramter", nil} } page = int(i) } if l := r.req.URL.Query().Get("limit"); l != "" { i, err := strconv.ParseUint(l, 10, 32) if err != nil { return 422, nil, &Error{422, "Invalid `limit` paramter", nil} } perPage = int(i) } if perPage == -1 && page != 1 { return 422, nil, &Error{422, "Cannot use `page' parameter with no `limit' paramter on a resource with no default pagination size", nil} } } lookup, e := route.Lookup() if e != nil { return e.Code, nil, e } list, err := route.Resource().Find(ctx, lookup, page, perPage) if err != nil { e = NewError(err) return e.Code, nil, e } for _, item := range list.Items { item.Payload, err = lookup.ApplySelector(route.Resource(), item.Payload, func(path string, value interface{}) (*resource.Resource, map[string]interface{}, error) { router, ok := IndexFromContext(ctx) if !ok { return nil, nil, errors.New("router not available in context") } rsrc, _, found := router.GetResource(path) if !found { return nil, nil, fmt.Errorf("invalid resource reference: %s", path) } l := resource.NewLookup() l.AddQuery(schema.Query{schema.Equal{Field: "id", Value: value}}) list, _ := rsrc.Find(ctx, l, 1, 1) if len(list.Items) == 1 { item := list.Items[0] return rsrc, item.Payload, nil } // If no item found, just return an empty dict so we don't error the main request return rsrc, map[string]interface{}{}, nil }) if err != nil { e = NewError(err) return e.Code, nil, e } } return 200, nil, list }
func callGetSort(s string, v schema.Validator) []string { l := resource.NewLookup() l.SetSort(s, v) return getSort(l) }
func callGetQuery(q schema.Query) (bson.M, error) { l := resource.NewLookup() l.AddQuery(q) return getQuery(l) }
}, }, }, } ) func SetAuthUserResource(us *resource.Resource) { field, _ := AuthSchema["access"] field.HookParams[0].Param = us } var CheckAccess = func(value interface{}, params []interface{}) interface{} { users, users_ok := params[0].(*resource.Resource) username, u_ok := params[1].(string) password, p_ok := params[2].(string) if users_ok && u_ok && p_ok { l := resource.NewLookup() l.AddQuery(schema.Query{schema.Equal{Field: "username", Value: username}}) list, err := users.Find(context.Background(), l, 1, 1) if err == nil && len(list.Items) == 1 { user := list.Items[0] if schema.VerifyPassword(user.Payload["password"], []byte(password)) { return true } } } return false }
func TestFind(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } s, err := mgo.Dial("") if !assert.NoError(t, err) { return } defer cleanup(s, "testfind")() h := NewHandler(s, "testfind", "test") h2 := NewHandler(s, "testfind", "test2") items := []*resource.Item{ {ID: "1", Payload: map[string]interface{}{"id": "1", "name": "a", "age": 1}}, {ID: "2", Payload: map[string]interface{}{"id": "2", "name": "b", "age": 2}}, {ID: "3", Payload: map[string]interface{}{"id": "3", "name": "c", "age": 3}}, {ID: "4", Payload: map[string]interface{}{"id": "4", "name": "d", "age": 4}}, } ctx := context.Background() assert.NoError(t, h.Insert(ctx, items)) assert.NoError(t, h2.Insert(ctx, items)) lookup := resource.NewLookup() l, err := h.Find(ctx, lookup, 1, -1) if assert.NoError(t, err) { assert.Equal(t, 1, l.Page) assert.Equal(t, -1, l.Total) // Mongo doesn't support total counting assert.Len(t, l.Items, 4) // Do not check result's content as its order is unpredictable } lookup = resource.NewLookupWithQuery(schema.Query{ schema.Equal{Field: "name", Value: "c"}, }) l, err = h.Find(ctx, lookup, 1, 100) if assert.NoError(t, err) { assert.Equal(t, 1, l.Page) assert.Equal(t, -1, l.Total) // Mongo doesn't support total counting if assert.Len(t, l.Items, 1) { item := l.Items[0] assert.Equal(t, "3", item.ID) assert.Equal(t, map[string]interface{}{"id": "3", "name": "c", "age": 3}, item.Payload) } } lookup = resource.NewLookupWithQuery(schema.Query{ schema.In{Field: "name", Values: []schema.Value{"c", "d"}}, }) lookup.SetSorts([]string{"name"}) l, err = h.Find(ctx, lookup, 1, 100) if assert.NoError(t, err) { assert.Equal(t, 1, l.Page) assert.Equal(t, -1, l.Total) // Mongo doesn't support total counting if assert.Len(t, l.Items, 2) { item := l.Items[0] assert.Equal(t, "3", item.ID) assert.Equal(t, map[string]interface{}{"id": "3", "name": "c", "age": 3}, item.Payload) item = l.Items[1] assert.Equal(t, "4", item.ID) assert.Equal(t, map[string]interface{}{"id": "4", "name": "d", "age": 4}, item.Payload) } } lookup = resource.NewLookupWithQuery(schema.Query{ schema.Equal{Field: "id", Value: "3"}, }) l, err = h.Find(ctx, lookup, 1, 1) if assert.NoError(t, err) { assert.Equal(t, 1, l.Page) assert.Equal(t, -1, l.Total) // Mongo doesn't support total counting if assert.Len(t, l.Items, 1) { item := l.Items[0] assert.Equal(t, "3", item.ID) assert.Equal(t, map[string]interface{}{"id": "3", "name": "c", "age": 3}, item.Payload) } } lookup = resource.NewLookupWithQuery(schema.Query{ schema.Equal{Field: "id", Value: "10"}, }) l, err = h.Find(ctx, lookup, 1, 1) if assert.NoError(t, err) { assert.Equal(t, 1, l.Page) assert.Equal(t, -1, l.Total) // Mongo doesn't support total counting assert.Len(t, l.Items, 0) } lookup = resource.NewLookupWithQuery(schema.Query{ schema.In{Field: "id", Values: []schema.Value{"3", "4", "10"}}, }) l, err = h.Find(ctx, lookup, 1, -1) if assert.NoError(t, err) { assert.Equal(t, 1, l.Page) assert.Equal(t, -1, l.Total) // Mongo doesn't support total counting assert.Len(t, l.Items, 2) } }
// findRoute recursively route a (sub)resource request func findRoute(ctx context.Context, path string, index resource.Index, route *RouteMatch) *Error { // Split the path into path components c := strings.Split(strings.Trim(path, "/"), "/") // Shift the resource name from the path components name, c := c[0], c[1:] resourcePath := name if prefix := route.ResourcePath.Path(); prefix != "" { resourcePath = strings.Join([]string{prefix, name}, ".") } // First component must match a resource if rsrc, _, found := index.GetResource(resourcePath); found { rp := ResourcePathComponent{ Name: name, Resource: rsrc, } if len(c) >= 1 { // If there are some components left, the path targets an item or an alias // Shift the item id from the path components var id string id, c = c[0], c[1:] // Handle sub-resources (/resource1/id1/resource2/id2) if len(c) >= 1 { subResourcePath := strings.Join([]string{resourcePath, c[0]}, ".") if _, field, found := index.GetResource(subResourcePath); found { // Check if the current (intermediate) item exists before going farther l := resource.NewLookup() q := schema.Query{} for _, rp := range route.ResourcePath { if rp.Value != nil { q = append(q, schema.Equal{Field: rp.Field, Value: rp.Value}) } } q = append(q, schema.Equal{Field: "id", Value: id}) l.AddQuery(q) list, err := rsrc.Find(ctx, l, 1, 1) if err != nil { return NewError(err) } else if len(list.Items) == 0 { return ErrNotFound } rp.Field = field rp.Value = id route.ResourcePath = append(route.ResourcePath, rp) // Recurse to match the sub-path path = strings.Join(c, "/") if err := findRoute(ctx, path, index, route); err != nil { return err } } else { route.ResourcePath = ResourcePath{} return &Error{404, "Resource Not Found", nil} } return nil } // Handle aliases (/resource/alias or /resource1/id1/resource2/alias) if alias, found := rsrc.GetAlias(id); found { // Apply aliases query to the request for key, values := range alias { for _, value := range values { route.Params.Add(key, value) } } } else { // Set the id route field rp.Field = "id" rp.Value = id } } route.ResourcePath = append(route.ResourcePath, rp) return nil } route.ResourcePath = ResourcePath{} return &Error{404, "Resource Not Found", nil} }