Brendonovich / prisma-client-rust

Type-safe database access for Rust
https://prisma.brendonovich.dev
Apache License 2.0
1.76k stars 104 forks source link

Wrong type in mutation binding generated for rspc #259

Closed beeb closed 1 year ago

beeb commented 1 year ago

Consider the following prisma schema:

model Settings {
    key           String    @id @unique
    valueBool     Boolean?
    valueInt      Int?
    valueFloat    Float?
    valueDecimal  Decimal?
    valueString   String?
    valueDatetime DateTime?
}

The generated PrismaClient shows the following:

#[derive(
        Debug,
        Clone,
        :: serde :: Serialize,
        :: serde :: Deserialize,
        :: prisma_client_rust :: rspc :: Type,
    )]
    #[specta(
        rename = "Settings",
        crate = "prisma_client_rust::rspc::internal::specta"
    )]
    pub struct Data {
        #[serde(rename = "key")]
        pub key: String,
        #[serde(rename = "valueBool")]
        pub value_bool: Option<bool>,
        #[serde(rename = "valueInt")]
        pub value_int: Option<i32>,
        #[serde(rename = "valueFloat")]
        pub value_float: Option<f64>,
        #[serde(rename = "valueDecimal")]
        pub value_decimal: Option<f64>,
        #[serde(rename = "valueString")]
        pub value_string: Option<String>,
        #[serde(rename = "valueDatetime")]
        pub value_datetime: Option<
            ::prisma_client_rust::chrono::DateTime<::prisma_client_rust::chrono::FixedOffset>,
        >,
    }

I setup a query and mutation as follows in the rspc router (note that the mutation doesn't return the same type as the query):

#[derive(Debug, Default, Serialize, Deserialize, Type)]
pub(crate) struct Settings {
    pub(crate) profile: Profile,
}

#[derive(Debug, Default, Serialize, Deserialize, Type)]
pub(crate) struct Profile {
    pub(crate) some_setting: Option<String>,
}

RouterBuilder::<Arc<PrismaClient>>::new().query("get", |t| {
            // get all settings
            t(|db, _: ()| async move {
                let some_setting = db
                    .settings()
                    .find_unique(settings::key::equals("my_key".to_string()))
                    .exec()
                    .await
                    .unwrap_or_default();
                Ok(Settings {
                    profile: Profile {
                        some_setting: some_setting.and_then(|s| s.value_string),
                    },
                })
            })
        })
        .mutation("set_string", |t| {
            #[derive(Debug, Serialize, Deserialize, Type)]
            struct SettingsSetStringInput {
                key: String,
                value_string: Option<String>,
            }
            t(|db, args: SettingsSetStringInput| async move {
                let value = db
                    .settings()
                    .upsert(
                        settings::key::equals(args.key.clone()),
                        settings::create(
                            args.key,
                            vec![settings::value_string::set(args.value_string.clone())],
                        ),
                        vec![settings::value_string::set(args.value_string)],
                    )
                    .exec()
                    .await?;
                Ok(value)
            })
        })

I expect the return value to be the same shape as Data above, but here is what rspc generates in TS-land in the end:

export type Procedures = {
    queries: 
        { key: "settings.get", input: never, result: Settings },
    mutations: 
        { key: "settings.set_string", input: SettingsSetStringInput, result: Settings },
    subscriptions: never
};

export interface Profile { some_setting: string | null }

export interface Settings { profile: Profile }

export interface SettingsSetStringInput { key: string, value_string: string | null }

So when in fact I get the following back from the rspc call in TS-land:

{key: "profile.some_setting", valueBool: null, /*...*/ valueString: "foobar"},

TypeScript thinks I'm getting { profile: { some_setting: "foobar" } }

I think the problem might stem from the following rename generated in prisma.rs:

#[specta(
        rename = "Settings",
        crate = "prisma_client_rust::rspc::internal::specta"
    )]

The Data struct below this statement is not equivalent to Settings which is my custom struct with a profile member. It seems rspc never generates a binding for the Data struct and also uses the wrong type for the return value of the mutation.

Brendonovich commented 1 year ago

I'm 99% sure this is just a naming conflict. All Data structs get a Specta rename to their model name and I'm not changing that, picking a different name for your custom Settings struct should fix it. Perhaps I could add an option for customisation of the name such as adding a prefix or even a transformer function in the CLI, but I'm not changing the default behaviour.

beeb commented 1 year ago

You were right! Name conflict, when I changed my struct to AppSettings, an additional binding was created. Might be worth thinking about documenting this or maybe printing our a warning if such a situation is encountered (provided it can be detected).

Thanks for the help!

oscartbeaumont commented 1 year ago

The next rspc release will have proper errors for duplicate type names. Sorry for the confusion with it!