Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ Below is an overview over recent breaking changes, starting from an arbitrary po
- Storage drivers need to accept pointer to `Expression` implementer in `query.Predicate`.
- `filter` parameters in sub-query will be validated for type match.
- `filter` parameters will be validated for type match only, instead of type & constrains.
- PR #228: `Reference` projection fields will be validated against referenced resource schema.
- PR #230: `Connection` projection fields will be validated against connected resource schema.

From the next release and onwards (0.2), this list will summarize breaking changes done to master since the last release.

Expand Down
2 changes: 1 addition & 1 deletion resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func (r *Resource) Bind(name, field string, s schema.Schema, h Storer, c Conf) *
Validator: &schema.Connection{
Path: "." + name,
Field: field,
Validator: s,
Validator: sr.validator,
},
Params: schema.Params{
"skip": schema.Param{
Expand Down
2 changes: 1 addition & 1 deletion resource/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func TestResourceBind(t *testing.T) {
Validator: &schema.Connection{
Path: ".bar",
Field: "foo",
Validator: barSchema,
Validator: bar.validator,
},
Params: schema.Params{
"skip": schema.Param{
Expand Down
46 changes: 31 additions & 15 deletions schema/query/projection_evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ type Resource interface {

// Validator returns the schema.Validator associated with the resource.
Validator() schema.Validator

// Path returns the full path of the resource composed of names of each
// intermediate resources separated by dots (i.e.: res1.res2.res3).
Path() string
}

// Eval evaluate the projection on the given payload with the help of the
Expand All @@ -31,10 +35,10 @@ type Resource interface {
func (p Projection) Eval(ctx context.Context, payload map[string]interface{}, rsc Resource) (map[string]interface{}, error) {
rbr := &referenceBatchResolver{}
validator := rsc.Validator()
payload, err := evalProjection(ctx, p, payload, validator, rbr)
payload, err := evalProjection(ctx, p, payload, validator, rbr, rsc)
if err == nil {
// Execute the batched reference resolutions.
err = rbr.execute(ctx, rsc)
err = rbr.execute(ctx)
}
return payload, err
}
Expand Down Expand Up @@ -80,7 +84,7 @@ func prepareProjection(p Projection, payload map[string]interface{}) (Projection
return proj, nil
}

func evalProjectionArray(ctx context.Context, pf ProjectionField, payload []interface{}, def *schema.Field, rbr *referenceBatchResolver) (*[]interface{}, error) {
func evalProjectionArray(ctx context.Context, pf ProjectionField, payload []interface{}, def *schema.Field, rbr *referenceBatchResolver, rsc Resource) (*[]interface{}, error) {
res := make([]interface{}, 0, len(payload))
// Return pointer to res, because it may be populated after this function ends, by referenceBatchResolver
// in `schema.Reference` case
Expand All @@ -102,11 +106,15 @@ func evalProjectionArray(ctx context.Context, pf ProjectionField, payload []inte
Projection: pf.Children,
Predicate: Predicate{e},
}
rbr.request(fieldType.Path, q, func(payloads []map[string]interface{}, validator schema.Validator) error {
subRsc, err := rsc.SubResource(ctx, fieldType.Path)
if err != nil {
return nil, err
}
rbr.request(subRsc, q, func(payloads []map[string]interface{}, validator schema.Validator, rsc Resource) error {
var v interface{}
var err error
for i := range payloads {
if payloads[i], err = evalProjection(ctx, pf.Children, payloads[i], validator, rbr); err != nil {
if payloads[i], err = evalProjection(ctx, pf.Children, payloads[i], validator, rbr, rsc); err != nil {
return fmt.Errorf("%s: error applying projection on sub-field item #%d: %v", pf.Name, i, err)
}
}
Expand All @@ -127,7 +135,7 @@ func evalProjectionArray(ctx context.Context, pf ProjectionField, payload []inte
if subval, ok := val.([]interface{}); ok {
var err error
var subvalp *[]interface{}
if subvalp, err = evalProjectionArray(ctx, pf, subval, &fieldType.Values, rbr); err != nil {
if subvalp, err = evalProjectionArray(ctx, pf, subval, &fieldType.Values, rbr, rsc); err != nil {
return nil, fmt.Errorf("%s: error applying projection on array item #%d: %v", pf.Name, i, err)
}
var v interface{}
Expand All @@ -146,7 +154,7 @@ func evalProjectionArray(ctx context.Context, pf ProjectionField, payload []inte
return nil, fmt.Errorf("%s: invalid value: not a dict/object", pf.Name)
}
var err error
if subval, err = evalProjection(ctx, pf.Children, subval, fieldType, rbr); err != nil {
if subval, err = evalProjection(ctx, pf.Children, subval, fieldType, rbr, rsc); err != nil {
return nil, fmt.Errorf("%s.%v", pf.Name, err)
}
var v interface{}
Expand All @@ -162,7 +170,7 @@ func evalProjectionArray(ctx context.Context, pf ProjectionField, payload []inte
return resp, nil
}

func evalProjection(ctx context.Context, p Projection, payload map[string]interface{}, fg schema.FieldGetter, rbr *referenceBatchResolver) (map[string]interface{}, error) {
func evalProjection(ctx context.Context, p Projection, payload map[string]interface{}, fg schema.FieldGetter, rbr *referenceBatchResolver, rsc Resource) (map[string]interface{}, error) {
res := map[string]interface{}{}
resMu := sync.Mutex{}
var err error
Expand Down Expand Up @@ -191,7 +199,7 @@ func evalProjection(ctx context.Context, p Projection, payload map[string]interf
return nil, fmt.Errorf("%s: invalid value: not a dict", pf.Name)
}
var err error
if subval, err = evalProjection(ctx, pf.Children, subval, def.Schema, rbr); err != nil {
if subval, err = evalProjection(ctx, pf.Children, subval, def.Schema, rbr, rsc); err != nil {
return nil, fmt.Errorf("%s.%v", pf.Name, err)
}
if res[name], err = resolveFieldHandler(ctx, pf, def, subval); err != nil {
Expand All @@ -203,10 +211,14 @@ func evalProjection(ctx context.Context, p Projection, payload map[string]interf
Projection: pf.Children,
Predicate: Predicate{&Equal{Field: "id", Value: val}},
}
rbr.request(ref.Path, q, func(payloads []map[string]interface{}, validator schema.Validator) error {
subRsc, err := rsc.SubResource(ctx, ref.Path)
if err != nil {
return nil, err
}
rbr.request(subRsc, q, func(payloads []map[string]interface{}, validator schema.Validator, rsc Resource) error {
var v interface{}
if len(payloads) == 1 {
payload, err := evalProjection(ctx, pf.Children, payloads[0], validator, rbr)
payload, err := evalProjection(ctx, pf.Children, payloads[0], validator, rbr, rsc)
if err != nil {
return fmt.Errorf("%s: error applying Projection on sub-field: %v", name, err)
}
Expand All @@ -223,7 +235,7 @@ func evalProjection(ctx context.Context, p Projection, payload map[string]interf
if payload, ok := val.([]interface{}); ok {
var err error
var subvalp *[]interface{}
if subvalp, err = evalProjectionArray(ctx, pf, payload, &array.Values, rbr); err != nil {
if subvalp, err = evalProjectionArray(ctx, pf, payload, &array.Values, rbr, rsc); err != nil {
return nil, fmt.Errorf("%s: error applying projection on array item #%d: %v", pf.Name, i, err)
}
if res[name], err = resolveFieldHandler(ctx, pf, &array.Values, subvalp); err != nil {
Expand All @@ -238,7 +250,7 @@ func evalProjection(ctx context.Context, p Projection, payload map[string]interf
return nil, fmt.Errorf("%s: invalid value: not a dict", pf.Name)
}
var err error
if subval, err = evalProjection(ctx, pf.Children, subval, fg, rbr); err != nil {
if subval, err = evalProjection(ctx, pf.Children, subval, fg, rbr, rsc); err != nil {
return nil, fmt.Errorf("%s.%v", pf.Name, err)
}
if res[name], err = resolveFieldHandler(ctx, pf, def, subval); err != nil {
Expand All @@ -264,9 +276,13 @@ func evalProjection(ctx context.Context, p Projection, payload map[string]interf
if err != nil {
return nil, err
}
rbr.request(ref.Path, q, func(payloads []map[string]interface{}, validator schema.Validator) (err error) {
subRsc, err := rsc.SubResource(ctx, ref.Path)
if err != nil {
return nil, err
}
rbr.request(subRsc, q, func(payloads []map[string]interface{}, validator schema.Validator, rsc Resource) (err error) {
for i := range payloads {
if payloads[i], err = evalProjection(ctx, pf.Children, payloads[i], validator, rbr); err != nil {
if payloads[i], err = evalProjection(ctx, pf.Children, payloads[i], validator, rbr, rsc); err != nil {
return fmt.Errorf("%s: error applying projection on sub-resource item #%d: %v", pf.Name, i, err)
}
}
Expand Down
78 changes: 75 additions & 3 deletions schema/query/projection_evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type resource struct {
validator schema.Validator
subResources map[string]resource
payloads map[string]map[string]interface{}
path string
}

func (r resource) Find(ctx context.Context, query *Query) ([]map[string]interface{}, error) {
Expand Down Expand Up @@ -52,13 +53,30 @@ func (r resource) SubResource(ctx context.Context, path string) (Resource, error
func (r resource) Validator() schema.Validator {
return r.validator
}
func (r resource) Path() string {
return r.path
}

func TestProjectionEval(t *testing.T) {
cnxShema := schema.Schema{Fields: schema.Fields{
"id": {},
"name": {},
"ref": {},
}}

cnxShema2 := schema.Schema{Fields: schema.Fields{
"id": {},
"name": {},
"ref": {},
"subconn": {
Validator: &schema.Connection{
Path: "cnx3",
Field: "ref",
Validator: cnxShema,
},
},
}}

r := resource{
validator: schema.Schema{Fields: schema.Fields{
"id": {},
Expand All @@ -75,7 +93,8 @@ func TestProjectionEval(t *testing.T) {
Validator: &schema.Dict{
Values: schema.Field{
Validator: &schema.Reference{
Path: "cnx",
Path: "cnx",
SchemaValidator: cnxShema,
},
},
},
Expand Down Expand Up @@ -112,8 +131,16 @@ func TestProjectionEval(t *testing.T) {
},
"connection": {
Validator: &schema.Connection{
Path: "cnx",
Field: "ref",
Path: "cnx",
Field: "ref",
Validator: cnxShema,
},
},
"connection2": {
Validator: &schema.Connection{
Path: "cnx2",
Field: "ref",
Validator: cnxShema2,
},
},
"with_params": {
Expand Down Expand Up @@ -145,6 +172,37 @@ func TestProjectionEval(t *testing.T) {
"4": map[string]interface{}{"id": "4", "name": "forth", "ref": "a"},
},
},
"cnx2": resource{
validator: schema.Schema{Fields: schema.Fields{
"id": {},
"name": {},
"ref": {},
"subconn": {
Validator: &schema.Connection{
Path: "cnx3",
Field: "ref",
Validator: cnxShema,
},
},
}},
subResources: map[string]resource{
"cnx3": resource{
validator: cnxShema,
payloads: map[string]map[string]interface{}{
"6": map[string]interface{}{"id": "6", "name": "first"},
"7": map[string]interface{}{"id": "7", "name": "second", "ref": "a"},
"8": map[string]interface{}{"id": "8", "name": "third", "ref": "b"},
"9": map[string]interface{}{"id": "9", "name": "forth", "ref": "c"},
},
},
},
payloads: map[string]map[string]interface{}{
"a": map[string]interface{}{"id": "a", "name": "first"},
"b": map[string]interface{}{"id": "b", "name": "second", "ref": "2"},
"c": map[string]interface{}{"id": "c", "name": "third", "ref": "3"},
"d": map[string]interface{}{"id": "d", "name": "forth", "ref": "4"},
},
},
},
}
cases := []struct {
Expand Down Expand Up @@ -252,6 +310,20 @@ func TestProjectionEval(t *testing.T) {
nil,
`{"connection":[{"name":"third"}]}`,
},
{
"Connection#3",
`connection2{name,subconn{name}}`,
`{"id":"1","simple":"foo"}`,
nil,
`{"connection2":[]}`,
},
{
"Connection#4",
`connection2{name,subconn{name}}`,
`{"id":"2","simple":"foo"}`,
nil,
`{"connection2":[{"name":"second","subconn":[{"name":"third"}]}]}`,
},
{
"Star",
`*`,
Expand Down
5 changes: 4 additions & 1 deletion schema/query/projection_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ func (pf ProjectionField) Validate(fg schema.FieldGetter) error {
if err := pf.Children.Validate(ref.SchemaValidator); err != nil {
return fmt.Errorf("%s.%v", pf.Name, err)
}
} else if _, ok := def.Validator.(*schema.Connection); ok {
} else if conn, ok := def.Validator.(*schema.Connection); ok {
// Sub-field on a sub resource (sub-request)
if err := pf.Children.Validate(conn.Validator); err != nil {
return fmt.Errorf("%s.%v", pf.Name, err)
}
} else if _, ok := def.Validator.(*schema.Dict); ok {
// Sub-field on a dict resource
} else if array, ok := def.Validator.(*schema.Array); ok {
Expand Down
13 changes: 13 additions & 0 deletions schema/query/projection_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ func TestProjectionValidate(t *testing.T) {
},
},
},
"connection": {
Validator: &schema.Connection{
Path: "cnx",
Field: "ref",
Validator: schema.Schema{Fields: schema.Fields{
"id": {},
"name": {},
}},
},
},
},
}
cases := []struct {
Expand Down Expand Up @@ -73,6 +83,9 @@ func TestProjectionValidate(t *testing.T) {
{`*,parent{*}`, nil},
{`*,parent{z:*}`, errors.New("parent.*: can't have an alias")},
{`*,parent{child{*}}`, errors.New("parent.child: field has no children")},
{`connection{name}`, nil},
{`connection{*}`, nil},
{`connection{foo}`, errors.New("connection.foo: unknown field")},
}
for i := range cases {
tc := cases[i]
Expand Down
Loading