Skip to content

Commit

Permalink
chore: Introduce query planner to router-bridge
Browse files Browse the repository at this point in the history
fixes #1088

This commit is built on top of @BrynCooke 's work on #812, and exposes a query planning functionality, which has been living in a separate branch for a while.

A few things happened:

General:
- Remove jest tests from the router bridge (they weren't being run anyway, and they are now superseded by the cargo tests)
- Add router-bridge js files to the prettier configuration for formatting and linting
- Add a dependency to query-planner-js to the router-bridge

TypeScript/Javascript:
- Add `do_plan.ts` that calls the query-planner behind the scenes.
- Use `new QueryPlanner(schema).buildQueryPlan` instead of the old `buildQueryPlan` function, that doesn't exist anymore (depending on how long we want to keep this code, we might be able to keep the QueryPlanner object around, and skip subsequent schema parsing / composing.
- Move the type `OperationResult` to a utils file (so we can use it in both `do_plan.ts` and `do_introspect.ts`

Rust:
- Use &str instead of String for schema SDL, since apollo-rs provides as_str() on the Schema type.
- Add a plan function, that is generic over it's return type ( `Result<T, PlanningErrors>` where `T: DeserializeOwned + 'static` so we can keep the `QueryPlan` structure on the router-core side.
- Add plan() tests that call plan<serde_json::Value>() so we have snapshots over the plan result.
  • Loading branch information
o0Ignition0o committed Oct 15, 2021
1 parent bbbfb3f commit 0e86671
Show file tree
Hide file tree
Showing 23 changed files with 576 additions and 118 deletions.
2 changes: 1 addition & 1 deletion .prettierrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module.exports = {
requirePragma: true,
overrides: [
{
files: '{docs/{,source/**},.,{gateway-js,federation-js,harmonizer,federation-integration-testsuite-js,query-planner-js,subgraph-js}/**,test}/{*.js,*.ts}',
files: '{docs/{,source/**},.,{gateway-js,federation-js,harmonizer,federation-integration-testsuite-js,query-planner-js,router-bridge,subgraph-js}/**,test}/{*.js,*.ts}',
options: {
requirePragma: false,
trailingComma: 'all',
Expand Down
7 changes: 4 additions & 3 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion router-bridge/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ fn main() {

match (&bridge_last_update, &js_dist_last_update) {
// the federation folder has evolved since the last time we built harmonizer.
(Ok(bridge), Some(federation)) if federation > bridge => update_bridge(),
(Ok(bridge), Some(js)) if js > bridge => update_bridge(),
// Os didn't allow to query for metadata, we can't know for sure the bridge is up to date.
(Err(_), _) => update_bridge(),
_ => {
Expand Down
7 changes: 3 additions & 4 deletions router-bridge/js-src/do_introspect.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import type { batchIntrospect } from './introspection';
import type { batchIntrospect } from '.';
import type { OperationResult } from './types';

/**
* 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 var bridge: { batchIntrospect: typeof batchIntrospect };

// TODO: Maybe tighten the type, and put it somewhere else
type OperationResult = { Ok: any, Err?: undefined } | { Ok?: undefined, Err: any };

declare var done: (operationResult: OperationResult) => void;
declare var sdl: string;
declare var queries: string[];
Expand Down
35 changes: 35 additions & 0 deletions router-bridge/js-src/do_plan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { plan } from '.';
import type { OperationResult } from './types';
import { BuildQueryPlanOptions } from '@apollo/query-planner';

/**
* 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 var bridge: { plan: typeof plan };

declare var done: (operationResult: OperationResult) => void;
declare var schemaString: string;
declare var queryString: string;
declare var options: BuildQueryPlanOptions;
declare var operationName: string | undefined;

if (!options) {
done({
Err: [{ message: 'Error in JS-Rust-land: options is missing.' }],
});
}

const planResult = bridge.plan(
schemaString,
queryString,
options,
operationName,
);

if (planResult.errors?.length > 0) {
done({ Err: planResult.errors });
} else {
done({ Ok: planResult.data });
}
4 changes: 2 additions & 2 deletions router-bridge/js-src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { introspect, batchIntrospect } from "./introspection";
export { parse as parseGraphqlDocument } from "graphql";
export { introspect, batchIntrospect } from './introspection';
export { plan } from './plan';
File renamed without changes.
73 changes: 0 additions & 73 deletions router-bridge/js-src/introspection/__tests__/index.test.ts

This file was deleted.

7 changes: 0 additions & 7 deletions router-bridge/js-src/introspection/__tests__/tsconfig.json

This file was deleted.

30 changes: 30 additions & 0 deletions router-bridge/js-src/plan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ExecutionResult, parse } from 'graphql';
import {
QueryPlanner,
buildOperationContext,
BuildQueryPlanOptions,
buildComposedSchema,
} from '@apollo/query-planner';

export function plan(
schemaString: string,
queryString: string,
options: BuildQueryPlanOptions,
operationName?: string,
): ExecutionResult {
try {
const schema = parse(schemaString);
const query = parse(queryString);
const composedSchema = buildComposedSchema(schema);
const operationContext = buildOperationContext(
composedSchema,
query,
operationName,
);

const planner = new QueryPlanner(composedSchema);
return { data: planner.buildQueryPlan(operationContext, options) };
} catch (e) {
return { errors: [e] };
}
}
6 changes: 3 additions & 3 deletions router-bridge/js-src/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ Deno.core.ops();
const _newline = new Uint8Array([10]);

function print(value) {
Deno.core.dispatchByName('op_print', 0, value.toString(), _newline);
Deno.core.dispatchByName('op_print', 0, value.toString(), _newline);
}

function done(result) {
Deno.core.opSync('op_result', result);
Deno.core.opSync('op_result', result);
}

// We build some of the preliminary objects that our Rollup-built package is
Expand All @@ -24,7 +24,7 @@ node_fetch_1 = {};
// particular, to determine whether or not we are running in a debug
// mode. For the purposes of harmonizer, we don't gain anything from
// running in such a mode.
process = {argv: [], env: {"NODE_ENV": "production"}};
process = { argv: [], env: { NODE_ENV: 'production' } };
// Some JS runtime implementation specific bits that we rely on that
// need to be initialized as empty objects.
global = {};
Expand Down
3 changes: 3 additions & 0 deletions router-bridge/js-src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type OperationResult =
| { Ok: any; Err?: undefined }
| { Ok?: undefined; Err: any };
4 changes: 2 additions & 2 deletions router-bridge/js-src/url_polyfill.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import "fast-text-encoding";
import * as url from "whatwg-url";
import 'fast-text-encoding';
import * as url from 'whatwg-url';
export default url;
5 changes: 2 additions & 3 deletions router-bridge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@
"author": "Apollo <opensource@apollographql.com>",
"license": "MIT",
"dependencies": {
"@apollo/query-planner": "file:../query-planner-js",
"fast-text-encoding": "1.0.3",
"graphql": "15.6.1",
"whatwg-url": "9.1.0"
},
"peerDependencies": {
"graphql": "^14.5.0 || ^15.0.0"
}
}
15 changes: 5 additions & 10 deletions router-bridge/src/introspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ pub type IntrospectionResult = Result<Vec<IntrospectionResponse>, IntrospectionE
/// The `batch_introspect` function receives a [`string`] representing the SDL and invokes JavaScript
/// introspection on it, with the `queries` to run against the SDL.
///
pub fn batch_introspect(sdl: String, queries: Vec<String>) -> IntrospectionResult {
pub fn batch_introspect(sdl: &str, queries: Vec<String>) -> IntrospectionResult {
Js::new()
.with_parameter("sdl", sdl)
.with_parameter("queries", queries)
Expand All @@ -112,11 +112,8 @@ mod tests {
}
"#;

let introspected = batch_introspect(
raw_sdl.to_string(),
vec![DEFAULT_INTROSPECTION_QUERY.to_string()],
)
.unwrap();
let introspected =
batch_introspect(raw_sdl, vec![DEFAULT_INTROSPECTION_QUERY.to_string()]).unwrap();
insta::assert_snapshot!(serde_json::to_string(&introspected).unwrap());
}

Expand All @@ -129,8 +126,7 @@ mod tests {
let response = batch_introspect(
"schema {
query: Query
}"
.to_string(),
}",
vec![DEFAULT_INTROSPECTION_QUERY.to_string()],
)
.unwrap();
Expand All @@ -147,8 +143,7 @@ mod tests {
let response = batch_introspect(
"schema {
query: Query
}"
.to_string(),
}",
vec![DEFAULT_INTROSPECTION_QUERY.to_string()],
)
.unwrap();
Expand Down
1 change: 1 addition & 0 deletions router-bridge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
#![warn(missing_docs, future_incompatible, unreachable_pub, rust_2018_idioms)]
pub mod introspect;
mod js;
pub mod plan;

0 comments on commit 0e86671

Please sign in to comment.