From f1dd5e68a1fe489725c9029c809ba2523441291f Mon Sep 17 00:00:00 2001 From: Jordan Date: Tue, 2 Jul 2019 17:24:51 -0700 Subject: [PATCH 01/17] feat(decorators): added `Directive` decorator --- dev.js | 1 + examples/apollo-federation/accounts/index.ts | 87 +++ examples/apollo-federation/index.ts | 35 + examples/apollo-federation/inventory/index.ts | 101 +++ examples/apollo-federation/products/index.ts | 93 +++ examples/apollo-federation/reviews/index.ts | 134 ++++ package-lock.json | 644 ++++++++++++++++++ src/decorators/Directive.ts | 39 ++ src/decorators/ObjectType.ts | 1 + src/decorators/index.ts | 1 + src/interfaces/ResolverInterface.ts | 2 +- src/metadata/definitions/class-metadata.ts | 2 + .../definitions/directive-metadata.ts | 7 + src/metadata/definitions/field-metadata.ts | 2 + src/metadata/metadata-storage.ts | 61 ++ src/schema/schema-generator.ts | 64 ++ src/utils/buildSchema.ts | 3 +- src/utils/buildTypeDefsAndResolvers.ts | 1 - src/utils/emitSchemaDefinitionFile.ts | 1 - src/utils/printSchema.ts | 1 + 20 files changed, 1276 insertions(+), 4 deletions(-) create mode 100644 examples/apollo-federation/accounts/index.ts create mode 100644 examples/apollo-federation/index.ts create mode 100644 examples/apollo-federation/inventory/index.ts create mode 100644 examples/apollo-federation/products/index.ts create mode 100644 examples/apollo-federation/reviews/index.ts create mode 100644 src/decorators/Directive.ts create mode 100644 src/metadata/definitions/directive-metadata.ts create mode 100644 src/utils/printSchema.ts diff --git a/dev.js b/dev.js index b9b2d7e2e..4e6ed8aba 100644 --- a/dev.js +++ b/dev.js @@ -17,3 +17,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; +} diff --git a/package-lock.json b/package-lock.json index a19c5af18..514f130d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,56 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@apollo/federation": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@apollo/federation/-/federation-0.6.8.tgz", + "integrity": "sha512-HvGCQ4H9FqrhfQrDGCVhduwPsB7g5i1cAKzS8d8QGsWLZR1ZFSOMCe5nZI9Fatr3raZkHuqb1YLfPZM8+CNGWg==", + "dev": true, + "requires": { + "apollo-env": "^0.5.1", + "apollo-graphql": "^0.3.3", + "apollo-server-env": "2.4.0" + }, + "dependencies": { + "apollo-env": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/apollo-env/-/apollo-env-0.5.1.tgz", + "integrity": "sha512-fndST2xojgSdH02k5hxk1cbqA9Ti8RX4YzzBoAB4oIe1Puhq7+YlhXGXfXB5Y4XN0al8dLg+5nAkyjNAR2qZTw==", + "dev": true, + "requires": { + "core-js": "^3.0.1", + "node-fetch": "^2.2.0", + "sha.js": "^2.4.11" + } + }, + "apollo-graphql": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.3.3.tgz", + "integrity": "sha512-t3CO/xIDVsCG2qOvx2MEbuu4b/6LzQjcBBwiVnxclmmFyAxYCIe7rpPlnLHSq7HyOMlCWDMozjoeWfdqYSaLqQ==", + "dev": true, + "requires": { + "apollo-env": "0.5.1", + "lodash.sortby": "^4.7.0" + } + }, + "apollo-server-env": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-2.4.0.tgz", + "integrity": "sha512-7ispR68lv92viFeu5zsRUVGP+oxsVI3WeeBNniM22Cx619maBUwcYTIC3+Y3LpXILhLZCzA1FASZwusgSlyN9w==", + "dev": true, + "requires": { + "node-fetch": "^2.1.2", + "util.promisify": "^1.0.0" + } + }, + "core-js": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.1.4.tgz", + "integrity": "sha512-YNZN8lt82XIMLnLirj9MhKDFZHalwzzrL9YLt6eb0T5D0EDl4IQ90IGkua8mHbnxNrkj1d8hbdizMc0Qmg1WnQ==", + "dev": true + } + } + }, "@apollographql/apollo-tools": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@apollographql/apollo-tools/-/apollo-tools-0.4.0.tgz", @@ -820,6 +870,600 @@ "upath": "^1.1.0" } }, + "fsevents": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", + "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", + "dev": true, + "optional": true, + "requires": { + "node-pre-gyp": "^0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "minipass": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true, + "optional": true + }, + "needle": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.4.tgz", + "integrity": "sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==", + "dev": true, + "optional": true, + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz", + "integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==", + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", + "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==", + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.2.0.tgz", + "integrity": "sha512-7Mni4Z8Xkx0/oegoqlcao/JpPCPEMtUvsmB0q7mgvlMinykJLSRTYuFqoQLYgGY8biuxIeiHO+QNJKbCfljewQ==", + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true, + "optional": true + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", + "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "dev": true + } + } + }, "is-glob": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", diff --git a/src/decorators/Directive.ts b/src/decorators/Directive.ts new file mode 100644 index 000000000..ee98f2555 --- /dev/null +++ b/src/decorators/Directive.ts @@ -0,0 +1,39 @@ +import { MethodAndPropDecorator } from "./types"; +import { SymbolKeysNotSupportedError } from "../errors"; +import { getMetadataStorage } from "../metadata/getMetadataStorage"; + +export interface DirectiveArgs { + [arg: string]: any; +} + +export function Directive( + name: string, + args?: DirectiveArgs, +): MethodAndPropDecorator & ClassDecorator; +export function Directive( + name: string, + args?: DirectiveArgs, +): MethodDecorator | PropertyDecorator | ClassDecorator { + return (targetOrPrototype, propertyKey, descriptor) => { + if (!propertyKey) { + getMetadataStorage().collectDirectiveClassMetadata({ + target: targetOrPrototype as Function, + name, + args, + }); + + return; + } + + if (typeof propertyKey === "symbol") { + throw new SymbolKeysNotSupportedError(); + } + + getMetadataStorage().collectDirectiveFieldMetadata({ + target: targetOrPrototype.constructor, + field: propertyKey, + name, + args, + }); + }; +} diff --git a/src/decorators/ObjectType.ts b/src/decorators/ObjectType.ts index 082d0eb0a..e6d0c0e63 100644 --- a/src/decorators/ObjectType.ts +++ b/src/decorators/ObjectType.ts @@ -1,6 +1,7 @@ import { getMetadataStorage } from "../metadata/getMetadataStorage"; import { getNameDecoratorParams } from "../helpers/decorators"; import { DescriptionOptions, AbstractClassOptions } from "./types"; +import { ObjectClassMetadata } from "../metadata/definitions/object-class-metdata"; export type ObjectOptions = DescriptionOptions & AbstractClassOptions & { diff --git a/src/decorators/index.ts b/src/decorators/index.ts index 11dbbd9db..b6fc8caf6 100644 --- a/src/decorators/index.ts +++ b/src/decorators/index.ts @@ -5,6 +5,7 @@ export { Authorized } from "./Authorized"; export { createParamDecorator } from "./createParamDecorator"; export { createMethodDecorator } from "./createMethodDecorator"; export { Ctx } from "./Ctx"; +export { Directive } from "./Directive"; export { registerEnumType } from "./enums"; export { Field } from "./Field"; export { FieldResolver } from "./FieldResolver"; diff --git a/src/interfaces/ResolverInterface.ts b/src/interfaces/ResolverInterface.ts index 7fa2d80f2..966b159f2 100644 --- a/src/interfaces/ResolverInterface.ts +++ b/src/interfaces/ResolverInterface.ts @@ -3,5 +3,5 @@ * to provide a proper resolver method signatures for fields of T. */ export type ResolverInterface = { - [P in keyof T]?: (root: T, ...args: any[]) => T[P] | Promise + [P in keyof T]?: (root: T, ...args: any[]) => T[P] | Promise; }; diff --git a/src/metadata/definitions/class-metadata.ts b/src/metadata/definitions/class-metadata.ts index 067831218..853347d98 100644 --- a/src/metadata/definitions/class-metadata.ts +++ b/src/metadata/definitions/class-metadata.ts @@ -1,4 +1,5 @@ import { FieldMetadata } from "./field-metadata"; +import { DirectiveClassMetadata } from "./directive-metadata"; export interface ClassMetadata { name: string; @@ -6,4 +7,5 @@ export interface ClassMetadata { fields?: FieldMetadata[]; description?: string; isAbstract?: boolean; + directives?: DirectiveClassMetadata[]; } diff --git a/src/metadata/definitions/directive-metadata.ts b/src/metadata/definitions/directive-metadata.ts new file mode 100644 index 000000000..aff619aaf --- /dev/null +++ b/src/metadata/definitions/directive-metadata.ts @@ -0,0 +1,7 @@ +export interface DirectiveMetadata { + name: string; + args?: { [key: string]: any }; +} + +export type DirectiveClassMetadata = DirectiveMetadata & { target: Function }; +export type DirectiveFieldMetadata = DirectiveMetadata & { target: Function; field: string }; diff --git a/src/metadata/definitions/field-metadata.ts b/src/metadata/definitions/field-metadata.ts index 7d3560f85..21b546ad9 100644 --- a/src/metadata/definitions/field-metadata.ts +++ b/src/metadata/definitions/field-metadata.ts @@ -2,6 +2,7 @@ import { ParamMetadata } from "./param-metadata"; import { TypeValueThunk, TypeOptions } from "../../decorators/types"; import { Middleware } from "../../interfaces/Middleware"; import { Complexity } from "../../interfaces"; +import { DirectiveFieldMetadata } from "./directive-metadata"; export interface FieldMetadata { target: Function; @@ -15,4 +16,5 @@ export interface FieldMetadata { params?: ParamMetadata[]; roles?: any[]; middlewares?: Array>; + directives?: DirectiveFieldMetadata[]; } diff --git a/src/metadata/metadata-storage.ts b/src/metadata/metadata-storage.ts index 8a5adc3fe..6ee7cbbf6 100644 --- a/src/metadata/metadata-storage.ts +++ b/src/metadata/metadata-storage.ts @@ -23,6 +23,7 @@ import { } from "./utils"; import { ObjectClassMetadata } from "./definitions/object-class-metdata"; import { InterfaceClassMetadata } from "./definitions/interface-class-metadata"; +import { DirectiveClassMetadata, DirectiveFieldMetadata } from "./definitions/directive-metadata"; export class MetadataStorage { queries: ResolverMetadata[] = []; @@ -37,6 +38,8 @@ export class MetadataStorage { enums: EnumMetadata[] = []; unions: UnionMetadataWithSymbol[] = []; middlewares: MiddlewareMetadata[] = []; + classDirectives: DirectiveClassMetadata[] = []; + fieldDirectives: DirectiveFieldMetadata[] = []; private resolverClasses: ResolverClassMetadata[] = []; private fields: FieldMetadata[] = []; @@ -98,6 +101,13 @@ export class MetadataStorage { this.params.push(definition); } + collectDirectiveClassMetadata(definition: DirectiveClassMetadata) { + this.classDirectives.push(definition); + } + collectDirectiveFieldMetadata(definition: DirectiveFieldMetadata) { + this.fieldDirectives.push(definition); + } + build() { // TODO: disable next build attempts @@ -113,6 +123,8 @@ export class MetadataStorage { this.buildResolversMetadata(this.subscriptions); this.buildExtendedResolversMetadata(); + + this.buildDirectiveMetadata(); } clear() { @@ -128,6 +140,8 @@ export class MetadataStorage { this.enums = []; this.unions = []; this.middlewares = []; + this.classDirectives = []; + this.fieldDirectives = []; this.resolverClasses = []; this.fields = []; @@ -244,6 +258,49 @@ export class MetadataStorage { }); } + private buildDirectiveMetadata() { + this.classDirectives.forEach(def => { + const objectType = this.objectTypes.find(it => it.target === def.target); + + if (objectType) { + objectType.directives = this.findClassDirectives(def.target); + } + }); + + const addFieldDirective = ( + directiveDef: DirectiveFieldMetadata, + classDefs: ObjectClassMetadata, + ) => { + (classDefs.fields || []).forEach(fieldDef => { + if (!fieldDef.directives) { + fieldDef.directives = []; + } + + if (fieldDef.name === directiveDef.field) { + fieldDef.directives.push(directiveDef); + } + }); + }; + + this.fieldDirectives.forEach(directiveDef => { + let objectType = this.objectTypes.find(it => it.target === directiveDef.target); + + // try to get directives from resolver classes + if (!objectType) { + const resolverCls = this.resolverClasses.find( + resolver => resolver.target === directiveDef.target, + ); + if (resolverCls) { + objectType = this.objectTypes.find(it => it.target === resolverCls.getObjectType()); + } + } + + if (objectType) { + addFieldDirective(directiveDef, objectType); + } + }); + } + private findFieldRoles(target: Function, fieldName: string): any[] | undefined { const authorizedField = this.authorizedFields.find( authField => authField.target === target && authField.fieldName === fieldName, @@ -253,4 +310,8 @@ export class MetadataStorage { } return authorizedField.roles; } + + private findClassDirectives(target: ClassMetadata["target"]): DirectiveClassMetadata[] { + return this.classDirectives.filter(it => it.target === target); + } } diff --git a/src/schema/schema-generator.ts b/src/schema/schema-generator.ts index 473bcbf85..2b023af75 100644 --- a/src/schema/schema-generator.ts +++ b/src/schema/schema-generator.ts @@ -15,6 +15,11 @@ import { GraphQLEnumValueConfigMap, GraphQLUnionType, GraphQLTypeResolver, + GraphQLDirective, + DirectiveNode, + ObjectTypeDefinitionNode, + FieldDefinitionNode, + ArgumentNode, } from "graphql"; import { withFilter, ResolverFn } from "graphql-subscriptions"; @@ -43,6 +48,10 @@ import { import { ResolverFilterData, ResolverTopicData, TypeResolver } from "../interfaces"; import { getFieldMetadataFromInputType, getFieldMetadataFromObjectType } from "./utils"; import { ensureInstalledCorrectGraphQLPackage } from "../utils/graphql-version"; +import { + DirectiveClassMetadata, + DirectiveFieldMetadata, +} from "../metadata/definitions/directive-metadata"; interface AbstractInfo { isAbstract: boolean; @@ -81,6 +90,7 @@ export interface SchemaGeneratorOptions extends BuildContextOptions { * Disable checking on build the correctness of a schema */ skipCheck?: boolean; + directives?: GraphQLDirective[]; } export abstract class SchemaGenerator { @@ -262,12 +272,66 @@ export abstract class SchemaGenerator { return superClassTypeInfo ? superClassTypeInfo.type : undefined; }; const interfaceClasses = objectType.interfaceClasses || []; + + const classDirectiveAstNodes = ( + name: string, + classMetas?: DirectiveClassMetadata[], + ): ObjectTypeDefinitionNode | undefined => { + if (!classMetas || !classMetas.length) { + return; + } + + const directives: DirectiveNode[] = classMetas.map(meta => + this.createDirective(meta.name, meta.args), + ); + + return { + kind: "ObjectTypeDefinition", + name: { + kind: "Name", + value: name, + }, + interfaces: [], + directives, + }; + }; + + const fieldDirectiveAstNodes = ( + name: string, + fieldMetas?: DirectiveFieldMetadata[], + ): FieldDefinitionNode | undefined => { + if (!fieldMetas || !fieldMetas.length) { + return; + } + + const directives: DirectiveNode[] = fieldMetas.map(meta => + this.createDirective(meta.name, meta.args), + ); + + return { + kind: "FieldDefinition", + type: { + kind: "NamedType", + name: { + kind: "Name", + value: name, + }, + }, + name: { + kind: "Name", + value: name, + }, + directives, + }; + }; + return { target: objectType.target, isAbstract: objectType.isAbstract || false, type: new GraphQLObjectType({ name: objectType.name, description: objectType.description, + astNode: classDirectiveAstNodes(objectType.name, objectType.directives), interfaces: () => { let interfaces = interfaceClasses.map( interfaceClass => diff --git a/src/utils/buildSchema.ts b/src/utils/buildSchema.ts index 00bcf5c05..d5dab503c 100644 --- a/src/utils/buildSchema.ts +++ b/src/utils/buildSchema.ts @@ -1,4 +1,4 @@ -import { GraphQLSchema } from "graphql"; +import { GraphQLSchema, GraphQLDirective } from "graphql"; import { Options as PrintSchemaOptions } from "graphql/utilities/schemaPrinter"; import * as path from "path"; @@ -24,6 +24,7 @@ export interface BuildSchemaOptions extends Omit { const resolvers = loadResolvers(options); diff --git a/src/utils/buildTypeDefsAndResolvers.ts b/src/utils/buildTypeDefsAndResolvers.ts index abafb39df..2aab530b0 100644 --- a/src/utils/buildTypeDefsAndResolvers.ts +++ b/src/utils/buildTypeDefsAndResolvers.ts @@ -1,5 +1,4 @@ import { printSchema } from "graphql"; - import { BuildSchemaOptions, buildSchema } from "./buildSchema"; import { createResolversMap } from "./createResolversMap"; diff --git a/src/utils/emitSchemaDefinitionFile.ts b/src/utils/emitSchemaDefinitionFile.ts index 3c9da5924..e04e6d92e 100644 --- a/src/utils/emitSchemaDefinitionFile.ts +++ b/src/utils/emitSchemaDefinitionFile.ts @@ -10,7 +10,6 @@ const generatedSchemaWarning = /* graphql */ `\ # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- - `; export function emitSchemaDefinitionFileSync( diff --git a/src/utils/printSchema.ts b/src/utils/printSchema.ts new file mode 100644 index 000000000..d00718f9e --- /dev/null +++ b/src/utils/printSchema.ts @@ -0,0 +1 @@ +export const defaultPrintSchemaOptions: PrintSchemaOptions = { commentDescriptions: false }; From 195a3dda6c431c59f2bf23b34478e9dd59d75e86 Mon Sep 17 00:00:00 2001 From: Jordan Date: Wed, 17 Jul 2019 17:29:37 -0700 Subject: [PATCH 02/17] feat(decorators): allow SDL on Directive decorator; add tests --- src/decorators/Directive.ts | 16 +- src/errors/InvalidDirectiveError.ts | 9 + src/metadata/definitions/class-metadata.ts | 4 +- .../definitions/directive-metadata.ts | 15 +- src/metadata/definitions/field-metadata.ts | 4 +- src/metadata/definitions/resolver-metadata.ts | 2 + src/metadata/metadata-storage.ts | 54 +- src/schema/schema-generator.ts | 125 +++-- tests/functional/directives.ts | 524 ++++++++++++++++++ 9 files changed, 635 insertions(+), 118 deletions(-) create mode 100644 src/errors/InvalidDirectiveError.ts create mode 100644 tests/functional/directives.ts diff --git a/src/decorators/Directive.ts b/src/decorators/Directive.ts index ee98f2555..4c20027ec 100644 --- a/src/decorators/Directive.ts +++ b/src/decorators/Directive.ts @@ -1,25 +1,28 @@ import { MethodAndPropDecorator } from "./types"; import { SymbolKeysNotSupportedError } from "../errors"; import { getMetadataStorage } from "../metadata/getMetadataStorage"; +import { DirectiveMetadata } from "../metadata/definitions/directive-metadata"; export interface DirectiveArgs { [arg: string]: any; } +export function Directive(sdl: string): MethodAndPropDecorator & ClassDecorator; export function Directive( - name: string, - args?: DirectiveArgs, + name: DirectiveMetadata["nameOrSDL"], + args?: DirectiveMetadata["args"], ): MethodAndPropDecorator & ClassDecorator; export function Directive( - name: string, + nameOrSDL: string, args?: DirectiveArgs, ): MethodDecorator | PropertyDecorator | ClassDecorator { return (targetOrPrototype, propertyKey, descriptor) => { + const directive = { nameOrSDL, args: args || {} }; + if (!propertyKey) { getMetadataStorage().collectDirectiveClassMetadata({ target: targetOrPrototype as Function, - name, - args, + directive, }); return; @@ -32,8 +35,7 @@ export function Directive( getMetadataStorage().collectDirectiveFieldMetadata({ target: targetOrPrototype.constructor, field: propertyKey, - name, - args, + directive, }); }; } diff --git a/src/errors/InvalidDirectiveError.ts b/src/errors/InvalidDirectiveError.ts new file mode 100644 index 000000000..b24111502 --- /dev/null +++ b/src/errors/InvalidDirectiveError.ts @@ -0,0 +1,9 @@ +import { DirectiveMetadata } from "../metadata/definitions/directive-metadata"; + +export class InvalidDirectiveError extends Error { + constructor(directive: DirectiveMetadata) { + super(`Invalid directive "${directive.nameOrSDL}" `); + + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/src/metadata/definitions/class-metadata.ts b/src/metadata/definitions/class-metadata.ts index 853347d98..be81b5a17 100644 --- a/src/metadata/definitions/class-metadata.ts +++ b/src/metadata/definitions/class-metadata.ts @@ -1,5 +1,5 @@ import { FieldMetadata } from "./field-metadata"; -import { DirectiveClassMetadata } from "./directive-metadata"; +import { DirectiveMetadata } from "./directive-metadata"; export interface ClassMetadata { name: string; @@ -7,5 +7,5 @@ export interface ClassMetadata { fields?: FieldMetadata[]; description?: string; isAbstract?: boolean; - directives?: DirectiveClassMetadata[]; + directives?: DirectiveMetadata[]; } diff --git a/src/metadata/definitions/directive-metadata.ts b/src/metadata/definitions/directive-metadata.ts index aff619aaf..bfafcd775 100644 --- a/src/metadata/definitions/directive-metadata.ts +++ b/src/metadata/definitions/directive-metadata.ts @@ -1,7 +1,14 @@ export interface DirectiveMetadata { - name: string; - args?: { [key: string]: any }; + nameOrSDL: string; + args: { [key: string]: string }; } -export type DirectiveClassMetadata = DirectiveMetadata & { target: Function }; -export type DirectiveFieldMetadata = DirectiveMetadata & { target: Function; field: string }; +export interface DirectiveClassMetadata { + target: Function; + directive: DirectiveMetadata; +} +export interface DirectiveFieldMetadata { + target: Function; + field: string; + directive: DirectiveMetadata; +} diff --git a/src/metadata/definitions/field-metadata.ts b/src/metadata/definitions/field-metadata.ts index 21b546ad9..6b86838c9 100644 --- a/src/metadata/definitions/field-metadata.ts +++ b/src/metadata/definitions/field-metadata.ts @@ -2,7 +2,7 @@ import { ParamMetadata } from "./param-metadata"; import { TypeValueThunk, TypeOptions } from "../../decorators/types"; import { Middleware } from "../../interfaces/Middleware"; import { Complexity } from "../../interfaces"; -import { DirectiveFieldMetadata } from "./directive-metadata"; +import { DirectiveMetadata } from "./directive-metadata"; export interface FieldMetadata { target: Function; @@ -16,5 +16,5 @@ export interface FieldMetadata { params?: ParamMetadata[]; roles?: any[]; middlewares?: Array>; - directives?: DirectiveFieldMetadata[]; + directives?: DirectiveMetadata[]; } diff --git a/src/metadata/definitions/resolver-metadata.ts b/src/metadata/definitions/resolver-metadata.ts index 04a1bc669..be349ba34 100644 --- a/src/metadata/definitions/resolver-metadata.ts +++ b/src/metadata/definitions/resolver-metadata.ts @@ -10,6 +10,7 @@ import { import { ParamMetadata } from "./param-metadata"; import { Middleware } from "../../interfaces/Middleware"; import { Complexity } from "../../interfaces"; +import { DirectiveMetadata } from "./directive-metadata"; export interface BaseResolverMetadata { methodName: string; @@ -20,6 +21,7 @@ export interface BaseResolverMetadata { params?: ParamMetadata[]; roles?: any[]; middlewares?: Array>; + directives?: DirectiveMetadata[]; } export interface ResolverMetadata extends BaseResolverMetadata { diff --git a/src/metadata/metadata-storage.ts b/src/metadata/metadata-storage.ts index 6ee7cbbf6..cb33c6634 100644 --- a/src/metadata/metadata-storage.ts +++ b/src/metadata/metadata-storage.ts @@ -123,8 +123,6 @@ export class MetadataStorage { this.buildResolversMetadata(this.subscriptions); this.buildExtendedResolversMetadata(); - - this.buildDirectiveMetadata(); } clear() { @@ -161,8 +159,14 @@ export class MetadataStorage { middleware => middleware.target === field.target && middleware.fieldName === field.name, ), ); + field.directives = this.fieldDirectives + .filter(it => it.target === field.target && it.field === field.name) + .map(it => it.directive); }); def.fields = fields; + def.directives = this.classDirectives + .filter(it => it.target === def.target) + .map(it => it.directive); }); } @@ -181,6 +185,9 @@ export class MetadataStorage { middleware => middleware.target === def.target && def.methodName === middleware.fieldName, ), ); + def.directives = this.fieldDirectives + .filter(it => it.target === def.target && it.field === def.methodName) + .map(it => it.directive); }); } @@ -258,49 +265,6 @@ export class MetadataStorage { }); } - private buildDirectiveMetadata() { - this.classDirectives.forEach(def => { - const objectType = this.objectTypes.find(it => it.target === def.target); - - if (objectType) { - objectType.directives = this.findClassDirectives(def.target); - } - }); - - const addFieldDirective = ( - directiveDef: DirectiveFieldMetadata, - classDefs: ObjectClassMetadata, - ) => { - (classDefs.fields || []).forEach(fieldDef => { - if (!fieldDef.directives) { - fieldDef.directives = []; - } - - if (fieldDef.name === directiveDef.field) { - fieldDef.directives.push(directiveDef); - } - }); - }; - - this.fieldDirectives.forEach(directiveDef => { - let objectType = this.objectTypes.find(it => it.target === directiveDef.target); - - // try to get directives from resolver classes - if (!objectType) { - const resolverCls = this.resolverClasses.find( - resolver => resolver.target === directiveDef.target, - ); - if (resolverCls) { - objectType = this.objectTypes.find(it => it.target === resolverCls.getObjectType()); - } - } - - if (objectType) { - addFieldDirective(directiveDef, objectType); - } - }); - } - private findFieldRoles(target: Function, fieldName: string): any[] | undefined { const authorizedField = this.authorizedFields.find( authField => authField.target === target && authField.fieldName === fieldName, diff --git a/src/schema/schema-generator.ts b/src/schema/schema-generator.ts index 2b023af75..ac74d3179 100644 --- a/src/schema/schema-generator.ts +++ b/src/schema/schema-generator.ts @@ -19,7 +19,11 @@ import { DirectiveNode, ObjectTypeDefinitionNode, FieldDefinitionNode, - ArgumentNode, + parse, + parseValue, + TypeDefinitionNode, + InputObjectTypeDefinitionNode, + InputValueDefinitionNode, } from "graphql"; import { withFilter, ResolverFn } from "graphql-subscriptions"; @@ -48,10 +52,8 @@ import { import { ResolverFilterData, ResolverTopicData, TypeResolver } from "../interfaces"; import { getFieldMetadataFromInputType, getFieldMetadataFromObjectType } from "./utils"; import { ensureInstalledCorrectGraphQLPackage } from "../utils/graphql-version"; -import { - DirectiveClassMetadata, - DirectiveFieldMetadata, -} from "../metadata/definitions/directive-metadata"; +import { DirectiveMetadata } from "../metadata/definitions/directive-metadata"; +import { InvalidDirectiveError } from "../errors/InvalidDirectiveError"; interface AbstractInfo { isAbstract: boolean; @@ -273,65 +275,16 @@ export abstract class SchemaGenerator { }; const interfaceClasses = objectType.interfaceClasses || []; - const classDirectiveAstNodes = ( - name: string, - classMetas?: DirectiveClassMetadata[], - ): ObjectTypeDefinitionNode | undefined => { - if (!classMetas || !classMetas.length) { - return; - } - - const directives: DirectiveNode[] = classMetas.map(meta => - this.createDirective(meta.name, meta.args), - ); - - return { - kind: "ObjectTypeDefinition", - name: { - kind: "Name", - value: name, - }, - interfaces: [], - directives, - }; - }; - - const fieldDirectiveAstNodes = ( - name: string, - fieldMetas?: DirectiveFieldMetadata[], - ): FieldDefinitionNode | undefined => { - if (!fieldMetas || !fieldMetas.length) { - return; - } - - const directives: DirectiveNode[] = fieldMetas.map(meta => - this.createDirective(meta.name, meta.args), - ); - - return { - kind: "FieldDefinition", - type: { - kind: "NamedType", - name: { - kind: "Name", - value: name, - }, - }, - name: { - kind: "Name", - value: name, - }, - directives, - }; - }; - return { target: objectType.target, isAbstract: objectType.isAbstract || false, type: new GraphQLObjectType({ name: objectType.name, description: objectType.description, - astNode: classDirectiveAstNodes(objectType.name, objectType.directives), + astNode: this.createClassDirectiveNodes( + objectType.name, + objectType.directives, + ) as ObjectTypeDefinitionNode, interfaces: () => { let interfaces = interfaceClasses.map( interfaceClass => @@ -429,6 +382,10 @@ export abstract class SchemaGenerator { description: field.description, type: this.getGraphQLInputType(field.name, field.getType(), field.typeOptions), defaultValue: field.typeOptions.defaultValue, + astNode: this.createFieldDirectiveNodes( + field.name, + field.directives, + ) as InputValueDefinitionNode, }; return fieldsMap; }, @@ -444,6 +401,10 @@ export abstract class SchemaGenerator { } return fields; }, + astNode: this.createClassDirectiveNodes( + inputType.name, + inputType.directives, + ) as InputObjectTypeDefinitionNode, }), }; }); @@ -714,4 +675,52 @@ export abstract class SchemaGenerator { .filter(it => !it.isAbstract && (!orphanedTypes || orphanedTypes.includes(it.target))) .map(it => it.type); } + + private static createDirectiveNodes(directive: DirectiveMetadata): DirectiveNode[] { + const { nameOrSDL, args } = directive; + + if (!nameOrSDL.startsWith("@")) { + return [ + { + kind: "Directive", + name: { + kind: "Name", + value: nameOrSDL, + }, + arguments: Object.keys(args).map(arg => ({ + kind: "Argument", + name: { + kind: "Name", + value: arg, + }, + value: parseValue(args[arg]), + })), + }, + ]; + } + + let directives: DirectiveNode[] = []; + + try { + const parsed = parse(`type String ${nameOrSDL}`); + + const definitions = parsed.definitions as ObjectTypeDefinitionNode[]; + + if (definitions && definitions.length > 0) { + definitions.forEach(def => { + if (def.directives && def.directives.length > 0) { + directives = [...directives, ...def.directives]; + } + }); + } + } catch (err) { + /** noop */ + } + + if (!directives) { + throw new InvalidDirectiveError(directive); + } + + return directives; + } } diff --git a/tests/functional/directives.ts b/tests/functional/directives.ts new file mode 100644 index 000000000..8af47b04d --- /dev/null +++ b/tests/functional/directives.ts @@ -0,0 +1,524 @@ +// tslint:disable:member-ordering +import "reflect-metadata"; + +import { + GraphQLSchema, + FieldDefinitionNode, + GraphQLField, + GraphQLDirective, + DirectiveLocation, + GraphQLString, + parseValue, + graphql, + GraphQLInputObjectType, + InputValueDefinitionNode, + InputObjectTypeDefinitionNode, + GraphQLInputField, + GraphQLScalarType, + GraphQLNonNull, +} from "graphql"; +import { + Field, + InputType, + Resolver, + Query, + Arg, + Directive, + buildSchema, + ObjectType, +} from "../../src"; +import { getMetadataStorage } from "../../src/metadata/getMetadataStorage"; +import Maybe from "graphql/tsutils/Maybe"; +import { SchemaDirectiveVisitor } from "graphql-tools"; +import { ApolloServer } from "apollo-server"; + +class UpperCaseType extends GraphQLScalarType { + constructor(type: any) { + super({ + name: "UpperCase", + parseValue: value => type.parseValue(value), + serialize: value => type.serialize(value), + parseLiteral: ast => { + const result = type.parseLiteral(ast); + + if (typeof result === "string") { + return result.toUpperCase(); + } + + return result; + }, + }); + } +} + +class UpperCaseDirective extends SchemaDirectiveVisitor { + visitFieldDefinition(field: GraphQLField) { + const resolve = field.resolve; + + field.resolve = async function(...args) { + const result = await resolve!.apply(this, args); + if (typeof result === "string") { + return result.toUpperCase(); + } + + return result; + }; + } + + visitInputObject(object: GraphQLInputObjectType) { + const fields = object.getFields(); + + Object.keys(fields).forEach(field => { + this.visitInputFieldDefinition(fields[field]); + }); + } + + visitInputFieldDefinition(field: GraphQLInputField) { + if (field.type instanceof UpperCaseType) { + /* noop */ + } else if ( + field.type instanceof GraphQLNonNull && + field.type.ofType instanceof GraphQLScalarType + ) { + field.type = new GraphQLNonNull(new UpperCaseType(field.type.ofType)); + } else if (field.type instanceof GraphQLScalarType) { + field.type = new UpperCaseType(field.type); + } else { + throw new Error(`Not a scalar type: ${field.type}`); + } + } + + static getDirectiveDeclaration(directiveName: string): GraphQLDirective { + return new GraphQLDirective({ + name: directiveName, + locations: [DirectiveLocation.FIELD_DEFINITION], + }); + } +} + +class AppendDirective extends SchemaDirectiveVisitor { + visitFieldDefinition(field: GraphQLField) { + const resolve = field.resolve; + + field.args.push({ + name: "append", + type: GraphQLString, + }); + + field.resolve = async function(source, { append, ...otherArgs }, context, info) { + const result = await resolve!.call(this, source, otherArgs, context, info); + + return `${result}${append}`; + }; + } + + static getDirectiveDeclaration(directiveName: string): GraphQLDirective { + return new GraphQLDirective({ + name: directiveName, + locations: [DirectiveLocation.FIELD_DEFINITION], + }); + } +} + +const assertValidFieldDirective = ( + astNode: Maybe, + name: string, + args?: { [key: string]: string }, +): void => { + if (!astNode) { + throw new Error(`Directive with name ${name} does not exist`); + } + + const directives = (astNode || {}).directives || []; + + const directive = directives.find(it => it.name.kind === "Name" && it.name.value === name); + + if (!directive) { + throw new Error(`Directive with name ${name} does not exist`); + } + + if (!args) { + if (Array.isArray(directive.arguments)) { + expect(directive.arguments).toHaveLength(0); + } else { + expect(directive.arguments).toBeFalsy(); + } + } else { + expect(directive.arguments).toEqual( + Object.keys(args).map(arg => ({ + kind: "Argument", + name: { kind: "Name", value: arg }, + value: parseValue(args[arg]), + })), + ); + } +}; + +describe("Directives", () => { + let schema: GraphQLSchema; + + describe("Schema", () => { + beforeAll(async () => { + getMetadataStorage().clear(); + + @InputType() + class DirectiveOnFieldInput { + @Field() + @Directive("@upper") + append: string; + } + + @InputType() + @Directive("@upper") + class DirectiveOnClassInput { + @Field() + append: string; + } + + @ObjectType() + class SampleObjectType { + @Field() + @Directive("foo") + withDirective: string = "withDirective"; + + @Field() + @Directive("bar", { baz: "true" }) + withDirectiveWithArgs: string = "withDirectiveWithArgs"; + + @Field() + @Directive("upper") + withUpper: string = "withUpper"; + + @Field() + @Directive("@upper") + withUpperSDL: string = "withUpperSDL"; + + @Field() + @Directive("append") + withAppend: string = "hello"; + + @Field() + @Directive("@append") + withAppendSDL: string = "hello"; + + @Field() + @Directive("append") + @Directive("upper") + withUpperAndAppend: string = "hello"; + + @Field() + @Directive("@append @upper") + withUpperAndAppendSDL: string = "hello"; + + @Field() + withInput(@Arg("input") input: DirectiveOnFieldInput): string { + return `hello${input.append}`; + } + + @Field() + @Directive("upper") + withInputUpper(@Arg("input") input: DirectiveOnFieldInput): string { + return `hello${input.append}`; + } + + @Field() + withInputOnClass(@Arg("input") input: DirectiveOnClassInput): string { + return `hello${input.append}`; + } + + @Field() + @Directive("upper") + withInputUpperOnClass(@Arg("input") input: DirectiveOnClassInput): string { + return `hello${input.append}`; + } + } + + @Resolver() + class SampleResolver { + @Query(() => SampleObjectType) + objectType(): SampleObjectType { + return new SampleObjectType(); + } + + @Query() + @Directive("foo") + queryWithDirective(): string { + return "queryWithDirective"; + } + + @Query() + @Directive("bar", { baz: "true" }) + queryWithDirectiveWithArgs(): string { + return "queryWithDirectiveWithArgs"; + } + + @Query() + @Directive("upper") + queryWithUpper(): string { + return "queryWithUpper"; + } + + @Query() + @Directive("@upper") + queryWithUpperSDL(): string { + return "queryWithUpper"; + } + + @Query() + @Directive("append") + queryWithAppend(): string { + return "hello"; + } + + @Query() + @Directive("@append") + queryWithAppendSDL(): string { + return "hello"; + } + + @Query() + @Directive("append") + @Directive("upper") + queryWithUpperAndAppend(): string { + return "hello"; + } + + @Query() + @Directive("@append @upper") + queryWithUpperAndAppendSDL(): string { + return "hello"; + } + } + + schema = await buildSchema({ + resolvers: [SampleResolver], + }); + + SchemaDirectiveVisitor.visitSchemaDirectives(schema, { + upper: UpperCaseDirective, + append: AppendDirective, + }); + }); + + it("should generate schema without errors", async () => { + expect(schema).toBeDefined(); + }); + + describe("Query", () => { + it("should add directives to query types", async () => { + const queryWithDirective = schema.getQueryType()!.getFields().queryWithDirective; + + assertValidFieldDirective(queryWithDirective.astNode, "foo"); + }); + + it("should add directives to query types with arguments", async () => { + const queryWithDirectiveWithArgs = schema.getQueryType()!.getFields() + .queryWithDirectiveWithArgs; + + assertValidFieldDirective(queryWithDirectiveWithArgs.astNode, "bar", { baz: "true" }); + }); + + it("calls directive 'upper'", async () => { + const query = `query { + queryWithUpper + }`; + + const { data } = await graphql(schema, query); + + expect(data).toHaveProperty("queryWithUpper"); + expect(data.queryWithUpper).toBe("QUERYWITHUPPER"); + }); + + it("calls directive 'upper' using SDL", async () => { + const query = `query { + queryWithUpperSDL + }`; + + const { data } = await graphql(schema, query); + + expect(data).toHaveProperty("queryWithUpperSDL"); + expect(data.queryWithUpperSDL).toBe("QUERYWITHUPPER"); + }); + + it("calls directive 'append'", async () => { + const query = `query { + queryWithAppend(append: ", world!") + }`; + + const { data } = await graphql(schema, query); + + expect(data).toHaveProperty("queryWithAppend"); + expect(data.queryWithAppend).toBe("hello, world!"); + }); + + it("calls directive 'append' using SDL", async () => { + const query = `query { + queryWithAppendSDL(append: ", world!") + }`; + + const { data } = await graphql(schema, query); + + expect(data).toHaveProperty("queryWithAppendSDL"); + expect(data.queryWithAppendSDL).toBe("hello, world!"); + }); + + it("calls directive 'upper' and 'append'", async () => { + const query = `query { + queryWithUpperAndAppend(append: ", world!") + }`; + + const { data } = await graphql(schema, query); + + expect(data).toHaveProperty("queryWithUpperAndAppend"); + expect(data.queryWithUpperAndAppend).toBe("HELLO, WORLD!"); + }); + + it("calls directive 'upper' and 'append' using SDL", async () => { + const query = `query { + queryWithUpperAndAppendSDL(append: ", world!") + }`; + + const { data } = await graphql(schema, query); + + expect(data).toHaveProperty("queryWithUpperAndAppendSDL"); + expect(data.queryWithUpperAndAppendSDL).toBe("HELLO, WORLD!"); + }); + }); + + describe("InputType", () => { + it("adds field directive to input types", async () => { + const inputType = schema.getType("DirectiveOnClassInput") as GraphQLInputObjectType; + + expect(inputType).toHaveProperty("astNode"); + assertValidFieldDirective(inputType.astNode, "upper"); + }); + + it("adds field directives to input type fields", async () => { + const fields = (schema.getType( + "DirectiveOnFieldInput", + ) as GraphQLInputObjectType).getFields(); + + expect(fields).toHaveProperty("append"); + expect(fields.append).toHaveProperty("astNode"); + assertValidFieldDirective(fields.append.astNode, "upper"); + }); + }); + + describe("ObjectType", () => { + it("calls object type directives", async () => { + const query = `query { + objectType { + withDirective + withDirectiveWithArgs + withUpper + withUpperSDL + withAppend(append: ", world!") + withAppendSDL(append: ", world!") + withUpperAndAppend(append: ", world!") + withUpperAndAppendSDL(append: ", world!") + withInput(input: { append: ", world!" }) + withInputUpper(input: { append: ", world!" }) + withInputOnClass(input: { append: ", world!" }) + withInputUpperOnClass(input: { append: ", world!" }) + } + }`; + + const { data } = await graphql(schema, query); + + expect(data).toHaveProperty("objectType"); + expect(data.objectType).toEqual({ + withDirective: "withDirective", + withDirectiveWithArgs: "withDirectiveWithArgs", + withUpper: "WITHUPPER", + withUpperSDL: "WITHUPPERSDL", + withAppend: "hello, world!", + withAppendSDL: "hello, world!", + withUpperAndAppend: "HELLO, WORLD!", + withUpperAndAppendSDL: "HELLO, WORLD!", + withInput: "hello, WORLD!", + withInputUpper: "HELLO, WORLD!", + withInputOnClass: "hello, WORLD!", + withInputUpperOnClass: "HELLO, WORLD!", + }); + }); + }); + }); + + describe("@cacheControl", () => { + it("works with ApolloServer and @cacheControl", async () => { + @ObjectType() + @Directive("@cacheControl(maxAge: 240)") + class Post { + @Field() + id: number = 1; + + @Field() + @Directive("@cacheControl(maxAge: 30)") + votes: number = 1; + + @Field(() => [Comment]) + comments: Comment[] = [new Comment()]; + + @Field() + @Directive("@cacheControl(scope: PRIVATE)") + readByCurrentUser: boolean = true; + } + + @ObjectType() + @Directive("@cacheControl(maxAge: 1000)") + class Comment { + @Field() + comment: string = "comment"; + } + + @Resolver() + class PostsResolver { + @Query(() => Post) + @Directive("@cacheControl(maxAge: 10)") + latestPost(): Post { + return new Post(); + } + } + + const server = new ApolloServer({ + schema: await buildSchema({ resolvers: [PostsResolver] }), + cacheControl: true, + }); + + const { extensions } = await server.executeOperation({ + query: `query { + latestPost { + id + votes + comments { comment } + readByCurrentUser + } + }`, + }); + + expect(extensions).toBeDefined(); + expect(extensions).toHaveProperty("cacheControl"); + expect(extensions!.cacheControl).toEqual({ + version: 1, + hints: [ + { + maxAge: 10, + path: ["latestPost"], + }, + { + maxAge: 30, + path: ["latestPost", "votes"], + }, + { + maxAge: 1000, + path: ["latestPost", "comments"], + }, + { + path: ["latestPost", "readByCurrentUser"], + scope: "PRIVATE", + }, + ], + }); + }); + }); +}); From c52f5e11240fcb5b20fe948a31e8289192089d3f Mon Sep 17 00:00:00 2001 From: Jordan Date: Wed, 17 Jul 2019 17:31:54 -0700 Subject: [PATCH 03/17] fix: remove unused file --- src/utils/printSchema.ts | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/utils/printSchema.ts diff --git a/src/utils/printSchema.ts b/src/utils/printSchema.ts deleted file mode 100644 index d00718f9e..000000000 --- a/src/utils/printSchema.ts +++ /dev/null @@ -1 +0,0 @@ -export const defaultPrintSchemaOptions: PrintSchemaOptions = { commentDescriptions: false }; From bafa7e08441d769f6bf3a33610472471785c72fb Mon Sep 17 00:00:00 2001 From: Jordan Date: Wed, 17 Jul 2019 17:39:42 -0700 Subject: [PATCH 04/17] ci(decorators): add mutation tests --- tests/functional/directives.ts | 131 +++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/tests/functional/directives.ts b/tests/functional/directives.ts index 8af47b04d..db52b5381 100644 --- a/tests/functional/directives.ts +++ b/tests/functional/directives.ts @@ -26,6 +26,7 @@ import { Directive, buildSchema, ObjectType, + Mutation, } from "../../src"; import { getMetadataStorage } from "../../src/metadata/getMetadataStorage"; import Maybe from "graphql/tsutils/Maybe"; @@ -288,6 +289,55 @@ describe("Directives", () => { queryWithUpperAndAppendSDL(): string { return "hello"; } + + @Mutation() + @Directive("foo") + mutationWithDirective(): string { + return "mutationWithDirective"; + } + + @Mutation() + @Directive("bar", { baz: "true" }) + mutationWithDirectiveWithArgs(): string { + return "mutationWithDirectiveWithArgs"; + } + + @Mutation() + @Directive("upper") + mutationWithUpper(): string { + return "mutationWithUpper"; + } + + @Mutation() + @Directive("@upper") + mutationWithUpperSDL(): string { + return "mutationWithUpper"; + } + + @Mutation() + @Directive("append") + mutationWithAppend(): string { + return "hello"; + } + + @Mutation() + @Directive("@append") + mutationWithAppendSDL(): string { + return "hello"; + } + + @Mutation() + @Directive("append") + @Directive("upper") + mutationWithUpperAndAppend(): string { + return "hello"; + } + + @Mutation() + @Directive("@append @upper") + mutationWithUpperAndAppendSDL(): string { + return "hello"; + } } schema = await buildSchema({ @@ -385,6 +435,87 @@ describe("Directives", () => { }); }); + describe("Mutation", () => { + it("should add directives to mutation types", async () => { + const mutationWithDirective = schema.getMutationType()!.getFields().mutationWithDirective; + + assertValidFieldDirective(mutationWithDirective.astNode, "foo"); + }); + + it("should add directives to mutation types with arguments", async () => { + const mutationWithDirectiveWithArgs = schema.getMutationType()!.getFields() + .mutationWithDirectiveWithArgs; + + assertValidFieldDirective(mutationWithDirectiveWithArgs.astNode, "bar", { baz: "true" }); + }); + + it("calls directive 'upper'", async () => { + const mutation = `mutation { + mutationWithUpper + }`; + + const { data } = await graphql(schema, mutation); + + expect(data).toHaveProperty("mutationWithUpper"); + expect(data.mutationWithUpper).toBe("MUTATIONWITHUPPER"); + }); + + it("calls directive 'upper' using SDL", async () => { + const mutation = `mutation { + mutationWithUpperSDL + }`; + + const { data } = await graphql(schema, mutation); + + expect(data).toHaveProperty("mutationWithUpperSDL"); + expect(data.mutationWithUpperSDL).toBe("MUTATIONWITHUPPER"); + }); + + it("calls directive 'append'", async () => { + const mutation = `mutation { + mutationWithAppend(append: ", world!") + }`; + + const { data } = await graphql(schema, mutation); + + expect(data).toHaveProperty("mutationWithAppend"); + expect(data.mutationWithAppend).toBe("hello, world!"); + }); + + it("calls directive 'append' using SDL", async () => { + const mutation = `mutation { + mutationWithAppendSDL(append: ", world!") + }`; + + const { data } = await graphql(schema, mutation); + + expect(data).toHaveProperty("mutationWithAppendSDL"); + expect(data.mutationWithAppendSDL).toBe("hello, world!"); + }); + + it("calls directive 'upper' and 'append'", async () => { + const mutation = `mutation { + mutationWithUpperAndAppend(append: ", world!") + }`; + + const { data } = await graphql(schema, mutation); + + expect(data).toHaveProperty("mutationWithUpperAndAppend"); + expect(data.mutationWithUpperAndAppend).toBe("HELLO, WORLD!"); + }); + + it("calls directive 'upper' and 'append' using SDL", async () => { + const mutation = `mutation { + mutationWithUpperAndAppendSDL(append: ", world!") + }`; + + const { data } = await graphql(schema, mutation); + + expect(data).toHaveProperty("mutationWithUpperAndAppendSDL"); + expect(data.mutationWithUpperAndAppendSDL).toBe("HELLO, WORLD!"); + }); + }); + describe("InputType", () => { it("adds field directive to input types", async () => { const inputType = schema.getType("DirectiveOnClassInput") as GraphQLInputObjectType; From 2025ea5bf669c82f0fb9f597c8fd88f0c3910e90 Mon Sep 17 00:00:00 2001 From: Jordan Date: Thu, 25 Jul 2019 10:34:01 -0700 Subject: [PATCH 05/17] docs: remove federation example --- dev.js | 1 - examples/apollo-federation/accounts/index.ts | 87 ------------ examples/apollo-federation/index.ts | 35 ----- examples/apollo-federation/inventory/index.ts | 101 ------------- examples/apollo-federation/products/index.ts | 93 ------------ examples/apollo-federation/reviews/index.ts | 134 ------------------ package-lock.json | 51 +------ 7 files changed, 1 insertion(+), 501 deletions(-) delete mode 100644 examples/apollo-federation/accounts/index.ts delete mode 100644 examples/apollo-federation/index.ts delete mode 100644 examples/apollo-federation/inventory/index.ts delete mode 100644 examples/apollo-federation/products/index.ts delete mode 100644 examples/apollo-federation/reviews/index.ts diff --git a/dev.js b/dev.js index 4e6ed8aba..b9b2d7e2e 100644 --- a/dev.js +++ b/dev.js @@ -17,4 +17,3 @@ 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 deleted file mode 100644 index 6d9722623..000000000 --- a/examples/apollo-federation/accounts/index.ts +++ /dev/null @@ -1,87 +0,0 @@ -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 deleted file mode 100644 index 7eb956e64..000000000 --- a/examples/apollo-federation/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index c9796ab8a..000000000 --- a/examples/apollo-federation/inventory/index.ts +++ /dev/null @@ -1,101 +0,0 @@ -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 deleted file mode 100644 index 6e8c950c9..000000000 --- a/examples/apollo-federation/products/index.ts +++ /dev/null @@ -1,93 +0,0 @@ -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 deleted file mode 100644 index 493c4ee31..000000000 --- a/examples/apollo-federation/reviews/index.ts +++ /dev/null @@ -1,134 +0,0 @@ -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; -} diff --git a/package-lock.json b/package-lock.json index 514f130d8..e7e46dc86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,56 +4,6 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "@apollo/federation": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/@apollo/federation/-/federation-0.6.8.tgz", - "integrity": "sha512-HvGCQ4H9FqrhfQrDGCVhduwPsB7g5i1cAKzS8d8QGsWLZR1ZFSOMCe5nZI9Fatr3raZkHuqb1YLfPZM8+CNGWg==", - "dev": true, - "requires": { - "apollo-env": "^0.5.1", - "apollo-graphql": "^0.3.3", - "apollo-server-env": "2.4.0" - }, - "dependencies": { - "apollo-env": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/apollo-env/-/apollo-env-0.5.1.tgz", - "integrity": "sha512-fndST2xojgSdH02k5hxk1cbqA9Ti8RX4YzzBoAB4oIe1Puhq7+YlhXGXfXB5Y4XN0al8dLg+5nAkyjNAR2qZTw==", - "dev": true, - "requires": { - "core-js": "^3.0.1", - "node-fetch": "^2.2.0", - "sha.js": "^2.4.11" - } - }, - "apollo-graphql": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.3.3.tgz", - "integrity": "sha512-t3CO/xIDVsCG2qOvx2MEbuu4b/6LzQjcBBwiVnxclmmFyAxYCIe7rpPlnLHSq7HyOMlCWDMozjoeWfdqYSaLqQ==", - "dev": true, - "requires": { - "apollo-env": "0.5.1", - "lodash.sortby": "^4.7.0" - } - }, - "apollo-server-env": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-2.4.0.tgz", - "integrity": "sha512-7ispR68lv92viFeu5zsRUVGP+oxsVI3WeeBNniM22Cx619maBUwcYTIC3+Y3LpXILhLZCzA1FASZwusgSlyN9w==", - "dev": true, - "requires": { - "node-fetch": "^2.1.2", - "util.promisify": "^1.0.0" - } - }, - "core-js": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.1.4.tgz", - "integrity": "sha512-YNZN8lt82XIMLnLirj9MhKDFZHalwzzrL9YLt6eb0T5D0EDl4IQ90IGkua8mHbnxNrkj1d8hbdizMc0Qmg1WnQ==", - "dev": true - } - } - }, "@apollographql/apollo-tools": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@apollographql/apollo-tools/-/apollo-tools-0.4.0.tgz", @@ -877,6 +827,7 @@ "dev": true, "optional": true, "requires": { + "nan": "^2.9.2", "node-pre-gyp": "^0.10.0" }, "dependencies": { From a38714ad2d2a771884c20acaec2dc51f2c1c3249 Mon Sep 17 00:00:00 2001 From: Jordan Date: Thu, 25 Jul 2019 12:38:26 -0700 Subject: [PATCH 06/17] feat: `@Directive` clean up --- src/decorators/Directive.ts | 35 +- src/decorators/ObjectType.ts | 1 - src/errors/InvalidDirectiveError.ts | 4 +- .../definitions/directive-metadata.ts | 4 +- src/metadata/metadata-storage.ts | 8 +- src/schema/schema-generator.ts | 85 +++-- tests/functional/apollo-cache-control.ts | 87 +++++ tests/functional/directives.ts | 344 +++--------------- tests/helpers/directives/AppendDirective.ts | 26 ++ .../helpers/directives/UpperCaseDirective.ts | 65 ++++ .../directives/assertValidDirective.ts | 41 +++ 11 files changed, 335 insertions(+), 365 deletions(-) create mode 100644 tests/functional/apollo-cache-control.ts create mode 100644 tests/helpers/directives/AppendDirective.ts create mode 100644 tests/helpers/directives/UpperCaseDirective.ts create mode 100644 tests/helpers/directives/assertValidDirective.ts diff --git a/src/decorators/Directive.ts b/src/decorators/Directive.ts index 4c20027ec..b7a3cd1c7 100644 --- a/src/decorators/Directive.ts +++ b/src/decorators/Directive.ts @@ -1,41 +1,34 @@ import { MethodAndPropDecorator } from "./types"; import { SymbolKeysNotSupportedError } from "../errors"; import { getMetadataStorage } from "../metadata/getMetadataStorage"; -import { DirectiveMetadata } from "../metadata/definitions/directive-metadata"; - -export interface DirectiveArgs { - [arg: string]: any; -} export function Directive(sdl: string): MethodAndPropDecorator & ClassDecorator; export function Directive( - name: DirectiveMetadata["nameOrSDL"], - args?: DirectiveMetadata["args"], + name: string, + args?: Record, ): MethodAndPropDecorator & ClassDecorator; export function Directive( - nameOrSDL: string, - args?: DirectiveArgs, + nameOrDefinition: string, + args?: Record, ): MethodDecorator | PropertyDecorator | ClassDecorator { return (targetOrPrototype, propertyKey, descriptor) => { - const directive = { nameOrSDL, args: args || {} }; + const directive = { nameOrDefinition, args: args || {} }; if (!propertyKey) { getMetadataStorage().collectDirectiveClassMetadata({ target: targetOrPrototype as Function, directive, }); + } else { + if (typeof propertyKey === "symbol") { + throw new SymbolKeysNotSupportedError(); + } - return; - } - - if (typeof propertyKey === "symbol") { - throw new SymbolKeysNotSupportedError(); + getMetadataStorage().collectDirectiveFieldMetadata({ + target: targetOrPrototype.constructor, + field: propertyKey, + directive, + }); } - - getMetadataStorage().collectDirectiveFieldMetadata({ - target: targetOrPrototype.constructor, - field: propertyKey, - directive, - }); }; } diff --git a/src/decorators/ObjectType.ts b/src/decorators/ObjectType.ts index e6d0c0e63..082d0eb0a 100644 --- a/src/decorators/ObjectType.ts +++ b/src/decorators/ObjectType.ts @@ -1,7 +1,6 @@ import { getMetadataStorage } from "../metadata/getMetadataStorage"; import { getNameDecoratorParams } from "../helpers/decorators"; import { DescriptionOptions, AbstractClassOptions } from "./types"; -import { ObjectClassMetadata } from "../metadata/definitions/object-class-metdata"; export type ObjectOptions = DescriptionOptions & AbstractClassOptions & { diff --git a/src/errors/InvalidDirectiveError.ts b/src/errors/InvalidDirectiveError.ts index b24111502..6886b6331 100644 --- a/src/errors/InvalidDirectiveError.ts +++ b/src/errors/InvalidDirectiveError.ts @@ -1,8 +1,8 @@ import { DirectiveMetadata } from "../metadata/definitions/directive-metadata"; export class InvalidDirectiveError extends Error { - constructor(directive: DirectiveMetadata) { - super(`Invalid directive "${directive.nameOrSDL}" `); + constructor(msg: string, directive: DirectiveMetadata) { + super(`${msg} "${directive.nameOrDefinition}" `); Object.setPrototypeOf(this, new.target.prototype); } diff --git a/src/metadata/definitions/directive-metadata.ts b/src/metadata/definitions/directive-metadata.ts index bfafcd775..b1ae51674 100644 --- a/src/metadata/definitions/directive-metadata.ts +++ b/src/metadata/definitions/directive-metadata.ts @@ -1,6 +1,6 @@ export interface DirectiveMetadata { - nameOrSDL: string; - args: { [key: string]: string }; + nameOrDefinition: string; + args: Record; } export interface DirectiveClassMetadata { diff --git a/src/metadata/metadata-storage.ts b/src/metadata/metadata-storage.ts index cb33c6634..f69d8037c 100644 --- a/src/metadata/metadata-storage.ts +++ b/src/metadata/metadata-storage.ts @@ -102,10 +102,10 @@ export class MetadataStorage { } collectDirectiveClassMetadata(definition: DirectiveClassMetadata) { - this.classDirectives.push(definition); + this.classDirectives.unshift(definition); } collectDirectiveFieldMetadata(definition: DirectiveFieldMetadata) { - this.fieldDirectives.push(definition); + this.fieldDirectives.unshift(definition); } build() { @@ -274,8 +274,4 @@ export class MetadataStorage { } return authorizedField.roles; } - - private findClassDirectives(target: ClassMetadata["target"]): DirectiveClassMetadata[] { - return this.classDirectives.filter(it => it.target === target); - } } diff --git a/src/schema/schema-generator.ts b/src/schema/schema-generator.ts index ac74d3179..70fd369c1 100644 --- a/src/schema/schema-generator.ts +++ b/src/schema/schema-generator.ts @@ -281,10 +281,7 @@ export abstract class SchemaGenerator { type: new GraphQLObjectType({ name: objectType.name, description: objectType.description, - astNode: this.createClassDirectiveNodes( - objectType.name, - objectType.directives, - ) as ObjectTypeDefinitionNode, + astNode: this.getObjectTypeDefinitionNode(objectType.name, objectType.directives), interfaces: () => { let interfaces = interfaceClasses.map( interfaceClass => @@ -382,10 +379,7 @@ export abstract class SchemaGenerator { description: field.description, type: this.getGraphQLInputType(field.name, field.getType(), field.typeOptions), defaultValue: field.typeOptions.defaultValue, - astNode: this.createFieldDirectiveNodes( - field.name, - field.directives, - ) as InputValueDefinitionNode, + astNode: this.getInputValueDefinitionNode(field.name, field.directives), }; return fieldsMap; }, @@ -401,10 +395,7 @@ export abstract class SchemaGenerator { } return fields; }, - astNode: this.createClassDirectiveNodes( - inputType.name, - inputType.directives, - ) as InputObjectTypeDefinitionNode, + astNode: this.getInputObjectTypeDefinitionNode(inputType.name, inputType.directives), }), }; }); @@ -676,33 +667,56 @@ export abstract class SchemaGenerator { .map(it => it.type); } - private static createDirectiveNodes(directive: DirectiveMetadata): DirectiveNode[] { - const { nameOrSDL, args } = directive; + private static getInputValueDefinitionNode( + name: string, + directiveMetas?: DirectiveMetadata[], + ): InputValueDefinitionNode | undefined { + if (!directiveMetas || !directiveMetas.length) { + return; + } + + return { + kind: "InputValueDefinition", + type: { + kind: "NamedType", + name: { + kind: "Name", + value: name, + }, + }, + name: { + kind: "Name", + value: name, + }, + directives: directiveMetas.map(this.getDirectiveNodes), + }; + } - if (!nameOrSDL.startsWith("@")) { - return [ - { - kind: "Directive", + private static getDirectiveNodes(directive: DirectiveMetadata): DirectiveNode { + const { nameOrDefinition, args } = directive; + + if (!nameOrDefinition.startsWith("@")) { + return { + kind: "Directive", + name: { + kind: "Name", + value: nameOrDefinition, + }, + arguments: Object.keys(args).map(arg => ({ + kind: "Argument", name: { kind: "Name", - value: nameOrSDL, + value: arg, }, - arguments: Object.keys(args).map(arg => ({ - kind: "Argument", - name: { - kind: "Name", - value: arg, - }, - value: parseValue(args[arg]), - })), - }, - ]; + value: parseValue(args[arg]), + })), + }; } let directives: DirectiveNode[] = []; try { - const parsed = parse(`type String ${nameOrSDL}`); + const parsed = parse(`type String ${nameOrDefinition}`); const definitions = parsed.definitions as ObjectTypeDefinitionNode[]; @@ -714,13 +728,16 @@ export abstract class SchemaGenerator { }); } } catch (err) { - /** noop */ + throw new InvalidDirectiveError("Error parsing directive", directive); } - if (!directives) { - throw new InvalidDirectiveError(directive); + if (directives.length !== 1) { + throw new InvalidDirectiveError( + "There must be exactly one directive defined for directive", + directive, + ); } - return directives; + return directives[0]; } } diff --git a/tests/functional/apollo-cache-control.ts b/tests/functional/apollo-cache-control.ts new file mode 100644 index 000000000..5caa7e5b9 --- /dev/null +++ b/tests/functional/apollo-cache-control.ts @@ -0,0 +1,87 @@ +// tslint:disable:member-ordering +import "reflect-metadata"; +import { Field, Resolver, Query, Directive, buildSchema, ObjectType } from "../../src"; +import { getMetadataStorage } from "../../src/metadata/getMetadataStorage"; +import { ApolloServer } from "apollo-server"; + +describe("Apollo @cacheControl", () => { + beforeAll(async () => { + getMetadataStorage().clear(); + }); + + it("works with ApolloServer and @cacheControl", async () => { + @ObjectType() + @Directive("@cacheControl(maxAge: 240)") + class Post { + @Field() + id: number = 1; + + @Field() + @Directive("@cacheControl(maxAge: 30)") + votes: number = 1; + + @Field(() => [Comment]) + comments: Comment[] = [new Comment()]; + + @Field() + @Directive("@cacheControl(scope: PRIVATE)") + readByCurrentUser: boolean = true; + } + + @ObjectType() + @Directive("@cacheControl(maxAge: 1000)") + class Comment { + @Field() + comment: string = "comment"; + } + + @Resolver() + class PostsResolver { + @Query(() => Post) + @Directive("@cacheControl(maxAge: 10)") + latestPost(): Post { + return new Post(); + } + } + + const server = new ApolloServer({ + schema: await buildSchema({ resolvers: [PostsResolver] }), + cacheControl: true, + }); + + const { extensions } = await server.executeOperation({ + query: `query { + latestPost { + id + votes + comments { comment } + readByCurrentUser + } + }`, + }); + + expect(extensions).toBeDefined(); + expect(extensions).toHaveProperty("cacheControl"); + expect(extensions!.cacheControl).toEqual({ + version: 1, + hints: [ + { + maxAge: 10, + path: ["latestPost"], + }, + { + maxAge: 30, + path: ["latestPost", "votes"], + }, + { + maxAge: 1000, + path: ["latestPost", "comments"], + }, + { + path: ["latestPost", "readByCurrentUser"], + scope: "PRIVATE", + }, + ], + }); + }); +}); diff --git a/tests/functional/directives.ts b/tests/functional/directives.ts index db52b5381..c5b73ea91 100644 --- a/tests/functional/directives.ts +++ b/tests/functional/directives.ts @@ -1,22 +1,7 @@ // tslint:disable:member-ordering import "reflect-metadata"; -import { - GraphQLSchema, - FieldDefinitionNode, - GraphQLField, - GraphQLDirective, - DirectiveLocation, - GraphQLString, - parseValue, - graphql, - GraphQLInputObjectType, - InputValueDefinitionNode, - InputObjectTypeDefinitionNode, - GraphQLInputField, - GraphQLScalarType, - GraphQLNonNull, -} from "graphql"; +import { GraphQLSchema, graphql, GraphQLInputObjectType } from "graphql"; import { Field, InputType, @@ -29,131 +14,10 @@ import { Mutation, } from "../../src"; import { getMetadataStorage } from "../../src/metadata/getMetadataStorage"; -import Maybe from "graphql/tsutils/Maybe"; import { SchemaDirectiveVisitor } from "graphql-tools"; -import { ApolloServer } from "apollo-server"; - -class UpperCaseType extends GraphQLScalarType { - constructor(type: any) { - super({ - name: "UpperCase", - parseValue: value => type.parseValue(value), - serialize: value => type.serialize(value), - parseLiteral: ast => { - const result = type.parseLiteral(ast); - - if (typeof result === "string") { - return result.toUpperCase(); - } - - return result; - }, - }); - } -} - -class UpperCaseDirective extends SchemaDirectiveVisitor { - visitFieldDefinition(field: GraphQLField) { - const resolve = field.resolve; - - field.resolve = async function(...args) { - const result = await resolve!.apply(this, args); - if (typeof result === "string") { - return result.toUpperCase(); - } - - return result; - }; - } - - visitInputObject(object: GraphQLInputObjectType) { - const fields = object.getFields(); - - Object.keys(fields).forEach(field => { - this.visitInputFieldDefinition(fields[field]); - }); - } - - visitInputFieldDefinition(field: GraphQLInputField) { - if (field.type instanceof UpperCaseType) { - /* noop */ - } else if ( - field.type instanceof GraphQLNonNull && - field.type.ofType instanceof GraphQLScalarType - ) { - field.type = new GraphQLNonNull(new UpperCaseType(field.type.ofType)); - } else if (field.type instanceof GraphQLScalarType) { - field.type = new UpperCaseType(field.type); - } else { - throw new Error(`Not a scalar type: ${field.type}`); - } - } - - static getDirectiveDeclaration(directiveName: string): GraphQLDirective { - return new GraphQLDirective({ - name: directiveName, - locations: [DirectiveLocation.FIELD_DEFINITION], - }); - } -} - -class AppendDirective extends SchemaDirectiveVisitor { - visitFieldDefinition(field: GraphQLField) { - const resolve = field.resolve; - - field.args.push({ - name: "append", - type: GraphQLString, - }); - - field.resolve = async function(source, { append, ...otherArgs }, context, info) { - const result = await resolve!.call(this, source, otherArgs, context, info); - - return `${result}${append}`; - }; - } - - static getDirectiveDeclaration(directiveName: string): GraphQLDirective { - return new GraphQLDirective({ - name: directiveName, - locations: [DirectiveLocation.FIELD_DEFINITION], - }); - } -} - -const assertValidFieldDirective = ( - astNode: Maybe, - name: string, - args?: { [key: string]: string }, -): void => { - if (!astNode) { - throw new Error(`Directive with name ${name} does not exist`); - } - - const directives = (astNode || {}).directives || []; - - const directive = directives.find(it => it.name.kind === "Name" && it.name.value === name); - - if (!directive) { - throw new Error(`Directive with name ${name} does not exist`); - } - - if (!args) { - if (Array.isArray(directive.arguments)) { - expect(directive.arguments).toHaveLength(0); - } else { - expect(directive.arguments).toBeFalsy(); - } - } else { - expect(directive.arguments).toEqual( - Object.keys(args).map(arg => ({ - kind: "Argument", - name: { kind: "Name", value: arg }, - value: parseValue(args[arg]), - })), - ); - } -}; +import { UpperCaseDirective } from "../helpers/directives/UpperCaseDirective"; +import { AppendDirective } from "../helpers/directives/AppendDirective"; +import { assertValidDirective } from "../helpers/directives/assertValidDirective"; describe("Directives", () => { let schema: GraphQLSchema; @@ -192,7 +56,7 @@ describe("Directives", () => { @Field() @Directive("@upper") - withUpperSDL: string = "withUpperSDL"; + withUpperDefinition: string = "withUpperDefinition"; @Field() @Directive("append") @@ -200,17 +64,13 @@ describe("Directives", () => { @Field() @Directive("@append") - withAppendSDL: string = "hello"; + withAppendDefinition: string = "hello"; @Field() @Directive("append") @Directive("upper") withUpperAndAppend: string = "hello"; - @Field() - @Directive("@append @upper") - withUpperAndAppendSDL: string = "hello"; - @Field() withInput(@Arg("input") input: DirectiveOnFieldInput): string { return `hello${input.append}`; @@ -261,7 +121,7 @@ describe("Directives", () => { @Query() @Directive("@upper") - queryWithUpperSDL(): string { + queryWithUpperDefinition(): string { return "queryWithUpper"; } @@ -273,7 +133,7 @@ describe("Directives", () => { @Query() @Directive("@append") - queryWithAppendSDL(): string { + queryWithAppendDefinition(): string { return "hello"; } @@ -284,12 +144,6 @@ describe("Directives", () => { return "hello"; } - @Query() - @Directive("@append @upper") - queryWithUpperAndAppendSDL(): string { - return "hello"; - } - @Mutation() @Directive("foo") mutationWithDirective(): string { @@ -310,7 +164,7 @@ describe("Directives", () => { @Mutation() @Directive("@upper") - mutationWithUpperSDL(): string { + mutationWithUpperDefinition(): string { return "mutationWithUpper"; } @@ -322,7 +176,7 @@ describe("Directives", () => { @Mutation() @Directive("@append") - mutationWithAppendSDL(): string { + mutationWithAppendDefinition(): string { return "hello"; } @@ -332,12 +186,6 @@ describe("Directives", () => { mutationWithUpperAndAppend(): string { return "hello"; } - - @Mutation() - @Directive("@append @upper") - mutationWithUpperAndAppendSDL(): string { - return "hello"; - } } schema = await buildSchema({ @@ -358,20 +206,20 @@ describe("Directives", () => { it("should add directives to query types", async () => { const queryWithDirective = schema.getQueryType()!.getFields().queryWithDirective; - assertValidFieldDirective(queryWithDirective.astNode, "foo"); + assertValidDirective(queryWithDirective.astNode, "foo"); }); it("should add directives to query types with arguments", async () => { const queryWithDirectiveWithArgs = schema.getQueryType()!.getFields() .queryWithDirectiveWithArgs; - assertValidFieldDirective(queryWithDirectiveWithArgs.astNode, "bar", { baz: "true" }); + assertValidDirective(queryWithDirectiveWithArgs.astNode, "bar", { baz: "true" }); }); it("calls directive 'upper'", async () => { const query = `query { - queryWithUpper - }`; + queryWithUpper + }`; const { data } = await graphql(schema, query); @@ -379,21 +227,21 @@ describe("Directives", () => { expect(data.queryWithUpper).toBe("QUERYWITHUPPER"); }); - it("calls directive 'upper' using SDL", async () => { + it("calls directive 'upper' using Definition", async () => { const query = `query { - queryWithUpperSDL - }`; + queryWithUpperDefinition + }`; const { data } = await graphql(schema, query); - expect(data).toHaveProperty("queryWithUpperSDL"); - expect(data.queryWithUpperSDL).toBe("QUERYWITHUPPER"); + expect(data).toHaveProperty("queryWithUpperDefinition"); + expect(data.queryWithUpperDefinition).toBe("QUERYWITHUPPER"); }); it("calls directive 'append'", async () => { const query = `query { - queryWithAppend(append: ", world!") - }`; + queryWithAppend(append: ", world!") + }`; const { data } = await graphql(schema, query); @@ -401,52 +249,41 @@ describe("Directives", () => { expect(data.queryWithAppend).toBe("hello, world!"); }); - it("calls directive 'append' using SDL", async () => { + it("calls directive 'append' using Definition", async () => { const query = `query { - queryWithAppendSDL(append: ", world!") - }`; + queryWithAppendDefinition(append: ", world!") + }`; const { data } = await graphql(schema, query); - expect(data).toHaveProperty("queryWithAppendSDL"); - expect(data.queryWithAppendSDL).toBe("hello, world!"); + expect(data).toHaveProperty("queryWithAppendDefinition"); + expect(data.queryWithAppendDefinition).toBe("hello, world!"); }); it("calls directive 'upper' and 'append'", async () => { const query = `query { - queryWithUpperAndAppend(append: ", world!") - }`; + queryWithUpperAndAppend(append: ", world!") + }`; const { data } = await graphql(schema, query); expect(data).toHaveProperty("queryWithUpperAndAppend"); expect(data.queryWithUpperAndAppend).toBe("HELLO, WORLD!"); }); - - it("calls directive 'upper' and 'append' using SDL", async () => { - const query = `query { - queryWithUpperAndAppendSDL(append: ", world!") - }`; - - const { data } = await graphql(schema, query); - - expect(data).toHaveProperty("queryWithUpperAndAppendSDL"); - expect(data.queryWithUpperAndAppendSDL).toBe("HELLO, WORLD!"); - }); }); describe("Mutation", () => { it("should add directives to mutation types", async () => { const mutationWithDirective = schema.getMutationType()!.getFields().mutationWithDirective; - assertValidFieldDirective(mutationWithDirective.astNode, "foo"); + assertValidDirective(mutationWithDirective.astNode, "foo"); }); it("should add directives to mutation types with arguments", async () => { const mutationWithDirectiveWithArgs = schema.getMutationType()!.getFields() .mutationWithDirectiveWithArgs; - assertValidFieldDirective(mutationWithDirectiveWithArgs.astNode, "bar", { baz: "true" }); + assertValidDirective(mutationWithDirectiveWithArgs.astNode, "bar", { baz: "true" }); }); it("calls directive 'upper'", async () => { @@ -460,15 +297,15 @@ describe("Directives", () => { expect(data.mutationWithUpper).toBe("MUTATIONWITHUPPER"); }); - it("calls directive 'upper' using SDL", async () => { + it("calls directive 'upper' using Definition", async () => { const mutation = `mutation { - mutationWithUpperSDL + mutationWithUpperDefinition }`; const { data } = await graphql(schema, mutation); - expect(data).toHaveProperty("mutationWithUpperSDL"); - expect(data.mutationWithUpperSDL).toBe("MUTATIONWITHUPPER"); + expect(data).toHaveProperty("mutationWithUpperDefinition"); + expect(data.mutationWithUpperDefinition).toBe("MUTATIONWITHUPPER"); }); it("calls directive 'append'", async () => { @@ -482,15 +319,15 @@ describe("Directives", () => { expect(data.mutationWithAppend).toBe("hello, world!"); }); - it("calls directive 'append' using SDL", async () => { + it("calls directive 'append' using Definition", async () => { const mutation = `mutation { - mutationWithAppendSDL(append: ", world!") - }`; + mutationWithAppendDefinition(append: ", world!") + }`; const { data } = await graphql(schema, mutation); - expect(data).toHaveProperty("mutationWithAppendSDL"); - expect(data.mutationWithAppendSDL).toBe("hello, world!"); + expect(data).toHaveProperty("mutationWithAppendDefinition"); + expect(data.mutationWithAppendDefinition).toBe("hello, world!"); }); it("calls directive 'upper' and 'append'", async () => { @@ -503,17 +340,6 @@ describe("Directives", () => { expect(data).toHaveProperty("mutationWithUpperAndAppend"); expect(data.mutationWithUpperAndAppend).toBe("HELLO, WORLD!"); }); - - it("calls directive 'upper' and 'append' using SDL", async () => { - const mutation = `mutation { - mutationWithUpperAndAppendSDL(append: ", world!") - }`; - - const { data } = await graphql(schema, mutation); - - expect(data).toHaveProperty("mutationWithUpperAndAppendSDL"); - expect(data.mutationWithUpperAndAppendSDL).toBe("HELLO, WORLD!"); - }); }); describe("InputType", () => { @@ -521,7 +347,7 @@ describe("Directives", () => { const inputType = schema.getType("DirectiveOnClassInput") as GraphQLInputObjectType; expect(inputType).toHaveProperty("astNode"); - assertValidFieldDirective(inputType.astNode, "upper"); + assertValidDirective(inputType.astNode, "upper"); }); it("adds field directives to input type fields", async () => { @@ -531,7 +357,7 @@ describe("Directives", () => { expect(fields).toHaveProperty("append"); expect(fields.append).toHaveProperty("astNode"); - assertValidFieldDirective(fields.append.astNode, "upper"); + assertValidDirective(fields.append.astNode, "upper"); }); }); @@ -542,11 +368,10 @@ describe("Directives", () => { withDirective withDirectiveWithArgs withUpper - withUpperSDL + withUpperDefinition withAppend(append: ", world!") - withAppendSDL(append: ", world!") + withAppendDefinition(append: ", world!") withUpperAndAppend(append: ", world!") - withUpperAndAppendSDL(append: ", world!") withInput(input: { append: ", world!" }) withInputUpper(input: { append: ", world!" }) withInputOnClass(input: { append: ", world!" }) @@ -561,11 +386,10 @@ describe("Directives", () => { withDirective: "withDirective", withDirectiveWithArgs: "withDirectiveWithArgs", withUpper: "WITHUPPER", - withUpperSDL: "WITHUPPERSDL", + withUpperDefinition: "WITHUPPERDEFINITION", withAppend: "hello, world!", - withAppendSDL: "hello, world!", + withAppendDefinition: "hello, world!", withUpperAndAppend: "HELLO, WORLD!", - withUpperAndAppendSDL: "HELLO, WORLD!", withInput: "hello, WORLD!", withInputUpper: "HELLO, WORLD!", withInputOnClass: "hello, WORLD!", @@ -574,82 +398,4 @@ describe("Directives", () => { }); }); }); - - describe("@cacheControl", () => { - it("works with ApolloServer and @cacheControl", async () => { - @ObjectType() - @Directive("@cacheControl(maxAge: 240)") - class Post { - @Field() - id: number = 1; - - @Field() - @Directive("@cacheControl(maxAge: 30)") - votes: number = 1; - - @Field(() => [Comment]) - comments: Comment[] = [new Comment()]; - - @Field() - @Directive("@cacheControl(scope: PRIVATE)") - readByCurrentUser: boolean = true; - } - - @ObjectType() - @Directive("@cacheControl(maxAge: 1000)") - class Comment { - @Field() - comment: string = "comment"; - } - - @Resolver() - class PostsResolver { - @Query(() => Post) - @Directive("@cacheControl(maxAge: 10)") - latestPost(): Post { - return new Post(); - } - } - - const server = new ApolloServer({ - schema: await buildSchema({ resolvers: [PostsResolver] }), - cacheControl: true, - }); - - const { extensions } = await server.executeOperation({ - query: `query { - latestPost { - id - votes - comments { comment } - readByCurrentUser - } - }`, - }); - - expect(extensions).toBeDefined(); - expect(extensions).toHaveProperty("cacheControl"); - expect(extensions!.cacheControl).toEqual({ - version: 1, - hints: [ - { - maxAge: 10, - path: ["latestPost"], - }, - { - maxAge: 30, - path: ["latestPost", "votes"], - }, - { - maxAge: 1000, - path: ["latestPost", "comments"], - }, - { - path: ["latestPost", "readByCurrentUser"], - scope: "PRIVATE", - }, - ], - }); - }); - }); }); diff --git a/tests/helpers/directives/AppendDirective.ts b/tests/helpers/directives/AppendDirective.ts new file mode 100644 index 000000000..a5a17c245 --- /dev/null +++ b/tests/helpers/directives/AppendDirective.ts @@ -0,0 +1,26 @@ +import { SchemaDirectiveVisitor } from "graphql-tools"; +import { GraphQLField, GraphQLString, GraphQLDirective, DirectiveLocation } from "graphql"; + +export class AppendDirective extends SchemaDirectiveVisitor { + static getDirectiveDeclaration(directiveName: string): GraphQLDirective { + return new GraphQLDirective({ + name: directiveName, + locations: [DirectiveLocation.FIELD_DEFINITION], + }); + } + + visitFieldDefinition(field: GraphQLField) { + const resolve = field.resolve; + + field.args.push({ + name: "append", + type: GraphQLString, + }); + + field.resolve = async function(source, { append, ...otherArgs }, context, info) { + const result = await resolve!.call(this, source, otherArgs, context, info); + + return `${result}${append}`; + }; + } +} diff --git a/tests/helpers/directives/UpperCaseDirective.ts b/tests/helpers/directives/UpperCaseDirective.ts new file mode 100644 index 000000000..9c8979b92 --- /dev/null +++ b/tests/helpers/directives/UpperCaseDirective.ts @@ -0,0 +1,65 @@ +import { + GraphQLField, + GraphQLDirective, + DirectiveLocation, + GraphQLInputObjectType, + GraphQLInputField, + GraphQLScalarType, + GraphQLNonNull, +} from "graphql"; +import { SchemaDirectiveVisitor } from "graphql-tools"; + +class UpperCaseType extends GraphQLScalarType { + constructor(type: any) { + super({ + name: "UpperCase", + parseValue: value => this.upper(type.parseValue(value)), + serialize: value => this.upper(type.serialize(value)), + parseLiteral: ast => this.upper(type.parseLiteral(ast)), + }); + } + + upper(value: any) { + return typeof value === "string" ? value.toUpperCase() : value; + } +} + +export class UpperCaseDirective extends SchemaDirectiveVisitor { + static getDirectiveDeclaration(directiveName: string): GraphQLDirective { + return new GraphQLDirective({ + name: directiveName, + locations: [DirectiveLocation.FIELD_DEFINITION], + }); + } + + visitFieldDefinition(field: GraphQLField) { + this.wrapField(field); + } + + visitInputObject(object: GraphQLInputObjectType) { + const fields = object.getFields(); + + Object.keys(fields).forEach(field => { + this.wrapField(fields[field]); + }); + } + + visitInputFieldDefinition(field: GraphQLInputField) { + this.wrapField(field); + } + + wrapField(field: GraphQLField | GraphQLInputField): void { + if (field.type instanceof UpperCaseType) { + /* noop */ + } else if ( + field.type instanceof GraphQLNonNull && + field.type.ofType instanceof GraphQLScalarType + ) { + field.type = new GraphQLNonNull(new UpperCaseType(field.type.ofType)); + } else if (field.type instanceof GraphQLScalarType) { + field.type = new UpperCaseType(field.type); + } else { + throw new Error(`Not a scalar type: ${field.type}`); + } + } +} diff --git a/tests/helpers/directives/assertValidDirective.ts b/tests/helpers/directives/assertValidDirective.ts new file mode 100644 index 000000000..32139ddbc --- /dev/null +++ b/tests/helpers/directives/assertValidDirective.ts @@ -0,0 +1,41 @@ +import { + FieldDefinitionNode, + InputObjectTypeDefinitionNode, + InputValueDefinitionNode, + parseValue, +} from "graphql"; +import Maybe from "graphql/tsutils/Maybe"; + +export function assertValidDirective( + astNode: Maybe, + name: string, + args?: { [key: string]: string }, +): void { + if (!astNode) { + throw new Error(`Directive with name ${name} does not exist`); + } + + const directives = (astNode || {}).directives || []; + + const directive = directives.find(it => it.name.kind === "Name" && it.name.value === name); + + if (!directive) { + throw new Error(`Directive with name ${name} does not exist`); + } + + if (!args) { + if (Array.isArray(directive.arguments)) { + expect(directive.arguments).toHaveLength(0); + } else { + expect(directive.arguments).toBeFalsy(); + } + } else { + expect(directive.arguments).toEqual( + Object.keys(args).map(arg => ({ + kind: "Argument", + name: { kind: "Name", value: arg }, + value: parseValue(args[arg]), + })), + ); + } +} From 004939c3b3d9f02d6307b20be1e9d4f992a83f0a Mon Sep 17 00:00:00 2001 From: Jordan Date: Thu, 25 Jul 2019 12:38:39 -0700 Subject: [PATCH 07/17] fix: emitSchemaDefinitionFile spacing --- src/utils/emitSchemaDefinitionFile.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/emitSchemaDefinitionFile.ts b/src/utils/emitSchemaDefinitionFile.ts index e04e6d92e..3c9da5924 100644 --- a/src/utils/emitSchemaDefinitionFile.ts +++ b/src/utils/emitSchemaDefinitionFile.ts @@ -10,6 +10,7 @@ const generatedSchemaWarning = /* graphql */ `\ # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- + `; export function emitSchemaDefinitionFileSync( From 511a49851f0082df034d1570139eaebde289eabf Mon Sep 17 00:00:00 2001 From: Jordan Date: Thu, 25 Jul 2019 12:59:46 -0700 Subject: [PATCH 08/17] ci: error tests --- src/schema/schema-generator.ts | 7 ++++ tests/functional/directives.ts | 74 ++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/src/schema/schema-generator.ts b/src/schema/schema-generator.ts index 70fd369c1..be8e5e774 100644 --- a/src/schema/schema-generator.ts +++ b/src/schema/schema-generator.ts @@ -695,6 +695,13 @@ export abstract class SchemaGenerator { private static getDirectiveNodes(directive: DirectiveMetadata): DirectiveNode { const { nameOrDefinition, args } = directive; + if (nameOrDefinition === "") { + throw new InvalidDirectiveError( + "There must be exactly one directive defined for directive", + directive, + ); + } + if (!nameOrDefinition.startsWith("@")) { return { kind: "Directive", diff --git a/tests/functional/directives.ts b/tests/functional/directives.ts index c5b73ea91..baf7fc61e 100644 --- a/tests/functional/directives.ts +++ b/tests/functional/directives.ts @@ -18,6 +18,7 @@ import { SchemaDirectiveVisitor } from "graphql-tools"; import { UpperCaseDirective } from "../helpers/directives/UpperCaseDirective"; import { AppendDirective } from "../helpers/directives/AppendDirective"; import { assertValidDirective } from "../helpers/directives/assertValidDirective"; +import { InvalidDirectiveError } from "../../src/errors/InvalidDirectiveError"; describe("Directives", () => { let schema: GraphQLSchema; @@ -398,4 +399,77 @@ describe("Directives", () => { }); }); }); + + describe("errors", () => { + beforeEach(async () => { + getMetadataStorage().clear(); + }); + + it("throws error on multiple directive definitions", async () => { + expect.assertions(2); + + @Resolver() + class InvalidQuery { + @Query() + @Directive("@upper @append") + invalid(): string { + return "invalid"; + } + } + + try { + await buildSchema({ resolvers: [InvalidQuery] }); + } catch (err) { + expect(err).toBeInstanceOf(InvalidDirectiveError); + const error: InvalidDirectiveError = err; + expect(error.message).toContain( + 'There must be exactly one directive defined for directive "@upper @append"', + ); + } + }); + + it("throws error when parsing invalid directives", async () => { + expect.assertions(2); + + @Resolver() + class InvalidQuery { + @Query() + @Directive("@invalid(@directive)") + invalid(): string { + return "invalid"; + } + } + + try { + await buildSchema({ resolvers: [InvalidQuery] }); + } catch (err) { + expect(err).toBeInstanceOf(InvalidDirectiveError); + const error: InvalidDirectiveError = err; + expect(error.message).toContain('Error parsing directive "@invalid(@directive)"'); + } + }); + + it("throws error when no directives are defined", async () => { + expect.assertions(2); + + @Resolver() + class InvalidQuery { + @Query() + @Directive("") + invalid(): string { + return "invalid"; + } + } + + try { + await buildSchema({ resolvers: [InvalidQuery] }); + } catch (err) { + expect(err).toBeInstanceOf(InvalidDirectiveError); + const error: InvalidDirectiveError = err; + expect(error.message).toContain( + 'There must be exactly one directive defined for directive ""', + ); + } + }); + }); }); From 137c5f90be19086854cf0f66046bb4897a6f0162 Mon Sep 17 00:00:00 2001 From: Jordan Date: Thu, 25 Jul 2019 13:07:15 -0700 Subject: [PATCH 09/17] fix: arg => argKey --- src/schema/schema-generator.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/schema/schema-generator.ts b/src/schema/schema-generator.ts index be8e5e774..c379a425a 100644 --- a/src/schema/schema-generator.ts +++ b/src/schema/schema-generator.ts @@ -709,13 +709,13 @@ export abstract class SchemaGenerator { kind: "Name", value: nameOrDefinition, }, - arguments: Object.keys(args).map(arg => ({ + arguments: Object.keys(args).map(argKey => ({ kind: "Argument", name: { kind: "Name", - value: arg, + value: argKey, }, - value: parseValue(args[arg]), + value: parseValue(args[argKey]), })), }; } From a3e8ca49e8f3730f90c804aa7f7431081e1da549 Mon Sep 17 00:00:00 2001 From: Jordan Date: Tue, 8 Oct 2019 12:45:13 -0700 Subject: [PATCH 10/17] requested directive fixes --- CHANGELOG.md | 1 + src/errors/InvalidDirectiveError.ts | 2 +- src/errors/index.ts | 1 + src/metadata/definitions/index.ts | 1 + src/metadata/metadata-storage.ts | 7 +- src/schema/schema-generator.ts | 79 +++++++++++++++-- src/utils/buildSchema.ts | 1 - tests/functional/apollo-cache-control.ts | 87 ------------------- tests/functional/directives.ts | 4 +- tests/helpers/directives/AppendDirective.ts | 4 + .../directives/assertValidDirective.ts | 2 +- 11 files changed, 89 insertions(+), 100 deletions(-) delete mode 100644 tests/functional/apollo-cache-control.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 51d543eec..3377f1f25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - **Breaking Change**: update `graphql-js` peer dependency to `^14.5.8` - **Breaking Change**: update `graphql-query-complexity` dependency to `^0.4.0` and drop support for `fieldConfigEstimator` (use `fieldExtensionsEstimator` instead) - update `TypeResolver` interface to match with `GraphQLTypeResolver` from `graphql-js` +- add basic support for directives with `@Directive()` decorator (#369) ### Fixes - refactor union types function syntax handling to prevent possible errors with circular refs diff --git a/src/errors/InvalidDirectiveError.ts b/src/errors/InvalidDirectiveError.ts index 6886b6331..ad060c1ee 100644 --- a/src/errors/InvalidDirectiveError.ts +++ b/src/errors/InvalidDirectiveError.ts @@ -2,7 +2,7 @@ import { DirectiveMetadata } from "../metadata/definitions/directive-metadata"; export class InvalidDirectiveError extends Error { constructor(msg: string, directive: DirectiveMetadata) { - super(`${msg} "${directive.nameOrDefinition}" `); + super(`${msg} "${directive.nameOrDefinition}"`); Object.setPrototypeOf(this, new.target.prototype); } diff --git a/src/errors/index.ts b/src/errors/index.ts index af0125e3c..b5d2a705f 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -5,6 +5,7 @@ export * from "./GeneratingSchemaError"; export * from "./ConflictingDefaultValuesError"; export * from "./ConflictingDefaultWithNullableError"; export * from "./InterfaceResolveTypeError"; +export * from "./InvalidDirectiveError"; export * from "./MissingSubscriptionTopicsError"; export * from "./NoExplicitTypeError"; export * from "./ReflectMetadataMissingError"; diff --git a/src/metadata/definitions/index.ts b/src/metadata/definitions/index.ts index 15aa7d652..70a7d6ea9 100644 --- a/src/metadata/definitions/index.ts +++ b/src/metadata/definitions/index.ts @@ -1,5 +1,6 @@ export * from "./authorized-metadata"; export * from "./class-metadata"; +export * from "./directive-metadata"; export * from "./enum-metadata"; export * from "./field-metadata"; export * from "./middleware-metadata"; diff --git a/src/metadata/metadata-storage.ts b/src/metadata/metadata-storage.ts index f69d8037c..5a6d5908c 100644 --- a/src/metadata/metadata-storage.ts +++ b/src/metadata/metadata-storage.ts @@ -102,15 +102,18 @@ export class MetadataStorage { } collectDirectiveClassMetadata(definition: DirectiveClassMetadata) { - this.classDirectives.unshift(definition); + this.classDirectives.push(definition); } collectDirectiveFieldMetadata(definition: DirectiveFieldMetadata) { - this.fieldDirectives.unshift(definition); + this.fieldDirectives.push(definition); } build() { // TODO: disable next build attempts + this.classDirectives.reverse(); + this.fieldDirectives.reverse(); + this.buildClassMetadata(this.objectTypes); this.buildClassMetadata(this.inputTypes); this.buildClassMetadata(this.argumentTypes); diff --git a/src/schema/schema-generator.ts b/src/schema/schema-generator.ts index c379a425a..0399f31ff 100644 --- a/src/schema/schema-generator.ts +++ b/src/schema/schema-generator.ts @@ -21,9 +21,9 @@ import { FieldDefinitionNode, parse, parseValue, - TypeDefinitionNode, InputObjectTypeDefinitionNode, InputValueDefinitionNode, + astFromValue, } from "graphql"; import { withFilter, ResolverFn } from "graphql-subscriptions"; @@ -32,6 +32,7 @@ import { ResolverMetadata, ParamMetadata, ClassMetadata, + DirectiveMetadata, SubscriptionResolverMetadata, } from "../metadata/definitions"; import { TypeOptions, TypeValue } from "../decorators/types"; @@ -48,12 +49,11 @@ import { MissingSubscriptionTopicsError, ConflictingDefaultValuesError, InterfaceResolveTypeError, + InvalidDirectiveError, } from "../errors"; import { ResolverFilterData, ResolverTopicData, TypeResolver } from "../interfaces"; import { getFieldMetadataFromInputType, getFieldMetadataFromObjectType } from "./utils"; import { ensureInstalledCorrectGraphQLPackage } from "../utils/graphql-version"; -import { DirectiveMetadata } from "../metadata/definitions/directive-metadata"; -import { InvalidDirectiveError } from "../errors/InvalidDirectiveError"; interface AbstractInfo { isAbstract: boolean; @@ -92,6 +92,10 @@ export interface SchemaGeneratorOptions extends BuildContextOptions { * Disable checking on build the correctness of a schema */ skipCheck?: boolean; + + /** + * Array of graphql directives + */ directives?: GraphQLDirective[]; } @@ -125,6 +129,7 @@ export abstract class SchemaGenerator { mutation: this.buildRootMutationType(options.resolvers), subscription: this.buildRootSubscriptionType(options.resolvers), types: this.buildOtherTypes(orphanedTypes), + directives: options.directives, }); BuildContext.reset(); @@ -274,7 +279,6 @@ export abstract class SchemaGenerator { return superClassTypeInfo ? superClassTypeInfo.type : undefined; }; const interfaceClasses = objectType.interfaceClasses || []; - return { target: objectType.target, isAbstract: objectType.isAbstract || false, @@ -315,6 +319,7 @@ export abstract class SchemaGenerator { : createSimpleFieldResolver(field), description: field.description, deprecationReason: field.deprecationReason, + astNode: this.getFieldDefinitionNode(field.name, field.directives), extensions: { complexity: field.complexity, }, @@ -475,6 +480,7 @@ export abstract class SchemaGenerator { resolve: createHandlerResolver(handler), description: handler.description, deprecationReason: handler.deprecationReason, + astNode: this.getFieldDefinitionNode(handler.schemaName, handler.directives), extensions: { complexity: handler.complexity, }, @@ -667,6 +673,67 @@ export abstract class SchemaGenerator { .map(it => it.type); } + private static getObjectTypeDefinitionNode( + name: string, + directiveMetas?: DirectiveMetadata[], + ): ObjectTypeDefinitionNode | undefined { + if (!directiveMetas || !directiveMetas.length) { + return; + } + + return { + kind: "ObjectTypeDefinition", + name: { + kind: "Name", + value: name, + }, + directives: directiveMetas.map(this.getDirectiveNodes), + }; + } + + private static getInputObjectTypeDefinitionNode( + name: string, + directiveMetas?: DirectiveMetadata[], + ): InputObjectTypeDefinitionNode | undefined { + if (!directiveMetas || !directiveMetas.length) { + return; + } + + return { + kind: "InputObjectTypeDefinition", + name: { + kind: "Name", + value: name, + }, + directives: directiveMetas.map(this.getDirectiveNodes), + }; + } + + private static getFieldDefinitionNode( + name: string, + directiveMetas?: DirectiveMetadata[], + ): FieldDefinitionNode | undefined { + if (!directiveMetas || !directiveMetas.length) { + return; + } + + return { + kind: "FieldDefinition", + type: { + kind: "NamedType", + name: { + kind: "Name", + value: name, + }, + }, + name: { + kind: "Name", + value: name, + }, + directives: directiveMetas.map(this.getDirectiveNodes), + }; + } + private static getInputValueDefinitionNode( name: string, directiveMetas?: DirectiveMetadata[], @@ -697,7 +764,7 @@ export abstract class SchemaGenerator { if (nameOrDefinition === "") { throw new InvalidDirectiveError( - "There must be exactly one directive defined for directive", + "You can pass only one directive to the @Directive decorator at a time for", directive, ); } @@ -740,7 +807,7 @@ export abstract class SchemaGenerator { if (directives.length !== 1) { throw new InvalidDirectiveError( - "There must be exactly one directive defined for directive", + "You can pass only one directive to the @Directive decorator at a time for", directive, ); } diff --git a/src/utils/buildSchema.ts b/src/utils/buildSchema.ts index d5dab503c..df019db86 100644 --- a/src/utils/buildSchema.ts +++ b/src/utils/buildSchema.ts @@ -24,7 +24,6 @@ export interface BuildSchemaOptions extends Omit { const resolvers = loadResolvers(options); diff --git a/tests/functional/apollo-cache-control.ts b/tests/functional/apollo-cache-control.ts deleted file mode 100644 index 5caa7e5b9..000000000 --- a/tests/functional/apollo-cache-control.ts +++ /dev/null @@ -1,87 +0,0 @@ -// tslint:disable:member-ordering -import "reflect-metadata"; -import { Field, Resolver, Query, Directive, buildSchema, ObjectType } from "../../src"; -import { getMetadataStorage } from "../../src/metadata/getMetadataStorage"; -import { ApolloServer } from "apollo-server"; - -describe("Apollo @cacheControl", () => { - beforeAll(async () => { - getMetadataStorage().clear(); - }); - - it("works with ApolloServer and @cacheControl", async () => { - @ObjectType() - @Directive("@cacheControl(maxAge: 240)") - class Post { - @Field() - id: number = 1; - - @Field() - @Directive("@cacheControl(maxAge: 30)") - votes: number = 1; - - @Field(() => [Comment]) - comments: Comment[] = [new Comment()]; - - @Field() - @Directive("@cacheControl(scope: PRIVATE)") - readByCurrentUser: boolean = true; - } - - @ObjectType() - @Directive("@cacheControl(maxAge: 1000)") - class Comment { - @Field() - comment: string = "comment"; - } - - @Resolver() - class PostsResolver { - @Query(() => Post) - @Directive("@cacheControl(maxAge: 10)") - latestPost(): Post { - return new Post(); - } - } - - const server = new ApolloServer({ - schema: await buildSchema({ resolvers: [PostsResolver] }), - cacheControl: true, - }); - - const { extensions } = await server.executeOperation({ - query: `query { - latestPost { - id - votes - comments { comment } - readByCurrentUser - } - }`, - }); - - expect(extensions).toBeDefined(); - expect(extensions).toHaveProperty("cacheControl"); - expect(extensions!.cacheControl).toEqual({ - version: 1, - hints: [ - { - maxAge: 10, - path: ["latestPost"], - }, - { - maxAge: 30, - path: ["latestPost", "votes"], - }, - { - maxAge: 1000, - path: ["latestPost", "comments"], - }, - { - path: ["latestPost", "readByCurrentUser"], - scope: "PRIVATE", - }, - ], - }); - }); -}); diff --git a/tests/functional/directives.ts b/tests/functional/directives.ts index baf7fc61e..14fe50c04 100644 --- a/tests/functional/directives.ts +++ b/tests/functional/directives.ts @@ -423,7 +423,7 @@ describe("Directives", () => { expect(err).toBeInstanceOf(InvalidDirectiveError); const error: InvalidDirectiveError = err; expect(error.message).toContain( - 'There must be exactly one directive defined for directive "@upper @append"', + 'You can pass only one directive to the @Directive decorator at a time for "@upper @append"', ); } }); @@ -467,7 +467,7 @@ describe("Directives", () => { expect(err).toBeInstanceOf(InvalidDirectiveError); const error: InvalidDirectiveError = err; expect(error.message).toContain( - 'There must be exactly one directive defined for directive ""', + 'You can pass only one directive to the @Directive decorator at a time for ""', ); } }); diff --git a/tests/helpers/directives/AppendDirective.ts b/tests/helpers/directives/AppendDirective.ts index a5a17c245..8f017794d 100644 --- a/tests/helpers/directives/AppendDirective.ts +++ b/tests/helpers/directives/AppendDirective.ts @@ -14,7 +14,11 @@ export class AppendDirective extends SchemaDirectiveVisitor { field.args.push({ name: "append", + description: "Appends a string to the end of a field", type: GraphQLString, + defaultValue: "", + extensions: {}, + astNode: undefined, }); field.resolve = async function(source, { append, ...otherArgs }, context, info) { diff --git a/tests/helpers/directives/assertValidDirective.ts b/tests/helpers/directives/assertValidDirective.ts index 32139ddbc..e50e79cf2 100644 --- a/tests/helpers/directives/assertValidDirective.ts +++ b/tests/helpers/directives/assertValidDirective.ts @@ -15,7 +15,7 @@ export function assertValidDirective( throw new Error(`Directive with name ${name} does not exist`); } - const directives = (astNode || {}).directives || []; + const directives = (astNode && astNode.directives) || []; const directive = directives.find(it => it.name.kind === "Name" && it.name.value === name); From 8c1795d33a8d7e95a13647747761bcc51cda953a Mon Sep 17 00:00:00 2001 From: Baptist BENOIST Date: Thu, 10 Oct 2019 18:44:08 +0200 Subject: [PATCH 11/17] Rewrite InvalidDirectiveError messages --- src/errors/InvalidDirectiveError.ts | 5 ++--- src/schema/schema-generator.ts | 10 +++++----- tests/functional/directives.ts | 8 +++++--- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/errors/InvalidDirectiveError.ts b/src/errors/InvalidDirectiveError.ts index ad060c1ee..d55a373d9 100644 --- a/src/errors/InvalidDirectiveError.ts +++ b/src/errors/InvalidDirectiveError.ts @@ -1,9 +1,8 @@ import { DirectiveMetadata } from "../metadata/definitions/directive-metadata"; export class InvalidDirectiveError extends Error { - constructor(msg: string, directive: DirectiveMetadata) { - super(`${msg} "${directive.nameOrDefinition}"`); - + constructor(msg: string) { + super(msg); Object.setPrototypeOf(this, new.target.prototype); } } diff --git a/src/schema/schema-generator.ts b/src/schema/schema-generator.ts index 0399f31ff..e080a2ee9 100644 --- a/src/schema/schema-generator.ts +++ b/src/schema/schema-generator.ts @@ -764,8 +764,7 @@ export abstract class SchemaGenerator { if (nameOrDefinition === "") { throw new InvalidDirectiveError( - "You can pass only one directive to the @Directive decorator at a time for", - directive, + "Please pass at-least one directive name or definition to the @Directive decorator", ); } @@ -802,13 +801,14 @@ export abstract class SchemaGenerator { }); } } catch (err) { - throw new InvalidDirectiveError("Error parsing directive", directive); + throw new InvalidDirectiveError( + `Error parsing directive definition "${directive.nameOrDefinition}"`, + ); } if (directives.length !== 1) { throw new InvalidDirectiveError( - "You can pass only one directive to the @Directive decorator at a time for", - directive, + `Please pass only one directive name or definition at a time to the @Directive decorator "${directive.nameOrDefinition}"`, ); } diff --git a/tests/functional/directives.ts b/tests/functional/directives.ts index 14fe50c04..785832307 100644 --- a/tests/functional/directives.ts +++ b/tests/functional/directives.ts @@ -423,7 +423,7 @@ describe("Directives", () => { expect(err).toBeInstanceOf(InvalidDirectiveError); const error: InvalidDirectiveError = err; expect(error.message).toContain( - 'You can pass only one directive to the @Directive decorator at a time for "@upper @append"', + 'Please pass only one directive name or definition at a time to the @Directive decorator "@upper @append"', ); } }); @@ -445,7 +445,9 @@ describe("Directives", () => { } catch (err) { expect(err).toBeInstanceOf(InvalidDirectiveError); const error: InvalidDirectiveError = err; - expect(error.message).toContain('Error parsing directive "@invalid(@directive)"'); + expect(error.message).toContain( + 'Error parsing directive definition "@invalid(@directive)"', + ); } }); @@ -467,7 +469,7 @@ describe("Directives", () => { expect(err).toBeInstanceOf(InvalidDirectiveError); const error: InvalidDirectiveError = err; expect(error.message).toContain( - 'You can pass only one directive to the @Directive decorator at a time for ""', + "Please pass at-least one directive name or definition to the @Directive decorator", ); } }); From 151d66503aee0e6eaeba85bbd0c3e2ce70b2e936 Mon Sep 17 00:00:00 2001 From: Baptist BENOIST Date: Thu, 10 Oct 2019 19:00:50 +0200 Subject: [PATCH 12/17] Remove useless import in InvalidDirectiveError --- src/errors/InvalidDirectiveError.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/errors/InvalidDirectiveError.ts b/src/errors/InvalidDirectiveError.ts index d55a373d9..a13d179e8 100644 --- a/src/errors/InvalidDirectiveError.ts +++ b/src/errors/InvalidDirectiveError.ts @@ -1,5 +1,3 @@ -import { DirectiveMetadata } from "../metadata/definitions/directive-metadata"; - export class InvalidDirectiveError extends Error { constructor(msg: string) { super(msg); From 335d4a7858b75267a9eee609418cc01f9fe893bb Mon Sep 17 00:00:00 2001 From: Baptist BENOIST Date: Thu, 10 Oct 2019 19:01:37 +0200 Subject: [PATCH 13/17] Fix typo in directive-metadata.ts --- src/metadata/definitions/directive-metadata.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/metadata/definitions/directive-metadata.ts b/src/metadata/definitions/directive-metadata.ts index b1ae51674..c972d8cf3 100644 --- a/src/metadata/definitions/directive-metadata.ts +++ b/src/metadata/definitions/directive-metadata.ts @@ -7,6 +7,7 @@ export interface DirectiveClassMetadata { target: Function; directive: DirectiveMetadata; } + export interface DirectiveFieldMetadata { target: Function; field: string; From 5855fb2874fdce42e4f18c34f973ee80d337aa41 Mon Sep 17 00:00:00 2001 From: Baptist BENOIST Date: Thu, 10 Oct 2019 19:17:16 +0200 Subject: [PATCH 14/17] Export actual GraphQL Input and Field types in their directive definitions --- src/schema/schema-generator.ts | 37 +++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/schema/schema-generator.ts b/src/schema/schema-generator.ts index e080a2ee9..2c37fdd60 100644 --- a/src/schema/schema-generator.ts +++ b/src/schema/schema-generator.ts @@ -311,15 +311,20 @@ export abstract class SchemaGenerator { (resolver.resolverClassMetadata === undefined || resolver.resolverClassMetadata.isAbstract === false), ); + const type = this.getGraphQLOutputType( + field.name, + field.getType(), + field.typeOptions, + ); fieldsMap[field.schemaName] = { - type: this.getGraphQLOutputType(field.name, field.getType(), field.typeOptions), + type, args: this.generateHandlerArgs(field.params!), resolve: fieldResolverMetadata ? createAdvancedFieldResolver(fieldResolverMetadata) : createSimpleFieldResolver(field), description: field.description, deprecationReason: field.deprecationReason, - astNode: this.getFieldDefinitionNode(field.name, field.directives), + astNode: this.getFieldDefinitionNode(field.name, type, field.directives), extensions: { complexity: field.complexity, }, @@ -380,11 +385,16 @@ export abstract class SchemaGenerator { inputType.name, ); + const type = this.getGraphQLInputType( + field.name, + field.getType(), + field.typeOptions, + ); fieldsMap[field.schemaName] = { description: field.description, - type: this.getGraphQLInputType(field.name, field.getType(), field.typeOptions), + type, defaultValue: field.typeOptions.defaultValue, - astNode: this.getInputValueDefinitionNode(field.name, field.directives), + astNode: this.getInputValueDefinitionNode(field.name, type, field.directives), }; return fieldsMap; }, @@ -470,17 +480,18 @@ export abstract class SchemaGenerator { if (handler.resolverClassMetadata && handler.resolverClassMetadata.isAbstract) { return fields; } + const type = this.getGraphQLOutputType( + handler.methodName, + handler.getReturnType(), + handler.returnTypeOptions, + ); fields[handler.schemaName] = { - type: this.getGraphQLOutputType( - handler.methodName, - handler.getReturnType(), - handler.returnTypeOptions, - ), + type, args: this.generateHandlerArgs(handler.params!), resolve: createHandlerResolver(handler), description: handler.description, deprecationReason: handler.deprecationReason, - astNode: this.getFieldDefinitionNode(handler.schemaName, handler.directives), + astNode: this.getFieldDefinitionNode(handler.schemaName, type, handler.directives), extensions: { complexity: handler.complexity, }, @@ -711,6 +722,7 @@ export abstract class SchemaGenerator { private static getFieldDefinitionNode( name: string, + type: GraphQLOutputType, directiveMetas?: DirectiveMetadata[], ): FieldDefinitionNode | undefined { if (!directiveMetas || !directiveMetas.length) { @@ -723,7 +735,7 @@ export abstract class SchemaGenerator { kind: "NamedType", name: { kind: "Name", - value: name, + value: type.toString(), }, }, name: { @@ -736,6 +748,7 @@ export abstract class SchemaGenerator { private static getInputValueDefinitionNode( name: string, + type: GraphQLInputType, directiveMetas?: DirectiveMetadata[], ): InputValueDefinitionNode | undefined { if (!directiveMetas || !directiveMetas.length) { @@ -748,7 +761,7 @@ export abstract class SchemaGenerator { kind: "NamedType", name: { kind: "Name", - value: name, + value: type.toString(), }, }, name: { From 2bb59ac33aa705a7a7daa1a0315cb5a46f9f815f Mon Sep 17 00:00:00 2001 From: Baptist BENOIST Date: Thu, 10 Oct 2019 19:19:02 +0200 Subject: [PATCH 15/17] Revert blank line at the beginning of buildTypeDefsAndResolvers.ts As requested by @MichalLytek in #369 --- src/utils/buildTypeDefsAndResolvers.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/buildTypeDefsAndResolvers.ts b/src/utils/buildTypeDefsAndResolvers.ts index 2aab530b0..abafb39df 100644 --- a/src/utils/buildTypeDefsAndResolvers.ts +++ b/src/utils/buildTypeDefsAndResolvers.ts @@ -1,4 +1,5 @@ import { printSchema } from "graphql"; + import { BuildSchemaOptions, buildSchema } from "./buildSchema"; import { createResolversMap } from "./createResolversMap"; From c15089199659759cea82fada9e6fc24a45971ad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Lytek?= Date: Tue, 29 Oct 2019 21:04:21 +0100 Subject: [PATCH 16/17] docs(directives): add proper docs about using directives --- docs/directives.md | 107 ++++++++++++++++++++++++++++++++++++++++++ website/i18n/en.json | 3 ++ website/sidebars.json | 2 +- 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 docs/directives.md diff --git a/docs/directives.md b/docs/directives.md new file mode 100644 index 000000000..3d6c4c5b0 --- /dev/null +++ b/docs/directives.md @@ -0,0 +1,107 @@ +--- +title: Directives +--- + +[GraphQL directives](https://www.apollographql.com/docs/graphql-tools/schema-directives/) though the syntax might remind the TS decorators: + +> A directive is an identifier preceded by a @ character, optionally followed by a list of named arguments, which can appear after almost any form of syntax in the GraphQL query or schema languages. + +But in fact, they are a purely SDL (Schema Definition Language) feature that allows you to put some metadata for selected type or its field: + +```graphql +type Foo @auth(requires: USER) { + field: String! +} + +type Bar { + field: String! @auth(requires: USER) +} +``` + +That metadata can be read in runtime to modify the structure and behavior of a GraphQL schema to support reusable code and tasks like authentication, permission, formatting, and plenty more. They are also really useful for some external services like [Apollo Cache Control](https://www.apollographql.com/docs/apollo-server/performance/caching/#adding-cache-hints-statically-in-your-schema) or [Apollo Federation](https://www.apollographql.com/docs/apollo-server/federation/introduction/#federated-schema-example). + +**TypeGraphQL** of course provides some basic support for using the schema directives via the `@Directive` decorator. + +## Usage + +### Declaring in schema + +Basically, there are two supported ways of declaring the usage of directives: + +- string based - just like in SDL, with the `@` syntax: + +```typescript +@Directive('@deprecated(reason: "Use newField")') +``` + +- object based - using a JS object to pass the named arguments + +```typescript +@Directive("deprecated", { reason: "Use newField" }) // syntax without `@` !!! +``` + +Currently, you can use the directives only on object types, input types and their fields, as well as queries and mutations. + +So the `@Directive` decorator can be placed over the class property/method or over the class itself, depending on the needs and the placements supported by the implementation: + +```typescript +@Directive("@auth(requires: USER)") +@ObjectType() +class Foo { + @Field() + field: string; +} + +@ObjectType() +class Bar { + @Directive("auth", { requires: "USER" }) + @Field() + field: string; +} + +@Resolver() +class FooBarResolver { + @Directive("@auth(requires: USER)") + @Query() + foobar(@Arg("baz") baz: string): string { + return "foobar"; + } +} +``` + +> Note that even as directives are a purely SDL thing, they won't appear in the generated schema definition file. Current implementation of directives in TypeGraphQL is using some crazy workarounds because [`graphql-js` doesn't support setting them by code](https://github.com/graphql/graphql-js/issues/1343) and the built-in `printSchema` utility omits the directives while printing. + +Also please note that `@Directive` can only contain a single GraphQL directive name or declaration. If you need to have multiple directives declared, just place multiple decorators: + +```typescript +@ObjectType() +class Foo { + @Directive("lowercase") + @Directive('@deprecated(reason: "Use `newField`")') + @Directive("hasRole", { role: Role.Manager }) + @Field() + bar: string; +} +``` + +### Providing the implementation + +Besides declaring the usage of directives, you also have to register the runtime part of the used directives. + +> Be aware that TypeGraphQL doesn't have any special way for implementing schema directives. You should use some [3rd party libraries](https://www.apollographql.com/docs/graphql-tools/schema-directives/#implementing-schema-directives) depending on the tool set you use in your project, e.g. `graphql-tools` or `ApolloServer`. + +Here is an example using the [`graphql-tools`](https://github.com/apollographql/graphql-tools): + +```typescript +import { SchemaDirectiveVisitor } from "graphql-tools"; + +// build the schema as always +const schema = buildSchemaSync({ + resolvers: [SampleResolver], +}); + +// register the used directives implementations +SchemaDirectiveVisitor.visitSchemaDirectives(schema, { + sample: SampleDirective, +}); +``` diff --git a/website/i18n/en.json b/website/i18n/en.json index 79ea443fa..94b2f79e3 100644 --- a/website/i18n/en.json +++ b/website/i18n/en.json @@ -23,6 +23,9 @@ "dependency-injection": { "title": "Dependency injection" }, + "directives": { + "title": "Directives" + }, "emit-schema": { "title": "Emitting the schema SDL" }, diff --git a/website/sidebars.json b/website/sidebars.json index a69baf93e..bab2e3dcc 100644 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -8,7 +8,7 @@ "resolvers", "bootstrap" ], - "Advanced guides": ["scalars", "enums", "unions", "interfaces", "subscriptions"], + "Advanced guides": ["scalars", "enums", "unions", "interfaces", "subscriptions", "directives"], "Features": [ "dependency-injection", "authorization", From b949823770d23cd43e4527ceeb5d1344f3c4592b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Lytek?= Date: Fri, 1 Nov 2019 19:57:29 +0100 Subject: [PATCH 17/17] Refactor directives generation --- src/decorators/Directive.ts | 23 ++- .../definitions/directive-metadata.ts | 2 +- src/metadata/metadata-storage.ts | 4 +- src/schema/definition-node.ts | 155 ++++++++++++++++ src/schema/schema-generator.ts | 170 ++---------------- src/utils/buildSchema.ts | 2 +- 6 files changed, 181 insertions(+), 175 deletions(-) create mode 100644 src/schema/definition-node.ts diff --git a/src/decorators/Directive.ts b/src/decorators/Directive.ts index b7a3cd1c7..6dbeeb666 100644 --- a/src/decorators/Directive.ts +++ b/src/decorators/Directive.ts @@ -9,24 +9,23 @@ export function Directive( ): MethodAndPropDecorator & ClassDecorator; export function Directive( nameOrDefinition: string, - args?: Record, + args: Record = {}, ): MethodDecorator | PropertyDecorator | ClassDecorator { return (targetOrPrototype, propertyKey, descriptor) => { - const directive = { nameOrDefinition, args: args || {} }; + const directive = { nameOrDefinition, args }; - if (!propertyKey) { - getMetadataStorage().collectDirectiveClassMetadata({ - target: targetOrPrototype as Function, + if (typeof propertyKey === "symbol") { + throw new SymbolKeysNotSupportedError(); + } + if (propertyKey) { + getMetadataStorage().collectDirectiveFieldMetadata({ + target: targetOrPrototype.constructor, + fieldName: propertyKey, directive, }); } else { - if (typeof propertyKey === "symbol") { - throw new SymbolKeysNotSupportedError(); - } - - getMetadataStorage().collectDirectiveFieldMetadata({ - target: targetOrPrototype.constructor, - field: propertyKey, + getMetadataStorage().collectDirectiveClassMetadata({ + target: targetOrPrototype as Function, directive, }); } diff --git a/src/metadata/definitions/directive-metadata.ts b/src/metadata/definitions/directive-metadata.ts index c972d8cf3..b5c46b169 100644 --- a/src/metadata/definitions/directive-metadata.ts +++ b/src/metadata/definitions/directive-metadata.ts @@ -10,6 +10,6 @@ export interface DirectiveClassMetadata { export interface DirectiveFieldMetadata { target: Function; - field: string; + fieldName: string; directive: DirectiveMetadata; } diff --git a/src/metadata/metadata-storage.ts b/src/metadata/metadata-storage.ts index 5a6d5908c..715fbdb74 100644 --- a/src/metadata/metadata-storage.ts +++ b/src/metadata/metadata-storage.ts @@ -163,7 +163,7 @@ export class MetadataStorage { ), ); field.directives = this.fieldDirectives - .filter(it => it.target === field.target && it.field === field.name) + .filter(it => it.target === field.target && it.fieldName === field.name) .map(it => it.directive); }); def.fields = fields; @@ -189,7 +189,7 @@ export class MetadataStorage { ), ); def.directives = this.fieldDirectives - .filter(it => it.target === def.target && it.field === def.methodName) + .filter(it => it.target === def.target && it.fieldName === def.methodName) .map(it => it.directive); }); } diff --git a/src/schema/definition-node.ts b/src/schema/definition-node.ts new file mode 100644 index 000000000..c76a51744 --- /dev/null +++ b/src/schema/definition-node.ts @@ -0,0 +1,155 @@ +import { + ObjectTypeDefinitionNode, + InputObjectTypeDefinitionNode, + GraphQLOutputType, + FieldDefinitionNode, + GraphQLInputType, + InputValueDefinitionNode, + DirectiveNode, + parseValue, + DocumentNode, + parse, +} from "graphql"; + +import { InvalidDirectiveError } from "../errors"; +import { DirectiveMetadata } from "../metadata/definitions"; + +export function getObjectTypeDefinitionNode( + name: string, + directiveMetadata?: DirectiveMetadata[], +): ObjectTypeDefinitionNode | undefined { + if (!directiveMetadata || !directiveMetadata.length) { + return; + } + + return { + kind: "ObjectTypeDefinition", + name: { + kind: "Name", + // FIXME: use proper AST representation + value: name, + }, + directives: directiveMetadata.map(getDirectiveNode), + }; +} + +export function getInputObjectTypeDefinitionNode( + name: string, + directiveMetadata?: DirectiveMetadata[], +): InputObjectTypeDefinitionNode | undefined { + if (!directiveMetadata || !directiveMetadata.length) { + return; + } + + return { + kind: "InputObjectTypeDefinition", + name: { + kind: "Name", + // FIXME: use proper AST representation + value: name, + }, + directives: directiveMetadata.map(getDirectiveNode), + }; +} + +export function getFieldDefinitionNode( + name: string, + type: GraphQLOutputType, + directiveMetadata?: DirectiveMetadata[], +): FieldDefinitionNode | undefined { + if (!directiveMetadata || !directiveMetadata.length) { + return; + } + + return { + kind: "FieldDefinition", + type: { + kind: "NamedType", + name: { + kind: "Name", + value: type.toString(), + }, + }, + name: { + kind: "Name", + value: name, + }, + directives: directiveMetadata.map(getDirectiveNode), + }; +} + +export function getInputValueDefinitionNode( + name: string, + type: GraphQLInputType, + directiveMetadata?: DirectiveMetadata[], +): InputValueDefinitionNode | undefined { + if (!directiveMetadata || !directiveMetadata.length) { + return; + } + + return { + kind: "InputValueDefinition", + type: { + kind: "NamedType", + name: { + kind: "Name", + value: type.toString(), + }, + }, + name: { + kind: "Name", + value: name, + }, + directives: directiveMetadata.map(getDirectiveNode), + }; +} + +export function getDirectiveNode(directive: DirectiveMetadata): DirectiveNode { + const { nameOrDefinition, args } = directive; + + if (nameOrDefinition === "") { + throw new InvalidDirectiveError( + "Please pass at-least one directive name or definition to the @Directive decorator", + ); + } + + if (!nameOrDefinition.startsWith("@")) { + return { + kind: "Directive", + name: { + kind: "Name", + value: nameOrDefinition, + }, + arguments: Object.keys(args).map(argKey => ({ + kind: "Argument", + name: { + kind: "Name", + value: argKey, + }, + value: parseValue(args[argKey]), + })), + }; + } + + let parsed: DocumentNode; + try { + parsed = parse(`type String ${nameOrDefinition}`); + } catch (err) { + throw new InvalidDirectiveError( + `Error parsing directive definition "${directive.nameOrDefinition}"`, + ); + } + + const definitions = parsed.definitions as ObjectTypeDefinitionNode[]; + const directives = definitions + .filter(it => it.directives && it.directives.length > 0) + .map(it => it.directives!) + .reduce((acc, item) => [...acc, ...item]); // flatten the array + + if (directives.length !== 1) { + throw new InvalidDirectiveError( + `Please pass only one directive name or definition at a time to the @Directive decorator "${directive.nameOrDefinition}"`, + ); + } + return directives[0]; +} diff --git a/src/schema/schema-generator.ts b/src/schema/schema-generator.ts index 2c37fdd60..6c77b29db 100644 --- a/src/schema/schema-generator.ts +++ b/src/schema/schema-generator.ts @@ -16,14 +16,6 @@ import { GraphQLUnionType, GraphQLTypeResolver, GraphQLDirective, - DirectiveNode, - ObjectTypeDefinitionNode, - FieldDefinitionNode, - parse, - parseValue, - InputObjectTypeDefinitionNode, - InputValueDefinitionNode, - astFromValue, } from "graphql"; import { withFilter, ResolverFn } from "graphql-subscriptions"; @@ -32,7 +24,6 @@ import { ResolverMetadata, ParamMetadata, ClassMetadata, - DirectiveMetadata, SubscriptionResolverMetadata, } from "../metadata/definitions"; import { TypeOptions, TypeValue } from "../decorators/types"; @@ -49,11 +40,16 @@ import { MissingSubscriptionTopicsError, ConflictingDefaultValuesError, InterfaceResolveTypeError, - InvalidDirectiveError, } from "../errors"; import { ResolverFilterData, ResolverTopicData, TypeResolver } from "../interfaces"; import { getFieldMetadataFromInputType, getFieldMetadataFromObjectType } from "./utils"; import { ensureInstalledCorrectGraphQLPackage } from "../utils/graphql-version"; +import { + getFieldDefinitionNode, + getInputObjectTypeDefinitionNode, + getInputValueDefinitionNode, + getObjectTypeDefinitionNode, +} from "./definition-node"; interface AbstractInfo { isAbstract: boolean; @@ -285,7 +281,7 @@ export abstract class SchemaGenerator { type: new GraphQLObjectType({ name: objectType.name, description: objectType.description, - astNode: this.getObjectTypeDefinitionNode(objectType.name, objectType.directives), + astNode: getObjectTypeDefinitionNode(objectType.name, objectType.directives), interfaces: () => { let interfaces = interfaceClasses.map( interfaceClass => @@ -324,7 +320,7 @@ export abstract class SchemaGenerator { : createSimpleFieldResolver(field), description: field.description, deprecationReason: field.deprecationReason, - astNode: this.getFieldDefinitionNode(field.name, type, field.directives), + astNode: getFieldDefinitionNode(field.name, type, field.directives), extensions: { complexity: field.complexity, }, @@ -394,7 +390,7 @@ export abstract class SchemaGenerator { description: field.description, type, defaultValue: field.typeOptions.defaultValue, - astNode: this.getInputValueDefinitionNode(field.name, type, field.directives), + astNode: getInputValueDefinitionNode(field.name, type, field.directives), }; return fieldsMap; }, @@ -410,7 +406,7 @@ export abstract class SchemaGenerator { } return fields; }, - astNode: this.getInputObjectTypeDefinitionNode(inputType.name, inputType.directives), + astNode: getInputObjectTypeDefinitionNode(inputType.name, inputType.directives), }), }; }); @@ -491,7 +487,7 @@ export abstract class SchemaGenerator { resolve: createHandlerResolver(handler), description: handler.description, deprecationReason: handler.deprecationReason, - astNode: this.getFieldDefinitionNode(handler.schemaName, type, handler.directives), + astNode: getFieldDefinitionNode(handler.schemaName, type, handler.directives), extensions: { complexity: handler.complexity, }, @@ -683,148 +679,4 @@ export abstract class SchemaGenerator { .filter(it => !it.isAbstract && (!orphanedTypes || orphanedTypes.includes(it.target))) .map(it => it.type); } - - private static getObjectTypeDefinitionNode( - name: string, - directiveMetas?: DirectiveMetadata[], - ): ObjectTypeDefinitionNode | undefined { - if (!directiveMetas || !directiveMetas.length) { - return; - } - - return { - kind: "ObjectTypeDefinition", - name: { - kind: "Name", - value: name, - }, - directives: directiveMetas.map(this.getDirectiveNodes), - }; - } - - private static getInputObjectTypeDefinitionNode( - name: string, - directiveMetas?: DirectiveMetadata[], - ): InputObjectTypeDefinitionNode | undefined { - if (!directiveMetas || !directiveMetas.length) { - return; - } - - return { - kind: "InputObjectTypeDefinition", - name: { - kind: "Name", - value: name, - }, - directives: directiveMetas.map(this.getDirectiveNodes), - }; - } - - private static getFieldDefinitionNode( - name: string, - type: GraphQLOutputType, - directiveMetas?: DirectiveMetadata[], - ): FieldDefinitionNode | undefined { - if (!directiveMetas || !directiveMetas.length) { - return; - } - - return { - kind: "FieldDefinition", - type: { - kind: "NamedType", - name: { - kind: "Name", - value: type.toString(), - }, - }, - name: { - kind: "Name", - value: name, - }, - directives: directiveMetas.map(this.getDirectiveNodes), - }; - } - - private static getInputValueDefinitionNode( - name: string, - type: GraphQLInputType, - directiveMetas?: DirectiveMetadata[], - ): InputValueDefinitionNode | undefined { - if (!directiveMetas || !directiveMetas.length) { - return; - } - - return { - kind: "InputValueDefinition", - type: { - kind: "NamedType", - name: { - kind: "Name", - value: type.toString(), - }, - }, - name: { - kind: "Name", - value: name, - }, - directives: directiveMetas.map(this.getDirectiveNodes), - }; - } - - private static getDirectiveNodes(directive: DirectiveMetadata): DirectiveNode { - const { nameOrDefinition, args } = directive; - - if (nameOrDefinition === "") { - throw new InvalidDirectiveError( - "Please pass at-least one directive name or definition to the @Directive decorator", - ); - } - - if (!nameOrDefinition.startsWith("@")) { - return { - kind: "Directive", - name: { - kind: "Name", - value: nameOrDefinition, - }, - arguments: Object.keys(args).map(argKey => ({ - kind: "Argument", - name: { - kind: "Name", - value: argKey, - }, - value: parseValue(args[argKey]), - })), - }; - } - - let directives: DirectiveNode[] = []; - - try { - const parsed = parse(`type String ${nameOrDefinition}`); - - const definitions = parsed.definitions as ObjectTypeDefinitionNode[]; - - if (definitions && definitions.length > 0) { - definitions.forEach(def => { - if (def.directives && def.directives.length > 0) { - directives = [...directives, ...def.directives]; - } - }); - } - } catch (err) { - throw new InvalidDirectiveError( - `Error parsing directive definition "${directive.nameOrDefinition}"`, - ); - } - - if (directives.length !== 1) { - throw new InvalidDirectiveError( - `Please pass only one directive name or definition at a time to the @Directive decorator "${directive.nameOrDefinition}"`, - ); - } - - return directives[0]; - } } diff --git a/src/utils/buildSchema.ts b/src/utils/buildSchema.ts index df019db86..00bcf5c05 100644 --- a/src/utils/buildSchema.ts +++ b/src/utils/buildSchema.ts @@ -1,4 +1,4 @@ -import { GraphQLSchema, GraphQLDirective } from "graphql"; +import { GraphQLSchema } from "graphql"; import { Options as PrintSchemaOptions } from "graphql/utilities/schemaPrinter"; import * as path from "path";