Skip to content

Commit e042035

Browse files
authored
fix: default create data to an empty object (#5707)
[TML-1034](https://linear.app/prisma-company/issue/TML-1034/create-method-has-weird-inconsistencies-and-correctness-issues-in) We currently allow omitting the data field if none of the properties are required, but that prevents the defaults from being evaluated. This PR changes the query document to check for top-level defaults and the `create_one` mutation to default the `data` object to an empty object.
1 parent 3f29865 commit e042035

5 files changed

Lines changed: 81 additions & 140 deletions

File tree

Lines changed: 26 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -1,143 +1,42 @@
11
{
22
"is_panic": false,
3-
"message": "`data`: A value is required but not set",
3+
"message": "Unable to match input value to any allowed input type for the field. Parse errors: [`data.published`: A value is required but not set, `data.published`: A value is required but not set]",
44
"meta": {
5-
"kind": "RequiredArgumentMissing",
6-
"inputTypes": [
5+
"kind": "Union",
6+
"errors": [
77
{
8-
"kind": "object",
9-
"name": "PostCreateInput",
10-
"fields": [
8+
"kind": "RequiredArgumentMissing",
9+
"inputTypes": [
1110
{
12-
"name": "id",
13-
"typeNames": [
14-
"String"
15-
],
16-
"required": false
17-
},
18-
{
19-
"name": "createdAt",
20-
"typeNames": [
21-
"DateTime"
22-
],
23-
"required": false
24-
},
25-
{
26-
"name": "updatedAt",
27-
"typeNames": [
28-
"DateTime"
29-
],
30-
"required": false
31-
},
32-
{
33-
"name": "published",
34-
"typeNames": [
35-
"Boolean"
36-
],
37-
"required": true
38-
},
39-
{
40-
"name": "title",
41-
"typeNames": [
42-
"String"
43-
],
44-
"required": true
45-
},
46-
{
47-
"name": "content",
48-
"typeNames": [
49-
"String",
50-
"Null"
51-
],
52-
"required": false
53-
},
54-
{
55-
"name": "author",
56-
"typeNames": [
57-
"UserCreateNestedOneWithoutPostsInput"
58-
],
59-
"required": false
60-
},
61-
{
62-
"name": "Like",
63-
"typeNames": [
64-
"LikeCreateNestedManyWithoutPostInput"
65-
],
66-
"required": false
11+
"kind": "scalar",
12+
"name": "Boolean"
6713
}
14+
],
15+
"argumentPath": [
16+
"data",
17+
"published"
18+
],
19+
"selectionPath": [
20+
"createOnePost"
6821
]
6922
},
7023
{
71-
"kind": "object",
72-
"name": "PostUncheckedCreateInput",
73-
"fields": [
74-
{
75-
"name": "id",
76-
"typeNames": [
77-
"String"
78-
],
79-
"required": false
80-
},
81-
{
82-
"name": "createdAt",
83-
"typeNames": [
84-
"DateTime"
85-
],
86-
"required": false
87-
},
88-
{
89-
"name": "updatedAt",
90-
"typeNames": [
91-
"DateTime"
92-
],
93-
"required": false
94-
},
95-
{
96-
"name": "published",
97-
"typeNames": [
98-
"Boolean"
99-
],
100-
"required": true
101-
},
102-
{
103-
"name": "title",
104-
"typeNames": [
105-
"String"
106-
],
107-
"required": true
108-
},
109-
{
110-
"name": "content",
111-
"typeNames": [
112-
"String",
113-
"Null"
114-
],
115-
"required": false
116-
},
117-
{
118-
"name": "authorId",
119-
"typeNames": [
120-
"String",
121-
"Null"
122-
],
123-
"required": false
124-
},
24+
"kind": "RequiredArgumentMissing",
25+
"inputTypes": [
12526
{
126-
"name": "Like",
127-
"typeNames": [
128-
"LikeUncheckedCreateNestedManyWithoutPostInput"
129-
],
130-
"required": false
27+
"kind": "scalar",
28+
"name": "Boolean"
13129
}
30+
],
31+
"argumentPath": [
32+
"data",
33+
"published"
34+
],
35+
"selectionPath": [
36+
"createOnePost"
13237
]
13338
}
134-
],
135-
"argumentPath": [
136-
"data"
137-
],
138-
"selectionPath": [
139-
"createOnePost"
14039
]
14140
},
142-
"error_code": "P2012"
41+
"error_code": "P2009"
14342
}

query-compiler/core/src/query_document/parser.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use bigdecimal::{BigDecimal, ToPrimitive};
44
use chrono::prelude::*;
55
use core::fmt;
66
use indexmap::{IndexMap, IndexSet};
7-
use query_structure::PrismaValue;
7+
use query_structure::{DefaultKind, PrismaValue};
88
use std::{borrow::Cow, convert::TryFrom, rc::Rc, str::FromStr};
99
use user_facing_errors::query_engine::validation::ValidationError;
1010
use uuid::Uuid;
@@ -182,18 +182,19 @@ impl QueryDocumentParser {
182182
.iter()
183183
.filter_map(|input_field| {
184184
// Match schema argument field to an argument field in the incoming document.
185-
let selection_arg: Option<(String, ArgumentValue)> = given_arguments
185+
let selection_arg = given_arguments
186186
.iter()
187187
.find(|given_argument| given_argument.0 == input_field.name)
188+
.map(|(_, value)| value)
188189
.cloned();
189190

190191
let argument_path = argument_path.add(input_field.name.clone().into_owned());
191192

192-
let validate_other_required_args = |name: &str| {
193+
let validate_other_required_args = || {
193194
for req_name in input_field.requires_other_fields() {
194195
if !given_arguments.iter().any(|(name, _)| name == req_name) {
195196
let Some(req_field) = schema_field.arguments().iter().find(|f| f.name == *req_name) else {
196-
panic!("argument {name} requires unknown argument {req_name}")
197+
panic!("argument {} requires unknown argument {req_name}", input_field.name)
197198
};
198199
return Err(ValidationError::conditionally_required_argument_missing(
199200
&selection_path.segments(),
@@ -207,10 +208,16 @@ impl QueryDocumentParser {
207208
};
208209

209210
// If optional and not present ignore the field.
210-
// If present, parse normally.
211+
// If present or has a default, process the value.
211212
// If not present but required, throw a validation error.
212-
match selection_arg {
213-
Some((name, value)) => Some(validate_other_required_args(&name).and_then(|_| {
213+
match selection_arg.or_else(|| {
214+
input_field
215+
.default_value
216+
.as_ref()
217+
.and_then(DefaultKind::get)
218+
.map(ArgumentValue::from)
219+
}) {
220+
Some(value) => Some(validate_other_required_args().and_then(|_| {
214221
self.parse_input_value(
215222
selection_path.clone(),
216223
argument_path,

query-compiler/schema/src/build/mutations/create_one.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::{
55
use constants::*;
66
use input_types::fields::{arguments, data_input_mapper::*};
77
use output_types::objects;
8-
use query_structure::{Model, RelationFieldRef};
8+
use query_structure::{DefaultKind, Model, PrismaValue, RelationFieldRef};
99

1010
/// Builds a create mutation field (e.g. createUser) for given model.
1111
pub(crate) fn create_one(ctx: &QuerySchema, model: Model) -> OutputField<'_> {
@@ -33,7 +33,12 @@ pub(crate) fn create_one_arguments(ctx: &QuerySchema, model: Model) -> Vec<Input
3333
.any(|f| f.is_required() && f.as_scalar().map(|f| f.default_value().is_none()).unwrap_or(true));
3434

3535
let create_types = create_one_input_types(ctx, model, None);
36-
let data_field = input_field(args::DATA, create_types, None).optional_if(!any_field_required);
36+
let data_field = input_field(
37+
args::DATA,
38+
create_types,
39+
Some(DefaultKind::Single(PrismaValue::Object(vec![]))),
40+
)
41+
.optional_if(!any_field_required);
3742

3843
std::iter::once(data_field)
3944
.chain(arguments::relation_load_strategy_argument(ctx))

query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/enum_type.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ mod enum_type {
113113
let res = runner
114114
.query(r#"{ findUniqueTestModel(where: { id: 1 }) { my_enum } }"#)
115115
.await?;
116-
res.assert_failure(None, Some("Value 'D' not found in enum 'MyEnum'".to_owned()));
116+
res.assert_failure(2023, Some("Value 'D' not found in enum 'MyEnum'".to_owned()));
117117
}
118118
EngineProtocol::Json => {
119119
let res = runner
@@ -133,7 +133,7 @@ mod enum_type {
133133
)
134134
.await?;
135135

136-
res.assert_failure(None, Some("Value 'D' not found in enum 'MyEnum'".to_owned()));
136+
res.assert_failure(2023, Some("Value 'D' not found in enum 'MyEnum'".to_owned()));
137137
}
138138
}
139139

@@ -152,7 +152,7 @@ mod enum_type {
152152
let res = runner
153153
.query(r#"{ findUniqueTestModel(where: { id: 1 }) { my_enum } }"#)
154154
.await?;
155-
res.assert_failure(None, Some("Value 'D' not found in enum 'MyEnum'".to_owned()));
155+
res.assert_failure(2023, Some("Value 'D' not found in enum 'MyEnum'".to_owned()));
156156
}
157157
EngineProtocol::Json => {
158158
let res = runner
@@ -172,7 +172,7 @@ mod enum_type {
172172
)
173173
.await?;
174174

175-
res.assert_failure(None, Some("Value 'D' not found in enum 'MyEnum'".to_owned()));
175+
res.assert_failure(2023, Some("Value 'D' not found in enum 'MyEnum'".to_owned()));
176176
}
177177
}
178178

query-engine/connector-test-kit-rs/query-engine-tests/tests/writes/top_level_mutations/default_value.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,36 @@ mod default_value {
55
use indoc::indoc;
66
use query_engine_tests::{run_query, run_query_json};
77

8+
fn schema_all_default() -> String {
9+
let schema = indoc! {
10+
r#"model Service {
11+
#id(id, String, @id, @default(cuid()))
12+
}"#
13+
};
14+
15+
schema.to_owned()
16+
}
17+
18+
#[connector_test(schema(schema_all_default))]
19+
async fn default_field_omitted_in_data(runner: Runner) -> TestResult<()> {
20+
runner
21+
.query(r#"mutation { createOneService(data: {}) { id } }"#)
22+
.await?
23+
.assert_success();
24+
25+
Ok(())
26+
}
27+
28+
#[connector_test(schema(schema_all_default))]
29+
async fn default_field_omitted_without_data(runner: Runner) -> TestResult<()> {
30+
runner
31+
.query(r#"mutation { createOneService { id } }"#)
32+
.await?
33+
.assert_success();
34+
35+
Ok(())
36+
}
37+
838
fn schema_string() -> String {
939
let schema = indoc! {
1040
r#"model ScalarModel {

0 commit comments

Comments
 (0)