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

feat: add /showcase/graphql endpoint #1015

Merged
merged 56 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
b694bad
chore: extend HttpIO trait impl to Arc<HttpIO>
mogery Jan 23, 2024
3c257aa
feat(showcase): add @server(showcase: bool) config option
mogery Jan 23, 2024
dc34bc1
feat: add /showcase/graphql endpoint
mogery Jan 23, 2024
c44dd4a
feat: add tests for /showcase/graphql
mogery Jan 23, 2024
0261cae
feat(cloudflare): port to use /showcase/graphql code
mogery Jan 23, 2024
8901d05
chore: lint
mogery Jan 23, 2024
fc16329
fix: httpio source inconsistency
mogery Jan 24, 2024
115169a
fix(http): faster showcase check order
mogery Jan 24, 2024
35ca8a0
fix(showcase): unused imports
mogery Jan 24, 2024
86e9b6b
fix(wasm): rework showcase to remove cli references
mogery Jan 24, 2024
9f571fd
Revert "fix(wasm): rework showcase to remove cli references"
mogery Jan 24, 2024
8f72ed5
fix(wasm): remove cli references from request_handler
mogery Jan 24, 2024
065f83f
chore: add invalid-config.graphql
mogery Jan 24, 2024
ac4052a
feat(tests/http): mock plaintext bodies
mogery Jan 24, 2024
afbe833
fix(showcase): fix tests and add test for validation errors
mogery Jan 24, 2024
1004da2
chore: lint
mogery Jan 24, 2024
9e862f6
fix(cloudflare): let the core check for graphiql instead
mogery Jan 26, 2024
d689686
chore: pull in new rustfmt
mogery Jan 26, 2024
83babf0
chore: fix weird merges
mogery Jan 26, 2024
d2c2e3b
fix: HttpIO/FileIO + Send + Sync disparities breaking multi-threaded …
mogery Jan 26, 2024
a51e6a7
chore: lint
mogery Jan 26, 2024
abf8a0d
feat: prettify showcase api
mogery Jan 26, 2024
b5aae9e
fix(showcase): keep up with API changes
mogery Jan 28, 2024
5690ce7
fix(http_spec): make textual body more explicit
mogery Jan 30, 2024
c23c63f
fix(showcase): fix DummyFileIO error checking
mogery Jan 30, 2024
1633c6c
chore: lint
mogery Jan 30, 2024
b581de5
chore: remove unused Arc HttpIO impl
mogery Jan 30, 2024
e8c7264
feat(showcase): add tests
mogery Jan 30, 2024
ae789e7
chore(showcase): lint
mogery Jan 30, 2024
35f7d29
chore: lint
mogery Jan 30, 2024
f484a9b
fix(cloudflare): force graphiql to be enabled
mogery Jan 30, 2024
38992f9
fix(cloudflare): add explicit graphiql path logic
mogery Jan 30, 2024
60a5305
fix(showcase): keep up with API changes
mogery Jan 31, 2024
9d9a73c
feat(ConfigReader): take an Option<Arc<dyn FileIO>>
mogery Jan 31, 2024
2a6b33d
chore: move the hot route up
tusharmath Feb 1, 2024
f2137bb
chore: rename variables to app_ctx
tusharmath Feb 1, 2024
27c9c36
chore: rename methods
tusharmath Feb 1, 2024
6bd71da
feat(showcase): adapt to TargetRuntime
mogery Feb 1, 2024
fd24c01
Merge branch 'main' into mog/showcase
mogery Feb 2, 2024
61bfbc1
Merge branch 'main' into mog/showcase
tusharmath Feb 3, 2024
146b006
chore: testconv showcase
mogery Feb 3, 2024
2511113
Merge branch 'main' into mog/showcase
mogery Feb 3, 2024
27e3f75
Merge branch 'main' into mog/showcase
mogery Feb 3, 2024
b34d975
fix(execution_spec): add textBody support
mogery Feb 3, 2024
801b147
Merge branch 'main' into mog/showcase
mogery Feb 3, 2024
89eb7c0
fix(showcase): keep up with changes
mogery Feb 3, 2024
e1e50a6
Merge branch 'main' into mog/showcase
tusharmath Feb 4, 2024
450b0c7
Merge branch 'main' into mog/showcase
mogery Feb 4, 2024
6325b6d
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 4, 2024
8b9f8f7
Merge branch 'main' into mog/showcase
mogery Feb 4, 2024
b754123
Merge branch 'main' into mog/showcase
mogery Feb 5, 2024
39f67c7
fix(cloudflare): remove unnecessary traits from init_http/init_file
mogery Feb 5, 2024
865e70c
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 5, 2024
8db2694
fix(showcase): remove unnecessary traits from init_file
mogery Feb 5, 2024
c63e0df
fix(showcase): use serde_qs
mogery Feb 5, 2024
da582ca
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ derive_setters = "0.1.6"
thiserror = "1.0.56"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_qs = "0.12"
serde_yaml = "0.9"
serde_urlencoded = "0.7.1"
url = { version = "2", features = ["serde"] }
Expand Down
4 changes: 3 additions & 1 deletion cloudflare/src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ impl CloudflareFileIO {
}
}

// TODO: avoid the unsafe impl
// Multi-threading is not enabled in Cloudflare,
// so this doesn't matter, and makes API compliance
// way easier.
unsafe impl Sync for CloudflareFileIO {}
unsafe impl Send for CloudflareFileIO {}

Expand Down
99 changes: 50 additions & 49 deletions cloudflare/src/handle.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
use std::borrow::Borrow;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::{Arc, RwLock};

use anyhow::anyhow;
use hyper::{Body, Method, Request, Response};
use lazy_static::lazy_static;
use tailcall::async_graphql_hyper::GraphQLRequest;
use tailcall::blueprint::Blueprint;
use tailcall::config::reader::ConfigReader;
use tailcall::config::ConfigSet;
use tailcall::http::{graphiql, handle_request, AppContext};
use tailcall::target_runtime::TargetRuntime;
use tailcall::http::{graphiql, handle_request, showcase, AppContext};

use crate::http::{to_request, to_response};
use crate::init_runtime;
Expand All @@ -20,67 +17,71 @@ lazy_static! {
///
/// The handler which handles requests on cloudflare
///
pub async fn fetch(req: worker::Request, env: worker::Env) -> anyhow::Result<worker::Response> {
pub async fn fetch(
req: worker::Request,
env: worker::Env,
_: worker::Context,
) -> anyhow::Result<worker::Response> {
log::info!(
"{} {:?}",
req.method().to_string(),
req.url().map(|u| u.to_string())
);
let env = Rc::new(env);
let hyper_req = to_request(req).await?;
if hyper_req.method() == hyper::Method::GET {
let response = graphiql(&hyper_req)?;
return to_response(response).await;
}
let query = hyper_req
.uri()
.query()
.ok_or(anyhow!("Unable parse extract query"))?;
let query = serde_qs::from_str::<HashMap<String, String>>(query)?;
let config_path = query
.get("config")
.ok_or(anyhow!("The key 'config' not found in the query"))?
.clone();
let req = to_request(req).await?;

log::info!("config-url: {}", config_path);
let app_ctx = get_app_ctx(env, config_path.as_str()).await?;
let resp = handle_request::<GraphQLRequest>(hyper_req, app_ctx).await?;
// Quick exit to GraphiQL
//
// Has to be done here, since when using GraphiQL, a config query parameter is not specified,
// and get_app_ctx will fail without it.
if req.method() == Method::GET {
return to_response(graphiql(&req)?).await;
}

let env = Rc::new(env);
let app_ctx = match get_app_ctx(env, &req).await? {
Ok(app_ctx) => app_ctx,
Err(e) => return to_response(e).await,
};
let resp = handle_request::<GraphQLRequest>(req, app_ctx).await?;
to_response(resp).await
}

///
/// Reads the configuration from the CONFIG environment variable.
///
async fn get_config(runtime: TargetRuntime, file_path: &str) -> anyhow::Result<ConfigSet> {
let reader = ConfigReader::init(runtime);
let config = reader.read(&file_path).await?;
Ok(config)
}

///
/// Initializes the worker once and caches the app context
/// for future requests.
///
async fn get_app_ctx(env: Rc<worker::Env>, file_path: &str) -> anyhow::Result<Arc<AppContext>> {
async fn get_app_ctx(
env: Rc<worker::Env>,
req: &Request<Body>,
) -> anyhow::Result<Result<Arc<AppContext>, Response<Body>>> {
// Read context from cache
if let Some(app_ctx) = read_app_ctx() {
if app_ctx.0 == file_path {
log::info!("Using cached application context");
return Ok(app_ctx.clone().1);
let file_path = req
.uri()
.query()
.and_then(|x| serde_qs::from_str::<HashMap<String, String>>(x).ok())
.and_then(|x| x.get("config").cloned());

if let Some(file_path) = &file_path {
if let Some(app_ctx) = read_app_ctx() {
if app_ctx.0 == file_path.borrow() {
log::info!("Using cached application context");
return Ok(Ok(app_ctx.clone().1));
}
}
}
// Create new context

let runtime = init_runtime(env)?;
let cfg = get_config(runtime.clone(), file_path).await?;
log::info!("Configuration read ... ok");
log::debug!("\n{}", cfg.to_sdl());
let blueprint = Blueprint::try_from(&cfg)?;
log::info!("Blueprint generated ... ok");
let app_ctx = Arc::new(AppContext::new(blueprint, runtime));
*APP_CTX.write().unwrap() = Some((file_path.to_string(), app_ctx.clone()));
log::info!("Initialized new application context");
Ok(app_ctx)
match showcase::create_app_ctx::<GraphQLRequest>(req, runtime, true).await? {
Ok(app_ctx) => {
let app_ctx: Arc<AppContext> = Arc::new(app_ctx);
if let Some(file_path) = file_path {
*APP_CTX.write().unwrap() = Some((file_path, app_ctx.clone()));
}
log::info!("Initialized new application context");
Ok(Ok(app_ctx))
}
Err(e) => Ok(Err(e)),
}
}

fn read_app_ctx() -> Option<(String, Arc<AppContext>)> {
Expand Down
8 changes: 5 additions & 3 deletions cloudflare/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ pub fn init_env(env: Rc<worker::Env>) -> Arc<dyn EnvIO> {
}

pub fn init_file(env: Rc<worker::Env>, bucket_id: String) -> anyhow::Result<Arc<dyn FileIO>> {
#[allow(clippy::arc_with_non_send_sync)]
Ok(Arc::new(file::CloudflareFileIO::init(env, bucket_id)?))
}

Expand All @@ -36,6 +35,9 @@ pub fn init_runtime(env: Rc<worker::Env>) -> anyhow::Result<TargetRuntime> {
let bucket_id = env_io
.get("BUCKET")
.ok_or(anyhow!("BUCKET var is not set"))?;

log::debug!("R2 Bucket ID: {}", bucket_id);

Ok(TargetRuntime {
http: http.clone(),
http2_only: http.clone(),
Expand All @@ -49,9 +51,9 @@ pub fn init_runtime(env: Rc<worker::Env>) -> anyhow::Result<TargetRuntime> {
async fn fetch(
req: worker::Request,
env: worker::Env,
_: worker::Context,
ctx: worker::Context,
) -> anyhow::Result<worker::Response> {
let result = handle::fetch(req, env).await;
let result = handle::fetch(req, env, ctx).await;

match result {
Ok(response) => Ok(response),
Expand Down
4 changes: 4 additions & 0 deletions generated/.tailcallrc.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,10 @@ directive @server(
"""
script: Script
"""
`showcase` enables the /showcase/graphql endpoint.
"""
showcase: Boolean
"""
This configuration defines local variables for server operations. Useful for storing
constant configurations, secrets, or shared information.
"""
Expand Down
7 changes: 7 additions & 0 deletions generated/.tailcallrc.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1419,6 +1419,13 @@
],
"description": "A link to an external JS file that listens on every HTTP request response event."
},
"showcase": {
"description": "`showcase` enables the /showcase/graphql endpoint.",
"type": [
"boolean",
"null"
]
},
"vars": {
"allOf": [
{
Expand Down
2 changes: 2 additions & 0 deletions src/blueprint/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub struct Server {
pub enable_query_validation: bool,
pub enable_response_validation: bool,
pub enable_batch_requests: bool,
pub enable_showcase: bool,
pub global_response_timeout: i64,
pub worker: usize,
pub port: u16,
Expand Down Expand Up @@ -100,6 +101,7 @@ impl TryFrom<crate::config::Server> for Server {
enable_query_validation: (config_server).enable_query_validation(),
enable_response_validation: (config_server).enable_http_validation(),
enable_batch_requests: (config_server).enable_batch_requests(),
enable_showcase: (config_server).enable_showcase(),
global_response_timeout: (config_server).get_global_response_timeout(),
http,
worker: (config_server).get_workers(),
Expand Down
2 changes: 1 addition & 1 deletion src/cli/javascript/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub use runtime::Runtime;

use crate::{blueprint, HttpIO};

pub fn init_http(http: impl HttpIO, script: blueprint::Script) -> Arc<dyn HttpIO> {
pub fn init_http(http: impl HttpIO, script: blueprint::Script) -> Arc<dyn HttpIO + Sync + Send> {
let script_io = Runtime::new(script);
Arc::new(HttpFilter::new(http, script_io))
}
7 changes: 7 additions & 0 deletions src/config/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ pub struct Server {
/// `globalResponseTimeout` sets the maximum query duration before termination, acting as a safeguard against long-running queries.
pub global_response_timeout: Option<i64>,
#[serde(default, skip_serializing_if = "is_default")]
/// `showcase` enables the /showcase/graphql endpoint.
pub showcase: Option<bool>,
#[serde(default, skip_serializing_if = "is_default")]
/// `workers` sets the number of worker threads. @default the number of system cores.
pub workers: Option<usize>,
#[serde(default, skip_serializing_if = "is_default")]
Expand Down Expand Up @@ -118,6 +121,9 @@ impl Server {
pub fn enable_batch_requests(&self) -> bool {
self.batch_requests.unwrap_or(false)
}
pub fn enable_showcase(&self) -> bool {
self.showcase.unwrap_or(false)
}

pub fn get_hostname(&self) -> String {
self.hostname.clone().unwrap_or("127.0.0.1".to_string())
Expand Down Expand Up @@ -150,6 +156,7 @@ impl Server {
self.global_response_timeout = other
.global_response_timeout
.or(self.global_response_timeout);
self.showcase = other.showcase.or(self.showcase);
self.workers = other.workers.or(self.workers);
self.port = other.port.or(self.port);
self.hostname = other.hostname.or(self.hostname);
Expand Down
1 change: 1 addition & 0 deletions src/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod request_context;
mod request_handler;
mod request_template;
mod response;
pub mod showcase;

pub use cache::*;
pub use data_loader::*;
Expand Down
20 changes: 10 additions & 10 deletions src/http/request_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,20 +98,20 @@ impl RequestContext {
}

impl From<&AppContext> for RequestContext {
fn from(server_ctx: &AppContext) -> Self {
fn from(app_ctx: &AppContext) -> Self {
Self {
h_client: server_ctx.runtime.http.clone(),
h2_client: server_ctx.runtime.http2_only.clone(),
server: server_ctx.blueprint.server.clone(),
upstream: server_ctx.blueprint.upstream.clone(),
h_client: app_ctx.runtime.http.clone(),
h2_client: app_ctx.runtime.http2_only.clone(),
server: app_ctx.blueprint.server.clone(),
upstream: app_ctx.blueprint.upstream.clone(),
req_headers: HeaderMap::new(),
http_data_loaders: server_ctx.http_data_loaders.clone(),
gql_data_loaders: server_ctx.gql_data_loaders.clone(),
cache: server_ctx.runtime.cache.clone(),
grpc_data_loaders: server_ctx.grpc_data_loaders.clone(),
http_data_loaders: app_ctx.http_data_loaders.clone(),
gql_data_loaders: app_ctx.gql_data_loaders.clone(),
cache: app_ctx.runtime.cache.clone(),
grpc_data_loaders: app_ctx.grpc_data_loaders.clone(),
min_max_age: Arc::new(Mutex::new(None)),
cache_public: Arc::new(Mutex::new(None)),
env_vars: server_ctx.runtime.env.clone(),
env_vars: app_ctx.runtime.env.clone(),
}
}
}
Expand Down
Loading