rspc icon indicating copy to clipboard operation
rspc copied to clipboard

Website for exploring router schema. Similar to GraphiQL

Open oscartbeaumont opened this issue 2 years ago • 4 comments

oscartbeaumont avatar Jul 31 '22 05:07 oscartbeaumont

This idea is cool! Essentially sort of like a generated docs page for our API.

marcustut avatar Aug 08 '22 06:08 marcustut

I thought so too! I have always loved using GraphQL Playground to explore the data before the frontend has been developed. It is something I can do in rspc because I have all of the types at runtime on the Rust side as required for the rest of the system to work. Something like trpc can't do the same thing because the types do not exist at runtime (in Javascript) although I am sure if they wanted to they could find a way to store them.

I have been working on this feature locally and have made some progress but I am not ready to publish it yet. Hopefully, it won't be long until an MVP of the playground is released.

oscartbeaumont avatar Aug 08 '22 06:08 oscartbeaumont

Was looking into spacedrive and found this repo. I'm in the middle of building an API for personal use, and decided to use Rust as I wanted to learn the language.

I have used TRPC before and it was great to see there's an alternative in Rust, too. As for now, I'm playing around with the axum_handler. However, I'm wondering how does one pass a connection pool to the router, do we use the global context?

In axum we could use the Extension helper to achieve this, is there any ways we could leverage the Extension helper too in RSPC router?

    let router = rspc::Router::<Ctx>::new()
        .config(Config::new().export_ts_bindings("./generated/bindings.ts"))
        .query("version", |_, _: ()| env!("CARGO_PKG_VERSION"))
        .query("echo", |_, v: String| v)
        .query("error", |_, _: ()| {
            Err(rspc::Error::new(
                rspc::ErrorCode::InternalServerError,
                "Something went wrong".into(),
            )) as Result<String, rspc::Error>
        })
        .query("transformMe", |_, _: ()| "Hello, world!".to_string())
        .mutation("sendMsg", |_, v: String| {
            println!("Client said '{}'", v);
            v
        })
        .build()
        .arced();

    let cors = CorsLayer::new()
        .allow_methods(Any)
        .allow_headers(Any)
        .allow_origin(Any);

    let pool = MySqlPoolOptions::new()
        .max_connections(5)
        .connect(&env::var("DATABASE_URL")?)
        .await
        .expect("Failed to connect to database");

    let app = axum::Router::new()
        .route("/", get(|| async { "Hello 'rspc'!" }))
        .route(
            "/rspc/:id",
            router.clone().axum_handler(|Path(path): Path<String>| {
                println!("Client requested operation '{}'", path);
                Ctx {}
            }),
        )
        .route("/rspcws", router.axum_ws_handler(|| Ctx {}))
        .route("/expenses", get(get_expenses))
        .layer(cors)
        .layer(Extension(pool));

This above is my code at the moment.

marcustut avatar Aug 08 '22 07:08 marcustut

You can technically use an Axum extractor (Path in that example could be an Extension extractor) but I think I am going to remove that within the week as I see it as an anti-pattern, it's less typesafe, is Axum specific (I want to support more webservers) and is hard to maintain on my end.

The best solution is to capture the variables into the axum_handler closure (ensure you have the move keyword on the closure) like .axum_handler(move || Ctx { db }). This will probably require your database to be Arced although if you're using a connection pool I would be surprised if this hasn't already been done for you. You can actually do this same thing in Axum handlers but it is less convenient so a lot of people use extensions that trades the conventience for a runtime time error if the expected extension does not exist. This page on the docs site explains why I took this approach for rspc.

I will get onto documenting this better because it seems to be something that many people have struggled with.

I would give #24 a look as it is someone with the same issue and refer to this example I created for that issue.

In Spacedrive we do:

tauri::Builder::default()
      .plugin(sdcore::rspc::integrations::tauri::plugin(router, {
	      let node = node.clone();
	      move || node.get_request_context()
      }))

And then node.get_request_contex() returns a struct containing the database (technically containing a thing to fetch the database because we use multiple but that's beside the point). The syntax shown in this example uses a scope that allows the node.clone(); to happen outside the closure as we use the node variable further down in the main function (where this snippit it from) and when you move a variable into a closure it can then only be used from within that closure unless you put it in an Arc which allows multiple references hence we run node.clone() for a new reference and move the reference into the closure.

oscartbeaumont avatar Aug 08 '22 08:08 oscartbeaumont