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(router-bridge): add validation function to export #408

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
6 changes: 2 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
[workspace]
members = [
"apollo-federation-types",
"xtask"
]
members = ["apollo-federation-types", "xtask"]
resolver = "2"
1 change: 1 addition & 0 deletions federation-2/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
[workspace]
members = ["harmonizer", "supergraph", "router-bridge"]
resolver = "2"
62 changes: 62 additions & 0 deletions federation-2/router-bridge/js-src/do_validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { validate } from ".";
import type { OperationResult } from "./types";

type JsError = {
name: string;
message: string;
stack?: string;
validationError?: boolean;
};

/**
* There are several global properties that we make available in our V8 runtime
* and these are the types for those that we expect to use within this script.
* They'll be stripped in the emitting of this file as JS, of course.
*/
declare let bridge: { validate: typeof validate };

declare let done: (operationResult: OperationResult) => void;
declare let schema: string;
declare let query: string;

const intoSerializableError = (error: Error): JsError => {
const {
name,
message,
stack,
validationError = false,
} = error as Error & { validationError?: boolean };
return {
name,
message,
stack,
validationError,
};
};

if (!schema) {
done({
Err: [{ message: "Error in JS-Rust-land: schema is empty." }],
});
}

if (!query) {
done({
Err: [{ message: "Error in JS-Rust-land: query is empty." }],
});
}

try {
const diagnostics = bridge
.validate(schema, query)
.map((e) => intoSerializableError(e));
if (diagnostics.length > 0) {
done({ Err: diagnostics });
} else {
done({ Ok: "successfully validated" });
}
} catch (e) {
done({
Err: [{ message: `An unknown error occured: ${e}` }],
});
}
1 change: 1 addition & 0 deletions federation-2/router-bridge/js-src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { apiSchema } from "./api_schema";
export { introspect, batchIntrospect } from "./introspection";
export { BridgeQueryPlanner } from "./plan";
export { validate } from "./validate";
16 changes: 16 additions & 0 deletions federation-2/router-bridge/js-src/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {
buildSchema,
GraphQLError,
parse,
Source,
validate as validateGraphQL,
} from "graphql";

export function validate(
schema: string,
query: string
): ReadonlyArray<GraphQLError> {
let ts = buildSchema(schema);
let op = parse(new Source(query, "op.graphql"));
return validateGraphQL(ts, op);
}
4 changes: 2 additions & 2 deletions federation-2/router-bridge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"version": "2.5.4",
"description": "Apollo Router JS Bridge Entrypoint",
"scripts": {
"build": "make-dir bundled js-dist && rm -f tsconfig.tsbuildinfo && tsc --build --verbose && node esbuild/bundler.js && cp js-dist/runtime.js js-dist/do_api_schema.js js-dist/do_introspect.js js-dist/plan_worker.js js-dist/test_logger_worker.js js-dist/test_get_random_values.js bundled/",
"build": "make-dir bundled js-dist && rm -f tsconfig.tsbuildinfo && tsc --build --verbose && node esbuild/bundler.js && cp js-dist/runtime.js js-dist/do_api_schema.js js-dist/do_introspect.js js-dist/do_validate.js js-dist/plan_worker.js js-dist/test_logger_worker.js js-dist/test_get_random_values.js bundled/",
"clean": "rm -rf ./node_modules ./js-dist ./bundled ./tsconfig.tsbuildinfo",
"lint": "prettier --check ./esbuild/**/*.js ./js-src/**/*.ts ./js-src/**/*.js",
"format": "prettier --write ./esbuild/**/*.js ./js-src/**/*.ts ./js-src/**/*.js"
Expand Down Expand Up @@ -49,4 +49,4 @@
"node": "16.20.0",
"npm": "9.7.1"
}
}
}
1 change: 1 addition & 0 deletions federation-2/router-bridge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ pub mod error;
pub mod introspect;
mod js;
pub mod planner;
pub mod validate;
mod worker;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
source: router-bridge/src/validate.rs
expression: validated.errors
---
[
{
"message": "Cannot query field \"me\" on type \"Query\"."
}
]
72 changes: 72 additions & 0 deletions federation-2/router-bridge/src/validate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*!
# Run introspection against a GraphQL schema and obtain the result
*/

use crate::error::Error;
use crate::js::Js;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use thiserror::Error;

/// An error which occurred during JavaScript validation.
///
/// The shape of this error is meant to mimick that of the error created within
/// JavaScript, which is a [`GraphQLError`] from the [`graphql-js`] library.
///
/// [`graphql-js']: https://npm.im/graphql
/// [`GraphQLError`]: https://github.com/graphql/graphql-js/blob/3869211/src/error/GraphQLError.js#L18-L75
#[derive(Debug, Error, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct ValidationError {
/// A human-readable description of the error that prevented introspection.
pub message: Option<String>,
}

impl Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.message.as_deref().unwrap_or("UNKNOWN"))
}
}

#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct ValidationResponse {
/// The introspection response if batch_introspect succeeded
#[serde(default)]
#[serde(rename = "Ok")]
data: Option<serde_json::Value>,
/// The errors raised on this specific query if any
#[serde(default)]
#[serde(rename = "Err")]
errors: Option<Vec<ValidationError>>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
data: Option<serde_json::Value>,
/// The errors raised on this specific query if any
#[serde(default)]
#[serde(rename = "Err")]
errors: Option<Vec<ValidationError>>,
pub data: Option<serde_json::Value>,
/// The errors raised on this specific query if any
#[serde(default)]
#[serde(rename = "Err")]
pub errors: Option<Vec<ValidationError>>,

I think these need to be pub so apollo-rs fuzzing can read them

}

pub fn validate(schema: &str, query: &str) -> Result<ValidationResponse, Error> {
Js::new("validate".to_string())
.with_parameter("schema", schema)?
.with_parameter("query", query)?
.execute::<ValidationResponse>("validate", include_str!("../bundled/do_validate.js"))
}

#[cfg(test)]
mod tests {
use crate::validate::validate;

#[test]
fn it_works() {
let schema = r#"
type Query {
hello: String
}
"#;

let query = r#"
{
me
}
"#;

let validated = validate(schema, query).unwrap();
dbg!(&validated);
assert_eq!(validated.errors.clone().unwrap().len(), 1);
insta::assert_json_snapshot!(validated.errors);
}
}
15 changes: 11 additions & 4 deletions federation-2/router-bridge/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,20 @@
"noUnusedLocals": true,
"useUnknownInCatchVariables": false,
"forceConsistentCasingInFileNames": true,
"lib": ["es2021", "esnext.asynciterable"],
"lib": [
"es2021",
"esnext.asynciterable"
],
"rootDir": "./js-src",
"outDir": "./js-dist",
"types": [],
"allowJs": true,
"strict": false,
},
"include": ["./js-src/**/*"],
"exclude": ["**/__tests__"]
}
"include": [
"./js-src/**/*"
],
"exclude": [
"**/__tests__"
]
}