From 986c92598d761c78a8d5a8fb6c7bec4015b58478 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 28 Jun 2018 17:06:45 -0600 Subject: [PATCH] feature: added generated graph types --- package-lock.json | 9 ++ package.json | 4 +- scripts/types.js | 96 +++++++++++++++++++ .../graph/management/resolvers/index.ts | 8 +- .../server/graph/management/schema/index.ts | 4 +- .../graph/management/schema/schema.graphql | 25 ++--- .../server/graph/tenant/loaders/comments.ts | 10 +- .../server/graph/tenant/mutators/comment.ts | 6 +- .../server/graph/tenant/resolvers/asset.ts | 9 +- .../server/graph/tenant/resolvers/comment.ts | 12 ++- .../server/graph/tenant/resolvers/index.ts | 8 +- .../server/graph/tenant/resolvers/mutation.ts | 22 +---- .../server/graph/tenant/resolvers/query.ts | 14 ++- src/core/server/graph/tenant/schema/index.ts | 4 +- .../server/graph/tenant/schema/schema.graphql | 27 ++++-- src/core/server/logger.ts | 5 +- src/core/server/models/comment.ts | 29 +++--- 17 files changed, 201 insertions(+), 91 deletions(-) create mode 100644 scripts/types.js diff --git a/package-lock.json b/package-lock.json index c61ddd7fb..0d6ef44fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9125,6 +9125,15 @@ "cross-fetch": "2.0.0" } }, + "graphql-schema-typescript": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/graphql-schema-typescript/-/graphql-schema-typescript-1.2.1.tgz", + "integrity": "sha512-ipZh3Epm/Kqcy6MF5FM6uxwCMFok07q+6qyxFOa7ViRufcjzH9Y3nECmECH5WgqRGl2wR6TmskbZd5qJJrGpoA==", + "dev": true, + "requires": { + "yargs": "^11.0.0" + } + }, "graphql-subscriptions": { "version": "0.5.8", "resolved": "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-0.5.8.tgz", diff --git a/package.json b/package.json index 2ccf6c47c..86ecfe387 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "lint": "npm-run-all --parallel lint:*", "lint:server": "tslint --project ./tsconfig.json", "lint:client": "tslint --project ./src/core/client/tsconfig.json", - "docz:watch": "docz dev" + "docz:watch": "docz dev", + "postinstall": "node ./scripts/types.js" }, "author": "", "license": "Apache-2.0", @@ -95,6 +96,7 @@ "fluent-langneg": "^0.1.0", "fluent-react": "^0.7.0", "graphql-playground-middleware-express": "^1.7.0", + "graphql-schema-typescript": "^1.2.1", "html-webpack-plugin": "^3.2.0", "jest": "^23.2.0", "loader-utils": "^1.1.0", diff --git a/scripts/types.js b/scripts/types.js new file mode 100644 index 000000000..3825548e7 --- /dev/null +++ b/scripts/types.js @@ -0,0 +1,96 @@ +const { Linter, Configuration } = require("tslint"); +const { generateTSTypesAsString } = require("graphql-schema-typescript"); +const { getGraphQLConfig } = require("graphql-config"); +const path = require("path"); +const fs = require("fs"); + +function lint(files) { + const linter = new Linter({ fix: true }); + + for (const { fileName, types } of files) { + const configuration = Configuration.findConfiguration(null, fileName) + .results; + linter.lint(fileName, types, configuration); + } +} + +function getFileName(name) { + return path.join( + __dirname, + "..", + "src", + "core", + "server", + "graph", + name, + "schema", + "__generated__", + "types.ts" + ); +} + +async function main() { + const config = getGraphQLConfig(__dirname); + const projects = config.getProjects(); + + const files = [ + { + name: "tenant", + fileName: getFileName("tenant"), + config: { + contextType: "TenantContext", + importStatements: [ + 'import { Cursor } from "talk-server/models/connection";', + 'import TenantContext from "talk-server/graph/tenant/context";', + ], + customScalarType: { Cursor: "Cursor", Time: "string" }, + }, + }, + { + name: "management", + fileName: getFileName("management"), + config: { + contextType: "ManagementContext", + importStatements: [ + 'import ManagementContext from "talk-server/graph/management/context";', + ], + }, + }, + ]; + + for (const file of files) { + // Load the graph schema. + const schema = projects[file.name].getSchema(); + + // Create the generated directory. + const dir = path.dirname(file.fileName); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } + + // Create the types for this file. + file.types = await generateTSTypesAsString(schema, { + tabSpaces: 2, + typePrefix: "GQL", + contextType: "any", + strictNulls: false, + minimizeInterfaceImplementation: true, + ...file.config, + }); + } + + // Send the files off to the linter to be linted and written. + lint(files); + + return files; +} + +main() + .then(files => { + for (const { fileName } of files) { + console.log(`Generated ${fileName}`); + } + }) + .catch(err => { + console.error(err); + }); diff --git a/src/core/server/graph/management/resolvers/index.ts b/src/core/server/graph/management/resolvers/index.ts index 42b27b820..b1bcbcef8 100644 --- a/src/core/server/graph/management/resolvers/index.ts +++ b/src/core/server/graph/management/resolvers/index.ts @@ -1,5 +1,5 @@ -import Cursor from "../../common/scalars/cursor"; +import { GQLResolver } from "talk-server/graph/management/schema/__generated__/types"; -export default { - Cursor, -}; +const Resolvers: GQLResolver = {}; + +export default Resolvers; diff --git a/src/core/server/graph/management/schema/index.ts b/src/core/server/graph/management/schema/index.ts index ab7cd2856..2f094a807 100644 --- a/src/core/server/graph/management/schema/index.ts +++ b/src/core/server/graph/management/schema/index.ts @@ -1,6 +1,8 @@ +import { IResolvers } from "graphql-tools"; + import loadSchema from "talk-server/graph/common/schema"; import resolvers from "talk-server/graph/management/resolvers"; export default function getManagementSchema() { - return loadSchema("management", resolvers); + return loadSchema("management", resolvers as IResolvers); } diff --git a/src/core/server/graph/management/schema/schema.graphql b/src/core/server/graph/management/schema/schema.graphql index 67655516d..e07ccbdd9 100644 --- a/src/core/server/graph/management/schema/schema.graphql +++ b/src/core/server/graph/management/schema/schema.graphql @@ -7,27 +7,22 @@ Time represented as an ISO8601 string. """ scalar Time -""" -Cursor represents a paginating cursor. -""" -scalar Cursor - ################################################################################ ## Tenant ################################################################################ type Tenant { - id: ID! + id: ID! - """ - organizationName is the name of the organization. - """ - organizationName: String + """ + organizationName is the name of the organization. + """ + organizationName: String - """ - organizationContactEmail is the email of the organization. - """ - organizationContactEmail: String + """ + organizationContactEmail is the email of the organization. + """ + organizationContactEmail: String } ################################################################################ @@ -35,5 +30,5 @@ type Tenant { ################################################################################ type Query { - tenant(id: ID!): Tenant + tenant(id: ID!): Tenant } diff --git a/src/core/server/graph/tenant/loaders/comments.ts b/src/core/server/graph/tenant/loaders/comments.ts index 6649a50ab..0e68876e2 100644 --- a/src/core/server/graph/tenant/loaders/comments.ts +++ b/src/core/server/graph/tenant/loaders/comments.ts @@ -1,7 +1,11 @@ import DataLoader from "dataloader"; + import Context from "talk-server/graph/tenant/context"; import { - ConnectionInput, + AssetToCommentsArgs, + CommentToRepliesArgs, +} from "talk-server/graph/tenant/schema/__generated__/types"; +import { retrieveAssetConnection, retrieveMany, retrieveRepliesConnection, @@ -11,8 +15,8 @@ export default (ctx: Context) => ({ comment: new DataLoader((ids: string[]) => retrieveMany(ctx.db, ctx.tenant.id, ids) ), - forAsset: (assetID: string, input: ConnectionInput) => + forAsset: (assetID: string, input: AssetToCommentsArgs) => retrieveAssetConnection(ctx.db, ctx.tenant.id, assetID, input), - forParent: (assetID: string, parentID: string, input: ConnectionInput) => + forParent: (assetID: string, parentID: string, input: CommentToRepliesArgs) => retrieveRepliesConnection(ctx.db, ctx.tenant.id, assetID, parentID, input), }); diff --git a/src/core/server/graph/tenant/mutators/comment.ts b/src/core/server/graph/tenant/mutators/comment.ts index 1046930b9..9025d3cef 100644 --- a/src/core/server/graph/tenant/mutators/comment.ts +++ b/src/core/server/graph/tenant/mutators/comment.ts @@ -1,12 +1,12 @@ import TenantContext from "talk-server/graph/tenant/context"; -import { CreateCommentInput } from "talk-server/graph/tenant/resolvers/mutation"; +import { GQLCreateCommentInput } from "talk-server/graph/tenant/schema/__generated__/types"; import { Comment } from "talk-server/models/comment"; import { create } from "talk-server/services/comments"; export default (ctx: TenantContext) => ({ - create: (input: CreateCommentInput): Promise => { + create: (input: GQLCreateCommentInput): Promise => { // FIXME: remove tenant + user ! - return create(ctx.db, ctx.tenant!.id, { + return create(ctx.db, ctx.tenant.id, { author_id: ctx.user!.id, asset_id: input.assetID, body: input.body, diff --git a/src/core/server/graph/tenant/resolvers/asset.ts b/src/core/server/graph/tenant/resolvers/asset.ts index 7f17e8861..e5487ba08 100644 --- a/src/core/server/graph/tenant/resolvers/asset.ts +++ b/src/core/server/graph/tenant/resolvers/asset.ts @@ -1,10 +1,11 @@ -import Context from "talk-server/graph/tenant/context"; +import { GQLAssetTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types"; import { Asset } from "talk-server/models/asset"; -import { ConnectionInput } from "talk-server/models/comment"; -export default { - comments: async (asset: Asset, input: ConnectionInput, ctx: Context) => +const Asset: GQLAssetTypeResolver = { + comments: (asset, input, ctx) => ctx.loaders.Comments.forAsset(asset.id, input), // TODO: implement this. isClosed: () => false, }; + +export default Asset; diff --git a/src/core/server/graph/tenant/resolvers/comment.ts b/src/core/server/graph/tenant/resolvers/comment.ts index 22f72b5e5..4b7f67023 100644 --- a/src/core/server/graph/tenant/resolvers/comment.ts +++ b/src/core/server/graph/tenant/resolvers/comment.ts @@ -1,9 +1,11 @@ -import Context from "talk-server/graph/tenant/context"; -import { Comment, ConnectionInput } from "talk-server/models/comment"; +import { GQLCommentTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types"; +import { Comment } from "talk-server/models/comment"; -export default { - author: async (comment: Comment, _: any, ctx: Context) => +const Comment: GQLCommentTypeResolver = { + author: async (comment, args, ctx) => ctx.loaders.Users.user.load(comment.author_id), - replies: async (comment: Comment, input: ConnectionInput, ctx: Context) => + replies: async (comment, input, ctx) => ctx.loaders.Comments.forParent(comment.asset_id, comment.id, input), }; + +export default Comment; diff --git a/src/core/server/graph/tenant/resolvers/index.ts b/src/core/server/graph/tenant/resolvers/index.ts index d1143f1ff..e8d33540d 100644 --- a/src/core/server/graph/tenant/resolvers/index.ts +++ b/src/core/server/graph/tenant/resolvers/index.ts @@ -1,13 +1,17 @@ -import Cursor from "../../common/scalars/cursor"; +import Cursor from "talk-server/graph/common/scalars/cursor"; +import { GQLResolver } from "talk-server/graph/tenant/schema/__generated__/types"; + import Asset from "./asset"; import Comment from "./comment"; import Mutation from "./mutation"; import Query from "./query"; -export default { +const Resolvers: GQLResolver = { Asset, Comment, Cursor, Query, Mutation, }; + +export default Resolvers; diff --git a/src/core/server/graph/tenant/resolvers/mutation.ts b/src/core/server/graph/tenant/resolvers/mutation.ts index 4d3b82a76..f51f0fc1b 100644 --- a/src/core/server/graph/tenant/resolvers/mutation.ts +++ b/src/core/server/graph/tenant/resolvers/mutation.ts @@ -1,23 +1,7 @@ -import { ClientMutationProps } from "talk-server/graph/common/resolvers/mutation"; -import TenantContext from "talk-server/graph/tenant/context"; -import { Comment } from "talk-server/models/comment"; +import { GQLMutationTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types"; -export interface CreateCommentInput extends ClientMutationProps { - assetID: string; - parentID?: string; - body: string; -} - -export interface CreateCommentPayload extends ClientMutationProps { - comment: Comment; -} - -const Mutation = { - createComment: async ( - source: void, - input: CreateCommentInput, - ctx: TenantContext - ): Promise => ({ +const Mutation: GQLMutationTypeResolver = { + createComment: async (source, { input }, ctx) => ({ comment: await ctx.mutators.Comment.create(input), clientMutationId: input.clientMutationId, }), diff --git a/src/core/server/graph/tenant/resolvers/query.ts b/src/core/server/graph/tenant/resolvers/query.ts index ae76679a9..caf5e0a3b 100644 --- a/src/core/server/graph/tenant/resolvers/query.ts +++ b/src/core/server/graph/tenant/resolvers/query.ts @@ -1,10 +1,8 @@ -import TenantContext from "talk-server/graph/tenant/context"; +import { GQLQueryTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types"; -export default { - asset: async ( - source: void, - { id }: { id: string; url: string }, - ctx: TenantContext - ) => ctx.loaders.Assets.asset.load(id), - settings: async (parent: any, args: any, ctx: TenantContext) => ctx.tenant, +const Query: GQLQueryTypeResolver = { + asset: (source, args, ctx) => ctx.loaders.Assets.asset.load(args.id), + settings: (parent, args, ctx) => ctx.tenant, }; + +export default Query; diff --git a/src/core/server/graph/tenant/schema/index.ts b/src/core/server/graph/tenant/schema/index.ts index 93641ac66..1105c358e 100644 --- a/src/core/server/graph/tenant/schema/index.ts +++ b/src/core/server/graph/tenant/schema/index.ts @@ -1,6 +1,8 @@ +import { IResolvers } from "graphql-tools"; + import loadSchema from "talk-server/graph/common/schema"; import resolvers from "talk-server/graph/tenant/resolvers"; export default function getTenantSchema() { - return loadSchema("tenant", resolvers); + return loadSchema("tenant", resolvers as IResolvers); } diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index eb060b6a4..96657351d 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -31,9 +31,9 @@ enum MODERATION_MODE { } """ -Wordlist describes all the available wordlists. +WordlistSettings describes all the available wordlists. """ -type Wordlist { +type WordlistSettings { """ banned words will by default reject the comment if it is found. """ @@ -46,7 +46,20 @@ type Wordlist { } # Settings stores the global settings for a given installation. + +################################################################################ +## Settings +################################################################################ + +""" +Settings stores the global settings for a given Tenant. +""" type Settings { + """ + domain is the domain that is associated with this Tenant. + """ + domain: String! + """ moderation is the moderation mode for all Asset's on the site. """ @@ -152,7 +165,7 @@ type Settings { """ wordlist will return a given list of words. """ - wordlist: Wordlist! + wordlist: WordlistSettings! """ domains will return a given list of whitelisted domains. @@ -222,8 +235,8 @@ type Comment { replies will return the replies to this comment. """ replies( - first: Int = 10 - orderBy: COMMENT_SORT = CREATED_AT_DESC + first: Int! = 10 + orderBy: COMMENT_SORT! = CREATED_AT_DESC after: Cursor ): CommentsConnection } @@ -299,8 +312,8 @@ type Asset { comments are the comments on the Asset. """ comments( - first: Int = 10 - orderBy: COMMENT_SORT = CREATED_AT_DESC + first: Int! = 10 + orderBy: COMMENT_SORT! = CREATED_AT_DESC after: Cursor ): CommentsConnection diff --git a/src/core/server/logger.ts b/src/core/server/logger.ts index c3b50c355..7a9ee886e 100644 --- a/src/core/server/logger.ts +++ b/src/core/server/logger.ts @@ -1,5 +1,8 @@ import bunyan from "bunyan"; -const logger = bunyan.createLogger({ name: "talk" }); +const logger = bunyan.createLogger({ + name: "talk", + serializers: bunyan.stdSerializers, +}); export default logger; diff --git a/src/core/server/models/comment.ts b/src/core/server/models/comment.ts index 81d046159..5c0726669 100644 --- a/src/core/server/models/comment.ts +++ b/src/core/server/models/comment.ts @@ -1,11 +1,13 @@ import { merge } from "lodash"; import { Db } from "mongodb"; +import uuid from "uuid"; + import { Omit, Sub } from "talk-common/types"; +import { GQLCOMMENT_SORT } from "talk-server/graph/tenant/schema/__generated__/types"; import { ActionCounts } from "talk-server/models/actions"; import { Connection, Cursor } from "talk-server/models/connection"; import Query from "talk-server/models/query"; import { TenantResource } from "talk-server/models/tenant"; -import uuid from "uuid"; function collection(db: Db) { return db.collection>("comments"); @@ -121,16 +123,9 @@ export async function retrieveMany(db: Db, tenantID: string, ids: string[]) { return ids.map(id => comments.find(comment => comment.id === id) || null); } -export enum CommentSort { - CREATED_AT_DESC = "CREATED_AT_DESC", - CREATED_AT_ASC = "CREATED_AT_ASC", - REPLIES_DESC = "REPLIES_DESC", - RESPECT_DESC = "RESPECT_DESC", -} - export interface ConnectionInput { first: number; - orderBy: CommentSort; + orderBy: GQLCOMMENT_SORT; after?: Cursor; } @@ -144,12 +139,12 @@ export interface ConnectionInput { function nodesToEdge(input: ConnectionInput, nodes: Comment[]) { let getCursor: (comment: Comment, index: number) => Cursor; switch (input.orderBy) { - case CommentSort.CREATED_AT_DESC: - case CommentSort.CREATED_AT_ASC: + case GQLCOMMENT_SORT.CREATED_AT_DESC: + case GQLCOMMENT_SORT.CREATED_AT_ASC: getCursor = comment => comment.created_at; break; - case CommentSort.REPLIES_DESC: - case CommentSort.RESPECT_DESC: + case GQLCOMMENT_SORT.REPLIES_DESC: + case GQLCOMMENT_SORT.RESPECT_DESC: getCursor = (_, index) => (input.after ? (input.after as number) : 0) + index + 1; break; @@ -226,25 +221,25 @@ async function retrieveConnection( ) { // Apply some sorting options. switch (input.orderBy) { - case CommentSort.CREATED_AT_DESC: + case GQLCOMMENT_SORT.CREATED_AT_DESC: query.orderBy({ created_at: -1 }); if (input.after) { query.where({ created_at: { $lt: input.after as Date } }); } break; - case CommentSort.CREATED_AT_ASC: + case GQLCOMMENT_SORT.CREATED_AT_ASC: query.orderBy({ created_at: 1 }); if (input.after) { query.where({ created_at: { $gt: input.after as Date } }); } break; - case CommentSort.REPLIES_DESC: + case GQLCOMMENT_SORT.REPLIES_DESC: query.orderBy({ reply_count: -1, created_at: -1 }); if (input.after) { query.after(input.after as number); } break; - case CommentSort.RESPECT_DESC: + case GQLCOMMENT_SORT.RESPECT_DESC: query.orderBy({ "action_counts.respect": -1, created_at: -1 }); if (input.after) { query.after(input.after as number);