Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP(schema-wasm): support schema split into multiple files #4787

Merged
merged 15 commits into from
Apr 8, 2024
Merged
3 changes: 1 addition & 2 deletions prisma-fmt/src/code_actions/multi_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,7 @@ pub(super) fn add_schema_to_schemas(
)
}
None => {
let has_properties = datasource.provider_defined()
|| datasource.url_defined()
let has_properties = datasource.provider_defined() | datasource.url_defined()
|| datasource.direct_url_defined()
|| datasource.shadow_url_defined()
|| datasource.relation_mode_defined()
Expand Down
49 changes: 29 additions & 20 deletions prisma-fmt/src/get_config.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use psl::Diagnostics;
use psl::{Diagnostics, ValidatedSchema};
use serde::Deserialize;
use serde_json::json;
use std::collections::HashMap;

use crate::validate::SCHEMA_PARSER_ERROR_CODE;
use crate::{schema_file_input::SchemaFileInput, validate::SCHEMA_PARSER_ERROR_CODE};

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct GetConfigParams {
prisma_schema: String,
prisma_schema: SchemaFileInput,
#[serde(default)]
ignore_env_var_errors: bool,
#[serde(default)]
Expand Down Expand Up @@ -43,29 +43,38 @@ pub(crate) fn get_config(params: &str) -> Result<String, String> {
}

fn get_config_impl(params: GetConfigParams) -> Result<serde_json::Value, GetConfigError> {
let wrap_get_config_err = |errors: Diagnostics| -> GetConfigError {
use std::fmt::Write as _;

let mut full_error = errors.to_pretty_string("schema.prisma", &params.prisma_schema);
write!(full_error, "\nValidation Error Count: {}", errors.errors().len()).unwrap();

GetConfigError {
// this mirrors user_facing_errors::common::SchemaParserError
error_code: Some(SCHEMA_PARSER_ERROR_CODE),
message: full_error,
}
};

let mut config = psl::parse_configuration(&params.prisma_schema).map_err(wrap_get_config_err)?;
let mut schema = psl::validate_multi_file(params.prisma_schema.into());
if schema.diagnostics.has_errors() {
return Err(create_get_config_error(&schema, &schema.diagnostics));
}

if !params.ignore_env_var_errors {
let overrides: Vec<(_, _)> = params.datasource_overrides.into_iter().collect();
config
schema
.configuration
.resolve_datasource_urls_prisma_fmt(&overrides, |key| params.env.get(key).map(String::from))
.map_err(wrap_get_config_err)?;
.map_err(|diagnostics| create_get_config_error(&schema, &diagnostics))?;
}

Ok(psl::get_config(&config))
Ok(psl::get_config(&schema.configuration))
}

fn create_get_config_error(schema: &ValidatedSchema, diagnostics: &Diagnostics) -> GetConfigError {
use std::fmt::Write as _;

let mut rendered_diagnostics = schema.render_diagnostics(diagnostics);
write!(
rendered_diagnostics,
"\nValidation Error Count: {}",
diagnostics.errors().len()
)
.unwrap();

GetConfigError {
// this mirrors user_facing_errors::common::SchemaParserError
error_code: Some(SCHEMA_PARSER_ERROR_CODE),
message: rendered_diagnostics,
}
}

#[cfg(test)]
Expand Down
47 changes: 44 additions & 3 deletions prisma-fmt/src/get_dmmf.rs

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions prisma-fmt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ mod code_actions;
mod get_config;
mod get_dmmf;
mod lint;
mod merge_schemas;
mod native;
mod preview;
mod schema_file_input;
mod text_document_completion;
mod validate;

Expand Down Expand Up @@ -89,6 +91,14 @@ pub fn validate(validate_params: String) -> Result<(), String> {
validate::validate(&validate_params)
}

/// Given a list of Prisma schema files (and their locations), returns the merged schema.
/// This is useful for `@prisma/client` generation, where the client needs a single - potentially large - schema,
/// while still allowing the user to split their schema copies into multiple files.
/// Internally, it uses `[validate]`.
pub fn merge_schemas(params: String) -> Result<String, String> {
merge_schemas::merge_schemas(&params)
}

pub fn native_types(schema: String) -> String {
native::run(&schema)
}
Expand Down
127 changes: 127 additions & 0 deletions prisma-fmt/src/merge_schemas.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
use psl::reformat_validated_schema_into_single;
use serde::Deserialize;

use crate::schema_file_input::SchemaFileInput;

#[derive(Debug, Deserialize)]
pub struct MergeSchemasParams {
schema: SchemaFileInput,
}

pub(crate) fn merge_schemas(params: &str) -> Result<String, String> {
let params: MergeSchemasParams = match serde_json::from_str(params) {
Ok(params) => params,
Err(serde_err) => {
panic!("Failed to deserialize MergeSchemasParams: {serde_err}");
}
};

let validated_schema = crate::validate::run(params.schema, false)?;

let indent_width = 2usize;
let merged_schema = reformat_validated_schema_into_single(validated_schema, indent_width).unwrap();

Ok(merged_schema)
}

#[cfg(test)]
mod tests {
use super::*;
use expect_test::expect;
use serde_json::json;

#[test]
fn merge_two_valid_schemas_succeeds() {
let schema = vec![
(
"b.prisma",
r#"
model B {
id String @id
a A?
}
"#,
),
(
"a.prisma",
r#"
datasource db {
provider = "postgresql"
url = env("DBURL")
}

model A {
id String @id
b_id String @unique
b B @relation(fields: [b_id], references: [id])
}
"#,
),
];

let request = json!({
"schema": schema,
});

let expected = expect![[r#"
model B {
id String @id
a A?
}

datasource db {
provider = "postgresql"
url = env("DBURL")
}

model A {
id String @id
b_id String @unique
b B @relation(fields: [b_id], references: [id])
}
"#]];

let response = merge_schemas(&request.to_string()).unwrap();
expected.assert_eq(&response);
}

#[test]
fn merge_two_invalid_schemas_panics() {
let schema = vec![
(
"b.prisma",
r#"
model B {
id String @id
a A?
}
"#,
),
(
"a.prisma",
r#"
datasource db {
provider = "postgresql"
url = env("DBURL")
}

model A {
id String @id
b_id String @unique
}
"#,
),
];

let request = json!({
"schema": schema,
});

let expected = expect![[
r#"{"error_code":"P1012","message":"\u001b[1;91merror\u001b[0m: \u001b[1mError validating field `a` in model `B`: The relation field `a` on model `B` is missing an opposite relation field on the model `A`. Either run `prisma format` or add it manually.\u001b[0m\n \u001b[1;94m-->\u001b[0m \u001b[4mb.prisma:4\u001b[0m\n\u001b[1;94m | \u001b[0m\n\u001b[1;94m 3 | \u001b[0m id String @id\n\u001b[1;94m 4 | \u001b[0m \u001b[1;91ma A?\u001b[0m\n\u001b[1;94m 5 | \u001b[0m }\n\u001b[1;94m | \u001b[0m\n\nValidation Error Count: 1"}"#
]];

let response = merge_schemas(&request.to_string()).unwrap_err();
expected.assert_eq(&response);
}
}
26 changes: 26 additions & 0 deletions prisma-fmt/src/schema_file_input.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use psl::SourceFile;
use serde::Deserialize;

/// Struct for supporting multiple files
/// in a backward-compatible way: can either accept
/// a single file contents or vector of (filePath, content) tuples.
/// Can be converted to the input for `psl::validate_multi_file` from
/// any of the variants.
#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub(crate) enum SchemaFileInput {
Single(String),
Multiple(Vec<(String, String)>),
}

impl From<SchemaFileInput> for Vec<(String, SourceFile)> {
fn from(value: SchemaFileInput) -> Self {
match value {
SchemaFileInput::Single(content) => vec![("schema.prisma".to_owned(), content.into())],
SchemaFileInput::Multiple(file_list) => file_list
.into_iter()
.map(|(filename, content)| (filename, content.into()))
.collect(),
}
}
}
93 changes: 87 additions & 6 deletions prisma-fmt/src/validate.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
use psl::ValidatedSchema;
use serde::Deserialize;
use serde_json::json;
use std::fmt::Write as _;

use crate::schema_file_input::SchemaFileInput;

// this mirrors user_facing_errors::common::SchemaParserError
pub(crate) static SCHEMA_PARSER_ERROR_CODE: &str = "P1012";

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct ValidateParams {
prisma_schema: String,
prisma_schema: SchemaFileInput,
#[serde(default)]
no_color: bool,
}
Expand All @@ -21,21 +24,22 @@ pub(crate) fn validate(params: &str) -> Result<(), String> {
}
};

run(&params.prisma_schema, params.no_color)
run(params.prisma_schema, params.no_color)?;
Ok(())
}

pub fn run(input_schema: &str, no_color: bool) -> Result<(), String> {
let validate_schema = psl::validate(input_schema.into());
pub fn run(input_schema: SchemaFileInput, no_color: bool) -> Result<ValidatedSchema, String> {
let validate_schema = psl::validate_multi_file(input_schema.into());
let diagnostics = &validate_schema.diagnostics;

if !diagnostics.has_errors() {
return Ok(());
return Ok(validate_schema);
}

// always colorise output regardless of the environment, which is important for Wasm
colored::control::set_override(!no_color);

let mut formatted_error = diagnostics.to_pretty_string("schema.prisma", input_schema);
let mut formatted_error = validate_schema.render_own_diagnostics();
write!(
formatted_error,
"\nValidation Error Count: {}",
Expand Down Expand Up @@ -109,6 +113,83 @@ mod tests {
validate(&request.to_string()).unwrap();
}

#[test]
fn validate_multiple_files() {
let schema = vec![
(
"a.prisma",
r#"
datasource thedb {
provider = "postgresql"
url = env("DBURL")
}

model A {
id String @id
b_id String @unique
b B @relation(fields: [b_id], references: [id])
}
"#,
),
(
"b.prisma",
r#"
model B {
id String @id
a A?
}
"#,
),
];

let request = json!({
"prismaSchema": schema,
});

validate(&request.to_string()).unwrap();
}

#[test]
fn validate_multiple_files_error() {
let schema = vec![
(
"a.prisma",
r#"
datasource thedb {
provider = "postgresql"
url = env("DBURL")
}

model A {
id String @id
b_id String @unique
b B @relation(fields: [b_id], references: [id])
}
"#,
),
(
"b.prisma",
r#"
model B {
id String @id
a A
}
"#,
),
];

let request = json!({
"prismaSchema": schema,
});

let expected = expect![[
r#"{"error_code":"P1012","message":"\u001b[1;91merror\u001b[0m: \u001b[1mError parsing attribute \"@relation\": The relation field `a` on Model `B` is required. This is no longer valid because it's not possible to enforce this constraint on the database level. Please change the field type from `A` to `A?` to fix this.\u001b[0m\n \u001b[1;94m-->\u001b[0m \u001b[4mb.prisma:4\u001b[0m\n\u001b[1;94m | \u001b[0m\n\u001b[1;94m 3 | \u001b[0m id String @id\n\u001b[1;94m 4 | \u001b[0m \u001b[1;91ma A\u001b[0m\n\u001b[1;94m 5 | \u001b[0m }\n\u001b[1;94m | \u001b[0m\n\nValidation Error Count: 1"}"#
]];

let response = validate(&request.to_string()).unwrap_err();
expected.assert_eq(&response);
}

#[test]
fn validate_using_both_relation_mode_and_referential_integrity() {
let schema = r#"
Expand Down
Loading