From e6015e1805cef0c67d776886d8c1a0d6f7a52db0 Mon Sep 17 00:00:00 2001 From: Max Koon Date: Tue, 3 Nov 2020 15:00:24 -0500 Subject: [PATCH] feat(graphql-service): added graphql service --- src/classes/ServiceContext.ts | 49 +++++- .../services/GraphQLServiceContext/index.ts | 152 ++++++++++++++++++ .../templates/index.ts.template | 30 ++++ .../TypescriptServiceContext/index.ts | 79 +++++++-- src/classes/services/index.ts | 18 ++- src/cmds/init.ts | 4 + src/cmds/make/index.ts | 1 + src/cmds/make/package.ts | 58 ++++++- src/constants.ts | 3 + src/types/GraphQLServiceContextType.ts | 11 ++ src/types/index.ts | 2 + src/util.ts | 14 +- 12 files changed, 384 insertions(+), 37 deletions(-) create mode 100644 src/classes/services/GraphQLServiceContext/index.ts create mode 100644 src/classes/services/GraphQLServiceContext/templates/index.ts.template create mode 100644 src/types/GraphQLServiceContextType.ts diff --git a/src/classes/ServiceContext.ts b/src/classes/ServiceContext.ts index d03dd7f..bb5c24b 100644 --- a/src/classes/ServiceContext.ts +++ b/src/classes/ServiceContext.ts @@ -12,14 +12,15 @@ import yaml = require('js-yaml'); import fs = require('fs'); import { ServiceContextType } from '../types'; import ServiceFile from './ServiceFile'; -import { dirname } from 'path'; +import { dirname, join, resolve } from 'path'; import path = require('path'); import { getServiceContextPath, titleCase } from '../util'; -import { Inquirer } from 'inquirer'; import inquirer = require('inquirer'); +import { user } from '../user'; export default abstract class ServiceContext { static NAME: string; + static DIRNAME: string = __dirname; /** * Abstract class to represent a service context @@ -43,7 +44,7 @@ export default abstract class ServiceContext { * @protected * @returns {string} Path to the directory */ - protected get serviceDirectory(): string { + get serviceDirectory(): string { return dirname(this.serviceFile.path); } @@ -124,7 +125,7 @@ export default abstract class ServiceContext { authors: [], created: currentTimestamp, modified: currentTimestamp, - license: 'CC', + license: 'CC-BY-1.0', termsOfServiceURL: 'nix2.io/tos', }, schemas: [], @@ -210,4 +211,44 @@ export default abstract class ServiceContext { this.schemas.splice(this.schemas.indexOf(schema), 1); return true; } + + getTemplate(scope: string, fileName: string) { + const templatePath = join( + __dirname, + `services/${scope}/templates/`, + `${fileName}.template`, + ); + return fs.readFileSync(templatePath, 'utf-8'); + } + + getREADMELines(): string[] { + return [`# ${this.info.label}`, `${this.info.description}`]; + } + + createREADME(): void { + const READMEContent = this.getREADMELines().join('\n'); + console.log(READMEContent); + + fs.writeFileSync( + join(this.serviceDirectory, 'README.md'), + READMEContent, + ); + } + + getFileHeaderLines(file: string): string[] { + let lines = [ + `File: ${file}`, + `Created: ${new Date().toISOString()}`, + '----', + 'Copyright: 2020 Nix² Technologies', + ]; + if (user != null) { + lines.push(`Author: ${user.name} (${user.email})`); + } + return lines; + } + + postInitLogic(): void { + this.createREADME(); + } } diff --git a/src/classes/services/GraphQLServiceContext/index.ts b/src/classes/services/GraphQLServiceContext/index.ts new file mode 100644 index 0000000..8590def --- /dev/null +++ b/src/classes/services/GraphQLServiceContext/index.ts @@ -0,0 +1,152 @@ +/* + * File: index.ts + * Created: 10/14/2020 13:03:39 + * ---- + * Copyright: 2020 Nix² Technologies + * Author: Max Koon (maxk@nix2.io) + */ +import { TypescriptServiceContext } from '..'; +import { Info, Schema } from '../..'; +import { GraphQLServiceContextType } from '../../../types'; +import ServiceFile from '../../ServiceFile'; + +type DependenciesType = Record; +export default class GraphQLServiceContext extends TypescriptServiceContext { + static NAME = 'graphql'; + static DIRNAME: string = __dirname; + + /** + * Class to represent an GraphQL Service context + * @class GraphQLServiceContext + * @param {string} filePath path to the service.yaml + * @param {Info} info info of the service + * @param {Array} schemas list of service schemas + */ + constructor(serviceFile: ServiceFile, info: Info, schemas: Schema[]) { + super(serviceFile, info, 'graphql', schemas); + } + + /** + * Deserialize an object into an `GraphQLServiceContext` instance + * @function deserialize + * @static + * @memberof GraphQLServiceContext + * @param {string} serviceFilePath path to the service.yaml + * @param {object} data Javascript object of the Info + * @returns {GraphQLServiceContext} Service context object + */ + static deserialize( + serviceFile: ServiceFile, + data: GraphQLServiceContextType, + ): GraphQLServiceContext { + // Test if the values are present + const vals = ['info', 'schemas']; + for (const val of vals) { + if (Object.keys(data).indexOf(val) == -1) + throw Error(val + ' not given'); + } + + return new GraphQLServiceContext( + serviceFile, + Info.deserialize(data.info), + Object.values(data.schemas).map((schema: any) => + Schema.deserialize(schema), + ), + ); + } + + static createObject( + data: { + identifier: string; + label: string; + description: string; + userLeadDev: boolean; + }, + user: any, + ): GraphQLServiceContextType { + return { + ...super.createObject(data, user), + ...{ + type: 'graphql', + }, + }; + } + + /** + * Serialize a `GraphQLServiceContext` instance into an object + * @function serialize + * @memberof GraphQLServiceContext + * @returns {GraphQLServiceContextType} Javascript object + */ + serialize(): GraphQLServiceContextType { + return { + ...super.serialize(), + ...{ + type: 'graphql', + }, + }; + } + + /** + * GraphQL specifc dependencies + * @memberof GraphQLServiceContext + * @function dependencies + * @returns {Record} Object of package name and version + */ + get dependencies(): DependenciesType { + return { + ...super.dependencies, + ...{ + 'apollo-server-express': '^2.19.0', + express: '^4.17.1', + graphql: '^15.4.0', + 'reflect-metadata': '^0.1.13', + 'type-graphql': '^1.1.0', + typeorm: '^0.2.29', + }, + }; + } + + /** + * Object of dev dependencies and their version + * @memberof GraphQLServiceContext + * @function devDependencies + * @returns {Record} Object of package name and version + */ + get devDependencies(): DependenciesType { + return { + ...super.devDependencies, + ...{ + '@types/express': '^4.17.8', + '@types/graphql': '^14.5.0', + nodemon: '^2.0.6', + }, + }; + } + + /** + * Object of the scripts + * @memberof GraphQLServiceContext + * @function scripts + * @returns {Record} Object of the scripts + */ + get scripts(): Record { + return { + ...super.scripts, + ...{ + start: 'nodemon --exec ts-node ./src/index.ts', + }, + }; + } + + getTemplate = (fileName: string) => + super.getTemplate('GraphQLServiceContext', fileName); + + getMainIndexFileContext(): string { + return super.getMainIndexFileContext() + this.getTemplate('index.ts'); + } + + postInitLogic() { + super.postInitLogic(); + } +} diff --git a/src/classes/services/GraphQLServiceContext/templates/index.ts.template b/src/classes/services/GraphQLServiceContext/templates/index.ts.template new file mode 100644 index 0000000..0946075 --- /dev/null +++ b/src/classes/services/GraphQLServiceContext/templates/index.ts.template @@ -0,0 +1,30 @@ +import "reflect-metadata"; +import { ApolloServer } from "apollo-server-express"; +import * as Express from "express"; +import { buildSchema, Query, Resolver } from "type-graphql"; + +@Resolver() +class HelloResolver { + @Query(() => String) + async hello() { + return "hi world"; + } +} + +const main = async () => { + const schema = await buildSchema({ + resolvers: [HelloResolver], + }); + + const apolloServer = new ApolloServer({ schema }); + + const app = Express(); + + apolloServer.applyMiddleware({ app }); + + app.listen(4000, () => { + console.log("started"); + }); +}; + +main(); diff --git a/src/classes/services/TypescriptServiceContext/index.ts b/src/classes/services/TypescriptServiceContext/index.ts index 681c346..21316e6 100644 --- a/src/classes/services/TypescriptServiceContext/index.ts +++ b/src/classes/services/TypescriptServiceContext/index.ts @@ -5,15 +5,15 @@ * Copyright: 2020 Nix² Technologies * Author: Max Koon (maxk@nix2.io) */ -import { existsSync, readFileSync } from 'fs'; +import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; import { join } from 'path'; import { Info, Schema, ServiceContext } from '../..'; import { PACKAGES } from '../../../constants'; import PackageJSONType from '../../../types/PackageJSONType'; import { authed, user } from '../../../user'; import ServiceFile from '../../ServiceFile'; +import { execSync } from 'child_process'; -type DependenciesType = Record; export default abstract class TypescriptServiceContext extends ServiceContext { static NAME = 'typescript'; @@ -32,8 +32,9 @@ export default abstract class TypescriptServiceContext extends ServiceContext { info: Info, type: string, schemas: Schema[], - private _dependencies: DependenciesType = {}, - private _devDependencies: DependenciesType = {}, + private _dependencies: Record = {}, + private _devDependencies: Record = {}, + private _scripts: Record = {}, ) { super(serviceFile, info, type, schemas); } @@ -44,7 +45,7 @@ export default abstract class TypescriptServiceContext extends ServiceContext { * @function dependencies * @returns {Record} Object of package name and version */ - get dependencies(): DependenciesType { + get dependencies(): Record { return { ...this._dependencies, ...{} }; } @@ -54,17 +55,24 @@ export default abstract class TypescriptServiceContext extends ServiceContext { * @function devDependencies * @returns {Record} Object of package name and version */ - get devDependencies(): DependenciesType { + get devDependencies(): Record { return { ...this._devDependencies, ...PACKAGES.TYPESCRIPT.dev }; } - readPackageFile(): PackageJSONType { - let tempdir = '.'; - // TODO: remove this - tempdir = join(this.serviceDirectory, '/serv'); - const packagePath = join(tempdir, '/package.json'); + /** + * Object of the scripts + * @memberof TypescriptServiceContext + * @function scripts + * @returns {Record} Object of the scripts + */ + get scripts(): Record { + return { ...this._scripts, ...{} }; + } + + readPackageFile(): PackageJSONType | null { + const packagePath = join(this.serviceDirectory, 'package.json'); if (!existsSync(packagePath)) { - throw Error('FILE_NOT_EXIST'); + return null; } const fileContent = readFileSync(packagePath, 'utf-8'); const fileObject = JSON.parse(fileContent); @@ -80,8 +88,55 @@ export default abstract class TypescriptServiceContext extends ServiceContext { license: this.info.license || 'CC-BY-1.0', dependencies: this.dependencies, devDependencies: this.devDependencies, + scripts: this.scripts, }; if (authed) packageContent.author = `${user?.name} <${user?.email}>`; return packageContent; } + + createPackageFile() { + writeFileSync( + join(this.serviceDirectory, 'package.json'), + JSON.stringify(this.createPackageContent(), null, 4), + ); + } + + getFileHeader(fileName: string) { + return ( + '/*\n' + + this.getFileHeaderLines(fileName) + .map((line) => ` * ${line}`) + .join('\n') + + '\n*/\n' + ); + } + + createSourceDirectory() { + const sourceDir = join(this.serviceDirectory, '/src'); + if (!existsSync(sourceDir)) mkdirSync(sourceDir); + return sourceDir; + } + + getMainIndexFileContext(): string { + return this.getFileHeader('index.ts'); + } + + createSourceFiles() { + const sourceDir = this.createSourceDirectory(); + writeFileSync( + join(sourceDir, 'index.ts'), + this.getMainIndexFileContext(), + ); + } + + installPackages() { + execSync(`yarn --cwd ${this.serviceDirectory}`); + } + + postInitLogic() { + super.postInitLogic(); + this.createPackageFile(); + this.createSourceFiles(); + this.installPackages(); + } } diff --git a/src/classes/services/index.ts b/src/classes/services/index.ts index 32ecca6..df2c8d5 100644 --- a/src/classes/services/index.ts +++ b/src/classes/services/index.ts @@ -15,6 +15,9 @@ export { APIServiceContext }; // gateway import GatewayServiceContext from './GatewayServiceContext'; export { GatewayServiceContext }; +// graph ql +import GraphQLServiceContext from './GraphQLServiceContext'; +export { GraphQLServiceContext }; // add any new types to here // TODO: find a better way to do this @@ -22,18 +25,23 @@ export { GatewayServiceContext }; // valid types export type VALID_SERVICE_TYPE_INSTANCES = | APIServiceContext - | GatewayServiceContext; + | GatewayServiceContext + | GraphQLServiceContext; export type VALID_SERVICE_TYPES = | typeof APIServiceContext - | typeof GatewayServiceContext; + | typeof GatewayServiceContext + | typeof GraphQLServiceContext; -const VALID_SERVICES = [APIServiceContext, GatewayServiceContext]; +const VALID_SERVICES = [ + APIServiceContext, + GatewayServiceContext, + GraphQLServiceContext, +]; +// create a new hash table for the service types and their classes export const SERVICE_TYPE_MAP = Object.assign( {}, ...VALID_SERVICES.map((service) => ({ [service.NAME]: service, })), ); - -// [APIServiceContext.NAME]: APIServiceContext, diff --git a/src/cmds/init.ts b/src/cmds/init.ts index 1d44a0e..8975dcc 100644 --- a/src/cmds/init.ts +++ b/src/cmds/init.ts @@ -110,6 +110,10 @@ export default (program: CommanderStatic): void => { console.log( colors.green(`${SYMBOLS.CHECK} Service initialized`), ); + console.log('Running post init logic'); + // create the new instance + const service = getServiceContext(options); + service?.postInitLogic(); }; // initialize without confirmation if (skipConfirm) return initialize(); diff --git a/src/cmds/make/index.ts b/src/cmds/make/index.ts index fe5122f..1da9d47 100644 --- a/src/cmds/make/index.ts +++ b/src/cmds/make/index.ts @@ -15,6 +15,7 @@ import { pkg } from './package'; export default (program: CommanderStatic): void => { const make = program .command('make') + .alias('mk') .description('make things related to your service'); // Apply all the functions to the program diff --git a/src/cmds/make/package.ts b/src/cmds/make/package.ts index d754ab0..cc9075f 100644 --- a/src/cmds/make/package.ts +++ b/src/cmds/make/package.ts @@ -1,23 +1,71 @@ import * as commander from 'commander'; import colors = require('colors'); import { getServiceContext } from '../../service'; -import { join } from 'path'; import { TypescriptServiceContext } from '../../classes'; +import { ERRORS, SYMBOLS } from '../../constants'; +import { prettyPrint } from '../../util'; +import { join } from 'path'; +import inquirer = require('inquirer'); export const pkg = (make: commander.Command): void => { make.command('package') + .alias('pkg') .description('make a package.json') + // confirm add flag + .option('-y, --yes', 'skip the confirmation screen') .action((options) => { // make sure there is a service context const serviceContext = getServiceContext(options); if (serviceContext == null) - return console.error(colors.red('No service context found')); + return console.error(colors.red(ERRORS.NO_SERVICE_EXISTS)); if (!(serviceContext instanceof TypescriptServiceContext)) { - return console.error(colors.red('Not a typescript service')); + return console.error( + colors.red(ERRORS.SERVICE_NOT_OF_TYPESCRIPT), + ); } + if (serviceContext.readPackageFile() != null) + return console.error(colors.red(ERRORS.PACKAGE_EXISTS)); + + const createPackageFile = () => { + try { + serviceContext.createPackageFile(); + console.log( + colors.green(`${SYMBOLS.CHECK} Created package.json`), + ); + } catch (err) { + console.error( + colors.red( + `Could not create package.json: ${err.message}`, + ), + ); + } + }; + + if (options.yes) return createPackageFile(); - console.log(serviceContext.readPackageFile()); + // prompt the user for confirmation + console.log( + colors.yellow( + `${SYMBOLS.WARNING} Creating ${join( + serviceContext.serviceDirectory, + 'package.json', + )}`, + ), + ); - const packagePath = join(process.cwd(), '/serv'); + prettyPrint(serviceContext.createPackageContent()); + console.log(); + inquirer + .prompt([ + { + type: 'confirm', + message: 'Proceed with adding file?', + name: 'confirm', + }, + ]) + .then((answer) => { + if (!answer.confirm) return console.log(ERRORS.ABORT); + createPackageFile(); + }); }); }; diff --git a/src/constants.ts b/src/constants.ts index ee9ccdc..9d12148 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -70,6 +70,9 @@ export const ERRORS = { SERVICE_EXISTS: colors.red( `${SERVICE_FILE_NAME} already exists in this directory`, ), + NO_SERVICE_EXISTS: `${SERVICE_FILE_NAME} does not exist in this directory`, + SERVICE_NOT_OF_TYPESCRIPT: `The service is not a Typescript service`, + PACKAGE_EXISTS: 'package.json already exists in this directory', ABORT: colors.red('\nAborted'), }; diff --git a/src/types/GraphQLServiceContextType.ts b/src/types/GraphQLServiceContextType.ts new file mode 100644 index 0000000..aac1b2b --- /dev/null +++ b/src/types/GraphQLServiceContextType.ts @@ -0,0 +1,11 @@ +/* + * File: GraphQLServiceContextType.ts + * Created: 11/03/2020 13:26:00 + * ---- + * Copyright: 2020 Nix² Technologies + * Author: Max Koon (maxk@nix2.io) + */ + +import { ServiceContextType } from '.'; + +export default interface GraphQLServiceContextType extends ServiceContextType {} diff --git a/src/types/index.ts b/src/types/index.ts index d04bd0f..17a6f1c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -14,5 +14,7 @@ export { default as ResponseType } from './ResponseType'; export { default as PathType } from './PathType'; export { default as SchemaType } from './SchemaType'; export { default as PackageJSONType } from './PackageJSONType'; +// service types export { default as ServiceContextType } from './ServiceContextType'; export { default as APIServiceContextType } from './APIServiceContextType'; +export { default as GraphQLServiceContextType } from './GraphQLServiceContextType'; diff --git a/src/util.ts b/src/util.ts index 175f89e..a33d5f4 100644 --- a/src/util.ts +++ b/src/util.ts @@ -55,6 +55,7 @@ const prettyFormat = ( tabIndex = 0, lastElementList = false, ): string => { + const tab = Array(tabIndex + 1).join(' '); // first few are simple if (typeof v == 'string') return colors.green(v); if (typeof v == 'number') return colors.cyan(v.toString()); @@ -65,14 +66,7 @@ const prettyFormat = ( if (Array.isArray(v)) return v - .map( - (i) => - `\n${Array(tabIndex).join(' ')}- ${prettyFormat( - i, - tabIndex + 1, - true, - )}`, - ) + .map((i) => `\n${tab}- ${prettyFormat(i, tabIndex + 1, true)}`) .join(''); if (typeof v == 'object') { @@ -80,9 +74,7 @@ const prettyFormat = ( let i = 0; for (let key in v) { s += `${ - i == 0 && lastElementList - ? '' - : `\n${Array(tabIndex).join(' ')}` + i == 0 && lastElementList ? '' : `\n${tab}` }${key}: ${prettyFormat(v[key], tabIndex + 1)}`; i++; }