Skip to content

Commit bf26a0d

Browse files
authored
feat: disallow mutations on views (#5536)
Disallow mutations on views. The query schema changes propagate automagically all the way to the client types generation so there's no need for any additional changes on TS side: <img width="1188" height="308" alt="image" src="https://github.com/user-attachments/assets/eb6e7d69-cd57-4f1c-a04e-4658b8d1c4ea" /> Closes: https://linear.app/prisma-company/issue/ORM-1227/disallow-writes-to-views
1 parent a557b65 commit bf26a0d

7 files changed

Lines changed: 99 additions & 3 deletions

File tree

libs/user-facing-errors/src/query_engine/validation.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,8 @@ impl ValidationError {
481481
}
482482
}
483483

484+
impl std::error::Error for ValidationError {}
485+
484486
#[derive(Debug, Serialize)]
485487
#[serde(rename_all = "camelCase")]
486488
pub struct OutputTypeDescription {

query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/views.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,85 @@ mod views {
7373
Ok(())
7474
}
7575

76+
#[connector_test]
77+
async fn no_create_one_mutation(runner: Runner) -> TestResult<()> {
78+
test_no_toplevel_mutation(
79+
runner,
80+
"no_create_one_mutation",
81+
r#"mutation { createOneTestView(data: { firstName: "Test", lastName: "User", fullName: "Test User" }) { id } }"#
82+
).await
83+
}
84+
85+
#[connector_test]
86+
async fn no_update_one_mutation(runner: Runner) -> TestResult<()> {
87+
test_no_toplevel_mutation(
88+
runner,
89+
"no_update_one_mutation",
90+
r#"mutation { updateOneTestView(where: { id: 1 }, data: { firstName: "Updated" }) { id } }"#,
91+
)
92+
.await
93+
}
94+
95+
#[connector_test]
96+
async fn no_delete_one_mutation(runner: Runner) -> TestResult<()> {
97+
test_no_toplevel_mutation(
98+
runner,
99+
"no_delete_one_mutation",
100+
r#"mutation { deleteOneTestView(where: { id: 1 }) { id } }"#,
101+
)
102+
.await
103+
}
104+
105+
#[connector_test]
106+
async fn no_upsert_one_mutation(runner: Runner) -> TestResult<()> {
107+
test_no_toplevel_mutation(
108+
runner,
109+
"no_upsert_one_mutation",
110+
r#"mutation { upsertOneTestView(where: { id: 1 }, create: { firstName: "New", lastName: "User", fullName: "New User" }, update: { firstName: "Updated" }) { id } }"#
111+
).await
112+
}
113+
114+
#[connector_test]
115+
async fn no_create_many_mutation(runner: Runner) -> TestResult<()> {
116+
test_no_toplevel_mutation(
117+
runner,
118+
"no_create_many_mutation",
119+
r#"mutation { createManyTestView(data: [{ firstName: "Test", lastName: "User", fullName: "Test User" }]) { count } }"#
120+
).await
121+
}
122+
123+
#[connector_test]
124+
async fn no_update_many_mutation(runner: Runner) -> TestResult<()> {
125+
test_no_toplevel_mutation(
126+
runner,
127+
"no_update_many_mutation",
128+
r#"mutation { updateManyTestView(where: { id: 1 }, data: { firstName: "Updated" }) { count } }"#,
129+
)
130+
.await
131+
}
132+
133+
#[connector_test]
134+
async fn no_delete_many_mutation(runner: Runner) -> TestResult<()> {
135+
test_no_toplevel_mutation(
136+
runner,
137+
"no_delete_many_mutation",
138+
r#"mutation { deleteManyTestView(where: { id: { gt: 0 } }) { count } }"#,
139+
)
140+
.await
141+
}
142+
143+
async fn test_no_toplevel_mutation(runner: Runner, schema_name: &str, query: &str) -> TestResult<()> {
144+
create_test_data(&runner, schema_name).await?;
145+
146+
match runner.query(query).await {
147+
Ok(res) => res.assert_failure(2009, None),
148+
Err(TestError::QueryConversionError(err)) if err.kind().code() == "P2009" => (),
149+
Err(err) => return Err(err),
150+
}
151+
152+
Ok(())
153+
}
154+
76155
async fn create_test_data(runner: &Runner, schema_name: &str) -> TestResult<()> {
77156
migrate_view(runner, schema_name).await?;
78157

query-engine/connector-test-kit-rs/query-tests-setup/src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::TemplatingError;
22
use quaint::error::Error as QuaintError;
33
use std::env::VarError;
44
use thiserror::Error;
5+
use user_facing_errors::query_engine::validation::ValidationError;
56

67
#[allow(clippy::large_enum_variant)]
78
#[derive(Debug, Error)]
@@ -32,6 +33,9 @@ pub enum TestError {
3233

3334
#[error("External process error: {0}")]
3435
External(#[from] Box<dyn std::error::Error + Send + Sync>),
36+
37+
#[error("Error converting GraphQL query to JSON: {0}")]
38+
QueryConversionError(#[from] ValidationError),
3539
}
3640

3741
impl TestError {

query-engine/connector-test-kit-rs/query-tests-setup/src/runner/json_adapter/request.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use query_core::{
1212
use query_structure::PrismaValue;
1313
use request_handlers::{Action, FieldQuery, GraphQLProtocolAdapter, JsonSingleQuery, SelectionSet, SelectionSetValue};
1414
use serde_json::{json, Value as JsonValue};
15+
use user_facing_errors::query_engine::validation::ValidationError;
1516

1617
pub struct JsonRequest;
1718

@@ -23,7 +24,9 @@ impl JsonRequest {
2324
let operation_name = operation.name();
2425
let schema_field = query_schema
2526
.find_query_field(operation_name)
26-
.unwrap_or_else(|| query_schema.find_mutation_field(operation_name).unwrap());
27+
.or_else(|| query_schema.find_mutation_field(operation_name))
28+
.ok_or_else(|| ValidationError::unknown_argument(vec![], vec![operation_name], vec![]))?;
29+
2730
let model_name = schema_field
2831
.model()
2932
.map(|m| query_schema.internal_data_model.walk(m).name().to_owned());

query-engine/connector-test-kit-rs/query-tests-setup/src/runner/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ impl Runner {
364364
let request_body = match self.protocol {
365365
EngineProtocol::Json => {
366366
// Translate the GraphQL query to JSON
367-
let json_query = JsonRequest::from_graphql(&query, self.query_schema()).unwrap();
367+
let json_query = JsonRequest::from_graphql(&query, self.query_schema())?;
368368
println!("{}", serde_json::to_string_pretty(&json_query).unwrap().green());
369369

370370
RequestBody::Json(JsonBody::Single(json_query))

query-engine/query-structure/src/model.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ impl Model {
6767
.scalar_fields()
6868
.any(|sf| sf.ast_field().arity.is_required() && sf.is_unsupported() && sf.default_value().is_none());
6969

70-
!has_unsupported_field
70+
!has_unsupported_field && !self.is_view()
7171
}
7272

7373
/// The name of the model in the database
@@ -86,6 +86,10 @@ impl Model {
8686
.filter(|idx| idx.is_unique())
8787
.filter(|index| !index.fields().any(|f| f.is_unsupported()))
8888
}
89+
90+
pub fn is_view(&self) -> bool {
91+
self.walker().ast_model().is_view()
92+
}
8993
}
9094

9195
impl std::fmt::Debug for Model {

query-engine/schema/src/build/output_types/mutation_type.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ pub(crate) fn mutation_fields(ctx: &QuerySchema) -> Vec<FieldFn> {
1616
}
1717

1818
for model in ctx.internal_data_model.models() {
19+
if model.is_view() {
20+
continue;
21+
}
22+
1923
if model.supports_create_operation() {
2024
field!(create_one, model);
2125

0 commit comments

Comments
 (0)