diff --git a/.prettierrc.js b/.prettierrc.js index 962659dced..6ca1df0d33 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -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', diff --git a/package-lock.json b/package-lock.json index 11fbc86fb3..3ed349ddd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24788,11 +24788,10 @@ "version": "0.1.0", "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" } }, "router-bridge/node_modules/whatwg-url": { @@ -24917,7 +24916,9 @@ "@apollo/router-bridge": { "version": "file:router-bridge", "requires": { + "@apollo/query-planner": "file:../query-planner-js", "fast-text-encoding": "1.0.3", + "graphql": "15.6.1", "whatwg-url": "9.1.0" }, "dependencies": { diff --git a/router-bridge/build.rs b/router-bridge/build.rs index fe94bd0445..37c2d63065 100644 --- a/router-bridge/build.rs +++ b/router-bridge/build.rs @@ -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(), _ => { diff --git a/router-bridge/js-src/do_introspect.ts b/router-bridge/js-src/do_introspect.ts index f0d7220911..9be6f1a21d 100644 --- a/router-bridge/js-src/do_introspect.ts +++ b/router-bridge/js-src/do_introspect.ts @@ -1,4 +1,6 @@ -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. @@ -6,9 +8,6 @@ import type { batchIntrospect } from './introspection'; */ 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[]; diff --git a/router-bridge/js-src/do_plan.ts b/router-bridge/js-src/do_plan.ts new file mode 100644 index 0000000000..3f98b89f33 --- /dev/null +++ b/router-bridge/js-src/do_plan.ts @@ -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 }); +} diff --git a/router-bridge/js-src/index.ts b/router-bridge/js-src/index.ts index 971c4df311..b22a20813e 100644 --- a/router-bridge/js-src/index.ts +++ b/router-bridge/js-src/index.ts @@ -1,2 +1,2 @@ -export { introspect, batchIntrospect } from "./introspection"; -export { parse as parseGraphqlDocument } from "graphql"; +export { introspect, batchIntrospect } from './introspection'; +export { plan } from './plan'; diff --git a/router-bridge/js-src/introspection/index.ts b/router-bridge/js-src/introspection.ts similarity index 100% rename from router-bridge/js-src/introspection/index.ts rename to router-bridge/js-src/introspection.ts diff --git a/router-bridge/js-src/introspection/__tests__/index.test.ts b/router-bridge/js-src/introspection/__tests__/index.test.ts deleted file mode 100644 index d669815c72..0000000000 --- a/router-bridge/js-src/introspection/__tests__/index.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { introspect, batchIntrospect } from '../'; -import { ExecutionResult, getIntrospectionQuery } from 'graphql'; - -describe('introspect', () => { - it('should introspect correctly on valid sdl', () => { - const validSDL = `schema - { - query: Query - } - - type Query { - hello: String - } - `; - - const query = getIntrospectionQuery(); - - const introspectionResult = introspect(validSDL, query); - const batchIntrospectionResult = batchIntrospect(validSDL, [query]); - - expect(introspectionResult.data).toBeDefined(); - assertIntrospectionSuccess(introspectionResult, JSON.stringify(introspectionResult)); - - expect(batchIntrospectionResult?.length).toEqual(1); - expect(batchIntrospectionResult[0]).toEqual(introspectionResult); - }); - - it('should fail introspection correctly on invalid sdl', () => { - const invalidSDL = "THIS SDL IS DEFINITELY NOT VALID"; - const query = getIntrospectionQuery(); - - const introspectionResult = introspect(invalidSDL, query); - const batchIntrospectionResult = batchIntrospect(invalidSDL, [query]); - - assertIntrospectionFailure(introspectionResult, JSON.stringify(introspectionResult)); - const { errors } = introspectionResult; - expect(errors).toBeDefined(); - expect(errors?.length).toEqual(1); - - expect(batchIntrospectionResult?.length).toEqual(1); - expect(batchIntrospectionResult[0]).toEqual(introspectionResult); - }); -}); - -export function introspectionHasErrors( - introspectionResult: ExecutionResult, -): boolean { - return !!introspectionResult.errors?.length; -} - -// This assertion function should be used for the sake of convenient type refinement. -// It should not be depended on for causing a test to fail. If an error is thrown -// from here, its use should be reconsidered. -function assertIntrospectionSuccess( - introspectionResult: ExecutionResult, - message?: string, -) { - if (introspectionHasErrors(introspectionResult)) { - throw new Error(message || 'Unexpected test failure'); - } -} - -// This assertion function should be used for the sake of convenient type refinement. -// It should not be depended on for causing a test to fail. If an error is thrown -// from here, its use should be reconsidered. -function assertIntrospectionFailure( - introspectionResult: ExecutionResult, - message?: string, -) { - if (!introspectionHasErrors(introspectionResult)) { - throw new Error(message || 'Unexpected test failure'); - } -} diff --git a/router-bridge/js-src/introspection/__tests__/tsconfig.json b/router-bridge/js-src/introspection/__tests__/tsconfig.json deleted file mode 100644 index b27c38e185..0000000000 --- a/router-bridge/js-src/introspection/__tests__/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../../tsconfig.test", - "include": ["**/*"], - "references": [ - { "path": "../../.." }, - ] -} diff --git a/router-bridge/js-src/plan.ts b/router-bridge/js-src/plan.ts new file mode 100644 index 0000000000..7dc85e40e7 --- /dev/null +++ b/router-bridge/js-src/plan.ts @@ -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] }; + } +} diff --git a/router-bridge/js-src/runtime.js b/router-bridge/js-src/runtime.js index 5b9a341d8f..0c5b7da5af 100644 --- a/router-bridge/js-src/runtime.js +++ b/router-bridge/js-src/runtime.js @@ -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 @@ -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 = {}; diff --git a/router-bridge/js-src/types.ts b/router-bridge/js-src/types.ts new file mode 100644 index 0000000000..9b2bc3b77d --- /dev/null +++ b/router-bridge/js-src/types.ts @@ -0,0 +1,3 @@ +export type OperationResult = + | { Ok: any; Err?: undefined } + | { Ok?: undefined; Err: any }; diff --git a/router-bridge/js-src/url_polyfill.ts b/router-bridge/js-src/url_polyfill.ts index 8e6a87812b..e671cd0d46 100644 --- a/router-bridge/js-src/url_polyfill.ts +++ b/router-bridge/js-src/url_polyfill.ts @@ -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; diff --git a/router-bridge/package.json b/router-bridge/package.json index cce96ac8ea..d5a1987d6d 100644 --- a/router-bridge/package.json +++ b/router-bridge/package.json @@ -14,10 +14,9 @@ "author": "Apollo ", "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" } } diff --git a/router-bridge/src/introspect.rs b/router-bridge/src/introspect.rs index 308b968638..9ddba8e545 100644 --- a/router-bridge/src/introspect.rs +++ b/router-bridge/src/introspect.rs @@ -87,7 +87,7 @@ pub type IntrospectionResult = Result, 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) -> IntrospectionResult { +pub fn batch_introspect(sdl: &str, queries: Vec) -> IntrospectionResult { Js::new() .with_parameter("sdl", sdl) .with_parameter("queries", queries) @@ -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()); } @@ -129,8 +126,7 @@ mod tests { let response = batch_introspect( "schema { query: Query - }" - .to_string(), + }", vec![DEFAULT_INTROSPECTION_QUERY.to_string()], ) .unwrap(); @@ -147,8 +143,7 @@ mod tests { let response = batch_introspect( "schema { query: Query - }" - .to_string(), + }", vec![DEFAULT_INTROSPECTION_QUERY.to_string()], ) .unwrap(); diff --git a/router-bridge/src/lib.rs b/router-bridge/src/lib.rs index af01447fd0..23bc31826e 100644 --- a/router-bridge/src/lib.rs +++ b/router-bridge/src/lib.rs @@ -7,3 +7,4 @@ #![warn(missing_docs, future_incompatible, unreachable_pub, rust_2018_idioms)] pub mod introspect; mod js; +pub mod plan; diff --git a/router-bridge/src/plan.rs b/router-bridge/src/plan.rs new file mode 100644 index 0000000000..e470c55193 --- /dev/null +++ b/router-bridge/src/plan.rs @@ -0,0 +1,186 @@ +/*! +# Create a query plan +*/ + +use crate::js::Js; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use thiserror::Error; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +/// Options for the query plan +pub struct QueryPlanOptions { + /// Use auto fragmentation + pub auto_fragmentization: bool, +} + +/// Default options for query planning +impl QueryPlanOptions { + /// Default query plan options + pub fn default() -> QueryPlanOptions { + QueryPlanOptions { + auto_fragmentization: false, + } + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +/// This is the context which provides +/// all the information to plan a query against a schema +pub struct OperationalContext { + /// The graphQL schema + pub schema: String, + /// The graphQL query + pub query: String, + /// The operation name + pub operation_name: String, +} + +#[derive(Debug, Error, Serialize, Deserialize, PartialEq)] +/// Container for planning errors +pub struct PlanningErrors { + /// The contained errors + pub errors: Vec, +} + +impl Display for PlanningErrors { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "Planning errors: {}", + self.errors + .iter() + .map(|e| e.to_string()) + .collect::>() + .join(", ") + )) + } +} + +/// An error which occurred during JavaScript planning. +/// +/// The shape of this error is meant to mimick that of the error created within +/// JavaScript. +/// +/// [`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)] +pub struct PlanningError { + /// A human-readable description of the error that prevented planning. + pub message: Option, + /// [`PlanningErrorExtensions`] + pub extensions: Option, +} + +impl Display for PlanningError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(msg) = &self.message { + f.write_fmt(format_args!("{code}: {msg}", code = self.code(), msg = msg)) + } else { + f.write_str(self.code()) + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +/// Error codes +pub struct PlanningErrorExtensions { + /// The error code + pub code: String, +} + +/// An error that was received during planning within JavaScript. +impl PlanningError { + /// Retrieve the error code from an error received during planning. + pub fn code(&self) -> &str { + match self.extensions { + Some(ref ext) => &*ext.code, + None => "UNKNOWN", + } + } +} + +/// Create the query plan by calling in to JS. +/// +/// We use a generic here because the output type `QueryPlan` is part of the router. +/// Since this bridge is temporary we don't to declare the `QueryPlan` structure in this crate. +/// We will instead let the caller define what structure the plan result should be deserialized into. +pub fn plan( + context: OperationalContext, + options: QueryPlanOptions, +) -> Result { + Js::new() + .with_parameter("schemaString", context.schema) + .with_parameter("queryString", context.query) + .with_parameter("options", options) + .with_parameter("operationName", context.operation_name) + .execute("do_plan", include_str!("../js-dist/do_plan.js")) + .map_err(|errors| PlanningErrors { errors }) +} + +#[cfg(test)] +mod tests { + use super::*; + + const SCHEMA: &str = include_str!("testdata/schema.graphql"); + const QUERY: &str = include_str!("testdata/query.graphql"); + + #[test] + fn it_works() { + insta::assert_snapshot!(serde_json::to_string_pretty( + &plan::( + OperationalContext { + schema: SCHEMA.to_string(), + query: QUERY.to_string(), + operation_name: "".to_string() + }, + QueryPlanOptions::default() + ) + .unwrap() + ) + .unwrap()); + } + + #[test] + fn invalid_schema_is_caught() { + let result = Err::(PlanningErrors { + errors: vec![PlanningError { + message: Some("Syntax Error: Unexpected Name \"Garbage\".".to_string()), + extensions: None, + }], + }); + assert_eq!( + result, + plan( + OperationalContext { + schema: "Garbage".to_string(), + query: QUERY.to_string(), + operation_name: "".to_string(), + }, + QueryPlanOptions::default(), + ) + ); + } + + #[test] + fn invalid_query_is_caught() { + let result = Err::(PlanningErrors { + errors: vec![PlanningError { + message: Some("Syntax Error: Unexpected Name \"Garbage\".".to_string()), + extensions: None, + }], + }); + assert_eq!( + result, + plan( + OperationalContext { + schema: SCHEMA.to_string(), + query: "Garbage".to_string(), + operation_name: "".to_string(), + }, + QueryPlanOptions::default(), + ) + ); + } +} diff --git a/router-bridge/src/snapshots/router_bridge__plan__tests__it_works.snap b/router-bridge/src/snapshots/router_bridge__plan__tests__it_works.snap new file mode 100644 index 0000000000..d14a68a95c --- /dev/null +++ b/router-bridge/src/snapshots/router_bridge__plan__tests__it_works.snap @@ -0,0 +1,14 @@ +--- +source: router-bridge/src/plan.rs +expression: "serde_json::to_string_pretty(&plan::(OperationalContext{schema:\n SCHEMA.to_string(),\n query:\n QUERY.to_string(),\n operation:\n \"\".to_string(),},\n QueryPlanOptions::default()).unwrap()).unwrap()" + +--- +{ + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [], + "operation": "{me{name{first last}}}" + } +} diff --git a/router-bridge/src/testdata/query.graphql b/router-bridge/src/testdata/query.graphql new file mode 100644 index 0000000000..3e1ed38033 --- /dev/null +++ b/router-bridge/src/testdata/query.graphql @@ -0,0 +1,8 @@ +query { + me { + name { + first + last + } + } +} diff --git a/router-bridge/src/testdata/schema.graphql b/router-bridge/src/testdata/schema.graphql new file mode 100644 index 0000000000..fdae5cec6c --- /dev/null +++ b/router-bridge/src/testdata/schema.graphql @@ -0,0 +1,275 @@ +schema +@core(feature: "https://specs.apollo.dev/core/v0.1") +@core(feature: "https://specs.apollo.dev/join/v0.1") { + query: Query + mutation: Mutation +} + +directive @core(feature: String!) repeatable on SCHEMA + +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet +) on FIELD_DEFINITION + +directive @join__type( + graph: join__Graph! + key: join__FieldSet +) repeatable on OBJECT | INTERFACE + +directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @stream on FIELD + +directive @transform(from: String!) on FIELD + +union AccountType = PasswordAccount | SMSAccount + +type Amazon { + referrer: String +} + +union Body = Image | Text + +type Book implements Product +@join__owner(graph: BOOKS) +@join__type(graph: BOOKS, key: "isbn") +@join__type(graph: INVENTORY, key: "isbn") +@join__type(graph: PRODUCT, key: "isbn") +@join__type(graph: REVIEWS, key: "isbn") { + isbn: String! @join__field(graph: BOOKS) + title: String @join__field(graph: BOOKS) + year: Int @join__field(graph: BOOKS) + similarBooks: [Book]! @join__field(graph: BOOKS) + metadata: [MetadataOrError] @join__field(graph: BOOKS) + inStock: Boolean @join__field(graph: INVENTORY) + isCheckedOut: Boolean @join__field(graph: INVENTORY) + upc: String! @join__field(graph: PRODUCT) + sku: String! @join__field(graph: PRODUCT) + name(delimeter: String = " "): String + @join__field(graph: PRODUCT, requires: "title year") + price: String @join__field(graph: PRODUCT) + details: ProductDetailsBook @join__field(graph: PRODUCT) + reviews: [Review] @join__field(graph: REVIEWS) + relatedReviews: [Review!]! + @join__field(graph: REVIEWS, requires: "similarBooks{isbn}") +} + +union Brand = Ikea | Amazon + +type Car implements Vehicle +@join__owner(graph: PRODUCT) +@join__type(graph: PRODUCT, key: "id") +@join__type(graph: REVIEWS, key: "id") { + id: String! @join__field(graph: PRODUCT) + description: String @join__field(graph: PRODUCT) + price: String @join__field(graph: PRODUCT) + retailPrice: String @join__field(graph: REVIEWS, requires: "price") +} + +type Error { + code: Int + message: String +} + +type Furniture implements Product +@join__owner(graph: PRODUCT) +@join__type(graph: PRODUCT, key: "upc") +@join__type(graph: PRODUCT, key: "sku") +@join__type(graph: INVENTORY, key: "sku") +@join__type(graph: REVIEWS, key: "upc") { + upc: String! @join__field(graph: PRODUCT) + sku: String! @join__field(graph: PRODUCT) + name: String @join__field(graph: PRODUCT) + price: String @join__field(graph: PRODUCT) + brand: Brand @join__field(graph: PRODUCT) + metadata: [MetadataOrError] @join__field(graph: PRODUCT) + details: ProductDetailsFurniture @join__field(graph: PRODUCT) + inStock: Boolean @join__field(graph: INVENTORY) + isHeavy: Boolean @join__field(graph: INVENTORY) + reviews: [Review] @join__field(graph: REVIEWS) +} + +type Ikea { + asile: Int +} + +type Image implements NamedObject { + name: String! + attributes: ImageAttributes! +} + +type ImageAttributes { + url: String! +} + +scalar join__FieldSet + +enum join__Graph { + ACCOUNTS @join__graph(name: "accounts", url: "") + BOOKS @join__graph(name: "books", url: "") + DOCUMENTS @join__graph(name: "documents", url: "") + INVENTORY @join__graph(name: "inventory", url: "") + PRODUCT @join__graph(name: "product", url: "") + REVIEWS @join__graph(name: "reviews", url: "") +} + +type KeyValue { + key: String! + value: String! +} + +type Library +@join__owner(graph: BOOKS) +@join__type(graph: BOOKS, key: "id") +@join__type(graph: ACCOUNTS, key: "id") { + id: ID! @join__field(graph: BOOKS) + name: String @join__field(graph: BOOKS) + userAccount(id: ID! = 1): User @join__field(graph: ACCOUNTS, requires: "name") +} + +union MetadataOrError = KeyValue | Error + +type Mutation { + login(username: String!, password: String!): User + @join__field(graph: ACCOUNTS) + reviewProduct(upc: String!, body: String!): Product + @join__field(graph: REVIEWS) + updateReview(review: UpdateReviewInput!): Review @join__field(graph: REVIEWS) + deleteReview(id: ID!): Boolean @join__field(graph: REVIEWS) +} + +type Name { + first: String + last: String +} + +interface NamedObject { + name: String! +} + +type PasswordAccount +@join__owner(graph: ACCOUNTS) +@join__type(graph: ACCOUNTS, key: "email") { + email: String! @join__field(graph: ACCOUNTS) +} + +interface Product { + upc: String! + sku: String! + name: String + price: String + details: ProductDetails + inStock: Boolean + reviews: [Review] +} + +interface ProductDetails { + country: String +} + +type ProductDetailsBook implements ProductDetails { + country: String + pages: Int +} + +type ProductDetailsFurniture implements ProductDetails { + country: String + color: String +} + +type Query { + user(id: ID!): User @join__field(graph: ACCOUNTS) + me: User @join__field(graph: ACCOUNTS) + book(isbn: String!): Book @join__field(graph: BOOKS) + books: [Book] @join__field(graph: BOOKS) + library(id: ID!): Library @join__field(graph: BOOKS) + body: Body! @join__field(graph: DOCUMENTS) + product(upc: String!): Product @join__field(graph: PRODUCT) + vehicle(id: String!): Vehicle @join__field(graph: PRODUCT) + topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCT) + topCars(first: Int = 5): [Car] @join__field(graph: PRODUCT) + topReviews(first: Int = 5): [Review] @join__field(graph: REVIEWS) +} + +type Review +@join__owner(graph: REVIEWS) +@join__type(graph: REVIEWS, key: "id") { + id: ID! @join__field(graph: REVIEWS) + body(format: Boolean = false): String @join__field(graph: REVIEWS) + author: User @join__field(graph: REVIEWS, provides: "username") + product: Product @join__field(graph: REVIEWS) + metadata: [MetadataOrError] @join__field(graph: REVIEWS) +} + +type SMSAccount +@join__owner(graph: ACCOUNTS) +@join__type(graph: ACCOUNTS, key: "number") { + number: String @join__field(graph: ACCOUNTS) +} + +type Text implements NamedObject { + name: String! + attributes: TextAttributes! +} + +type TextAttributes { + bold: Boolean + text: String +} + +union Thing = Car | Ikea + +input UpdateReviewInput { + id: ID! + body: String +} + +type User +@join__owner(graph: ACCOUNTS) +@join__type(graph: ACCOUNTS, key: "id") +@join__type(graph: ACCOUNTS, key: "username name{first last}") +@join__type(graph: INVENTORY, key: "id") +@join__type(graph: PRODUCT, key: "id") +@join__type(graph: REVIEWS, key: "id") { + id: ID! @join__field(graph: ACCOUNTS) + name: Name @join__field(graph: ACCOUNTS) + username: String @join__field(graph: ACCOUNTS) + birthDate(locale: String): String @join__field(graph: ACCOUNTS) + account: AccountType @join__field(graph: ACCOUNTS) + metadata: [UserMetadata] @join__field(graph: ACCOUNTS) + goodDescription: Boolean + @join__field(graph: INVENTORY, requires: "metadata{description}") + vehicle: Vehicle @join__field(graph: PRODUCT) + thing: Thing @join__field(graph: PRODUCT) + reviews: [Review] @join__field(graph: REVIEWS) + numberOfReviews: Int! @join__field(graph: REVIEWS) + goodAddress: Boolean + @join__field(graph: REVIEWS, requires: "metadata{address}") +} + +type UserMetadata { + name: String + address: String + description: String +} + +type Van implements Vehicle +@join__owner(graph: PRODUCT) +@join__type(graph: PRODUCT, key: "id") +@join__type(graph: REVIEWS, key: "id") { + id: String! @join__field(graph: PRODUCT) + description: String @join__field(graph: PRODUCT) + price: String @join__field(graph: PRODUCT) + retailPrice: String @join__field(graph: REVIEWS, requires: "price") +} + +interface Vehicle { + id: String! + description: String + price: String + retailPrice: String +} diff --git a/router-bridge/tsconfig.test.json b/router-bridge/tsconfig.test.json deleted file mode 100644 index 189bd93707..0000000000 --- a/router-bridge/tsconfig.test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../tsconfig.test.base", - "include": ["**/__tests__/**/*"], - "references": [ - { "path": "./" }, - ] -} diff --git a/tsconfig.build-stage-01.json b/tsconfig.build-stage-01.json index 04ee860255..7cb3ddff86 100644 --- a/tsconfig.build-stage-01.json +++ b/tsconfig.build-stage-01.json @@ -5,8 +5,8 @@ "files": [], "include": [], "references": [ - { "path": "./router-bridge" }, { "path": "./federation-js" }, { "path": "./query-planner-js" }, + { "path": "./router-bridge" }, ] } diff --git a/tsconfig.test.json b/tsconfig.test.json index ef3b217eaf..3d8e76dc51 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -5,7 +5,6 @@ { "path": "./federation-js/tsconfig.test.json" }, { "path": "./gateway-js/tsconfig.test.json" }, { "path": "./query-planner-js/tsconfig.test.json" }, - { "path": "./router-bridge/tsconfig.test.json" }, { "path": "./subgraph-js/tsconfig.test.json" }, ] }