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(extensions): Add extensions support #2634

Closed
wants to merge 27 commits into from
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a9ebf89
Tailcall extensions initial draft
karatakis Aug 9, 2024
1fe8984
Merge branch 'main' into feat/tailcall-extensions
karatakis Aug 9, 2024
64e3943
Fix example to use jsonplaceholder, and lint
karatakis Aug 9, 2024
644a020
Update examples/extension-i18n/main.graphql
karatakis Aug 9, 2024
5b42b3a
Commit new package
karatakis Aug 9, 2024
7a6bf36
Merge branch 'main' into feat/tailcall-extensions
karatakis Aug 9, 2024
a8f9605
Add extension example
karatakis Aug 10, 2024
5a7cf8a
Merge branch 'main' into feat/tailcall-extensions
karatakis Aug 10, 2024
cb97c81
Add features crate
karatakis Aug 10, 2024
58c30f2
Add readme for example
karatakis Aug 10, 2024
85825a5
Attempt to make tests
karatakis Aug 12, 2024
f8ea852
Fix lint
karatakis Aug 12, 2024
51c047a
Fix test
karatakis Aug 12, 2024
ac70681
Merge branch 'main' into feat/tailcall-extensions
karatakis Aug 12, 2024
9356edc
Fix discussion issues
karatakis Aug 12, 2024
cb94266
Fix readme
karatakis Aug 12, 2024
fc174c2
Remove dead code
karatakis Aug 12, 2024
e627d0c
Fix file to compile
karatakis Aug 12, 2024
0f376cc
Separate extensions into their own files
karatakis Aug 16, 2024
d18a30e
Read translations from file
karatakis Aug 16, 2024
04cd151
Merge branch 'main' into feat/tailcall-extensions
karatakis Aug 16, 2024
f1ea784
Fix lint
karatakis Aug 16, 2024
4a6bfee
Fix code
karatakis Aug 16, 2024
f08b31b
Update tailcall spec
karatakis Aug 16, 2024
217dc1d
Merge branch 'main' into feat/tailcall-extensions
karatakis Aug 19, 2024
16e11bf
Merge branch 'main' into feat/tailcall-extensions
karatakis Aug 19, 2024
656255d
Merge branch 'main' into feat/tailcall-extensions
karatakis Aug 23, 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
13 changes: 13 additions & 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 @@ -242,6 +242,7 @@ members = [
,
"tailcall-http-cache",
"tailcall-version",
"examples/extension-i18n",
]

# Boost execution_spec snapshot diffing performance
Expand Down
13 changes: 13 additions & 0 deletions examples/extension-i18n/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "extension-i18n"
version = "0.1.0"
edition = "2021"

[dependencies]
tailcall = { path = "../.." }
tracing = { workspace = true }
dotenvy = "0.15.7"
anyhow = { workspace = true }
tokio = { workspace = true }
async-graphql-value = "7.0.3"
futures = "0.3.30"
21 changes: 21 additions & 0 deletions examples/extension-i18n/main.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
schema @server(port: 8000) @upstream(baseURL: "http://jsonplaceholder.typicode.com", batch: {maxSize: 10, delay: 10}) {
query: Query
}

type Query {
users: [User] @http(path: "/users")
user(id: ID!): User @http(path: "/users/{{.args.id}}") @extension(name: "modify_ir", params: ["{{.args.id}}"])
}

type User {
id: ID
name: String @extension(name: "translate", params: ["userName", "{{.value.name}}"])
username: String
company: Company
}

type Company {
name: String
catchPhrase: String @extension(name: "translate", params: ["catchPhrase", "{{.value.catchPhrase}}"])
bs: String
}
140 changes: 140 additions & 0 deletions examples/extension-i18n/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
use std::sync::Arc;

use async_graphql_value::ConstValue;
use dotenvy::dotenv;
use futures::executor::block_on;
use tailcall::cli::runtime;
use tailcall::cli::server::Server;
use tailcall::core::blueprint::{Blueprint, ExtensionLoader};
use tailcall::core::config::reader::ConfigReader;
use tailcall::core::config::KeyValue;
use tailcall::core::helpers::headers::to_mustache_headers;
use tailcall::core::valid::Validator;

#[derive(Clone, Debug)]
pub struct TranslateExtension;

impl ExtensionLoader for TranslateExtension {
fn load(&self) {
println!("TranslateExtension...loaded!")
}

fn prepare(
&self,
ir: Box<tailcall::core::ir::model::IR>,
params: ConstValue,
) -> Box<tailcall::core::ir::model::IR> {
println!("params: {:?}", params);
println!("ir: {:?}", ir);
ir
}

fn process(
&self,
params: ConstValue,
value: ConstValue,
) -> Result<ConstValue, tailcall::core::ir::Error> {
println!("params: {:?}", params);
println!("value: {:?}", value);
if let ConstValue::String(value) = value {
let new_value = block_on(translate(&value));
Ok(ConstValue::String(new_value))
} else {
Ok(value)
}
}
}

#[derive(Clone, Debug)]
pub struct ModifyIrExtension;

impl ExtensionLoader for ModifyIrExtension {
fn load(&self) {
println!("ModifyIrExtension...loaded!")
}

fn prepare(
&self,
ir: Box<tailcall::core::ir::model::IR>,
params: ConstValue,
) -> Box<tailcall::core::ir::model::IR> {
println!("params: {:?}", params);
println!("ir: {:?}", ir);

if let tailcall::core::ir::model::IR::IO(tailcall::core::ir::model::IO::Http {
req_template,
group_by,
dl_id,
http_filter,
}) = *ir
{
let mut req_template = req_template;
let headers = to_mustache_headers(&[KeyValue {
key: "Authorization".to_string(),
value: "Bearer 1234".to_string(),
}]);

match headers.to_result() {
Ok(mut headers) => {
req_template.headers.append(&mut headers);
}
Err(_) => panic!("Headers are not structured properly"),
};

let ir = tailcall::core::ir::model::IR::IO(tailcall::core::ir::model::IO::Http {
group_by,
dl_id,
http_filter,
req_template,
});
Box::new(ir)
} else {
ir
}
}

fn process(
&self,
params: ConstValue,
value: ConstValue,
) -> Result<ConstValue, tailcall::core::ir::Error> {
println!("params: {:?}", params);
println!("value: {:?}", value);
Ok(value)
}
}

async fn translate(value: &str) -> String {
match value {
"Multi-layered client-server neural-net" => {
"Red neuronal cliente-servidor multicapa".to_string()
}
"Leanne Graham" => "Leona Grahm".to_string(),
_ => value.to_string(),
}
}
karatakis marked this conversation as resolved.
Show resolved Hide resolved

#[tokio::main]
async fn main() -> anyhow::Result<()> {
println!("Extensions Example");
let translate_ext = Arc::new(TranslateExtension {});
let modify_ir_ext = Arc::new(ModifyIrExtension {});
if let Ok(path) = dotenv() {
tracing::info!("Env file: {:?} loaded", path);
}
let runtime = runtime::init(&Blueprint::default());
let config_reader = ConfigReader::init(runtime.clone());
let file_paths = ["./examples/extension-i18n/main.graphql"];
let config_module = config_reader.read_all(file_paths.as_ref()).await?;
let mut extensions = config_module.extensions().clone();
extensions
.plugin_extensions
.insert("translate".to_string(), translate_ext);
extensions
.plugin_extensions
.insert("modify_ir".to_string(), modify_ir_ext);
let config_module = config_module.merge_extensions(extensions);
let server = Server::new(config_module);
server.fork_start().await?;
Ok(())
}
28 changes: 28 additions & 0 deletions generated/.tailcallrc.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,23 @@
},
"additionalProperties": false
},
"Extension": {
"description": "Provides the ability to load custom extensions that are executed before the execution of the IR for the field and after the field value is resolved.",
"type": "object",
"required": [
"name",
"params"
],
"properties": {
"name": {
"description": "Used to define where the extension is located so tailcall can load it.",
"type": "string"
},
"params": {
"description": "Used to define parameters that are calculated and injected into the IR. Mustache syntax can be used on the parameters field."
}
}
},
"Field": {
"description": "A field definition containing all the metadata information about resolving a field.",
"type": "object",
Expand Down Expand Up @@ -423,6 +440,17 @@
}
]
},
"extension": {
"description": "Inserts an Extension for the field.",
"anyOf": [
{
"$ref": "#/definitions/Extension"
},
{
"type": "null"
}
]
},
"graphql": {
"description": "Inserts a GraphQL resolver for the field.",
"anyOf": [
Expand Down
1 change: 1 addition & 0 deletions src/core/blueprint/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,7 @@ pub fn to_field_definition(
.and(update_protected(object_name).trace(Protected::trace_name().as_str()))
.and(update_enum_alias())
.and(update_union_resolver())
.and(update_extension())
.try_fold(
&(config_module, field, type_of, name),
FieldDefinition::default(),
Expand Down
67 changes: 67 additions & 0 deletions src/core/blueprint/operators/extension.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use async_graphql_value::ConstValue;

use crate::core::blueprint::*;
use crate::core::config;
use crate::core::config::Field;
use crate::core::ir::model::IR;
use crate::core::ir::Error;
use crate::core::try_fold::TryFold;
use crate::core::valid::Valid;

pub fn update_extension<'a>(
) -> TryFold<'a, (&'a ConfigModule, &'a Field, &'a config::Type, &'a str), FieldDefinition, String>
{
TryFold::<(&ConfigModule, &Field, &config::Type, &'a str), FieldDefinition, String>::new(
|(config_module, field, _type_of, name), mut b_field| {
if let Some(extension) = &field.extension {
let params = match DynamicValue::try_from(&extension.params) {
Ok(params) => params,

Check warning on line 18 in src/core/blueprint/operators/extension.rs

View check run for this annotation

Codecov / codecov/patch

src/core/blueprint/operators/extension.rs#L17-L18

Added lines #L17 - L18 were not covered by tests
Err(_) => {
return Valid::fail(format!(
"Could not prepare dynamic value for `{}`",
name
))

Check warning on line 23 in src/core/blueprint/operators/extension.rs

View check run for this annotation

Codecov / codecov/patch

src/core/blueprint/operators/extension.rs#L20-L23

Added lines #L20 - L23 were not covered by tests
}
};
let plugin = match config_module
.extensions()
.plugin_extensions
.get(&extension.name)

Check warning on line 29 in src/core/blueprint/operators/extension.rs

View check run for this annotation

Codecov / codecov/patch

src/core/blueprint/operators/extension.rs#L26-L29

Added lines #L26 - L29 were not covered by tests
{
Some(plugin) => plugin.clone(),

Check warning on line 31 in src/core/blueprint/operators/extension.rs

View check run for this annotation

Codecov / codecov/patch

src/core/blueprint/operators/extension.rs#L31

Added line #L31 was not covered by tests
None => {
return Valid::fail(format!(
"Could not find extension `{}` for `{}`",
extension.name, name
))

Check warning on line 36 in src/core/blueprint/operators/extension.rs

View check run for this annotation

Codecov / codecov/patch

src/core/blueprint/operators/extension.rs#L33-L36

Added lines #L33 - L36 were not covered by tests
}
}
.clone();
plugin.load();
let extension_resolver = IR::Extension {
plugin,
params,
ir: Box::new(
b_field
.resolver
.unwrap_or(IR::ContextPath(vec![b_field.name.clone()])),
),
};
b_field.resolver = Some(extension_resolver);

Check warning on line 50 in src/core/blueprint/operators/extension.rs

View check run for this annotation

Codecov / codecov/patch

src/core/blueprint/operators/extension.rs#L39-L50

Added lines #L39 - L50 were not covered by tests
}
Valid::succeed(b_field)
},
)
}

pub trait ExtensionLoader: std::fmt::Debug + Send + Sync {
fn load(&self) {}

Check warning on line 58 in src/core/blueprint/operators/extension.rs

View check run for this annotation

Codecov / codecov/patch

src/core/blueprint/operators/extension.rs#L58

Added line #L58 was not covered by tests

fn modify_inner(&self, ir: Box<IR>) -> Box<IR> {
karatakis marked this conversation as resolved.
Show resolved Hide resolved
ir
}

Check warning on line 62 in src/core/blueprint/operators/extension.rs

View check run for this annotation

Codecov / codecov/patch

src/core/blueprint/operators/extension.rs#L60-L62

Added lines #L60 - L62 were not covered by tests

fn prepare(&self, ir: Box<IR>, params: ConstValue) -> Box<IR>;

fn process(&self, params: ConstValue, value: ConstValue) -> Result<ConstValue, Error>;
karatakis marked this conversation as resolved.
Show resolved Hide resolved
}
2 changes: 2 additions & 0 deletions src/core/blueprint/operators/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod call;
mod enum_alias;
mod expr;
mod extension;
mod graphql;
mod grpc;
mod http;
Expand All @@ -11,6 +12,7 @@ mod protected;
pub use call::*;
pub use enum_alias::*;
pub use expr::*;
pub use extension::*;
pub use graphql::*;
pub use grpc::*;
pub use http::*;
Expand Down
28 changes: 28 additions & 0 deletions src/core/config/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,10 @@
#[serde(default, skip_serializing_if = "is_default")]
pub http: Option<Http>,

///
/// Inserts an Extension for the field.
pub extension: Option<Extension>,

///
/// Inserts a call resolver for the field.
#[serde(default, skip_serializing_if = "is_default")]
Expand Down Expand Up @@ -618,6 +622,30 @@
pub steps: Vec<Step>,
}

///
/// Provides the ability to load custom extensions that are executed before the
/// execution of the IR for the field and after the field value is resolved.
#[derive(
Serialize,
Deserialize,

Check warning on line 630 in src/core/config/config.rs

View check run for this annotation

Codecov / codecov/patch

src/core/config/config.rs#L630

Added line #L630 was not covered by tests
Clone,
Debug,
Default,
PartialEq,
Eq,
schemars::JsonSchema,
DirectiveDefinition,

Check warning on line 637 in src/core/config/config.rs

View check run for this annotation

Codecov / codecov/patch

src/core/config/config.rs#L637

Added line #L637 was not covered by tests
)]
#[directive_definition(locations = "FieldDefinition")]
pub struct Extension {
/// Used to define where the extension is located so tailcall can load it.
pub name: String,

/// Used to define parameters that are calculated and injected into the IR.
/// Mustache syntax can be used on the parameters field.
pub params: Value,
karatakis marked this conversation as resolved.
Show resolved Hide resolved
}

///
/// Provides the ability to refer to a field defined in the root Query or
/// Mutation.
Expand Down
3 changes: 3 additions & 0 deletions src/core/config/config_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use jsonwebtoken::jwk::JwkSet;
use prost_reflect::prost_types::{FileDescriptorProto, FileDescriptorSet};
use rustls_pki_types::{CertificateDer, PrivateKeyDer};

use crate::core::blueprint::ExtensionLoader;
use crate::core::config::Config;
use crate::core::macros::MergeRight;
use crate::core::merge_right::MergeRight;
Expand Down Expand Up @@ -131,6 +132,8 @@ pub struct Extensions {
pub htpasswd: Vec<Content<String>>,

pub jwks: Vec<Content<JwkSet>>,

pub plugin_extensions: HashMap<String, Arc<dyn ExtensionLoader>>,
}

impl Extensions {
Expand Down
Loading
Loading