From c0c1b080023194ba9652e5165f9d7e2a5a27a570 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Sat, 9 Dec 2017 17:03:25 +0100 Subject: [PATCH 01/16] Add graphql lib and examples --- .env.example | 6 + .env.test | 6 + package.json | 3 + src/api/queries/GetUsersQuery.ts | 22 +++ src/api/types/UserType.ts | 32 ++++ src/app.ts | 2 + src/core/banner.ts | 3 + src/core/env.ts | 8 + src/lib/graphql/AbstractGraphQLHooks.ts | 35 ++++ src/lib/graphql/AbstractGraphQLQuery.ts | 20 ++ src/lib/graphql/GraphQLContext.ts | 10 + src/lib/graphql/MetadataArgsStorage.ts | 52 +++++ src/lib/graphql/Mutation.ts | 10 + src/lib/graphql/MutationMetadataArgs.ts | 6 + src/lib/graphql/Query.ts | 10 + src/lib/graphql/QueryMetadataArgs.ts | 6 + src/lib/graphql/graphql-error-handling.ts | 125 ++++++++++++ .../graphql/importClassesFromDirectories.ts | 34 ++++ src/lib/graphql/index.ts | 178 ++++++++++++++++++ src/loaders/graphqlLoader.ts | 18 ++ yarn.lock | 31 ++- 21 files changed, 613 insertions(+), 4 deletions(-) create mode 100644 src/api/queries/GetUsersQuery.ts create mode 100644 src/api/types/UserType.ts create mode 100644 src/lib/graphql/AbstractGraphQLHooks.ts create mode 100644 src/lib/graphql/AbstractGraphQLQuery.ts create mode 100644 src/lib/graphql/GraphQLContext.ts create mode 100644 src/lib/graphql/MetadataArgsStorage.ts create mode 100644 src/lib/graphql/Mutation.ts create mode 100644 src/lib/graphql/MutationMetadataArgs.ts create mode 100644 src/lib/graphql/Query.ts create mode 100644 src/lib/graphql/QueryMetadataArgs.ts create mode 100644 src/lib/graphql/graphql-error-handling.ts create mode 100644 src/lib/graphql/importClassesFromDirectories.ts create mode 100644 src/lib/graphql/index.ts create mode 100644 src/loaders/graphqlLoader.ts diff --git a/.env.example b/.env.example index a682af6b..3fcac450 100644 --- a/.env.example +++ b/.env.example @@ -30,6 +30,12 @@ DB_DATABASE="my_database" DB_SYNCHRONIZE=false DB_LOGGING=false +# +# GraphQL +# +GRAPHQL_ENABLED=true +GRAPHQL_ROUTE="/graphql" + # # Swagger # diff --git a/.env.test b/.env.test index f08dec70..f6e6b728 100644 --- a/.env.test +++ b/.env.test @@ -30,6 +30,12 @@ DB_DATABASE="my_database" DB_SYNCHRONIZE=false DB_LOGGING=false +# +# GraphQL +# +GRAPHQL_ENABLED=true +GRAPHQL_ROUTE="/graphql" + # # Swagger # diff --git a/package.json b/package.json index 7d979a98..0a221d99 100644 --- a/package.json +++ b/package.json @@ -59,14 +59,17 @@ "compression": "^1.7.1", "copyfiles": "^1.2.0", "cors": "^2.8.4", + "dataloader": "^1.3.0", "dotenv": "^4.0.0", "event-dispatch": "^0.4.1", "express": "^4.16.2", "express-basic-auth": "^1.1.3", + "express-graphql": "^0.6.11", "express-status-monitor": "^1.0.1", "faker": "^4.1.0", "figlet": "^1.2.0", "glob": "^7.1.2", + "graphql": "^0.11.7", "helmet": "^3.9.0", "jsonfile": "^4.0.0", "lodash": "^4.17.4", diff --git a/src/api/queries/GetUsersQuery.ts b/src/api/queries/GetUsersQuery.ts new file mode 100644 index 00000000..d0d8ee5c --- /dev/null +++ b/src/api/queries/GetUsersQuery.ts @@ -0,0 +1,22 @@ +import { GraphQLFieldConfig, GraphQLList } from 'graphql'; +import { Query, AbstractQuery, GraphQLContext } from './../../lib/graphql'; +import { UserService } from '../services/UserService'; +import { UserType } from './../types/UserType'; +import { User } from '../models/User'; +import { Logger } from '../../core/Logger'; + +@Query() +export class GetUsersQuery extends AbstractQuery, User[], any> implements GraphQLFieldConfig { + public type = new GraphQLList(UserType); + public allow = []; + public args = {}; + + private log = new Logger(__filename); + + public async run(root: any, args: any, context: GraphQLContext): Promise { + const users = await context.container.get(UserService).find(); + this.log.info(`Found ${users.length} users`); + return users; + } + +} diff --git a/src/api/types/UserType.ts b/src/api/types/UserType.ts new file mode 100644 index 00000000..70e2a5ba --- /dev/null +++ b/src/api/types/UserType.ts @@ -0,0 +1,32 @@ +import { + GraphQLID, + GraphQLString, + GraphQLObjectType, +} from 'graphql'; + +export const UserType = new GraphQLObjectType({ + name: 'User', + description: 'A single user.', + fields: { + id: { + type: GraphQLID, + description: 'The ID', + }, + firstName: { + type: GraphQLString, + description: 'The first name of the user.', + }, + lastName: { + type: GraphQLString, + description: 'The last name of the user.', + }, + email: { + type: GraphQLString, + description: 'The email of this user.', + }, + // pets: { + // type: GraphQLString, + // description: 'The personal number of this user.‚', + // }, + }, +}); diff --git a/src/app.ts b/src/app.ts index 86846c6d..d93e7b64 100644 --- a/src/app.ts +++ b/src/app.ts @@ -20,6 +20,7 @@ import { monitorLoader } from './loaders/monitorLoader'; import { homeLoader } from './loaders/homeLoader'; import { publicLoader } from './loaders/publicLoader'; import { iocLoader } from './loaders/iocLoader'; +import { graphqlLoader } from './loaders/graphqlLoader'; import { eventDispatchLoader } from './loaders/eventDispatchLoader'; @@ -38,6 +39,7 @@ bootstrapMicroframework({ monitorLoader, homeLoader, publicLoader, + graphqlLoader, ], }) .then(() => banner(log)) diff --git a/src/core/banner.ts b/src/core/banner.ts index ef494f10..a336cc3e 100644 --- a/src/core/banner.ts +++ b/src/core/banner.ts @@ -13,6 +13,9 @@ export function banner(log: Logger): void { log.info(`Version : ${env.app.version}`); log.info(``); log.info(`API Info : ${env.app.route}${env.app.routePrefix}`); + if (env.graphql.enabled) { + log.info(`GraphQL : ${env.app.route}${env.graphql.route}`); + } if (env.swagger.enabled) { log.info(`Swagger : ${env.app.route}${env.swagger.route}`); } diff --git a/src/core/env.ts b/src/core/env.ts index 73a6f444..5ab2cfa1 100644 --- a/src/core/env.ts +++ b/src/core/env.ts @@ -13,6 +13,8 @@ dotenv.config({ path: path.join(process.cwd(), `.env${((process.env.NODE_ENV === export const env = { node: process.env.NODE_ENV || 'development', isProduction: process.env.NODE_ENV === 'production', + isTest: process.env.NODE_ENV === 'test', + isDevelopment: process.env.NODE_ENV === 'development', app: { name: getOsEnv('APP_NAME'), version: (pkg as any).version, @@ -29,6 +31,8 @@ export const env = { controllers: [path.join(__dirname, '..', 'api/**/*Controller{.js,.ts}')], middlewares: [path.join(__dirname, '..', 'api/**/*Middleware{.js,.ts}')], interceptors: [path.join(__dirname, '..', 'api/**/*Interceptor{.js,.ts}')], + queries: [path.join(__dirname, '..', 'api/**/*Query{.js,.ts}')], + mutations: [path.join(__dirname, '..', 'api/**/*Mutation{.js,.ts}')], }, }, log: { @@ -49,6 +53,10 @@ export const env = { synchronize: toBool(getOsEnv('DB_SYNCHRONIZE')), logging: toBool(getOsEnv('DB_LOGGING')), }, + graphql: { + enabled: toBool(getOsEnv('GRAPHQL_ENABLED')), + route: getOsEnv('GRAPHQL_ROUTE'), + }, swagger: { enabled: toBool(getOsEnv('SWAGGER_ENABLED')), route: getOsEnv('SWAGGER_ROUTE'), diff --git a/src/lib/graphql/AbstractGraphQLHooks.ts b/src/lib/graphql/AbstractGraphQLHooks.ts new file mode 100644 index 00000000..64f36c54 --- /dev/null +++ b/src/lib/graphql/AbstractGraphQLHooks.ts @@ -0,0 +1,35 @@ +import { UserError } from './graphql-error-handling'; + +// export interface GraphQLHooks { +// before(context: C, args: A, source?: S): Promise | A; +// after(result: R, context: C, args: A, source?: S): Promise | R; +// run(rootOrSource: S, args: A, context: C): Promise | Promise | R | undefined; +// }implements GraphQLHooks + +export abstract class AbstractGraphQLHooks { + + /** + * This is our before hook. Here you are able + * to alter the args object before the actual resolver(execute) + * will be called. + */ + public before(context: TContext, args: TArgs, source?: S): Promise | TArgs { + return args; + } + + /** + * This our after hook. It will be called ater the actual resolver(execute). + * There you are able to alter the result before it is send to the client. + */ + public after(result: TResult, context: TContext, args?: TArgs, source?: S): Promise | TResult { + return result; + } + + /** + * This our resolver, which should gather the needed data; + */ + public run(rootOrSource: S, args: TArgs, context: TContext): Promise | TResult { + throw new UserError('Query not implemented!'); + } + +} diff --git a/src/lib/graphql/AbstractGraphQLQuery.ts b/src/lib/graphql/AbstractGraphQLQuery.ts new file mode 100644 index 00000000..5ccc5a3f --- /dev/null +++ b/src/lib/graphql/AbstractGraphQLQuery.ts @@ -0,0 +1,20 @@ +import { AbstractGraphQLHooks } from './AbstractGraphQLHooks'; + + +export abstract class AbstractQuery extends AbstractGraphQLHooks { + + /** + * This will be called by graphQL and they need to have it not as a + * member function of this class. We use this hook to add some more logic + * to it, like permission checking and before and after hooks to alter some data. + */ + public resolve = async (root: S, args: TArgs, context: TContext): Promise => { + // We need to store the query arguments in the context so they can be accessed by subsequent resolvers + (context as any).resolveArgs = args; + args = await this.before(context, args); + let result = await this.run(root, args, context); + result = await this.after(result, context, args); + return (result as TResult); + } + +} diff --git a/src/lib/graphql/GraphQLContext.ts b/src/lib/graphql/GraphQLContext.ts new file mode 100644 index 00000000..d0a0df25 --- /dev/null +++ b/src/lib/graphql/GraphQLContext.ts @@ -0,0 +1,10 @@ +import * as express from 'express'; +import { Container } from 'typedi'; + +export interface GraphQLContext { + container: typeof Container; + request: express.Request; + response: express.Response; + resolveArgs?: TResolveArgs; + data?: TData; +} diff --git a/src/lib/graphql/MetadataArgsStorage.ts b/src/lib/graphql/MetadataArgsStorage.ts new file mode 100644 index 00000000..e7862a7d --- /dev/null +++ b/src/lib/graphql/MetadataArgsStorage.ts @@ -0,0 +1,52 @@ +import { QueryMetadataArgs } from './QueryMetadataArgs'; +import { MutationMetadataArgs } from './MutationMetadataArgs'; + +/** + * Storage all metadatas read from decorators. + */ +export class MetadataArgsStorage { + + // ------------------------------------------------------------------------- + // Properties + // ------------------------------------------------------------------------- + + /** + * Registered controller metadata args. + */ + public queries: QueryMetadataArgs[] = []; + + /** + * Registered middleware metadata args. + */ + public mutations: MutationMetadataArgs[] = []; + + + // ------------------------------------------------------------------------- + // Public Methods + // ------------------------------------------------------------------------- + /** + * Filters registered queries by a given classes. + */ + public filterQueryMetadatasForClasses(classes: Array<() => void>): MutationMetadataArgs[] { + return this.queries.filter(ctrl => { + return classes.filter(cls => ctrl.target === cls).length > 0; + }); + } + /** + * Filters registered mutations by a given classes. + */ + public filterMutationMetadatasForClasses(classes: Array<() => void>): MutationMetadataArgs[] { + return this.mutations.filter(ctrl => { + return classes.filter(cls => ctrl.target === cls).length > 0; + }); + } + + /** + * Removes all saved metadata. + */ + public reset(): void { + this.queries = []; + this.mutations = []; + } + +} diff --git a/src/lib/graphql/Mutation.ts b/src/lib/graphql/Mutation.ts new file mode 100644 index 00000000..9cdf203a --- /dev/null +++ b/src/lib/graphql/Mutation.ts @@ -0,0 +1,10 @@ +import { getMetadataArgsStorage } from './index'; + + +export function Mutation(): any { + return (object: () => void) => { + getMetadataArgsStorage().mutations.push({ + target: object, + }); + }; +} diff --git a/src/lib/graphql/MutationMetadataArgs.ts b/src/lib/graphql/MutationMetadataArgs.ts new file mode 100644 index 00000000..7331f0da --- /dev/null +++ b/src/lib/graphql/MutationMetadataArgs.ts @@ -0,0 +1,6 @@ +export interface MutationMetadataArgs { + /** + * Indicates object which is used by this controller. + */ + target: () => void; +} diff --git a/src/lib/graphql/Query.ts b/src/lib/graphql/Query.ts new file mode 100644 index 00000000..0a6386c0 --- /dev/null +++ b/src/lib/graphql/Query.ts @@ -0,0 +1,10 @@ +import { getMetadataArgsStorage } from './index'; + + +export function Query(): any { + return (object: () => void) => { + getMetadataArgsStorage().queries.push({ + target: object, + }); + }; +} diff --git a/src/lib/graphql/QueryMetadataArgs.ts b/src/lib/graphql/QueryMetadataArgs.ts new file mode 100644 index 00000000..86ea5929 --- /dev/null +++ b/src/lib/graphql/QueryMetadataArgs.ts @@ -0,0 +1,6 @@ +export interface QueryMetadataArgs { + /** + * Indicates object which is used by this controller. + */ + target: () => void; +} diff --git a/src/lib/graphql/graphql-error-handling.ts b/src/lib/graphql/graphql-error-handling.ts new file mode 100644 index 00000000..d286c303 --- /dev/null +++ b/src/lib/graphql/graphql-error-handling.ts @@ -0,0 +1,125 @@ +// This feature is a copy from https://github.com/kadirahq/graphql-errors +import * as uuid from 'uuid'; +import { GraphQLObjectType, GraphQLSchema } from 'graphql'; + +import { env } from '../../core/env'; +import { Logger } from '../../core/Logger'; +const logger = new Logger('app:errors'); + + +// Mark field/type/schema +export const Processed = Symbol(); + +// Used to identify UserErrors +export const IsUserError = Symbol(); + +// UserErrors will be sent to the user +export class UserError extends Error { + constructor(...args: any[]) { + super(args[0]); + this.name = 'Error'; + this.message = args[0]; + this[IsUserError] = true; + Error.captureStackTrace(this); + } +} + +// Modifies errors before sending to the user +export let defaultHandler = (err?) => { + if (err[IsUserError]) { + return err; + } + const errId = uuid.v4(); + err.message = `${err.message}: ${errId}`; + if (!env.isTest) { + console.error(err && err.stack || err); + } + if (env.isProduction) { + logger.error(err); + } + err.message = `500: Internal Error: ${errId}`; + return err; +}; + +const maskField = (field, fn) => { + const resolveFn = field.resolve; + if (field[Processed] || !resolveFn) { + return; + } + + field[Processed] = true; + field.resolve = async (...args) => { + try { + const out = resolveFn.call(undefined, ...args); + return await Promise.resolve(out); + } catch (e) { + throw fn(e); + } + }; + + // save the original resolve function + field.resolve._resolveFn = resolveFn; +}; + +const maskType = (type, fn) => { + if (type[Processed] || !type.getFields) { + return; + } + + const fields = type.getFields(); + for (const fieldName in fields) { + if (!Object.hasOwnProperty.call(fields, fieldName)) { + continue; + } + maskField(fields[fieldName], fn); + } +}; + +const maskSchema = (schema, fn) => { + const types = schema.getTypeMap(); + for (const typeName in types) { + if (!Object.hasOwnProperty.call(types, typeName)) { + continue; + } + maskType(types[typeName], fn); + } +}; + +// Changes the default error handler function +export const setDefaultHandler = (handlerFn) => { + defaultHandler = handlerFn; +}; + +// Masks graphql schemas, types or individual fields +export const handlingErrors = (thing, fn = defaultHandler) => { + if (thing instanceof GraphQLSchema) { + maskSchema(thing, fn); + } else if (thing instanceof GraphQLObjectType) { + maskType(thing, fn); + } else { + maskField(thing, fn); + } +}; + +export const getErrorCode = (message: string): string => { + if (hasErrorCode(message)) { + return message.substring(0, 3); + } + return '000'; // unkown error code +}; + +export const getErrorMessage = (message: string): string => { + if (hasErrorCode(message)) { + return message.substring(5); + } + return message; +}; + +export const hasErrorCode = (error: any): boolean => { + let message = error; + if (error.message) { + message = error.message; + } + const reg = new RegExp('^[0-9]{3}: '); + return reg.test(message); +}; diff --git a/src/lib/graphql/importClassesFromDirectories.ts b/src/lib/graphql/importClassesFromDirectories.ts new file mode 100644 index 00000000..299194ef --- /dev/null +++ b/src/lib/graphql/importClassesFromDirectories.ts @@ -0,0 +1,34 @@ +import * as path from 'path'; + +/** + * Loads all exported classes from the given directory. + */ +export function importClassesFromDirectories(directories: string[], formats: string[] = ['.js', '.ts']): Array<() => void> { + + const loadFileClasses = (exported: any, allLoaded: Array<() => void>) => { + if (exported instanceof Function) { + allLoaded.push(exported); + } else if (exported instanceof Array) { + exported.forEach((i: any) => loadFileClasses(i, allLoaded)); + } else if (exported instanceof Object || typeof exported === 'object') { + Object.keys(exported).forEach(key => loadFileClasses(exported[key], allLoaded)); + } + + return allLoaded; + }; + + const allFiles = directories.reduce((allDirs, dir) => { + return allDirs.concat(require('glob').sync(path.normalize(dir))); + }, [] as string[]); + + const dirs = allFiles + .filter(file => { + const dtsExtension = file.substring(file.length - 5, file.length); + return formats.indexOf(path.extname(file)) !== -1 && dtsExtension !== '.d.ts'; + }) + .map(file => { + return require(file); + }); + + return loadFileClasses(dirs, []); +} diff --git a/src/lib/graphql/index.ts b/src/lib/graphql/index.ts new file mode 100644 index 00000000..d76c953e --- /dev/null +++ b/src/lib/graphql/index.ts @@ -0,0 +1,178 @@ +import * as express from 'express'; +import * as GraphQLHTTP from 'express-graphql'; +import { GraphQLSchema, GraphQLObjectType } from 'graphql'; +import { Container as container } from 'typedi'; + +import { GraphQLContext } from './GraphQLContext'; +import { MetadataArgsStorage } from './MetadataArgsStorage'; +import { importClassesFromDirectories } from './importClassesFromDirectories'; +import { handlingErrors, getErrorCode, getErrorMessage } from './graphql-error-handling'; + +// ------------------------------------------------------------------------- +// Main exports +// ------------------------------------------------------------------------- + +export * from './Query'; +export * from './Mutation'; + +export * from './AbstractGraphQLHooks'; +export * from './AbstractGraphQLQuery'; +export * from './GraphQLContext'; +export * from './graphql-error-handling'; + +// ------------------------------------------------------------------------- +// Main Functions +// ------------------------------------------------------------------------- + +/** + * Defines the options to create a GraphQLServer + */ +export interface GraphQLServerOptions { + queries: string[]; + mutations: string[]; + route?: string; + editorEnabled?: boolean; + contextData?: TData; +} + +/** + * Create GraphQL Server and bind it to the gieven express app + */ +export function createGraphQLServer(expressApp: express.Application, options: GraphQLServerOptions): void { + // collect queries & mutaions for our graphql schema + const schema = createSchema({ + queries: options.queries, + mutations: options.mutations, + }); + + // Handles internal errors and prints the stack to the console + handlingErrors(schema); + + // Add graphql layer to the express app + expressApp.use(options.route || '/graphql', (request: express.Request, response: express.Response) => { + + // Build GraphQLContext + const context: GraphQLContext = { + container, + request, + response, + resolveArgs: {}, + data: options.contextData, + }; + + // Setup GraphQL Server + GraphQLHTTP({ + schema, + context, + graphiql: options.editorEnabled || true, + formatError: error => ({ + code: getErrorCode(error.message), + message: getErrorMessage(error.message), + path: error.path, + }), + })(request, response); + }); +} + +/** + * Gets metadata args storage. + * Metadata args storage follows the best practices and stores metadata in a global variable. + */ +export function getMetadataArgsStorage(): MetadataArgsStorage { + if (!(global as any).graphqlMetadataArgsStorage) { + (global as any).graphqlMetadataArgsStorage = new MetadataArgsStorage(); + } + + return (global as any).graphqlMetadataArgsStorage; +} + +/** + * Create query name out of the class name + */ +export function createQueryName(name: string): string { + return lowercaseFirstLetter(removeSuffix(name, 'Query')); +} + +/** + * Create mutation name out of the class name + */ +export function createMutationName(name: string): string { + return lowercaseFirstLetter(removeSuffix(name, 'Mutation')); +} + +/** + * Removes the suffix + */ +export function removeSuffix(value: string, suffix: string): string { + return value.slice(0, value.length - suffix.length); +} + +/** + * LowerCase first letter + */ +export function lowercaseFirstLetter(s: string): string { + return s.charAt(0).toLowerCase() + s.slice(1); +} + +/** + * GraphQL schema options for building it + */ +export interface GraphQLSchemaOptions { + queries: string[]; + mutations: string[]; +} + +/** + * Create schema out of the @Query and @Mutation + */ +export function createSchema(options: GraphQLSchemaOptions): GraphQLSchema { + + // import all queries + let queryClasses: Array<() => void> = []; + if (options && options.queries && options.queries.length) { + queryClasses = (options.queries as any[]).filter(query => query instanceof Function); + const queryDirs = (options.queries as any[]).filter(query => typeof query === 'string'); + queryClasses.push(...importClassesFromDirectories(queryDirs)); + } + + const queries = {}; + queryClasses.forEach(queryClass => { + queries[createQueryName(queryClass.name)] = new queryClass(); + }); + + const RootQuery = new GraphQLObjectType({ + name: 'Query', + fields: queries, + }); + + // import all mutations + let mutationClasses: Array<() => void> = []; + if (options && options.mutations && options.mutations.length) { + mutationClasses = (options.mutations as any[]).filter(mutation => mutation instanceof Function); + const mutationDirs = (options.mutations as any[]).filter(mutation => typeof mutation === 'string'); + mutationClasses.push(...importClassesFromDirectories(mutationDirs)); + } + + const mutations = {}; + mutationClasses.forEach(mutationClass => { + mutations[createMutationName(mutationClass.name)] = new mutationClass(); + }); + + const RootMutation: GraphQLObjectType = new GraphQLObjectType({ + name: 'Mutation', + fields: mutations, + }); + + const schemaOptions: any = {}; + + if (queryClasses && queryClasses.length) { + schemaOptions.query = RootQuery; + } + + if (mutationClasses && mutationClasses.length) { + schemaOptions.mutation = RootMutation; + } + + return new GraphQLSchema(schemaOptions); + +} diff --git a/src/loaders/graphqlLoader.ts b/src/loaders/graphqlLoader.ts new file mode 100644 index 00000000..3df59f71 --- /dev/null +++ b/src/loaders/graphqlLoader.ts @@ -0,0 +1,18 @@ +import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework'; +import { createGraphQLServer } from '../lib/graphql'; +import { env } from '../core/env'; + + +export const graphqlLoader: MicroframeworkLoader = (settings: MicroframeworkSettings | undefined) => { + if (settings && env.swagger.enabled) { + const expressApp = settings.getData('express_app'); + + createGraphQLServer(expressApp, { + route: env.graphql.route, + editorEnabled: env.graphql.enabled, + queries: env.app.dirs.queries, + mutations: env.app.dirs.queries, + }); + + } +}; diff --git a/yarn.lock b/yarn.lock index 9472fbd1..c0509dee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -140,7 +140,7 @@ accepts@1.3.3: mime-types "~2.1.11" negotiator "0.6.1" -accepts@^1.3.4, accepts@~1.3.4: +accepts@^1.3.0, accepts@^1.3.4, accepts@~1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" dependencies: @@ -940,7 +940,7 @@ content-type-parser@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.2.tgz#caabe80623e63638b2502fd4c7f12ff4ce2352e7" -content-type@^1.0.4, content-type@~1.0.4: +content-type@^1.0.2, content-type@^1.0.4, content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" @@ -1110,6 +1110,10 @@ dashify@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/dashify/-/dashify-0.2.2.tgz#6a07415a01c91faf4a32e38d9dfba71f61cb20fe" +dataloader@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.3.0.tgz#6fec5be4b30a712e4afd30b86b4334566b97673b" + date-fns@^1.23.0: version "1.29.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" @@ -1388,6 +1392,15 @@ express-basic-auth@^1.1.3: dependencies: basic-auth "^1.1.0" +express-graphql@^0.6.11: + version "0.6.11" + resolved "https://registry.yarnpkg.com/express-graphql/-/express-graphql-0.6.11.tgz#3dce78d0643e78e7e3606646ce162025ba0585ab" + dependencies: + accepts "^1.3.0" + content-type "^1.0.2" + http-errors "^1.3.0" + raw-body "^2.1.0" + express-status-monitor@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/express-status-monitor/-/express-status-monitor-1.0.1.tgz#311288347b7aabfeaec0a01547e55c77652bb298" @@ -1736,6 +1749,12 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" +graphql@^0.11.7: + version "0.11.7" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.11.7.tgz#e5abaa9cb7b7cccb84e9f0836bf4370d268750c6" + dependencies: + iterall "1.1.3" + growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" @@ -1898,7 +1917,7 @@ html-encoding-sniffer@^1.0.1: dependencies: whatwg-encoding "^1.0.1" -http-errors@1.6.2, http-errors@~1.6.2: +http-errors@1.6.2, http-errors@^1.3.0, http-errors@~1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" dependencies: @@ -2210,6 +2229,10 @@ istanbul-reports@^1.1.3: dependencies: handlebars "^4.0.3" +iterall@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.1.3.tgz#1cbbff96204056dde6656e2ed2e2226d0e6d72c9" + jest-changed-files@^21.2.0: version "21.2.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-21.2.0.tgz#5dbeecad42f5d88b482334902ce1cba6d9798d29" @@ -3426,7 +3449,7 @@ range-parser@^1.2.0, range-parser@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" -raw-body@2.3.2: +raw-body@2.3.2, raw-body@^2.1.0: version "2.3.2" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" dependencies: From 1e72adc3f2daa1c86903fa4de2570faa844385f1 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Sat, 9 Dec 2017 17:14:07 +0100 Subject: [PATCH 02/16] Add pets relations --- src/api/services/PetService.ts | 10 +++++++++ src/api/types/PetType.ts | 25 +++++++++++++++++++++ src/api/types/UserType.ts | 15 +++++++++---- src/types/auth0.d.ts | 41 ---------------------------------- 4 files changed, 46 insertions(+), 45 deletions(-) create mode 100644 src/api/types/PetType.ts delete mode 100644 src/types/auth0.d.ts diff --git a/src/api/services/PetService.ts b/src/api/services/PetService.ts index 9a063be9..c381975c 100644 --- a/src/api/services/PetService.ts +++ b/src/api/services/PetService.ts @@ -5,6 +5,7 @@ import { Pet } from '../models/Pet'; import { events } from '../subscribers/events'; import { EventDispatcher, EventDispatcherInterface } from '../../decorators/EventDispatcher'; import { Logger, LoggerInterface } from '../../decorators/Logger'; +import { User } from '../models/User'; @Service() @@ -21,6 +22,15 @@ export class PetService { return this.petRepository.find(); } + public findByUser(user: User): Promise { + this.log.info('Find all pets of the user', user.toString()); + return this.petRepository.find({ + where: { + userId: user.id, + }, + }); + } + public findOne(id: string): Promise { this.log.info('Find all pets'); return this.petRepository.findOne({ id }); diff --git a/src/api/types/PetType.ts b/src/api/types/PetType.ts new file mode 100644 index 00000000..f7754519 --- /dev/null +++ b/src/api/types/PetType.ts @@ -0,0 +1,25 @@ +import { + GraphQLID, + GraphQLString, + GraphQLInt, + GraphQLObjectType, +} from 'graphql'; + +export const PetType = new GraphQLObjectType({ + name: 'Pet', + description: 'A single pet.', + fields: { + id: { + type: GraphQLID, + description: 'The ID', + }, + name: { + type: GraphQLString, + description: 'The name of the pet.', + }, + age: { + type: GraphQLInt, + description: 'The age of the pet in years.', + }, + }, +}); diff --git a/src/api/types/UserType.ts b/src/api/types/UserType.ts index 70e2a5ba..3738de48 100644 --- a/src/api/types/UserType.ts +++ b/src/api/types/UserType.ts @@ -1,8 +1,13 @@ +import { PetService } from './../services/PetService'; import { GraphQLID, GraphQLString, GraphQLObjectType, + GraphQLList, } from 'graphql'; +import { PetType } from './PetType'; +import { User } from '../models/User'; +import { GraphQLContext } from '../../lib/graphql'; export const UserType = new GraphQLObjectType({ name: 'User', @@ -24,9 +29,11 @@ export const UserType = new GraphQLObjectType({ type: GraphQLString, description: 'The email of this user.', }, - // pets: { - // type: GraphQLString, - // description: 'The personal number of this user.‚', - // }, + pets: { + type: new GraphQLList(PetType), + description: 'The pets of a user', + resolve: (user: User, args: any, context: GraphQLContext) => + context.container.get(PetService).findByUser(user), + }, }, }); diff --git a/src/types/auth0.d.ts b/src/types/auth0.d.ts deleted file mode 100644 index 317ab01e..00000000 --- a/src/types/auth0.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * auth0 - * ---------------------------------------- - * - * Type definitions for the auth0 responses. - */ - -declare namespace auth0 { - - interface User { - user_id: string; - email: string; - email_verified: boolean; - picture: string; - created_at: Date; - updated_at: Date; - clientID?: string; - nickname?: string; - name?: string; - global_client_id?: string; - identities?: UserIdentities[]; - } - - interface UserIdentities { - user_id: string; - provider: string; - connection: string; - isSocial: boolean; - } - - interface Body { - client_id: string; - client_secret: string; - audience: string; - grant_type: string; - } - -} - -export as namespace auth0; -export = auth0; From 23191da6142de495d2142ddbe0496db6972b6c63 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Sun, 10 Dec 2017 11:36:27 +0100 Subject: [PATCH 03/16] Add data-loaders --- src/api/models/Pet.ts | 6 ++- src/api/queries/GetPetsQuery.ts | 22 ++++++++++ src/api/repositories/PetRepository.ts | 12 +++++- src/api/types/PetType.ts | 46 ++++++++++++++------ src/api/types/UserType.ts | 61 ++++++++++++++++----------- src/lib/graphql/GraphQLContext.ts | 6 +++ src/lib/graphql/dataloader.ts | 21 +++++++++ src/lib/graphql/index.ts | 36 +++++++++++++++- src/loaders/graphqlLoader.ts | 10 ++++- 9 files changed, 178 insertions(+), 42 deletions(-) create mode 100644 src/api/queries/GetPetsQuery.ts create mode 100644 src/lib/graphql/dataloader.ts diff --git a/src/api/models/Pet.ts b/src/api/models/Pet.ts index 7556ff5d..b1e3faac 100644 --- a/src/api/models/Pet.ts +++ b/src/api/models/Pet.ts @@ -1,4 +1,4 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; import { IsNotEmpty } from 'class-validator'; import { User } from './User'; @@ -17,7 +17,11 @@ export class Pet { @Column() public age: number; + @Column() + public userId: number; + @ManyToOne(type => User, user => user.pets) + @JoinColumn({ name: 'userId' }) public user: User; public toString(): string { diff --git a/src/api/queries/GetPetsQuery.ts b/src/api/queries/GetPetsQuery.ts new file mode 100644 index 00000000..c7bc10c2 --- /dev/null +++ b/src/api/queries/GetPetsQuery.ts @@ -0,0 +1,22 @@ +import { GraphQLFieldConfig, GraphQLList } from 'graphql'; +import { Query, AbstractQuery, GraphQLContext } from './../../lib/graphql'; +import { PetService } from '../services/PetService'; +import { PetType } from './../types/PetType'; +import { Logger } from '../../core/Logger'; +import { Pet } from '../models/Pet'; + +@Query() +export class GetPetsQuery extends AbstractQuery, Pet[], any> implements GraphQLFieldConfig { + public type = new GraphQLList(PetType); + public allow = []; + public args = {}; + + private log = new Logger(__filename); + + public async run(root: any, args: any, context: GraphQLContext): Promise { + const pets = await context.container.get(PetService).find(); + this.log.info(`Found ${pets.length} pets`); + return pets; + } + +} diff --git a/src/api/repositories/PetRepository.ts b/src/api/repositories/PetRepository.ts index 77e031ae..44673f0c 100644 --- a/src/api/repositories/PetRepository.ts +++ b/src/api/repositories/PetRepository.ts @@ -2,6 +2,16 @@ import { Repository, EntityRepository } from 'typeorm'; import { Pet } from '../models/Pet'; @EntityRepository(Pet) -export class PetRepository extends Repository { +export class PetRepository extends Repository { + + /** + * Find by userId is used for our data-loader to get all needed pets in one query. + */ + public findByUserIds(ids: string[]): Promise { + return this.createQueryBuilder() + .select() + .where(`pet.userId IN (${ids.map(id => `'${id}'`).join(', ')})`) + .getMany(); + } } diff --git a/src/api/types/PetType.ts b/src/api/types/PetType.ts index f7754519..df461545 100644 --- a/src/api/types/PetType.ts +++ b/src/api/types/PetType.ts @@ -3,23 +3,43 @@ import { GraphQLString, GraphQLInt, GraphQLObjectType, + GraphQLFieldConfigMap, } from 'graphql'; +import { merge } from 'lodash'; +import { OwnerType } from './UserType'; +import { Pet } from '../models/Pet'; +import { GraphQLContext } from '../../lib/graphql'; + +const PetFields: GraphQLFieldConfigMap = { + id: { + type: GraphQLID, + description: 'The ID', + }, + name: { + type: GraphQLString, + description: 'The name of the pet.', + }, + age: { + type: GraphQLInt, + description: 'The age of the pet in years.', + }, +}; + +export const PetOfUserType = new GraphQLObjectType({ + name: 'PetOfUser', + description: 'A users pet', + fields: () => merge(PetFields, {}), +}); export const PetType = new GraphQLObjectType({ name: 'Pet', description: 'A single pet.', - fields: { - id: { - type: GraphQLID, - description: 'The ID', - }, - name: { - type: GraphQLString, - description: 'The name of the pet.', + fields: () => merge(PetFields, { + owner: { + type: OwnerType, + description: 'The owner of the pet', + resolve: (pet: Pet, args: any, context: GraphQLContext) => + context.dataLoaders.users.load(pet.userId), }, - age: { - type: GraphQLInt, - description: 'The age of the pet in years.', - }, - }, + }), }); diff --git a/src/api/types/UserType.ts b/src/api/types/UserType.ts index 3738de48..b1fecbc9 100644 --- a/src/api/types/UserType.ts +++ b/src/api/types/UserType.ts @@ -1,39 +1,52 @@ -import { PetService } from './../services/PetService'; import { GraphQLID, GraphQLString, GraphQLObjectType, + GraphQLFieldConfigMap, GraphQLList, } from 'graphql'; -import { PetType } from './PetType'; -import { User } from '../models/User'; +import { merge } from 'lodash'; import { GraphQLContext } from '../../lib/graphql'; +import { PetOfUserType } from './PetType'; +import { User } from '../models/User'; + +const UserFields: GraphQLFieldConfigMap = { + id: { + type: GraphQLID, + description: 'The ID', + }, + firstName: { + type: GraphQLString, + description: 'The first name of the user.', + }, + lastName: { + type: GraphQLString, + description: 'The last name of the user.', + }, + email: { + type: GraphQLString, + description: 'The email of this user.', + }, +}; export const UserType = new GraphQLObjectType({ name: 'User', description: 'A single user.', - fields: { - id: { - type: GraphQLID, - description: 'The ID', - }, - firstName: { - type: GraphQLString, - description: 'The first name of the user.', - }, - lastName: { - type: GraphQLString, - description: 'The last name of the user.', - }, - email: { - type: GraphQLString, - description: 'The email of this user.', - }, + fields: () => merge(UserFields, { pets: { - type: new GraphQLList(PetType), + type: new GraphQLList(PetOfUserType), description: 'The pets of a user', - resolve: (user: User, args: any, context: GraphQLContext) => - context.container.get(PetService).findByUser(user), + resolve: async (user: User, args: any, context: GraphQLContext) => + // We use data-loaders to save db queries + context.dataLoaders.petByUserIds.loadMany([user.id]), + // This would be the case with a normal service, but not very fast + // context.container.get(PetService).findByUser(user), }, - }, + }), +}); + +export const OwnerType = new GraphQLObjectType({ + name: 'Owner', + description: 'The owner of a pet', + fields: () => merge(UserFields, {}), }); diff --git a/src/lib/graphql/GraphQLContext.ts b/src/lib/graphql/GraphQLContext.ts index d0a0df25..e8bb0250 100644 --- a/src/lib/graphql/GraphQLContext.ts +++ b/src/lib/graphql/GraphQLContext.ts @@ -1,10 +1,16 @@ import * as express from 'express'; +import * as DataLoader from 'dataloader'; import { Container } from 'typedi'; export interface GraphQLContext { container: typeof Container; request: express.Request; response: express.Response; + dataLoaders: GraphQLContextDataLoader; resolveArgs?: TResolveArgs; data?: TData; } + +export interface GraphQLContextDataLoader { + [key: string]: DataLoader; +} diff --git a/src/lib/graphql/dataloader.ts b/src/lib/graphql/dataloader.ts new file mode 100644 index 00000000..7482ebd0 --- /dev/null +++ b/src/lib/graphql/dataloader.ts @@ -0,0 +1,21 @@ +export interface Identifiable { + id?: number | number; +} + +export function ensureInputOrder(ids: number[] | string[], result: T[], key: string): T[] { + // For the dataloader batching to work, the results must be in the same order and of the + // same length as the ids. See: https://github.com/facebook/dataloader#batch-function + const orderedResult: T[] = []; + for (const id of ids) { + const item = result.find(t => t[key] === id); + if (item) { + orderedResult.push(item); + } else { + /* tslint:disable */ + // @ts-ignore + orderedResult.push(null); + /* tslint:enable */ + } + } + return orderedResult; +} diff --git a/src/lib/graphql/index.ts b/src/lib/graphql/index.ts index d76c953e..38ffaa89 100644 --- a/src/lib/graphql/index.ts +++ b/src/lib/graphql/index.ts @@ -1,12 +1,15 @@ import * as express from 'express'; import * as GraphQLHTTP from 'express-graphql'; +import * as DataLoader from 'dataloader'; import { GraphQLSchema, GraphQLObjectType } from 'graphql'; -import { Container as container } from 'typedi'; +import { Container as container, ObjectType } from 'typedi'; -import { GraphQLContext } from './GraphQLContext'; +import { GraphQLContext, GraphQLContextDataLoader } from './GraphQLContext'; import { MetadataArgsStorage } from './MetadataArgsStorage'; import { importClassesFromDirectories } from './importClassesFromDirectories'; import { handlingErrors, getErrorCode, getErrorMessage } from './graphql-error-handling'; +import { ensureInputOrder } from './dataloader'; +import { Repository, getCustomRepository, getRepository } from 'typeorm'; // ------------------------------------------------------------------------- // Main exports @@ -24,6 +27,33 @@ export * from './graphql-error-handling'; // Main Functions // ------------------------------------------------------------------------- +/** + * Creates a new dataloader wiht the typorm repository + */ +export function createDataLoader(obj: ObjectType, method?: string, key?: string): DataLoader { + let repository; + try { + repository = getCustomRepository>(obj); + } catch (errorRepo) { + try { + repository = getRepository(obj); + } catch (errorModel) { + throw new Error('Could not create a dataloader, because obj is nether model or repository!'); + } + } + + return new DataLoader(async (ids: number[]) => { + let items = []; + if (method) { + items = await repository[method](ids); + } else { + items = await repository.findByIds(ids); + } + + return ensureInputOrder(ids, items, key || 'id'); + }); +} + /** * Defines the options to create a GraphQLServer */ @@ -31,6 +61,7 @@ export interface GraphQLServerOptions { queries: string[]; mutations: string[]; route?: string; + dataLoaders?: GraphQLContextDataLoader; editorEnabled?: boolean; contextData?: TData; } @@ -56,6 +87,7 @@ export function createGraphQLServer(expressApp: express.Application, opti container, request, response, + dataLoaders: options.dataLoaders || {}, resolveArgs: {}, data: options.contextData, }; diff --git a/src/loaders/graphqlLoader.ts b/src/loaders/graphqlLoader.ts index 3df59f71..7eb2ce61 100644 --- a/src/loaders/graphqlLoader.ts +++ b/src/loaders/graphqlLoader.ts @@ -1,6 +1,9 @@ import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework'; -import { createGraphQLServer } from '../lib/graphql'; +import { createGraphQLServer, createDataLoader } from '../lib/graphql'; import { env } from '../core/env'; +import { PetRepository } from './../api/repositories/PetRepository'; +import { Pet } from './../api/models/Pet'; +import { UserRepository } from './../api/repositories/UserRepository'; export const graphqlLoader: MicroframeworkLoader = (settings: MicroframeworkSettings | undefined) => { @@ -12,6 +15,11 @@ export const graphqlLoader: MicroframeworkLoader = (settings: MicroframeworkSett editorEnabled: env.graphql.enabled, queries: env.app.dirs.queries, mutations: env.app.dirs.queries, + dataLoaders: { + users: createDataLoader(UserRepository), + pets: createDataLoader(Pet), + petByUserIds: createDataLoader(PetRepository, 'findByUserIds', 'userId'), + }, }); } From 8d7c6d03baaca6e78e6695da530ada1ccc34e22d Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Sun, 10 Dec 2017 16:27:02 +0100 Subject: [PATCH 04/16] Fix spelling mistake --- src/api/middlewares/CompressionMiddleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/middlewares/CompressionMiddleware.ts b/src/api/middlewares/CompressionMiddleware.ts index c508e675..a487e207 100644 --- a/src/api/middlewares/CompressionMiddleware.ts +++ b/src/api/middlewares/CompressionMiddleware.ts @@ -5,7 +5,7 @@ import { ExpressMiddlewareInterface, Middleware } from 'routing-controllers'; @Middleware({ type: 'before' }) -export class SecurityMiddleware implements ExpressMiddlewareInterface { +export class CompressionMiddleware implements ExpressMiddlewareInterface { public use(req: express.Request, res: express.Response, next: express.NextFunction): any { return compression()(req, res, next); From 3c9b9da969510f6a6cfed62207f4d4165b5138db Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Sun, 10 Dec 2017 16:45:11 +0100 Subject: [PATCH 05/16] Add mutation example --- src/api/mutations/CreatePetMutation.ts | 32 ++++++++++++++++++++++ src/lib/graphql/AbstractGraphQLMutation.ts | 20 ++++++++++++++ src/loaders/graphqlLoader.ts | 2 +- 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 src/api/mutations/CreatePetMutation.ts create mode 100644 src/lib/graphql/AbstractGraphQLMutation.ts diff --git a/src/api/mutations/CreatePetMutation.ts b/src/api/mutations/CreatePetMutation.ts new file mode 100644 index 00000000..8c92d32e --- /dev/null +++ b/src/api/mutations/CreatePetMutation.ts @@ -0,0 +1,32 @@ +import { + GraphQLFieldConfig, + GraphQLNonNull, + GraphQLString, + GraphQLInt +} from 'graphql'; +import { plainToClass } from 'class-transformer'; +import { AbstractGraphQLMutation } from '../../lib/graphql/AbstractGraphQLMutation'; +import { PetType } from '../types/PetType'; +import { PetService } from '../services/PetService'; +import { GraphQLContext, Mutation } from '../../lib/graphql'; +import { Pet } from '../models/Pet'; + +interface CreatePetMutationArguments { + name: string; + age: number; +} + +@Mutation() +export class CreatePetMutation extends AbstractGraphQLMutation, Pet, CreatePetMutationArguments> implements GraphQLFieldConfig { + public type = PetType; + public args = { + name: { type: new GraphQLNonNull(GraphQLString) }, + age: { type: new GraphQLNonNull(GraphQLInt) }, + }; + + public async run(root: any, args: CreatePetMutationArguments, context: GraphQLContext): Promise { + const petService = context.container.get(PetService); + const pet = await petService.create(plainToClass(Pet, args)); + return pet; + } +} diff --git a/src/lib/graphql/AbstractGraphQLMutation.ts b/src/lib/graphql/AbstractGraphQLMutation.ts new file mode 100644 index 00000000..81ebeed2 --- /dev/null +++ b/src/lib/graphql/AbstractGraphQLMutation.ts @@ -0,0 +1,20 @@ +import { AbstractGraphQLHooks } from './AbstractGraphQLHooks'; + + +export abstract class AbstractGraphQLMutation extends AbstractGraphQLHooks { + + /** + * This will be called by graphQL and they need to have it not as a + * member function of this class. We use this hook to add some more logic + * to it, like permission checking and before and after hooks to alter some data. + */ + public resolve = async (root: S, args: TArgs, context: TContext): Promise => { + // We need to store the query arguments in the context so they can be accessed by subsequent resolvers + (context as any).resolveArgs = args; + args = await this.before(context, args); + let result = await this.run(root, args, context); + result = await this.after(result, context, args); + return (result as TResult); + } + +} diff --git a/src/loaders/graphqlLoader.ts b/src/loaders/graphqlLoader.ts index 7eb2ce61..a426367f 100644 --- a/src/loaders/graphqlLoader.ts +++ b/src/loaders/graphqlLoader.ts @@ -14,7 +14,7 @@ export const graphqlLoader: MicroframeworkLoader = (settings: MicroframeworkSett route: env.graphql.route, editorEnabled: env.graphql.enabled, queries: env.app.dirs.queries, - mutations: env.app.dirs.queries, + mutations: env.app.dirs.mutations, dataLoaders: { users: createDataLoader(UserRepository), pets: createDataLoader(Pet), From dc914585eedb0132553897fb8939f714c8df3e94 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Sun, 10 Dec 2017 17:46:06 +0100 Subject: [PATCH 06/16] Add integration tests --- .env.test | 9 ++------- mydb.sql | Bin 0 -> 28672 bytes package-scripts.js | 18 +++++++++++++++++ package.json | 1 + src/api/models/Pet.ts | 4 +++- test/integration/PetService.test.ts | 30 ++++++++++++++++++++++++++++ test/integration/utils/database.ts | 18 +++++++++++++++++ yarn.lock | 13 +++++++++++- 8 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 mydb.sql create mode 100644 test/integration/PetService.test.ts create mode 100644 test/integration/utils/database.ts diff --git a/.env.test b/.env.test index f6e6b728..eafcd105 100644 --- a/.env.test +++ b/.env.test @@ -21,13 +21,8 @@ AUTH_ROUTE="http://localhost:3333/tokeninfo" # # DATABASE # -DB_TYPE="mysql" -DB_HOST="localhost" -DB_PORT=3306 -DB_USERNAME="root" -DB_PASSWORD="" -DB_DATABASE="my_database" -DB_SYNCHRONIZE=false +DB_TYPE="sqlite" +DB_DATABASE="./mydb.sql" DB_LOGGING=false # diff --git a/mydb.sql b/mydb.sql new file mode 100644 index 0000000000000000000000000000000000000000..dfc736dc8477687909094d9cf35cffa25db4faab GIT binary patch literal 28672 zcmeI)Z)@5>0LSr*wz>|ig(5yZ9K#0cXtZT5eKJ?lA+vU~7S;zNqyx2- z3wtel5qmQBY%gUGcW05+{-cjJTE8Ken7bt8_sJ0mCfRvgZiPxVyFtefWkJjd%Mvf7 z6hh>6iR&`0OGKBix=iUZ8m!TeTjqK3%zBrYT@_P_Gm*5DKN4rj$n2`FU_bx?1Q0*~ z0R#|00D(yq=G`K&cew10of^*>Ta4*V zznS3u&liGE{jcEm(^xFMHh)tReFi*7Ph$NiRy>_bSp$1`)TUpUa;Z<6sym*sW-I80 z-i;ly?cdg_I)1BtwIkcvNG!dcx+#n~I?plFsJ_rVG8dbKnar690|E#jfB*srAb User, user => user.pets) diff --git a/test/integration/PetService.test.ts b/test/integration/PetService.test.ts new file mode 100644 index 00000000..5ced7d88 --- /dev/null +++ b/test/integration/PetService.test.ts @@ -0,0 +1,30 @@ +import { Container } from 'typedi'; +import { createConnection, useContainer, Connection } from 'typeorm'; + +import { Pet } from '../../src/api/models/Pet'; +import { PetService } from './../../src/api/services/PetService'; +import { createDatabaseConnection, prepareDatabase, closeDatabase } from './utils/database'; + +describe('PetService', () => { + + let connection: Connection; + beforeAll(async () => connection = await createDatabaseConnection()); + beforeEach(() => prepareDatabase(connection)); + afterAll(() => closeDatabase(connection)); + + test('should create a new pet in the database', async (done) => { + const pet = new Pet(); + pet.name = 'test'; + pet.age = 1; + const service = Container.get(PetService); + const resultCreate = await service.create(pet); + expect(resultCreate.name).toBe(pet.name); + expect(resultCreate.age).toBe(pet.age); + + const resultFind = await service.findOne(resultCreate.id); + expect(resultFind.name).toBe(pet.name); + expect(resultFind.age).toBe(pet.age); + done(); + }); + +}); diff --git a/test/integration/utils/database.ts b/test/integration/utils/database.ts new file mode 100644 index 00000000..bee31dba --- /dev/null +++ b/test/integration/utils/database.ts @@ -0,0 +1,18 @@ +import { Container } from 'typedi'; +import { createConnection, useContainer, Connection } from 'typeorm'; +import { env } from '../../../src/core/env'; + +export const createDatabaseConnection = async (): Promise => { + useContainer(Container); + const connection = await createConnection({ + type: env.db.type as any, // See createConnection options for valid types + database: env.db.database, + logging: env.db.logging, + entities: env.app.dirs.entities, + }); + return connection; +}; + +export const prepareDatabase = (connection: Connection) => { + return connection.synchronize(true); +}; diff --git a/yarn.lock b/yarn.lock index c0509dee..a8b5fe90 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2923,6 +2923,10 @@ nan@^2.3.0: version "2.8.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" +nan@~2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -2972,7 +2976,7 @@ node-notifier@^5.0.2: shellwords "^0.1.0" which "^1.2.12" -node-pre-gyp@^0.6.39: +node-pre-gyp@^0.6.39, node-pre-gyp@~0.6.38: version "0.6.39" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649" dependencies: @@ -3916,6 +3920,13 @@ sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" +sqlite3@^3.1.13: + version "3.1.13" + resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-3.1.13.tgz#d990a05627392768de6278bafd1a31fdfe907dd9" + dependencies: + nan "~2.7.0" + node-pre-gyp "~0.6.38" + sqlstring@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.0.tgz#525b8a4fd26d6f71aa61e822a6caf976d31ad2a8" From f9c0a2e9465422157a6f46679130207fdb8485ea Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Sun, 10 Dec 2017 17:53:25 +0100 Subject: [PATCH 07/16] Fix some typos and duplicated code --- src/api/queries/GetPetsQuery.ts | 4 ++-- src/api/queries/GetUsersQuery.ts | 4 ++-- src/lib/graphql/AbstractGraphQLHooks.ts | 6 ------ src/lib/graphql/AbstractGraphQLMutation.ts | 19 ++----------------- src/lib/graphql/AbstractGraphQLQuery.ts | 2 +- 5 files changed, 7 insertions(+), 28 deletions(-) diff --git a/src/api/queries/GetPetsQuery.ts b/src/api/queries/GetPetsQuery.ts index c7bc10c2..375137a0 100644 --- a/src/api/queries/GetPetsQuery.ts +++ b/src/api/queries/GetPetsQuery.ts @@ -1,12 +1,12 @@ import { GraphQLFieldConfig, GraphQLList } from 'graphql'; -import { Query, AbstractQuery, GraphQLContext } from './../../lib/graphql'; +import { Query, AbstractGraphQLQuery, GraphQLContext } from './../../lib/graphql'; import { PetService } from '../services/PetService'; import { PetType } from './../types/PetType'; import { Logger } from '../../core/Logger'; import { Pet } from '../models/Pet'; @Query() -export class GetPetsQuery extends AbstractQuery, Pet[], any> implements GraphQLFieldConfig { +export class GetPetsQuery extends AbstractGraphQLQuery, Pet[], any> implements GraphQLFieldConfig { public type = new GraphQLList(PetType); public allow = []; public args = {}; diff --git a/src/api/queries/GetUsersQuery.ts b/src/api/queries/GetUsersQuery.ts index d0d8ee5c..fec2f681 100644 --- a/src/api/queries/GetUsersQuery.ts +++ b/src/api/queries/GetUsersQuery.ts @@ -1,12 +1,12 @@ import { GraphQLFieldConfig, GraphQLList } from 'graphql'; -import { Query, AbstractQuery, GraphQLContext } from './../../lib/graphql'; +import { Query, AbstractGraphQLQuery, GraphQLContext } from './../../lib/graphql'; import { UserService } from '../services/UserService'; import { UserType } from './../types/UserType'; import { User } from '../models/User'; import { Logger } from '../../core/Logger'; @Query() -export class GetUsersQuery extends AbstractQuery, User[], any> implements GraphQLFieldConfig { +export class GetUsersQuery extends AbstractGraphQLQuery, User[], any> implements GraphQLFieldConfig { public type = new GraphQLList(UserType); public allow = []; public args = {}; diff --git a/src/lib/graphql/AbstractGraphQLHooks.ts b/src/lib/graphql/AbstractGraphQLHooks.ts index 64f36c54..cd4ffae0 100644 --- a/src/lib/graphql/AbstractGraphQLHooks.ts +++ b/src/lib/graphql/AbstractGraphQLHooks.ts @@ -1,11 +1,5 @@ import { UserError } from './graphql-error-handling'; -// export interface GraphQLHooks { -// before(context: C, args: A, source?: S): Promise | A; -// after(result: R, context: C, args: A, source?: S): Promise | R; -// run(rootOrSource: S, args: A, context: C): Promise | Promise | R | undefined; -// }implements GraphQLHooks - export abstract class AbstractGraphQLHooks { /** diff --git a/src/lib/graphql/AbstractGraphQLMutation.ts b/src/lib/graphql/AbstractGraphQLMutation.ts index 81ebeed2..2f8564c1 100644 --- a/src/lib/graphql/AbstractGraphQLMutation.ts +++ b/src/lib/graphql/AbstractGraphQLMutation.ts @@ -1,20 +1,5 @@ -import { AbstractGraphQLHooks } from './AbstractGraphQLHooks'; +import { AbstractGraphQLQuery } from './AbstractGraphQLQuery'; -export abstract class AbstractGraphQLMutation extends AbstractGraphQLHooks { - - /** - * This will be called by graphQL and they need to have it not as a - * member function of this class. We use this hook to add some more logic - * to it, like permission checking and before and after hooks to alter some data. - */ - public resolve = async (root: S, args: TArgs, context: TContext): Promise => { - // We need to store the query arguments in the context so they can be accessed by subsequent resolvers - (context as any).resolveArgs = args; - args = await this.before(context, args); - let result = await this.run(root, args, context); - result = await this.after(result, context, args); - return (result as TResult); - } - +export abstract class AbstractGraphQLMutation extends AbstractGraphQLQuery { } diff --git a/src/lib/graphql/AbstractGraphQLQuery.ts b/src/lib/graphql/AbstractGraphQLQuery.ts index 5ccc5a3f..a417434e 100644 --- a/src/lib/graphql/AbstractGraphQLQuery.ts +++ b/src/lib/graphql/AbstractGraphQLQuery.ts @@ -1,7 +1,7 @@ import { AbstractGraphQLHooks } from './AbstractGraphQLHooks'; -export abstract class AbstractQuery extends AbstractGraphQLHooks { +export abstract class AbstractGraphQLQuery extends AbstractGraphQLHooks { /** * This will be called by graphQL and they need to have it not as a From 5ebc69a08860448309f5a0d75b907506547dd06e Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Sun, 10 Dec 2017 18:19:53 +0100 Subject: [PATCH 08/16] Add graphql to the readme --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index ca0259f7..b4195b87 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ [Jest](https://facebook.github.io/jest/), [Swagger](http://swagger.io/), [validatejs](https://validatejs.org/), +[GraphQL](http://graphql.org/), +[DataLoaders](https://github.com/facebook/dataloader), by [w3tech](https://github.com/w3tecch) ## Why @@ -44,6 +46,8 @@ Try it!! We are happy to hear your feedback or any kind of new features. - **Easy event dispatching** thanks to [event-dispatch](https://github.com/pleerock/event-dispatch). - **Fast Database Building** with simple migration from [TypeORM](https://github.com/typeorm/typeorm). - **Easy Data Seeding** with our own factories. +- **GraphQL** provides as a awesome query language for our api [GraphQL](http://graphql.org/). +- **DataLoaders** helps with performance thanks to caching and batching [DataLoaders](https://github.com/facebook/dataloader). ### Comming soon @@ -128,6 +132,7 @@ All script are defined in the package.json file, but the most important ones are ### Tests - Run the unit tests using `npm start test` (There is also a vscode task for this called `test`). +- Run the integration tests using `npm start test:integration`. - Run the e2e tests using `npm start test:e2e` and don't forget to start your application and your [Auth0 Mock Server](https://github.com/hirsch88/auth0-mock-server). ### Running in dev mode @@ -165,9 +170,11 @@ The swagger and the monitor route can be altered in the `.env` file. | Route | Description | | -------------- | ----------- | | **/api** | Shows us the name, description and the version of the package.json | +| **/graphql** | Route to the graphql editor or your query/mutations requests | | **/swagger** | This is the Swagger UI with our API documentation | | **/monitor** | Shows a small monitor page for the server | | **/api/users** | Example entity endpoint | +| **/api/pets** | Example entity endpoint | ## Project Structure @@ -187,6 +194,9 @@ The swagger and the monitor route can be altered in the `.env` file. | **src/api/services/** | Service layer | | **src/api/subscribers/** | Event subscribers | | **src/api/validators/** | Custom validators, which can be used in the request classes | +| **src/api/queries/** | GraphQL queries | +| **src/api/mutations/** | GraphQL mutations | +| **src/api/types/** | GraphQL types | | **src/api/** swagger.json | Swagger documentation | | **src/auth/** | Authentication checkers and services | | **src/core/** | The core features like logger and env variables | @@ -199,8 +209,10 @@ The swagger and the monitor route can be altered in the `.env` file. | **src/types/** *.d.ts | Custom type definitions and files that aren't on DefinitelyTyped | | **test** | Tests | | **test/e2e/** *.test.ts | End-2-End tests (like e2e) | +| **test/integration/** *.test.ts | Integration test with SQLite3 | | **test/unit/** *.test.ts | Unit tests | | .env.example | Environment configurations | +| .env.test | Test environment configurations | | ormconfig.json | TypeORM configuration for the database. Used by seeds and the migration. (generated file) | ## Logging @@ -378,6 +390,9 @@ npm start db.seed | [Auth0 API Documentation](https://auth0.com/docs/api/management/v2) | Authentification service | | [Jest](http://facebook.github.io/jest/) | Delightful JavaScript Testing Library for unit and e2e tests | | [swagger Documentation](http://swagger.io/) | API Tool to describe and document your api. | +| [SQLite Documentation](https://www.sitepoint.com/getting-started-sqlite3-basic-commands/) | Getting Started with SQLite3 – Basic Commands. | +| [GraphQL Documentation](http://graphql.org/graphql-js/) | A query language for your API. | +| [DataLoader Documentation](https://github.com/facebook/dataloader) | DataLoader is a generic utility to be used as part of your application's data fetching layer to provide a consistent API over various backends and reduce requests to those backends via batching and caching. | ## Related Projects From abaff124cd1f27049e3ef524e17a6b53ea1d5f5b Mon Sep 17 00:00:00 2001 From: David Weber Date: Mon, 11 Dec 2017 08:25:39 +0100 Subject: [PATCH 09/16] Move sqlite to better place in database to logically group database stuff. Update readme --- .env.test | 2 +- README.md | 1 + mydb.sql => src/database/mydb.sql | Bin 28672 -> 28672 bytes 3 files changed, 2 insertions(+), 1 deletion(-) rename mydb.sql => src/database/mydb.sql (98%) diff --git a/.env.test b/.env.test index eafcd105..2780671e 100644 --- a/.env.test +++ b/.env.test @@ -22,7 +22,7 @@ AUTH_ROUTE="http://localhost:3333/tokeninfo" # DATABASE # DB_TYPE="sqlite" -DB_DATABASE="./mydb.sql" +DB_DATABASE="./src/database/mydb.sql" DB_LOGGING=false # diff --git a/README.md b/README.md index b4195b87..862048a5 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,7 @@ The swagger and the monitor route can be altered in the `.env` file. | **src/database/factories** | Factory the generate fake entities | | **src/database/migrations** | Database migration scripts | | **src/database/seeds** | Seeds to create some data in the database | +| **src/database/mydb.sql | SQLite database for integration tests | | **src/decorators/** | Custom decorators like @Logger & @EventDispatch | | **src/loaders/** | Loader is a place where you can configure your app | | **src/public/** | Static assets (fonts, css, js, img). | diff --git a/mydb.sql b/src/database/mydb.sql similarity index 98% rename from mydb.sql rename to src/database/mydb.sql index dfc736dc8477687909094d9cf35cffa25db4faab..e9343f79214632fd487b73ddaddedb4940df74ff 100644 GIT binary patch delta 159 zcmZp8z}WDBae}mwwVaxvdlM%K-m{MQX81qgG3 zlz^3P78KaSuc6Ky%9&!8Vqlt*YNl&$X_BOCVqsvSYmt^>rfX=DmXuwwNaxvdlMz+nG{MQX82C#2t z5BSMH$w8PCq!g@Wv!K8pehqczP) Date: Mon, 11 Dec 2017 08:29:04 +0100 Subject: [PATCH 10/16] Refactor package-scripts. We don't need the script key as this is default for nps --- package-scripts.js | 194 +++++++++++++++++---------------------------- 1 file changed, 71 insertions(+), 123 deletions(-) diff --git a/package-scripts.js b/package-scripts.js index 9fb6f88e..a232686e 100644 --- a/package-scripts.js +++ b/package-scripts.js @@ -6,77 +6,57 @@ const { series, crossEnv, concurrent, rimraf, runInNewWindow } = require('nps-ut module.exports = { scripts: { - default: { - script: 'nps start' - }, + default: 'nps start', /** * Starts the builded app from the dist directory */ - start: { - script: 'node dist/app.js' - }, + start: 'node dist/app.js', /** * Serves the current app and watches for changes to restart it */ - serve: { - script: series( - 'nps banner.serve', - 'nodemon --watch src --watch .env' - ) - }, + serve: series( + 'nps banner.serve', + 'nodemon --watch src --watch .env' + ), /** * Setup's the development environment and the database */ - setup: { - script: series( - 'yarn install', - 'nps db.migrate', - 'nps db.seed' - ) - }, + setup: series( + 'yarn install', + 'nps db.migrate', + 'nps db.seed' + ), /** * Builds the app into the dist directory */ - build: { - script: series( - 'nps banner.build', - 'nps lint', - 'nps clean.dist', - 'nps transpile', - 'nps copy' - ) - }, + build: series( + 'nps banner.build', + 'nps lint', + 'nps clean.dist', + 'nps transpile', + 'nps copy' + ), /** * Database scripts */ db: { - migrate: { - script: series( - 'nps banner.migrate', - 'nps db.config', - runFast('./node_modules/typeorm/cli.js migrations:run') - ) - }, - revert: { - script: series( - 'nps banner.revert', - 'nps db.config', - runFast('./node_modules/typeorm/cli.js migrations:revert') - ) - }, - seed: { - script: series( - 'nps banner.seed', - 'nps db.config', - runFast('./src/lib/seeds/') - ) - }, - config: { - script: runFast('./src/lib/ormconfig.ts') - }, - drop: { - script: runFast('./node_modules/typeorm/cli.js schema:drop') - } + migrate: series( + 'nps banner.migrate', + 'nps db.config', + runFast('./node_modules/typeorm/cli.js migrations:run') + ), + revert: series( + 'nps banner.revert', + 'nps db.config', + runFast('./node_modules/typeorm/cli.js migrations:revert') + ), + seed: series( + 'nps banner.seed', + 'nps db.config', + runFast('./src/lib/seeds/') + ), + config: runFast('./src/lib/ormconfig.ts'), + drop: runFast('./node_modules/typeorm/cli.js schema:drop') }, /** * These run various kinds of tests. Default is unit. @@ -84,43 +64,25 @@ module.exports = { test: { default: 'nps test.unit', unit: { - default: { - script: series( - 'nps banner.test', - 'nps test.unit.pretest', - 'nps test.unit.run' - ) - }, - pretest: { - script: 'tslint -c ./tslint.json -t stylish ./test/unit/**/*.ts' - }, - run: { - script: 'cross-env NODE_ENV=test jest --testPathPattern=unit' - }, - verbose: { - script: 'nps "test --verbose"' - }, - coverage: { - script: 'nps "test --coverage"' - } + default: series( + 'nps banner.test', + 'nps test.unit.pretest', + 'nps test.unit.run' + ), + pretest: 'tslint -c ./tslint.json -t stylish ./test/unit/**/*.ts', + run: 'cross-env NODE_ENV=test jest --testPathPattern=unit', + verbose: 'nps "test --verbose"', + coverage: 'nps "test --coverage"' }, integration: { - default: { - script: series( - 'nps banner.test', - 'nps test.integration.pretest', - 'nps test.integration.run' - ) - }, - pretest: { - script: 'tslint -c ./tslint.json -t stylish ./test/integration/**/*.ts' - }, - verbose: { - script: 'nps "test.integration --verbose"' - }, - run: { - script: 'cross-env NODE_ENV=test jest --testPathPattern=integration -i' - }, + default: series( + 'nps banner.test', + 'nps test.integration.pretest', + 'nps test.integration.run' + ), + pretest: 'tslint -c ./tslint.json -t stylish ./test/integration/**/*.ts', + verbose: 'nps "test.integration --verbose"', + run: 'cross-env NODE_ENV=test jest --testPathPattern=integration -i', }, e2e: { default: { @@ -146,51 +108,37 @@ module.exports = { /** * Runs TSLint over your project */ - lint: { - script: `tslint -c ./tslint.json -p tsconfig.json src/**/*.ts --format stylish` - }, + lint: `tslint -c ./tslint.json -p tsconfig.json src/**/*.ts --format stylish`, /** * Transpile your app into javascript */ - transpile: { - script: `tsc` - }, + transpile: `tsc`, /** * Clean files and folders */ clean: { - default: { - script: series( - `nps banner.clean`, - `nps clean.dist` - ) - }, - dist: { - script: rimraf('./dist') - } + default: series( + `nps banner.clean`, + `nps clean.dist` + ), + dist: rimraf('./dist') }, /** * Copies static files to the build folder */ copy: { - default: { - script: series( - `nps copy.swagger`, - `nps copy.public` - ) - }, - swagger: { - script: copy( - './src/api/swagger.json', - './dist' - ) - }, - public: { - script: copy( - './src/public/*', - './dist' - ) - } + default: series( + `nps copy.swagger`, + `nps copy.public` + ), + swagger: copy( + './src/api/swagger.json', + './dist' + ), + public: copy( + './src/public/*', + './dist' + ) }, /** * This creates pretty banner to the terminal From e488c1a5d0b4e840dda7e3ecbff30a468fc41f91 Mon Sep 17 00:00:00 2001 From: David Weber Date: Mon, 11 Dec 2017 08:40:10 +0100 Subject: [PATCH 11/16] Fix naming of method and add missing closeDatabase function --- test/integration/PetService.test.ts | 4 ++-- test/integration/utils/database.ts | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/test/integration/PetService.test.ts b/test/integration/PetService.test.ts index 5ced7d88..e84c384d 100644 --- a/test/integration/PetService.test.ts +++ b/test/integration/PetService.test.ts @@ -3,13 +3,13 @@ import { createConnection, useContainer, Connection } from 'typeorm'; import { Pet } from '../../src/api/models/Pet'; import { PetService } from './../../src/api/services/PetService'; -import { createDatabaseConnection, prepareDatabase, closeDatabase } from './utils/database'; +import { createDatabaseConnection, synchronizeDatabase, closeDatabase } from './utils/database'; describe('PetService', () => { let connection: Connection; beforeAll(async () => connection = await createDatabaseConnection()); - beforeEach(() => prepareDatabase(connection)); + beforeEach(() => synchronizeDatabase(connection)); afterAll(() => closeDatabase(connection)); test('should create a new pet in the database', async (done) => { diff --git a/test/integration/utils/database.ts b/test/integration/utils/database.ts index bee31dba..f3f3a569 100644 --- a/test/integration/utils/database.ts +++ b/test/integration/utils/database.ts @@ -13,6 +13,10 @@ export const createDatabaseConnection = async (): Promise => { return connection; }; -export const prepareDatabase = (connection: Connection) => { +export const synchronizeDatabase = (connection: Connection) => { return connection.synchronize(true); }; + +export const closeDatabase = (connection: Connection) => { + return connection.close(); +}; From 51294b58765d75e05adac953971db8f1bfc287c7 Mon Sep 17 00:00:00 2001 From: David Weber Date: Mon, 11 Dec 2017 08:46:40 +0100 Subject: [PATCH 12/16] Fix graphql loader - wrong enable setting - use seperate setting for editor --- .env.example | 1 + src/core/env.ts | 1 + src/loaders/graphqlLoader.ts | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 3fcac450..79e94b5b 100644 --- a/.env.example +++ b/.env.example @@ -35,6 +35,7 @@ DB_LOGGING=false # GRAPHQL_ENABLED=true GRAPHQL_ROUTE="/graphql" +GRAPHQL_EDITOR=true # # Swagger diff --git a/src/core/env.ts b/src/core/env.ts index 5ab2cfa1..3bf68160 100644 --- a/src/core/env.ts +++ b/src/core/env.ts @@ -56,6 +56,7 @@ export const env = { graphql: { enabled: toBool(getOsEnv('GRAPHQL_ENABLED')), route: getOsEnv('GRAPHQL_ROUTE'), + editor: toBool(getOsEnv('GRAPHQL_EDITOR')), }, swagger: { enabled: toBool(getOsEnv('SWAGGER_ENABLED')), diff --git a/src/loaders/graphqlLoader.ts b/src/loaders/graphqlLoader.ts index a426367f..b53e9ca9 100644 --- a/src/loaders/graphqlLoader.ts +++ b/src/loaders/graphqlLoader.ts @@ -7,12 +7,12 @@ import { UserRepository } from './../api/repositories/UserRepository'; export const graphqlLoader: MicroframeworkLoader = (settings: MicroframeworkSettings | undefined) => { - if (settings && env.swagger.enabled) { + if (settings && env.graphql.enabled) { const expressApp = settings.getData('express_app'); createGraphQLServer(expressApp, { route: env.graphql.route, - editorEnabled: env.graphql.enabled, + editorEnabled: env.graphql.editor, queries: env.app.dirs.queries, mutations: env.app.dirs.mutations, dataLoaders: { From 92605f1501874eb13aa1d6e5fab479d9da561d95 Mon Sep 17 00:00:00 2001 From: David Weber Date: Mon, 11 Dec 2017 08:54:09 +0100 Subject: [PATCH 13/16] Use native merging - No need for lodash overhead - merge of lodash does recursivly merge which can be bad and time consuming --- src/api/types/PetType.ts | 7 +++---- src/api/types/UserType.ts | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/api/types/PetType.ts b/src/api/types/PetType.ts index df461545..ced8148e 100644 --- a/src/api/types/PetType.ts +++ b/src/api/types/PetType.ts @@ -5,7 +5,6 @@ import { GraphQLObjectType, GraphQLFieldConfigMap, } from 'graphql'; -import { merge } from 'lodash'; import { OwnerType } from './UserType'; import { Pet } from '../models/Pet'; import { GraphQLContext } from '../../lib/graphql'; @@ -28,18 +27,18 @@ const PetFields: GraphQLFieldConfigMap = { export const PetOfUserType = new GraphQLObjectType({ name: 'PetOfUser', description: 'A users pet', - fields: () => merge(PetFields, {}), + fields: () => ({ ...PetFields, ...{} }), }); export const PetType = new GraphQLObjectType({ name: 'Pet', description: 'A single pet.', - fields: () => merge(PetFields, { + fields: () => ({ ...PetFields, ...{ owner: { type: OwnerType, description: 'The owner of the pet', resolve: (pet: Pet, args: any, context: GraphQLContext) => context.dataLoaders.users.load(pet.userId), }, - }), + } }), }); diff --git a/src/api/types/UserType.ts b/src/api/types/UserType.ts index b1fecbc9..45bccc4e 100644 --- a/src/api/types/UserType.ts +++ b/src/api/types/UserType.ts @@ -5,7 +5,6 @@ import { GraphQLFieldConfigMap, GraphQLList, } from 'graphql'; -import { merge } from 'lodash'; import { GraphQLContext } from '../../lib/graphql'; import { PetOfUserType } from './PetType'; import { User } from '../models/User'; @@ -32,7 +31,7 @@ const UserFields: GraphQLFieldConfigMap = { export const UserType = new GraphQLObjectType({ name: 'User', description: 'A single user.', - fields: () => merge(UserFields, { + fields: () => ({ ...UserFields, ...{ pets: { type: new GraphQLList(PetOfUserType), description: 'The pets of a user', @@ -42,11 +41,11 @@ export const UserType = new GraphQLObjectType({ // This would be the case with a normal service, but not very fast // context.container.get(PetService).findByUser(user), }, - }), + } }), }); export const OwnerType = new GraphQLObjectType({ name: 'Owner', description: 'The owner of a pet', - fields: () => merge(UserFields, {}), + fields: () => ({ ...UserFields, ...{} }), }); From be81c753a493bfa036961ecd8addcd66735883a9 Mon Sep 17 00:00:00 2001 From: David Weber Date: Mon, 11 Dec 2017 09:17:36 +0100 Subject: [PATCH 14/16] Remove sqlite db --- .env.test | 2 +- .gitignore | 1 + README.md | 2 +- src/database/mydb.sql | Bin 28672 -> 0 bytes 4 files changed, 3 insertions(+), 2 deletions(-) delete mode 100644 src/database/mydb.sql diff --git a/.env.test b/.env.test index 2780671e..eafcd105 100644 --- a/.env.test +++ b/.env.test @@ -22,7 +22,7 @@ AUTH_ROUTE="http://localhost:3333/tokeninfo" # DATABASE # DB_TYPE="sqlite" -DB_DATABASE="./src/database/mydb.sql" +DB_DATABASE="./mydb.sql" DB_LOGGING=false # diff --git a/.gitignore b/.gitignore index 032e2e7e..73268fb2 100755 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ test/**/*.js test/**/*.js.map coverage/ !test/preprocessor.js +mydb.sql diff --git a/README.md b/README.md index 862048a5..c638d300 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,6 @@ The swagger and the monitor route can be altered in the `.env` file. | **src/database/factories** | Factory the generate fake entities | | **src/database/migrations** | Database migration scripts | | **src/database/seeds** | Seeds to create some data in the database | -| **src/database/mydb.sql | SQLite database for integration tests | | **src/decorators/** | Custom decorators like @Logger & @EventDispatch | | **src/loaders/** | Loader is a place where you can configure your app | | **src/public/** | Static assets (fonts, css, js, img). | @@ -215,6 +214,7 @@ The swagger and the monitor route can be altered in the `.env` file. | .env.example | Environment configurations | | .env.test | Test environment configurations | | ormconfig.json | TypeORM configuration for the database. Used by seeds and the migration. (generated file) | +| mydb.sql | SQLite database for integration tests. Ignored by git and only available after integration tests | ## Logging diff --git a/src/database/mydb.sql b/src/database/mydb.sql deleted file mode 100644 index e9343f79214632fd487b73ddaddedb4940df74ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28672 zcmeI)Z)@5>0LSr*t-21Z1reVfj$s3JRHn|-C+kW&WY%uBh4sM*u{k?I`zNMj>yx2- z8G9dl5qmQBY%gUGcV{Q7Z7qGYQTGkG#M~tzzfX=ph{^V=T02y-brf{HP(Bt5!m`9u zDTNRvUE;dLbcyKlS(jN|MuRo_al^bM9$Bvw^A$0hI1@=b`7LpljLcVb1p@*IAb@WDM+;n+fCz2U6wIC5w0S@CUcq1RPcSJDmMfv#^KhU!55*;v<~ z_Eb>4X1_~vW2@m*pV!Tf7tLz@X7o~w{@!`b;N-es{Z-wXN?C(9!&V(ukMZ${y5=th zyPC(uwq?WFa2ig1-Pu03$PSrpJNZ+5-OO2{edw$Ay_1f=-+3{{_Rb8*$x!RHKTqKrRO-~AAj?OKZ>5RV6+%p%OgqbXu2?GKMAb|ugPTc0%f2exl EFGMyv#{d8T From 90ce2a4683060dc2fb6952592f0f592d594bde48 Mon Sep 17 00:00:00 2001 From: David Weber Date: Mon, 11 Dec 2017 09:18:22 +0100 Subject: [PATCH 15/16] Correct some typos in lib --- src/lib/graphql/AbstractGraphQLHooks.ts | 6 +++--- src/lib/graphql/AbstractGraphQLQuery.ts | 8 ++++---- src/lib/graphql/index.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lib/graphql/AbstractGraphQLHooks.ts b/src/lib/graphql/AbstractGraphQLHooks.ts index cd4ffae0..915cc12a 100644 --- a/src/lib/graphql/AbstractGraphQLHooks.ts +++ b/src/lib/graphql/AbstractGraphQLHooks.ts @@ -3,7 +3,7 @@ import { UserError } from './graphql-error-handling'; export abstract class AbstractGraphQLHooks { /** - * This is our before hook. Here you are able + * This is our before hook. Here we are able * to alter the args object before the actual resolver(execute) * will be called. */ @@ -12,7 +12,7 @@ export abstract class AbstractGraphQLHooks { } /** - * This our after hook. It will be called ater the actual resolver(execute). + * This is our after hook. It will be called ater the actual resolver(execute). * There you are able to alter the result before it is send to the client. */ public after(result: TResult, context: TContext, args?: TArgs, source?: S): Promise | TResult { @@ -20,7 +20,7 @@ export abstract class AbstractGraphQLHooks { } /** - * This our resolver, which should gather the needed data; + * This is our resolver, which should gather the needed data; */ public run(rootOrSource: S, args: TArgs, context: TContext): Promise | TResult { throw new UserError('Query not implemented!'); diff --git a/src/lib/graphql/AbstractGraphQLQuery.ts b/src/lib/graphql/AbstractGraphQLQuery.ts index a417434e..1a34e77b 100644 --- a/src/lib/graphql/AbstractGraphQLQuery.ts +++ b/src/lib/graphql/AbstractGraphQLQuery.ts @@ -4,9 +4,9 @@ import { AbstractGraphQLHooks } from './AbstractGraphQLHooks'; export abstract class AbstractGraphQLQuery extends AbstractGraphQLHooks { /** - * This will be called by graphQL and they need to have it not as a - * member function of this class. We use this hook to add some more logic - * to it, like permission checking and before and after hooks to alter some data. + * This will be called by graphQL and they need it as a property function. + * We use this hook to add some more logic to it, + * like permission checking, before- and after hooks to alter some data. */ public resolve = async (root: S, args: TArgs, context: TContext): Promise => { // We need to store the query arguments in the context so they can be accessed by subsequent resolvers @@ -14,7 +14,7 @@ export abstract class AbstractGraphQLQuery extends Abs args = await this.before(context, args); let result = await this.run(root, args, context); result = await this.after(result, context, args); - return (result as TResult); + return result as TResult; } } diff --git a/src/lib/graphql/index.ts b/src/lib/graphql/index.ts index 38ffaa89..766c8e44 100644 --- a/src/lib/graphql/index.ts +++ b/src/lib/graphql/index.ts @@ -28,7 +28,7 @@ export * from './graphql-error-handling'; // ------------------------------------------------------------------------- /** - * Creates a new dataloader wiht the typorm repository + * Creates a new dataloader with the typorm repository */ export function createDataLoader(obj: ObjectType, method?: string, key?: string): DataLoader { let repository; From 0fd8b208d66cbabe14846225ec3b242a93591c59 Mon Sep 17 00:00:00 2001 From: David Weber Date: Mon, 11 Dec 2017 09:20:00 +0100 Subject: [PATCH 16/16] Never use 000 as status code as this can have side effects in other tools curl interprets 000 as timeout --- src/lib/graphql/graphql-error-handling.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/graphql/graphql-error-handling.ts b/src/lib/graphql/graphql-error-handling.ts index d286c303..a8d2edff 100644 --- a/src/lib/graphql/graphql-error-handling.ts +++ b/src/lib/graphql/graphql-error-handling.ts @@ -105,7 +105,7 @@ export const getErrorCode = (message: string): string => { if (hasErrorCode(message)) { return message.substring(0, 3); } - return '000'; // unkown error code + return '500'; // unkown error code }; export const getErrorMessage = (message: string): string => {