Skip to content

Commit d8fe98d

Browse files
fix(typegraph): implement additional_props (#980)
<!-- Pull requests are squashed and merged using: - their title as the commit message - their description as the commit body Having a good title and description is important for the users to get readable changelog. --> <!-- 1. Explain WHAT the change is about --> - Closes [MET-843](https://linear.app/metatypedev/issue/MET-843/addiditioonalprops-in-struct-options-doesnt-work). <!-- 3. Explain HOW users should update their code --> #### Migration notes --- - [x] The change comes with new or modified tests - [ ] Hard-to-understand functions have explanatory comments - [ ] End-user documentation is updated to reflect the change <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a configurable flag in object definitions that controls whether extra, unspecified properties are allowed. This enhances schema validation and data conversion by permitting flexible input when enabled. - Updated validation logic to conditionally bypass errors for additional properties when permitted. - **Tests** - Added new test cases and a helper function to verify input stringification and validation for both simple and nested structures. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 0531939 commit d8fe98d

File tree

12 files changed

+42
-4
lines changed

12 files changed

+42
-4
lines changed

src/metagen/src/fdk_rs/stubs.rs

+1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ mod test {
116116
policies: Default::default(),
117117
id: vec![],
118118
required: vec![],
119+
additional_props: false,
119120
},
120121
base: TypeNodeBase {
121122
..default_type_node_base()

src/metagen/src/fdk_rs/types.rs

+8
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ mod test {
442442
id: vec![],
443443
// FIXME: remove required
444444
required: vec![],
445+
additional_props: false,
445446
},
446447
base: TypeNodeBase {
447448
title: "my_obj".into(),
@@ -617,6 +618,7 @@ pub enum MyUnion {
617618
id: vec![],
618619
required: ["obj_b"].into_iter().map(Into::into).collect(),
619620
policies: Default::default(),
621+
additional_props: false,
620622
},
621623
base: TypeNodeBase {
622624
title: "ObjA".into(),
@@ -629,6 +631,7 @@ pub enum MyUnion {
629631
policies: Default::default(),
630632
id: vec![],
631633
required: ["obj_c"].into_iter().map(Into::into).collect(),
634+
additional_props: false,
632635
},
633636
base: TypeNodeBase {
634637
title: "ObjB".into(),
@@ -641,6 +644,7 @@ pub enum MyUnion {
641644
policies: Default::default(),
642645
id: vec![],
643646
required: ["obj_a"].into_iter().map(Into::into).collect(),
647+
additional_props: false,
644648
},
645649
base: TypeNodeBase {
646650
title: "ObjC".into(),
@@ -672,6 +676,7 @@ pub struct ObjC {
672676
policies: Default::default(),
673677
id: vec![],
674678
required: ["obj_b"].into_iter().map(Into::into).collect(),
679+
additional_props: false,
675680
},
676681
base: TypeNodeBase {
677682
title: "ObjA".into(),
@@ -684,6 +689,7 @@ pub struct ObjC {
684689
policies: Default::default(),
685690
id: vec![],
686691
required: ["union_c"].into_iter().map(Into::into).collect(),
692+
additional_props: false,
687693
},
688694
base: TypeNodeBase {
689695
title: "ObjB".into(),
@@ -723,6 +729,7 @@ pub enum CUnion {
723729
policies: Default::default(),
724730
id: vec![],
725731
required: ["obj_b"].into_iter().map(Into::into).collect(),
732+
additional_props: false,
726733
},
727734
base: TypeNodeBase {
728735
title: "ObjA".into(),
@@ -735,6 +742,7 @@ pub enum CUnion {
735742
policies: Default::default(),
736743
id: vec![],
737744
required: ["either_c"].into_iter().map(Into::into).collect(),
745+
additional_props: false,
738746
},
739747
base: TypeNodeBase {
740748
title: "ObjB".into(),

src/metagen/src/tests/fixtures.rs

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ pub fn test_typegraph_2() -> Typegraph {
5858
policies: Default::default(),
5959
id: vec![],
6060
required: vec![],
61+
additional_props: false,
6162
},
6263
base: TypeNodeBase {
6364
..default_type_node_base()

src/typegate/src/engine/planner/args.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,7 @@ class ArgumentCollector {
675675
}
676676

677677
const unexpectedProps = Object.keys(fieldByKeys);
678-
if (unexpectedProps.length > 0) {
678+
if (!typ.additionalProps && unexpectedProps.length > 0) {
679679
throw new UnexpectedPropertiesError(
680680
unexpectedProps,
681681
this.currentNodeDetails,

src/typegate/src/engine/typecheck/inline_validators/object.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import { check } from "./common.ts";
1010

1111
export function generateObjectValidator(
12-
_typeNode: ObjectNode,
12+
typeNode: ObjectNode,
1313
varName: string,
1414
path: string,
1515
keys: {
@@ -30,6 +30,7 @@ export function generateObjectValidator(
3030
`for (const key of ${varKeys}) {`,
3131
` if (${varRequired}.has(key)) { ${varRequired}.delete(key); continue; }`,
3232
` if (${varOptional}.has(key)) { ${varOptional}.delete(key); continue; }`,
33+
` if (${typeNode.additionalProps}) { continue; }`,
3334
` throw new Error(\`At "${path}": unexpected key: \${key}\`);`,
3435
`}`,
3536
check(

src/typegate/src/typegraph/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export type ObjectNode = {
6767
properties: {
6868
[k: string]: number;
6969
};
70+
additionalProps?: boolean;
7071
required?: string[];
7172
id?: string[];
7273
};

src/typegraph/core/src/typedef/struct_.rs

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ impl TypeConversion for Struct {
8181
id: self.data.find_id_fields()?,
8282
required: Vec::new(),
8383
policies: self.data.collect_policies(ctx)?,
84+
additional_props: self.data.additional_props,
8485
},
8586
})
8687
}

src/typegraph/core/src/typegraph.rs

+1
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ pub fn init(params: TypegraphInitParams) -> Result<()> {
137137
policies: Default::default(),
138138
id: vec![],
139139
required: vec![],
140+
additional_props: false,
140141
},
141142
}));
142143

src/typegraph/schema/src/types.rs

+4
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ pub struct FileTypeData {
152152

153153
#[skip_serializing_none]
154154
#[derive(Serialize, Deserialize, Clone, Debug)]
155+
#[serde(rename_all = "camelCase")]
155156
pub struct ObjectTypeData<Id = TypeId> {
156157
pub properties: IndexMap<String, Id>,
157158
pub id: Vec<String>,
@@ -160,6 +161,9 @@ pub struct ObjectTypeData<Id = TypeId> {
160161
#[serde(skip_serializing_if = "IndexMap::is_empty")]
161162
#[serde(default)]
162163
pub policies: IndexMap<String, Vec<PolicyIndices>>,
164+
#[serde(skip_serializing_if = "std::ops::Not::not")]
165+
#[serde(default)]
166+
pub additional_props: bool,
163167
}
164168

165169
#[skip_serializing_none]

tests/typecheck/__snapshots__/typecheck_test.ts.snap

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export const snapshot = {};
22

33
snapshot[`typecheck 1`] = `
4-
"function validate_54_1(value, path, errors, context) {
4+
"function validate_57_1(value, path, errors, context) {
55
if (typeof value !== \\"object\\") {
66
errors.push([path, \`expected an object, got \${typeof value}\`]);
77
}
@@ -122,7 +122,7 @@ errors.push([path, \\"string does not match to the pattern /^[a-z]+\$/\\"]);
122122
}
123123
}
124124
}
125-
return validate_54_1"
125+
return validate_57_1"
126126
`;
127127
128128
snapshot[`typecheck 2`] = `

tests/typecheck/input_validator_test.ts

+13
Original file line numberDiff line numberDiff line change
@@ -239,4 +239,17 @@ Meta.test("input validator compiler", async (t) => {
239239
})
240240
.on(e);
241241
});
242+
243+
await t.should("pass for any struct", async () => {
244+
await gql`
245+
query structs {
246+
simple: stringifyStruct(params: { str: "value", int: 0 })
247+
nested: stringifyStruct(params: { obj: { key: "value" } })
248+
}
249+
`
250+
.expectBody((body) => {
251+
assert(body.errors == null);
252+
})
253+
.on(e);
254+
});
242255
});

tests/typecheck/typecheck.py

+7
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ def typecheck(g: Graph):
106106

107107
empty = t.struct({}).rename("WillNotHaveAnyEffectLikeOtherScalars")
108108

109+
stringify_struct = deno.func(
110+
t.struct({"params": t.struct({}, additional_props=True, name="params")}),
111+
t.string(),
112+
code="({params}) => JSON.stringify(params)",
113+
)
114+
109115
g.expose(
110116
my_policy,
111117
createUser=create_user,
@@ -115,4 +121,5 @@ def typecheck(g: Graph):
115121
enums=deno.identity(enums),
116122
findProduct=deno.identity(product),
117123
emptyObjectOutput=deno.static(empty, {}),
124+
stringifyStruct=stringify_struct,
118125
)

0 commit comments

Comments
 (0)