Skip to content

Commit c3ebffd

Browse files
Dragomir-Ivanovsmyrman
authored andcommitted
Fix fields projections on nested sub-resources
Quering fields on sub-resources were broken for sub-resource, deeper than 2nd level. Adds validation on field projection for `Connected` resources.
1 parent 2f104c4 commit c3ebffd

File tree

8 files changed

+152
-53
lines changed

8 files changed

+152
-53
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ Below is an overview over recent breaking changes, starting from an arbitrary po
8787
- Storage drivers need to accept pointer to `Expression` implementer in `query.Predicate`.
8888
- `filter` parameters in sub-query will be validated for type match.
8989
- `filter` parameters will be validated for type match only, instead of type & constrains.
90+
- PR #228: `Reference` projection fields will be validated against referenced resource schema.
91+
- PR #230: `Connection` projection fields will be validated against connected resource schema.
9092

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

resource/resource.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ func (r *Resource) Bind(name, field string, s schema.Schema, h Storer, c Conf) *
145145
Validator: &schema.Connection{
146146
Path: "." + name,
147147
Field: field,
148-
Validator: s,
148+
Validator: sr.validator,
149149
},
150150
Params: schema.Params{
151151
"skip": schema.Param{

resource/resource_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func TestResourceBind(t *testing.T) {
2929
Validator: &schema.Connection{
3030
Path: ".bar",
3131
Field: "foo",
32-
Validator: barSchema,
32+
Validator: bar.validator,
3333
},
3434
Params: schema.Params{
3535
"skip": schema.Param{

schema/query/projection_evaluator.go

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ type Resource interface {
2323

2424
// Validator returns the schema.Validator associated with the resource.
2525
Validator() schema.Validator
26+
27+
// Path returns the full path of the resource composed of names of each
28+
// intermediate resources separated by dots (i.e.: res1.res2.res3).
29+
Path() string
2630
}
2731

2832
// Eval evaluate the projection on the given payload with the help of the
@@ -31,10 +35,10 @@ type Resource interface {
3135
func (p Projection) Eval(ctx context.Context, payload map[string]interface{}, rsc Resource) (map[string]interface{}, error) {
3236
rbr := &referenceBatchResolver{}
3337
validator := rsc.Validator()
34-
payload, err := evalProjection(ctx, p, payload, validator, rbr)
38+
payload, err := evalProjection(ctx, p, payload, validator, rbr, rsc)
3539
if err == nil {
3640
// Execute the batched reference resolutions.
37-
err = rbr.execute(ctx, rsc)
41+
err = rbr.execute(ctx)
3842
}
3943
return payload, err
4044
}
@@ -80,7 +84,7 @@ func prepareProjection(p Projection, payload map[string]interface{}) (Projection
8084
return proj, nil
8185
}
8286

83-
func evalProjectionArray(ctx context.Context, pf ProjectionField, payload []interface{}, def *schema.Field, rbr *referenceBatchResolver) (*[]interface{}, error) {
87+
func evalProjectionArray(ctx context.Context, pf ProjectionField, payload []interface{}, def *schema.Field, rbr *referenceBatchResolver, rsc Resource) (*[]interface{}, error) {
8488
res := make([]interface{}, 0, len(payload))
8589
// Return pointer to res, because it may be populated after this function ends, by referenceBatchResolver
8690
// in `schema.Reference` case
@@ -102,11 +106,15 @@ func evalProjectionArray(ctx context.Context, pf ProjectionField, payload []inte
102106
Projection: pf.Children,
103107
Predicate: Predicate{e},
104108
}
105-
rbr.request(fieldType.Path, q, func(payloads []map[string]interface{}, validator schema.Validator) error {
109+
subRsc, err := rsc.SubResource(ctx, fieldType.Path)
110+
if err != nil {
111+
return nil, err
112+
}
113+
rbr.request(subRsc, q, func(payloads []map[string]interface{}, validator schema.Validator, rsc Resource) error {
106114
var v interface{}
107115
var err error
108116
for i := range payloads {
109-
if payloads[i], err = evalProjection(ctx, pf.Children, payloads[i], validator, rbr); err != nil {
117+
if payloads[i], err = evalProjection(ctx, pf.Children, payloads[i], validator, rbr, rsc); err != nil {
110118
return fmt.Errorf("%s: error applying projection on sub-field item #%d: %v", pf.Name, i, err)
111119
}
112120
}
@@ -127,7 +135,7 @@ func evalProjectionArray(ctx context.Context, pf ProjectionField, payload []inte
127135
if subval, ok := val.([]interface{}); ok {
128136
var err error
129137
var subvalp *[]interface{}
130-
if subvalp, err = evalProjectionArray(ctx, pf, subval, &fieldType.Values, rbr); err != nil {
138+
if subvalp, err = evalProjectionArray(ctx, pf, subval, &fieldType.Values, rbr, rsc); err != nil {
131139
return nil, fmt.Errorf("%s: error applying projection on array item #%d: %v", pf.Name, i, err)
132140
}
133141
var v interface{}
@@ -146,7 +154,7 @@ func evalProjectionArray(ctx context.Context, pf ProjectionField, payload []inte
146154
return nil, fmt.Errorf("%s: invalid value: not a dict/object", pf.Name)
147155
}
148156
var err error
149-
if subval, err = evalProjection(ctx, pf.Children, subval, fieldType, rbr); err != nil {
157+
if subval, err = evalProjection(ctx, pf.Children, subval, fieldType, rbr, rsc); err != nil {
150158
return nil, fmt.Errorf("%s.%v", pf.Name, err)
151159
}
152160
var v interface{}
@@ -162,7 +170,7 @@ func evalProjectionArray(ctx context.Context, pf ProjectionField, payload []inte
162170
return resp, nil
163171
}
164172

165-
func evalProjection(ctx context.Context, p Projection, payload map[string]interface{}, fg schema.FieldGetter, rbr *referenceBatchResolver) (map[string]interface{}, error) {
173+
func evalProjection(ctx context.Context, p Projection, payload map[string]interface{}, fg schema.FieldGetter, rbr *referenceBatchResolver, rsc Resource) (map[string]interface{}, error) {
166174
res := map[string]interface{}{}
167175
resMu := sync.Mutex{}
168176
var err error
@@ -191,7 +199,7 @@ func evalProjection(ctx context.Context, p Projection, payload map[string]interf
191199
return nil, fmt.Errorf("%s: invalid value: not a dict", pf.Name)
192200
}
193201
var err error
194-
if subval, err = evalProjection(ctx, pf.Children, subval, def.Schema, rbr); err != nil {
202+
if subval, err = evalProjection(ctx, pf.Children, subval, def.Schema, rbr, rsc); err != nil {
195203
return nil, fmt.Errorf("%s.%v", pf.Name, err)
196204
}
197205
if res[name], err = resolveFieldHandler(ctx, pf, def, subval); err != nil {
@@ -203,10 +211,14 @@ func evalProjection(ctx context.Context, p Projection, payload map[string]interf
203211
Projection: pf.Children,
204212
Predicate: Predicate{&Equal{Field: "id", Value: val}},
205213
}
206-
rbr.request(ref.Path, q, func(payloads []map[string]interface{}, validator schema.Validator) error {
214+
subRsc, err := rsc.SubResource(ctx, ref.Path)
215+
if err != nil {
216+
return nil, err
217+
}
218+
rbr.request(subRsc, q, func(payloads []map[string]interface{}, validator schema.Validator, rsc Resource) error {
207219
var v interface{}
208220
if len(payloads) == 1 {
209-
payload, err := evalProjection(ctx, pf.Children, payloads[0], validator, rbr)
221+
payload, err := evalProjection(ctx, pf.Children, payloads[0], validator, rbr, rsc)
210222
if err != nil {
211223
return fmt.Errorf("%s: error applying Projection on sub-field: %v", name, err)
212224
}
@@ -223,7 +235,7 @@ func evalProjection(ctx context.Context, p Projection, payload map[string]interf
223235
if payload, ok := val.([]interface{}); ok {
224236
var err error
225237
var subvalp *[]interface{}
226-
if subvalp, err = evalProjectionArray(ctx, pf, payload, &array.Values, rbr); err != nil {
238+
if subvalp, err = evalProjectionArray(ctx, pf, payload, &array.Values, rbr, rsc); err != nil {
227239
return nil, fmt.Errorf("%s: error applying projection on array item #%d: %v", pf.Name, i, err)
228240
}
229241
if res[name], err = resolveFieldHandler(ctx, pf, &array.Values, subvalp); err != nil {
@@ -238,7 +250,7 @@ func evalProjection(ctx context.Context, p Projection, payload map[string]interf
238250
return nil, fmt.Errorf("%s: invalid value: not a dict", pf.Name)
239251
}
240252
var err error
241-
if subval, err = evalProjection(ctx, pf.Children, subval, fg, rbr); err != nil {
253+
if subval, err = evalProjection(ctx, pf.Children, subval, fg, rbr, rsc); err != nil {
242254
return nil, fmt.Errorf("%s.%v", pf.Name, err)
243255
}
244256
if res[name], err = resolveFieldHandler(ctx, pf, def, subval); err != nil {
@@ -264,9 +276,13 @@ func evalProjection(ctx context.Context, p Projection, payload map[string]interf
264276
if err != nil {
265277
return nil, err
266278
}
267-
rbr.request(ref.Path, q, func(payloads []map[string]interface{}, validator schema.Validator) (err error) {
279+
subRsc, err := rsc.SubResource(ctx, ref.Path)
280+
if err != nil {
281+
return nil, err
282+
}
283+
rbr.request(subRsc, q, func(payloads []map[string]interface{}, validator schema.Validator, rsc Resource) (err error) {
268284
for i := range payloads {
269-
if payloads[i], err = evalProjection(ctx, pf.Children, payloads[i], validator, rbr); err != nil {
285+
if payloads[i], err = evalProjection(ctx, pf.Children, payloads[i], validator, rbr, rsc); err != nil {
270286
return fmt.Errorf("%s: error applying projection on sub-resource item #%d: %v", pf.Name, i, err)
271287
}
272288
}

schema/query/projection_evaluator_test.go

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type resource struct {
1717
validator schema.Validator
1818
subResources map[string]resource
1919
payloads map[string]map[string]interface{}
20+
path string
2021
}
2122

2223
func (r resource) Find(ctx context.Context, query *Query) ([]map[string]interface{}, error) {
@@ -52,13 +53,30 @@ func (r resource) SubResource(ctx context.Context, path string) (Resource, error
5253
func (r resource) Validator() schema.Validator {
5354
return r.validator
5455
}
56+
func (r resource) Path() string {
57+
return r.path
58+
}
5559

5660
func TestProjectionEval(t *testing.T) {
5761
cnxShema := schema.Schema{Fields: schema.Fields{
62+
"id": {},
5863
"name": {},
5964
"ref": {},
6065
}}
6166

67+
cnxShema2 := schema.Schema{Fields: schema.Fields{
68+
"id": {},
69+
"name": {},
70+
"ref": {},
71+
"subconn": {
72+
Validator: &schema.Connection{
73+
Path: "cnx3",
74+
Field: "ref",
75+
Validator: cnxShema,
76+
},
77+
},
78+
}}
79+
6280
r := resource{
6381
validator: schema.Schema{Fields: schema.Fields{
6482
"id": {},
@@ -75,7 +93,8 @@ func TestProjectionEval(t *testing.T) {
7593
Validator: &schema.Dict{
7694
Values: schema.Field{
7795
Validator: &schema.Reference{
78-
Path: "cnx",
96+
Path: "cnx",
97+
SchemaValidator: cnxShema,
7998
},
8099
},
81100
},
@@ -112,8 +131,16 @@ func TestProjectionEval(t *testing.T) {
112131
},
113132
"connection": {
114133
Validator: &schema.Connection{
115-
Path: "cnx",
116-
Field: "ref",
134+
Path: "cnx",
135+
Field: "ref",
136+
Validator: cnxShema,
137+
},
138+
},
139+
"connection2": {
140+
Validator: &schema.Connection{
141+
Path: "cnx2",
142+
Field: "ref",
143+
Validator: cnxShema2,
117144
},
118145
},
119146
"with_params": {
@@ -145,6 +172,37 @@ func TestProjectionEval(t *testing.T) {
145172
"4": map[string]interface{}{"id": "4", "name": "forth", "ref": "a"},
146173
},
147174
},
175+
"cnx2": resource{
176+
validator: schema.Schema{Fields: schema.Fields{
177+
"id": {},
178+
"name": {},
179+
"ref": {},
180+
"subconn": {
181+
Validator: &schema.Connection{
182+
Path: "cnx3",
183+
Field: "ref",
184+
Validator: cnxShema,
185+
},
186+
},
187+
}},
188+
subResources: map[string]resource{
189+
"cnx3": resource{
190+
validator: cnxShema,
191+
payloads: map[string]map[string]interface{}{
192+
"6": map[string]interface{}{"id": "6", "name": "first"},
193+
"7": map[string]interface{}{"id": "7", "name": "second", "ref": "a"},
194+
"8": map[string]interface{}{"id": "8", "name": "third", "ref": "b"},
195+
"9": map[string]interface{}{"id": "9", "name": "forth", "ref": "c"},
196+
},
197+
},
198+
},
199+
payloads: map[string]map[string]interface{}{
200+
"a": map[string]interface{}{"id": "a", "name": "first"},
201+
"b": map[string]interface{}{"id": "b", "name": "second", "ref": "2"},
202+
"c": map[string]interface{}{"id": "c", "name": "third", "ref": "3"},
203+
"d": map[string]interface{}{"id": "d", "name": "forth", "ref": "4"},
204+
},
205+
},
148206
},
149207
}
150208
cases := []struct {
@@ -252,6 +310,20 @@ func TestProjectionEval(t *testing.T) {
252310
nil,
253311
`{"connection":[{"name":"third"}]}`,
254312
},
313+
{
314+
"Connection#3",
315+
`connection2{name,subconn{name}}`,
316+
`{"id":"1","simple":"foo"}`,
317+
nil,
318+
`{"connection2":[]}`,
319+
},
320+
{
321+
"Connection#4",
322+
`connection2{name,subconn{name}}`,
323+
`{"id":"2","simple":"foo"}`,
324+
nil,
325+
`{"connection2":[{"name":"second","subconn":[{"name":"third"}]}]}`,
326+
},
255327
{
256328
"Star",
257329
`*`,

schema/query/projection_validator.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,11 @@ func (pf ProjectionField) Validate(fg schema.FieldGetter) error {
3434
if err := pf.Children.Validate(ref.SchemaValidator); err != nil {
3535
return fmt.Errorf("%s.%v", pf.Name, err)
3636
}
37-
} else if _, ok := def.Validator.(*schema.Connection); ok {
37+
} else if conn, ok := def.Validator.(*schema.Connection); ok {
3838
// Sub-field on a sub resource (sub-request)
39+
if err := pf.Children.Validate(conn.Validator); err != nil {
40+
return fmt.Errorf("%s.%v", pf.Name, err)
41+
}
3942
} else if _, ok := def.Validator.(*schema.Dict); ok {
4043
// Sub-field on a dict resource
4144
} else if array, ok := def.Validator.(*schema.Array); ok {

schema/query/projection_validator_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ func TestProjectionValidate(t *testing.T) {
4141
},
4242
},
4343
},
44+
"connection": {
45+
Validator: &schema.Connection{
46+
Path: "cnx",
47+
Field: "ref",
48+
Validator: schema.Schema{Fields: schema.Fields{
49+
"id": {},
50+
"name": {},
51+
}},
52+
},
53+
},
4454
},
4555
}
4656
cases := []struct {
@@ -73,6 +83,9 @@ func TestProjectionValidate(t *testing.T) {
7383
{`*,parent{*}`, nil},
7484
{`*,parent{z:*}`, errors.New("parent.*: can't have an alias")},
7585
{`*,parent{child{*}}`, errors.New("parent.child: field has no children")},
86+
{`connection{name}`, nil},
87+
{`connection{*}`, nil},
88+
{`connection{foo}`, errors.New("connection.foo: unknown field")},
7689
}
7790
for i := range cases {
7891
tc := cases[i]

0 commit comments

Comments
 (0)