diff --git a/dev.js b/dev.js index 375124708..5bcde96a5 100644 --- a/dev.js +++ b/dev.js @@ -16,3 +16,4 @@ require("ts-node/register/transpile-only"); // require("./examples/typeorm-lazy-relations/index.ts"); // require("./examples/using-container/index.ts"); // require("./examples/using-scoped-container/index.ts"); +// require("./examples/apollo-federation/index.ts"); diff --git a/examples/apollo-federation/accounts/index.ts b/examples/apollo-federation/accounts/index.ts new file mode 100644 index 000000000..6d9722623 --- /dev/null +++ b/examples/apollo-federation/accounts/index.ts @@ -0,0 +1,87 @@ +import { + ObjectType, + Field, + Resolver, + Query, + buildSchema, + ID, + FieldResolver, + Root, + Directive, +} from "../../../src"; +import federationDirectives from "@apollo/federation/dist/directives"; +import { buildFederatedSchema } from "@apollo/federation"; +import { specifiedDirectives } from "graphql"; +import { ApolloServer } from "apollo-server"; +import { plainToClass } from "class-transformer"; +import { getMetadataStorage } from "../../../src/metadata/getMetadataStorage"; + +const buildTypeSchema = async () => { + getMetadataStorage().clear(); + + @ObjectType() + @Directive("key", { fields: "id" }) + class User { + @Field(() => ID) + id: string; + + @Field() + username: string; + + @Field() + name: string; + + @Field() + birthDate: string; + } + + const users: User[] = plainToClass(User, [ + { + id: "1", + name: "Ada Lovelace", + birthDate: "1815-12-10", + username: "@ada", + }, + { + id: "2", + name: "Alan Turing", + birthDate: "1912-06-23", + username: "@complete", + }, + ]); + + @Resolver(() => User) + class AccountsResolver { + @Query(() => User) + me(): User { + return users[0]; + } + + @FieldResolver(() => User, { nullable: true }) + async __resolveReference(@Root() reference: Partial): Promise { + return users.find(u => u.id === reference.id); + } + } + + return await buildSchema({ + resolvers: [AccountsResolver], + directives: [...specifiedDirectives, ...federationDirectives], + skipCheck: true, + }); +}; + +export async function listen(port: number): Promise { + const schema = buildFederatedSchema(await buildTypeSchema()); + + const server = new ApolloServer({ + schema, + tracing: false, + playground: true, + }); + + const { url } = await server.listen({ port }); + + console.log(`🚀 Accounts service ready at ${url}`); + + return url; +} diff --git a/examples/apollo-federation/index.ts b/examples/apollo-federation/index.ts new file mode 100644 index 000000000..7eb956e64 --- /dev/null +++ b/examples/apollo-federation/index.ts @@ -0,0 +1,35 @@ +import "reflect-metadata"; +import { ApolloGateway } from "@apollo/gateway"; +import { ApolloServer } from "apollo-server"; +import * as accounts from "./accounts"; +import * as reviews from "./reviews"; +import * as products from "./products"; +import * as inventory from "./inventory"; + +async function bootstrap() { + const serviceList = [ + { name: "accounts", url: await accounts.listen(3001) }, + { name: "reviews", url: await reviews.listen(3002) }, + { name: "products", url: await products.listen(3003) }, + { name: "inventory", url: await inventory.listen(3004) }, + ]; + + const gateway = new ApolloGateway({ + serviceList, + }); + + const { schema, executor } = await gateway.load(); + + const server = new ApolloServer({ + schema, + executor, + tracing: false, + playground: true, + }); + + server.listen({ port: 3000 }).then(({ url }) => { + console.log(`🚀 Apollo Gateway ready at ${url}`); + }); +} + +bootstrap(); diff --git a/examples/apollo-federation/inventory/index.ts b/examples/apollo-federation/inventory/index.ts new file mode 100644 index 000000000..c9796ab8a --- /dev/null +++ b/examples/apollo-federation/inventory/index.ts @@ -0,0 +1,101 @@ +import federationDirectives from "@apollo/federation/dist/directives"; +import { buildFederatedSchema } from "@apollo/federation"; +import { specifiedDirectives } from "graphql"; +import { ApolloServer } from "apollo-server"; +import { plainToClass } from "class-transformer"; +import { + ObjectType, + Field, + Resolver, + buildSchema, + FieldResolver, + Root, + Directive, +} from "../../../src"; +import { getMetadataStorage } from "../../../src/metadata/getMetadataStorage"; + +const buildTypeSchema = async () => { + getMetadataStorage().clear(); + + @ObjectType() + @Directive("extends") + @Directive("key", { fields: "upc" }) + class Product { + @Field() + @Directive("external") + upc: string; + + @Field() + @Directive("external") + weight: number; + + @Field() + @Directive("external") + price: number; + + @Field() + inStock: boolean; + } + + interface Inventory { + upc: string; + inStock: boolean; + } + + const inventory: Inventory[] = [ + { upc: "1", inStock: true }, + { upc: "2", inStock: false }, + { upc: "3", inStock: true }, + ]; + + @Resolver(() => Product) + class InventoryResolver { + @FieldResolver(() => Number) + @Directive("requires", { fields: "price weight" }) + async shippingEstimate(@Root() product: Product): Promise { + // free for expensive items + if (product.price > 1000) { + return 0; + } + + // estimate is based on weight + return product.weight * 0.5; + } + + @FieldResolver(() => Product, { nullable: true }) + async __resolveReference(@Root() reference: Partial): Promise { + const found = inventory.find(i => i.upc === reference.upc); + + if (!found) { + return; + } + + return plainToClass(Product, { + ...reference, + ...found, + }); + } + } + + return await buildSchema({ + resolvers: [InventoryResolver], + directives: [...specifiedDirectives, ...federationDirectives], + skipCheck: true, + }); +}; + +export async function listen(port: number): Promise { + const schema = buildFederatedSchema(await buildTypeSchema()); + + const server = new ApolloServer({ + schema, + tracing: false, + playground: true, + }); + + const { url } = await server.listen({ port }); + + console.log(`🚀 Inventory service ready at ${url}`); + + return url; +} diff --git a/examples/apollo-federation/products/index.ts b/examples/apollo-federation/products/index.ts new file mode 100644 index 000000000..6e8c950c9 --- /dev/null +++ b/examples/apollo-federation/products/index.ts @@ -0,0 +1,93 @@ +import federationDirectives from "@apollo/federation/dist/directives"; +import { buildFederatedSchema } from "@apollo/federation"; +import { specifiedDirectives } from "graphql"; +import { ApolloServer } from "apollo-server"; +import { plainToClass } from "class-transformer"; +import { + ObjectType, + Field, + Resolver, + Query, + buildSchema, + FieldResolver, + Arg, + Root, + Directive, +} from "../../../src"; +import { getMetadataStorage } from "../../../src/metadata/getMetadataStorage"; + +const buildTypeSchema = async () => { + getMetadataStorage().clear(); + + @ObjectType() + @Directive("key", { fields: "upc" }) + class Product { + @Field() + upc: string; + + @Field() + name: string; + + @Field() + price: number; + + @Field() + weight: number; + } + + const products: Product[] = plainToClass(Product, [ + { + upc: "1", + name: "Table", + price: 899, + weight: 100, + }, + { + upc: "2", + name: "Couch", + price: 1299, + weight: 1000, + }, + { + upc: "3", + name: "Chair", + price: 54, + weight: 50, + }, + ]); + + @Resolver(() => Product) + class ProductsResolver { + @Query(() => [Product]) + async topProducts(@Arg("first", { defaultValue: 5 }) first: number): Promise { + return products.slice(0, first); + } + + @FieldResolver(() => Product, { nullable: true }) + async __resolveReference(@Root() reference: Partial): Promise { + return products.find(p => p.upc === reference.upc); + } + } + + return await buildSchema({ + resolvers: [ProductsResolver], + directives: [...specifiedDirectives, ...federationDirectives], + skipCheck: true, + }); +}; + +export async function listen(port: number): Promise { + const schema = buildFederatedSchema(await buildTypeSchema()); + + const server = new ApolloServer({ + schema, + tracing: false, + playground: true, + }); + + const { url } = await server.listen({ port }); + + console.log(`🚀 Products service ready at ${url}`); + + return url; +} diff --git a/examples/apollo-federation/reviews/index.ts b/examples/apollo-federation/reviews/index.ts new file mode 100644 index 000000000..493c4ee31 --- /dev/null +++ b/examples/apollo-federation/reviews/index.ts @@ -0,0 +1,134 @@ +import federationDirectives from "@apollo/federation/dist/directives"; +import { buildFederatedSchema } from "@apollo/federation"; +import { specifiedDirectives } from "graphql"; +import { ApolloServer } from "apollo-server"; +import { + ObjectType, + Field, + Resolver, + buildSchema, + ID, + FieldResolver, + Root, + Directive, +} from "../../../src"; +import { getMetadataStorage } from "../../../src/metadata/getMetadataStorage"; +import { Type } from "class-transformer"; + +const buildTypeSchema = async () => { + getMetadataStorage().clear(); + + @ObjectType() + @Directive("extends") + @Directive("key", { fields: "id" }) + class User { + @Field(() => ID) + @Directive("external") + id: string; + + @Field(() => String) + @Directive("external") + username: string; + } + + @ObjectType() + @Directive("extends") + @Directive("key", { fields: "upc" }) + class Product { + @Field() + @Directive("external") + upc: string; + } + + @ObjectType() + @Directive("key", { fields: "id" }) + class Review { + @Field(() => ID) + id: string; + + @Field() + body: string; + + @Type(() => User) + @Field(() => User) + @Directive("provides", { fields: "username" }) + author: User; + + @Type(() => Product) + @Field(() => Product) + product: Product; + } + + const reviews: Review[] = [ + { + id: "1", + author: { id: "1", username: "@ada" }, + product: { upc: "1" }, + body: "Love it!", + }, + { + id: "2", + author: { id: "1", username: "@ada" }, + product: { upc: "2" }, + body: "Too expensive.", + }, + { + id: "3", + author: { id: "2", username: "@complete" }, + product: { upc: "3" }, + body: "Could be better.", + }, + { + id: "4", + author: { id: "2", username: "@complete" }, + product: { upc: "1" }, + body: "Prefer something else.", + }, + ]; + + @Resolver(() => Review) + class ReviewsResolver { + @FieldResolver(() => [Review]) + async reviews(): Promise { + return reviews; + } + } + + @Resolver(() => Product) + class ProductReviewsResolver { + @FieldResolver(() => [Review]) + async reviews(@Root() product: Product): Promise { + return reviews.filter(review => review.product.upc === product.upc); + } + } + + @Resolver(() => User) + class UserReviewsResolver { + @FieldResolver(() => [Review]) + async reviews(@Root() user: User): Promise { + return reviews.filter(review => review.author.id === user.id); + } + } + + return await buildSchema({ + resolvers: [ReviewsResolver, ProductReviewsResolver, UserReviewsResolver], + directives: [...specifiedDirectives, ...federationDirectives], + skipCheck: true, + }); +}; + +export async function listen(port: number): Promise { + const schema = buildFederatedSchema(await buildTypeSchema()); + + const server = new ApolloServer({ + schema, + tracing: false, + playground: true, + }); + + const { url } = await server.listen({ port }); + + console.log(`🚀 Reviews service ready at ${url}`); + + return url; +}