├── app ├── .gitignore ├── connector │ ├── projects │ │ ├── .gitignore │ │ ├── info │ │ │ ├── index.ts │ │ │ └── user.ts │ │ └── blog │ │ │ ├── index.ts │ │ │ ├── comment.ts │ │ │ └── article.ts │ └── index.ts ├── lib │ └── redis.ts ├── graphql │ ├── common │ │ ├── resolver.ts │ │ ├── schema.gql │ │ └── scalars │ │ │ ├── date.ts │ │ │ └── JSON.ts │ ├── projects │ │ ├── comment │ │ │ ├── schema.gql │ │ │ ├── resolver.ts │ │ │ └── model.ts │ │ ├── user │ │ │ ├── resolver.ts │ │ │ ├── schema.gql │ │ │ └── model.ts │ │ └── article │ │ │ ├── schema.gql │ │ │ ├── resolver.ts │ │ │ └── model.ts │ ├── base_model.ts │ ├── data_loader.ts │ ├── remotes │ │ └── index.ts │ └── index.ts ├── utils │ ├── generate_object_key.ts │ └── logger.ts ├── database │ └── connections │ │ ├── index.ts │ │ ├── blog.ts │ │ └── info.ts ├── routes │ ├── hello.ts │ └── graphql.ts ├── config │ ├── index.ts │ └── development.ts ├── middleware │ ├── items │ │ ├── general.ts │ │ └── cache.ts │ └── index.ts └── index.ts ├── front_end ├── README.md ├── src │ ├── index.css │ ├── App.test.tsx │ ├── index.js │ ├── index.tsx │ ├── App.css │ ├── App.tsx │ ├── logo.svg │ ├── registerServiceWorker.js │ └── registerServiceWorker.ts ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── tsconfig.test.json ├── config │ ├── jest │ │ ├── typescriptTransform.js │ │ ├── fileTransform.js │ │ └── cssTransform.js │ ├── polyfills.js │ ├── paths.js │ ├── env.js │ ├── webpackDevServer.config.js │ ├── webpack.config.dev.js │ └── webpack.config.prod.js ├── tsconfig.json ├── scripts │ ├── test.js │ ├── start.js │ └── build.js ├── package.json └── tslint.json ├── index.d.ts ├── .vscode └── settings.json ├── .gitignore ├── nodemon.json ├── README.md ├── tsconfig.json ├── tslint.json └── package.json /app/.gitignore: -------------------------------------------------------------------------------- 1 | *.js -------------------------------------------------------------------------------- /app/connector/projects/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /front_end/README.md: -------------------------------------------------------------------------------- 1 | /Users/jimmydaddy/Documents/MacBooster/Duplicates Backups/README.md -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'koa' { 2 | interface Context { 3 | model: any 4 | } 5 | } -------------------------------------------------------------------------------- /front_end/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /app/connector/projects/info/index.ts: -------------------------------------------------------------------------------- 1 | import InfoUser from './user'; 2 | 3 | export { 4 | InfoUser, 5 | }; 6 | -------------------------------------------------------------------------------- /app/lib/redis.ts: -------------------------------------------------------------------------------- 1 | import * as Redis from 'ioredis'; 2 | 3 | const redis = new Redis(); 4 | 5 | export default redis; 6 | -------------------------------------------------------------------------------- /front_end/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GraphQL-Party/graphql-demo-ts/HEAD/front_end/public/favicon.ico -------------------------------------------------------------------------------- /front_end/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } -------------------------------------------------------------------------------- /app/graphql/common/resolver.ts: -------------------------------------------------------------------------------- 1 | import Date from './scalars/date'; 2 | import JSON from './scalars/JSON'; 3 | export default { 4 | Date, 5 | JSON, 6 | }; 7 | -------------------------------------------------------------------------------- /app/utils/generate_object_key.ts: -------------------------------------------------------------------------------- 1 | import * as MD5 from 'md5'; 2 | import * as R from 'ramda'; 3 | 4 | export default (obj: any) => { 5 | return MD5(R.toString(obj)); 6 | }; 7 | -------------------------------------------------------------------------------- /app/connector/projects/blog/index.ts: -------------------------------------------------------------------------------- 1 | import BlogArticle from './article'; 2 | import BlogComment from './comment'; 3 | 4 | export { 5 | BlogArticle, 6 | BlogComment, 7 | }; 8 | -------------------------------------------------------------------------------- /app/database/connections/index.ts: -------------------------------------------------------------------------------- 1 | import BlogConnection from './blog'; 2 | import InfoConnection from './info'; 3 | 4 | export { 5 | InfoConnection, 6 | BlogConnection, 7 | }; 8 | -------------------------------------------------------------------------------- /front_end/config/jest/typescriptTransform.js: -------------------------------------------------------------------------------- 1 | // Copyright 2004-present Facebook. All Rights Reserved. 2 | 3 | 'use strict'; 4 | 5 | const tsJestPreprocessor = require('ts-jest/preprocessor'); 6 | 7 | module.exports = tsJestPreprocessor; 8 | -------------------------------------------------------------------------------- /front_end/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "USE_GITIGNORE": true, 4 | "**/*.js": { 5 | "when": "$(basename).ts" 6 | }, 7 | "**/*.map": true, 8 | "run": true, 9 | "logs": true, 10 | "out": true, 11 | "coverage": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /front_end/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | registerServiceWorker(); 9 | -------------------------------------------------------------------------------- /app/graphql/projects/comment/schema.gql: -------------------------------------------------------------------------------- 1 | # 评论 2 | type Comment { 3 | # 评论 id 4 | _id: ID! 5 | # 评论内容 6 | content: String 7 | # 作者信息 8 | author: User 9 | # 创建日期 10 | gmtCreated: Date 11 | # 最近修改日期 12 | lastModified: Date 13 | } 14 | 15 | type Mutation { 16 | addComment: Comment 17 | } -------------------------------------------------------------------------------- /app/graphql/base_model.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'koa'; 2 | import * as keyUtil from '../utils/generate_object_key'; 3 | 4 | export default abstract class Model { 5 | ctx: Context; 6 | keyUtil = keyUtil; 7 | constructor(ctx: Context) { 8 | this.ctx = ctx; 9 | } 10 | 11 | public abstract count(); 12 | } 13 | -------------------------------------------------------------------------------- /app/routes/hello.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param } from 'routing-controllers'; 2 | import logger from '../utils/logger'; 3 | 4 | @Controller('') 5 | export default class HelloController { 6 | @Get('/hello/:id') 7 | helloworld (@Param('id') id: number) { 8 | logger.warn(id); 9 | return 'hello'; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/graphql/common/schema.gql: -------------------------------------------------------------------------------- 1 | scalar Date 2 | scalar JSON 3 | 4 | ## gps location 5 | type GPS { 6 | coordinates: [Float] 7 | type: String 8 | } 9 | 10 | interface Response { 11 | errorCode: String 12 | errorMsg: String 13 | } 14 | 15 | # fragment ErrorFrag on Error{ 16 | # errocode: String, 17 | # errorMsg: String 18 | # } -------------------------------------------------------------------------------- /front_end/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import registerServiceWorker from './registerServiceWorker'; 5 | import './index.css'; 6 | 7 | ReactDOM.render( 8 | , 9 | document.getElementById('root') as HTMLElement 10 | ); 11 | registerServiceWorker(); 12 | -------------------------------------------------------------------------------- /app/config/index.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import * as path from 'path'; 3 | 4 | const { resolve } = path; 5 | 6 | const host = process.env.HOST || 'localhost'; 7 | const env = process.env.NODE_ENV || 'development'; 8 | 9 | const conf = require(resolve(__dirname, `./${env}.ts`)); 10 | 11 | export default _.assign({ 12 | env, 13 | host, 14 | }, conf); 15 | -------------------------------------------------------------------------------- /front_end/config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /front_end/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /front_end/config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /dist/* 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | package-lock.json 25 | yarn.lock 26 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "restartable": "rs", 3 | "ignore": [ 4 | ".git", 5 | "node_modules/**/node_modules", 6 | "*.js" 7 | ], 8 | "verbose": true, 9 | "exec": "tsc && ts-node app/index.ts", 10 | "events": { 11 | "restart": "osascript -e 'display notification \"App restarted due to:\n'$FILENAME'\" with title \"nodemon\"'" 12 | }, 13 | "watch": [ 14 | "app/" 15 | ], 16 | "env": { 17 | "NODE_ENV": "development" 18 | }, 19 | "ext": "ts" 20 | } -------------------------------------------------------------------------------- /app/graphql/common/scalars/date.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType, Kind } from 'graphql'; 2 | 3 | export default new GraphQLScalarType({ 4 | name: 'Date', 5 | description: 'Date custom scalar type', 6 | parseValue(value) { 7 | return new Date(value); 8 | }, 9 | serialize(value) { 10 | return new Date(value).getTime(); 11 | }, 12 | parseLiteral(ast) { 13 | if (ast.kind === Kind.INT) { 14 | return parseInt(ast.value, 10); 15 | } 16 | return null; 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /front_end/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-title { 18 | font-size: 1.5em; 19 | } 20 | 21 | .App-intro { 22 | font-size: large; 23 | } 24 | 25 | @keyframes App-logo-spin { 26 | from { transform: rotate(0deg); } 27 | to { transform: rotate(360deg); } 28 | } 29 | -------------------------------------------------------------------------------- /app/graphql/data_loader.ts: -------------------------------------------------------------------------------- 1 | import * as DataLoader from 'dataloader'; 2 | import * as R from 'ramda'; 3 | 4 | export default class MyDataLoader extends DataLoader { 5 | loadOne(key: any): Promise { 6 | if (!key) { 7 | return null; 8 | } else { 9 | return this.load(key); 10 | } 11 | } 12 | 13 | loadManyItems(keys: any[]): Promise { 14 | const targetKeys = R.filter((item) => !R.isEmpty(item) && !R.isNil(item), (keys || [])); 15 | return this.loadMany(targetKeys); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphQL demo 2 | 3 | graphql demo 仅做参考 4 | 5 | ## 安装 6 | 7 | * 克隆仓库 8 | 9 | ```bash 10 | git clone https://github.com/GraphQL-Party/graphql-demo-ts.git 11 | npm install 12 | ``` 13 | 14 | * 本地安装 `mongodb` 15 | 16 | ## 使用 17 | 18 | * 启动程序 19 | 20 | ```bash 21 | npm start 22 | ``` 23 | 24 | * 访问 [graphiql]('http://localhost:3000/graphiql') 25 | 26 | http://localhost:3000/graphiql 27 | 28 | * 访问 [voyager]('http://localhost:3000/voyager') 29 | 30 | http://localhost:3000/graphiql 31 | 32 | ## 其他说明 33 | 34 | * 项目使用 ts 开发 35 | 36 | * 后端数据源不局限于 mongoDB 37 | 38 | * resolver 分层结构不局限于此 39 | -------------------------------------------------------------------------------- /app/graphql/projects/user/resolver.ts: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | Query: { 4 | users: async (root, args, context) => { 5 | const result = await context.model.UserModel.findAll(); 6 | return result; 7 | }, 8 | user: async (root, { userName }: { userName: string }, context) => { 9 | const result = await context.model.UserModel.findByName(userName); 10 | return result; 11 | }, 12 | }, 13 | Mutation: { 14 | addUser: async (root, { user }: { user: any }, context) => { 15 | const result = await context.model.UserModel.addUser(user); 16 | return result; 17 | }, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /front_end/src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './App.css'; 3 | 4 | const logo = require('./logo.svg'); 5 | 6 | class App extends React.Component { 7 | render() { 8 | return ( 9 |
10 |
11 | logo 12 |

Welcome to React

13 |
14 |

15 | To get started, edit src/App.tsx and save to reload. 16 |

17 |
18 | ); 19 | } 20 | } 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /app/connector/projects/blog/comment.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | import { BlogConnection } from '../../../database/connections'; 3 | const { Schema } = mongoose; 4 | 5 | const commentSchema = new Schema({ 6 | content: String, 7 | authorId: Number, 8 | gmtCreated: Date, 9 | lastModified: Date, 10 | }, { 11 | versionKey: false, 12 | timestamps: { createdAt: 'gmtCreated', updatedAt: 'lastModified' }, 13 | toObject: { 14 | virtuals: true, 15 | }, 16 | toJSON: { 17 | virtuals: true, 18 | }, 19 | collection: 'comment', 20 | }); 21 | 22 | export default BlogConnection.getConn().model('BlogComment', commentSchema); 23 | -------------------------------------------------------------------------------- /app/connector/projects/info/user.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | import { InfoConnection } from '../../../database/connections'; 3 | const { Schema } = mongoose; 4 | 5 | const userSchema = new Schema({ 6 | userName: String, 7 | mobilePhone: String, 8 | gmtCreated: Date, 9 | lastModified: Date, 10 | lastLoginTime: Date, 11 | lastLoginIp: String, 12 | }, { 13 | versionKey: false, 14 | timestamps: { createdAt: 'gmtCreated', updatedAt: 'lastModified' }, 15 | toObject: { 16 | virtuals: true, 17 | }, 18 | toJSON: { 19 | virtuals: true, 20 | }, 21 | }); 22 | 23 | export default InfoConnection.getConn().model('InfoUser', userSchema); 24 | -------------------------------------------------------------------------------- /app/graphql/projects/comment/resolver.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | Comment: { 3 | author: async ({ authorId }: { authorId: string }, args, context) => { 4 | const result = await context.model.UserModel.findById(authorId); 5 | return result; 6 | }, 7 | }, 8 | Mutation: { 9 | addComment: async (root, { comment }: { comment: any }, context) => { 10 | const commentResult = await context.model.CommentModel.addComment(comment); 11 | const { articleId } = comment; 12 | const { id } = commentResult; 13 | const result = await context.model.ArticleModel.insertComment(articleId, id); 14 | return result; 15 | }, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /app/graphql/projects/user/schema.gql: -------------------------------------------------------------------------------- 1 | # 用户信息 2 | type User { 3 | # 用户 id 4 | id: String 5 | # 用户名 6 | userName: String 7 | # 用户联系电话 8 | mobilePhone: String 9 | # 创建日期 10 | gmtCreated: Date 11 | # 最近更新日期 12 | lastModified: Date 13 | # 最近登录时间 14 | lastLoginTime: Date 15 | # 最近登录 ip 16 | lastLoginIp: String 17 | } 18 | 19 | input UserInputType { 20 | # 用户名 21 | userName: String 22 | # 用户联系电话 23 | mobilePhone: String 24 | } 25 | 26 | type Query { 27 | # 获取用户列表 28 | users: [User] 29 | # 获取用户信息 30 | user(userName: String): [User] 31 | } 32 | 33 | type Mutation { 34 | ## 添加用户 35 | addUser(user: UserInputType): User 36 | } 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/connector/projects/blog/article.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | import { BlogConnection } from '../../../database/connections'; 3 | const { Schema } = mongoose; 4 | 5 | const articleSchema = new Schema({ 6 | title: String, 7 | content: String, 8 | authorId: String, 9 | commentIds: [String], 10 | gmtCreated: Date, 11 | lastModified: Date, 12 | }, { 13 | versionKey: false, 14 | timestamps: { createdAt: 'gmtCreated', updatedAt: 'lastModified' }, 15 | toObject: { 16 | virtuals: true, 17 | }, 18 | toJSON: { 19 | virtuals: true, 20 | }, 21 | collection: 'article', 22 | }); 23 | 24 | export default BlogConnection.getConn().model('BlogArticle', articleSchema); 25 | -------------------------------------------------------------------------------- /app/graphql/projects/article/schema.gql: -------------------------------------------------------------------------------- 1 | # 文章 2 | type Article { 3 | # 文章 id 4 | _id: ID! 5 | # 文章标题 6 | title: String 7 | # 文章内容 8 | content: String 9 | # 作者信息 10 | author: User 11 | # 评论列表 12 | comments: [Comment] 13 | # 创建日期 14 | gmtCreated: Date 15 | # 最近修改日期 16 | lastModified: Date 17 | } 18 | 19 | input ArticleInpuType { 20 | # 文章标题 21 | title: String 22 | # 文章内容 23 | content: String 24 | # 作者信息 25 | authorId: String 26 | } 27 | 28 | type Query { 29 | # 获取文章列表 30 | articles: [Article] 31 | # 获取文章信息 32 | article(title: String): [Article] 33 | } 34 | 35 | type Mutation { 36 | # 添加文章 37 | addArticle(article: ArticleInpuType): Article 38 | } 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/utils/logger.ts: -------------------------------------------------------------------------------- 1 | // const emoji = r equire('node-emoji'); 2 | import chalk from 'chalk'; 3 | import * as emoji from 'node-emoji'; 4 | 5 | const info = chalk.bold.green; 6 | const warn = chalk.bold.yellow; 7 | const error = chalk.bold.red; 8 | 9 | export default { 10 | warn: (...args) => { 11 | console.log(emoji.get('warning'), '-->', warn(...args)); 12 | }, 13 | info: (...args) => { 14 | console.log(emoji.random().emoji, '-->', info(...args)); 15 | }, 16 | error: (...args) => { 17 | console.error(emoji.get('x'), '-->', error(...args)); 18 | }, 19 | log: (emojiSelect: string, color: string, ...args) => { 20 | console.log(emoji.get(emojiSelect), '-->', chalk.keyword(color)(...args)); 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /app/middleware/items/general.ts: -------------------------------------------------------------------------------- 1 | import * as Application from 'koa'; 2 | import * as koaBody from 'koa-bodyparser'; 3 | import * as compress from 'koa-compress'; 4 | import * as logger from 'koa-logger'; 5 | import * as serve from 'koa-static'; 6 | import * as path from 'path'; 7 | 8 | const { resolve } = path; 9 | 10 | const r = (pathstr: string) => resolve(__dirname, pathstr); 11 | 12 | export const addBody = (app: Application) => { 13 | app.use(koaBody()); 14 | }; 15 | 16 | export const addServe = (app: Application) => { 17 | app.use(serve(r('../../public'))); 18 | }; 19 | 20 | export const addLogger = (app: Application) => { 21 | app.use(logger()); 22 | }; 23 | 24 | export const addCompress = (app: Application) => { 25 | app.use(compress()); 26 | }; 27 | -------------------------------------------------------------------------------- /front_end/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src/", 4 | "outDir": "build/dist", 5 | "module": "esnext", 6 | "target": "es5", 7 | "lib": ["es6", "dom"], 8 | "sourceMap": true, 9 | "allowJs": true, 10 | "jsx": "react", 11 | "moduleResolution": "node", 12 | "rootDir": "src", 13 | "forceConsistentCasingInFileNames": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "noUnusedLocals": true 20 | }, 21 | "exclude": [ 22 | "node_modules", 23 | "build", 24 | "scripts", 25 | "acceptance-tests", 26 | "webpack", 27 | "jest", 28 | "src/setupTests.ts" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /app/config/development.ts: -------------------------------------------------------------------------------- 1 | export = { 2 | dataSources: { 3 | dbBlog: { 4 | uri: 'mongodb://127.0.0.1:27017/blog', 5 | options: { 6 | autoReconnect: true, 7 | }, 8 | }, 9 | dbInfo: { 10 | uri: 'mongodb://127.0.0.1:27017/test', 11 | options: { 12 | autoReconnect: true, 13 | }, 14 | }, 15 | }, 16 | mongoClientOptions: { 17 | autoReconnect: true, 18 | reconnectTries: 20, 19 | reconnectInterval: 500, 20 | connectTimeoutMS: 30000, 21 | }, 22 | port: 3000, 23 | redis: { 24 | endpoints: [ 25 | { 26 | host: '127.0.0.1', 27 | port: 6379, 28 | }, 29 | ], 30 | }, 31 | base: 'http://localhost:3000', 32 | graphql: { 33 | // remotes: ['https://otherGQService.com/graphql'], 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /app/middleware/index.ts: -------------------------------------------------------------------------------- 1 | import * as glob from 'glob'; 2 | import * as Application from 'koa'; 3 | import * as path from 'path'; 4 | import * as R from 'ramda'; 5 | import Logger from '../utils/logger'; 6 | const { resolve } = path; 7 | 8 | export default (app: Application) => { 9 | try { 10 | Logger.info('loading middlewares'); 11 | const baseDir = resolve(__dirname, './items/'); 12 | const pattern = baseDir + '/**/*.js'; 13 | const g = new glob.Glob(pattern, { 14 | mark: true, 15 | sync: true, 16 | }); 17 | const load = R.compose( 18 | R.map(R.compose( 19 | R.map((i: (app: Application) => {}) => i(app)), 20 | require, 21 | ), 22 | ), 23 | ); 24 | load(g.found); 25 | } catch (error) { 26 | Logger.error(error); 27 | throw error; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | // "outDir": "dist", 5 | "allowJs": false, 6 | "allowUnreachableCode": false, 7 | "allowUnusedLabels": false, 8 | "charset": "utf8", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "importHelpers": true, 12 | "inlineSourceMap": true, 13 | "module": "commonjs", 14 | "moduleResolution": "node", 15 | "noEmitOnError": false, 16 | "noFallthroughCasesInSwitch": true, 17 | "noImplicitAny": false, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": false, 20 | "pretty": true, 21 | "skipDefaultLibCheck": true, 22 | "skipLibCheck": true, 23 | "target": "es2017" 24 | }, 25 | "include": ["app.ts", "app/**/*.ts", "config/**/*.ts", "test/**/*.ts", "app/**/*.json"] 26 | } 27 | -------------------------------------------------------------------------------- /app/middleware/items/cache.ts: -------------------------------------------------------------------------------- 1 | import * as Application from 'koa'; 2 | import * as session from 'koa-session'; 3 | import redis from '../../lib/redis'; 4 | 5 | export const clientCache = (app: Application) => { 6 | app.keys = ['sxc_rgb']; 7 | const store = { 8 | get: async (key: string) => { 9 | const res = await redis.get(key); 10 | return JSON.parse(res); 11 | }, 12 | set: async (key: string, sess: any) => { 13 | const json = JSON.stringify(sess); 14 | await redis.set(key, json); 15 | }, 16 | destroy: async (key: string) => { 17 | await redis.del(key); 18 | }, 19 | }; 20 | const CONFIG = { 21 | key: 'koa:sess', 22 | maxAge: 86400000, 23 | overwrite: true, 24 | signed: true, 25 | rolling: false, 26 | store, 27 | }; 28 | 29 | app.use(session(CONFIG, app)); 30 | }; 31 | -------------------------------------------------------------------------------- /front_end/scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const jest = require('jest'); 19 | const argv = process.argv.slice(2); 20 | 21 | // Watch unless on CI or in coverage mode 22 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 23 | argv.push('--watch'); 24 | } 25 | 26 | 27 | jest.run(argv); 28 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:latest", 3 | "rules": { 4 | "quotemark": [true, "single", "jsx-double"], 5 | "no-console": false, 6 | "space-before-function-paren": false, 7 | "interface-name": [true, "no-prefix"], 8 | "adjacent-overload-signatures": true, 9 | "member-access": [false], 10 | "member-ordering": [ 11 | true, 12 | { 13 | "order": "fields-first" 14 | } 15 | ], 16 | "object-literal-sort-keys": false, 17 | "max-classes-per-file": [true, 10], 18 | "variable-name": [true, "allow-leading-underscore"], 19 | "align": [true, "statements"], 20 | "no-reference": false, 21 | "no-this-assignment": [true, {"allowed-names": ["^self$"], "allow-destructuring": true}], 22 | "no-var-requires": false, 23 | "max-line-length": [true, 140], 24 | "no-submodule-imports": false 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/graphql/remotes/index.ts: -------------------------------------------------------------------------------- 1 | import { createApolloFetch } from 'apollo-fetch'; 2 | import * as Promise from 'bluebird'; 3 | import { 4 | introspectSchema, 5 | makeRemoteExecutableSchema, 6 | } from 'graphql-tools'; 7 | import * as _ from 'lodash'; 8 | import config from '../../config'; 9 | import logger from '../../utils/logger'; 10 | 11 | export default async () => { 12 | if (config.graphql && !_.isEmpty(config.graphql.remotes)) { 13 | const result = Promise.map(config.graphql.remotes, (async (uri: string) => { 14 | logger.info(`loading remote schema from { ${uri} }`); 15 | const fetcher = createApolloFetch({ uri }); 16 | const schema = makeRemoteExecutableSchema({ 17 | schema: await introspectSchema(fetcher), 18 | fetcher, 19 | }); 20 | return schema; 21 | })); 22 | return result; 23 | } else { 24 | return []; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /front_end/config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | 18 | // In tests, polyfill requestAnimationFrame since jsdom doesn't provide it yet. 19 | // We don't polyfill it in the browser--this is user's responsibility. 20 | if (process.env.NODE_ENV === 'test') { 21 | require('raf').polyfill(global); 22 | } 23 | -------------------------------------------------------------------------------- /app/graphql/projects/article/resolver.ts: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | Query: { 4 | articles: async (root, args, context) => { 5 | const result = await context.model.ArticleModel.findAll(); 6 | return result; 7 | }, 8 | article: async (root, { title }: { title: string }, context) => { 9 | const result = await context.model.ArticleModel.findByTitle(title); 10 | return result; 11 | }, 12 | }, 13 | Article: { 14 | author: async ({ authorId }: { authorId: number }, args, context) => { 15 | const result = await context.model.UserModel.findById(authorId); 16 | return result; 17 | }, 18 | comments: async ({ commentIds }: { commentIds: [number] }, args, context) => { 19 | const result = await context.model.CommentModel.getByIds(commentIds); 20 | return result; 21 | }, 22 | }, 23 | Mutation: { 24 | addArticle: async (root, {article}: {article: any}, context) => { 25 | const result = await context.model.ArticleModel.addArticle(article); 26 | return result; 27 | }, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /app/graphql/common/scalars/JSON.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType, Kind } from 'graphql'; 2 | 3 | function identity(value) { 4 | return value; 5 | } 6 | 7 | function parseLiteral(ast) { 8 | switch (ast.kind) { 9 | case Kind.STRING: 10 | case Kind.BOOLEAN: 11 | return ast.value; 12 | case Kind.INT: 13 | case Kind.FLOAT: 14 | return parseFloat(ast.value); 15 | case Kind.OBJECT: { 16 | const value = Object.create(null); 17 | ast.fields.forEach((field) => { 18 | value[field.name.value] = parseLiteral(field.value); 19 | }); 20 | 21 | return value; 22 | } 23 | case Kind.LIST: 24 | return ast.values.map(parseLiteral); 25 | default: 26 | return null; 27 | } 28 | } 29 | 30 | export default new GraphQLScalarType({ 31 | name: 'JSON', 32 | description: 33 | 'The `JSON` scalar type represents JSON values as specified by ' + 34 | '[ECMA-404](http://www.ecma-international.org/' + 35 | 'publications/files/ECMA-ST/ECMA-404.pdf).', 36 | serialize: identity, 37 | parseValue: identity, 38 | parseLiteral, 39 | }); 40 | -------------------------------------------------------------------------------- /app/connector/index.ts: -------------------------------------------------------------------------------- 1 | 2 | // import * as fs from 'fs'; 3 | // import { Description } from 'graphql-decorators'; 4 | import * as glob from 'glob'; 5 | import { Context } from 'koa'; 6 | import * as path from 'path'; 7 | import * as R from 'ramda'; 8 | import logger from '../utils/logger'; 9 | 10 | const { resolve } = path; 11 | const SYMBOL_CONNECTOR_CLASS = Symbol('Application#Context#connector'); 12 | 13 | export const loadConnectors = (context: Context): void => { 14 | try { 15 | logger.info('loading connectors'); 16 | const baseDir = resolve(__dirname, './projects/'); 17 | const pattern = baseDir + '/**/index.ts'; 18 | const g = new glob.Glob(pattern, { 19 | mark: true, 20 | sync: true, 21 | }); 22 | Object.defineProperty(context, 'connector', { 23 | get() { 24 | if (!this[SYMBOL_CONNECTOR_CLASS]) { 25 | let connectorsMap: any = {}; 26 | R.map(R.compose( 27 | (connectorss: object) => { 28 | connectorsMap = R.merge(connectorsMap, connectorss); 29 | }, 30 | require, 31 | ))(g.found); 32 | this[SYMBOL_CONNECTOR_CLASS] = connectorsMap; 33 | } 34 | return this[SYMBOL_CONNECTOR_CLASS]; 35 | }, 36 | }); 37 | } catch (error) { 38 | logger.error(error); 39 | throw error; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /app/index.ts: -------------------------------------------------------------------------------- 1 | // import fs from 'fs' 2 | // import * as Koa from 'koa'; 3 | import { Context } from 'koa'; 4 | import { resolve } from 'path'; 5 | import 'reflect-metadata'; 6 | import { createKoaServer } from 'routing-controllers'; 7 | import conf from './config'; 8 | import { loadConnectors } from './connector'; 9 | import { 10 | BlogConnection, 11 | InfoConnection, 12 | } from './database/connections'; 13 | import { loadSchemasAndModels } from './graphql'; 14 | import loadMiddlewares from './middleware'; 15 | import Logger from './utils/logger'; 16 | 17 | // load routes 18 | const app = createKoaServer({ 19 | cors: {}, 20 | controllers: [resolve(__dirname, './routes/*.js')], 21 | }); 22 | 23 | (async () => { 24 | try { 25 | Logger.info('create db connections'); 26 | const dBConnection = new InfoConnection(); 27 | await dBConnection.initDB(); 28 | const blogConnection = new BlogConnection(); 29 | await blogConnection.initDB(); 30 | } catch (err) { 31 | Logger.error(err); 32 | throw err; 33 | } 34 | 35 | loadMiddlewares(app); 36 | loadConnectors(app.context); 37 | await loadSchemasAndModels(app.context); 38 | app.use(async (ctx: Context) => { 39 | ctx.status = 200; 40 | }); 41 | 42 | // const { errorHandler } = require('./lib/error'); 43 | 44 | // app.on('error', errorHandler); 45 | app.listen(conf.port, conf.host); 46 | Logger.log('100', 'green', '服务启动: http://' + conf.host + ':' + conf.port); 47 | })(); 48 | -------------------------------------------------------------------------------- /app/database/connections/blog.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | import config from '../../config'; 3 | import Logger from '../../utils/logger'; 4 | 5 | require('mongoose').Promise = Promise; 6 | 7 | export default class ConnTrack { 8 | static connInst: ConnTrack = null; 9 | 10 | conn: mongoose.Connection; 11 | 12 | constructor () { 13 | this.conn = mongoose.createConnection(config.dataSources.dbBlog.uri, config.dataSources.dbBlog.options); 14 | } 15 | 16 | static getConn () { 17 | if (ConnTrack.connInst) { 18 | return ConnTrack.connInst.conn; 19 | } else { 20 | ConnTrack.connInst = new ConnTrack(); 21 | return ConnTrack.connInst.conn; 22 | } 23 | } 24 | 25 | async initDB () { 26 | return new Promise((resolve, reject) => { 27 | if (!this.conn) { 28 | this.conn = mongoose.createConnection(config.dataSources.dbBlog.uri, config.dataSources.dbBlog.options); 29 | } 30 | 31 | if (config.env === 'development') { 32 | mongoose.set('debug', true); 33 | } 34 | 35 | this.conn.on('disconnected', () => { 36 | mongoose.connect(config.dataSources.dbBlog.uri); 37 | }); 38 | 39 | this.conn.on('error', (err) => { 40 | reject(err); 41 | }); 42 | 43 | this.conn.on('open', () => { 44 | resolve(this.conn); 45 | }); 46 | 47 | this.conn.once('open', () => { 48 | Logger.info('Connected to MongoDB', config.dataSources.dbBlog.uri); 49 | }); 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/database/connections/info.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from 'mongoose'; 2 | import * as mongoose from 'mongoose'; 3 | import config from '../../config'; 4 | import Logger from '../../utils/logger'; 5 | 6 | require('mongoose').Promise = Promise; 7 | 8 | export default class ConnDB { 9 | static connInst: ConnDB = null; 10 | 11 | conn: Connection; 12 | 13 | constructor () { 14 | this.conn = mongoose.createConnection(config.dataSources.dbInfo.uri, config.dataSources.dbInfo.options); 15 | } 16 | 17 | static getConn () { 18 | if (ConnDB.connInst) { 19 | return ConnDB.connInst.conn; 20 | } else { 21 | ConnDB.connInst = new ConnDB(); 22 | return ConnDB.connInst.conn; 23 | } 24 | } 25 | 26 | async initDB () { 27 | return new Promise((resolve, reject) => { 28 | if (!this.conn) { 29 | this.conn = mongoose.createConnection(config.dataSources.dbInfo.uri, config.dataSources.dbInfo.options); 30 | } 31 | 32 | if (config.env === 'development') { 33 | mongoose.set('debug', true); 34 | } 35 | 36 | this.conn.on('disconnected', () => { 37 | mongoose.connect(config.dataSources.dbInfo.uri); 38 | }); 39 | 40 | this.conn.on('error', (err) => { 41 | reject(err); 42 | }); 43 | 44 | this.conn.on('open', () => { 45 | resolve(this.conn); 46 | }); 47 | 48 | this.conn.once('open', () => { 49 | Logger.info('Connected to MongoDB', config.dataSources.dbInfo.uri); 50 | }); 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/graphql/projects/comment/model.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'koa'; 2 | import BaseModel from '../../base_model'; 3 | import DataLoader from '../../data_loader'; 4 | export default class CommentModel extends BaseModel { 5 | dataLoader: DataLoader; 6 | constructor(ctx: Context) { 7 | super(ctx); 8 | this.dataLoader = new DataLoader((keys) => new Promise((resolve, reject) => { 9 | this.ctx.connector.BlogComment.find({ 10 | _id: { 11 | $in: keys, 12 | }, 13 | }).then((res) => { 14 | resolve(res); 15 | }).catch((err) => { 16 | return reject(err); 17 | }); 18 | })); 19 | } 20 | async findAll() { 21 | const result = await this.ctx.connector.BlogComment.find(); 22 | return result; 23 | } 24 | 25 | async addComment (comment: any) { 26 | const { BlogComment } = this.ctx.connector; 27 | const commentInfo = new BlogComment(comment); 28 | const result = await commentInfo.save(); 29 | // const result = await this.getCommentById(user.userName); 30 | return result; 31 | } 32 | 33 | async findById(id: number) { 34 | const result = await this.getCommentById(id); 35 | return result; 36 | } 37 | 38 | async count() { 39 | const result = await this.ctx.connector.BlogComment.count(); 40 | return result; 41 | } 42 | 43 | async getByIds(ids: [number]) { 44 | const result = await this.dataLoader.loadManyItems(ids); 45 | return result; 46 | } 47 | 48 | async getCommentById(id: number) { 49 | const result = await this.dataLoader.loadOne(id); 50 | return result; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/routes/graphql.ts: -------------------------------------------------------------------------------- 1 | import { graphiqlKoa, graphqlKoa } from 'apollo-server-koa'; 2 | import costAnalysis from 'graphql-cost-analysis'; 3 | import * as depthLimit from 'graphql-depth-limit'; 4 | import { koa as koaVoyager } from 'graphql-voyager/middleware'; 5 | import { Context } from 'koa'; 6 | import { Controller, Ctx, Get, Post } from 'routing-controllers'; 7 | 8 | @Controller('') 9 | export default class GraphqlController { 10 | 11 | @Get('/graphql') 12 | @Post('/graphql') 13 | async graphql(@Ctx() ctx: Context, next: () => {}) { 14 | await graphqlKoa({ 15 | schema: ctx.graphqlSchema, 16 | tracing: true, 17 | cacheControl: false, 18 | context: ctx, 19 | formatResponse: (res, { query }) => { 20 | // return { 21 | // data: res.data, 22 | // }; 23 | return res; 24 | }, 25 | validationRules: [ 26 | costAnalysis({ 27 | maximumCost: 1000, 28 | createError: (maximumCost: number, cost: number) => { 29 | throw new Error(`Query is too complex: ${cost}. Maximum allowed complexity: ${maximumCost}`); 30 | }, 31 | }), 32 | depthLimit(4), 33 | ], 34 | })(ctx, next); 35 | return ctx; 36 | } 37 | 38 | @Get('/graphiql') 39 | async graphiql(@Ctx() ctx: Context) { 40 | await graphiqlKoa({ 41 | endpointURL: '/graphql', 42 | })(ctx); 43 | return ctx; 44 | } 45 | 46 | @Get('/voyager') 47 | async voyager(@Ctx() ctx: Context) { 48 | await koaVoyager({ 49 | endpointUrl: '/graphql', 50 | displayOptions: {}, 51 | })(ctx, () => null); 52 | return ctx; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /app/graphql/projects/user/model.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'koa'; 2 | import BaseModel from '../../base_model'; 3 | import DataLoader from '../../data_loader'; 4 | export default class UserModel extends BaseModel { 5 | dataLoader: DataLoader; 6 | constructor(ctx: Context) { 7 | super(ctx); 8 | this.dataLoader = new DataLoader((keys) => new Promise((resolve, reject) => { 9 | this.ctx.connector.InfoUser.find({ 10 | _id: { 11 | $in: keys, 12 | }, 13 | }).then((res) => { 14 | resolve(res); 15 | }).catch((err) => { 16 | return reject(err); 17 | }); 18 | })); 19 | } 20 | async findAll() { 21 | const result = await this.ctx.connector.InfoUser.find(); 22 | return result; 23 | } 24 | 25 | async addUser (user: any) { 26 | const { InfoUser } = this.ctx.connector; 27 | const userInfo = new InfoUser(user); 28 | const res = await userInfo.save(); 29 | return res; 30 | } 31 | 32 | async findById(userId: string) { 33 | const result = await this.getUserById(userId); 34 | return result; 35 | } 36 | 37 | async findByName(userName: string) { 38 | const result = await this.ctx.connector.InfoUser.find({ 39 | userName, 40 | }); 41 | return result; 42 | } 43 | 44 | async count() { 45 | const result = await this.ctx.connector.InfoUser.count(); 46 | return result; 47 | } 48 | 49 | async getUsersByIds(ids: [string]) { 50 | const result = await this.dataLoader.loadManyItems(ids); 51 | return result; 52 | } 53 | 54 | async getUserById(id: string) { 55 | const result = await this.dataLoader.loadOne(id); 56 | return result; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /front_end/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rgb", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node scripts/start.js", 7 | "build": "node scripts/build.js", 8 | "test": "node scripts/test.js --env=jsdom" 9 | }, 10 | "jest": { 11 | "collectCoverageFrom": [ 12 | "src/**/*.{js,jsx,ts,tsx}" 13 | ], 14 | "setupFiles": [ 15 | "/config/polyfills.js" 16 | ], 17 | "testMatch": [ 18 | "/src/**/__tests__/**/*.ts?(x)", 19 | "/src/**/?(*.)(spec|test).ts?(x)" 20 | ], 21 | "testEnvironment": "node", 22 | "testURL": "http://localhost", 23 | "transform": { 24 | "^.+\\.tsx?$": "/config/jest/typescriptTransform.js", 25 | "^.+\\.css$": "/config/jest/cssTransform.js", 26 | "^(?!.*\\.(js|jsx|mjs|css|json)$)": "/config/jest/fileTransform.js" 27 | }, 28 | "transformIgnorePatterns": [ 29 | "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|ts|tsx)$" 30 | ], 31 | "moduleNameMapper": { 32 | "^react-native$": "react-native-web" 33 | }, 34 | "moduleFileExtensions": [ 35 | "mjs", 36 | "web.ts", 37 | "ts", 38 | "web.tsx", 39 | "tsx", 40 | "web.js", 41 | "js", 42 | "web.jsx", 43 | "jsx", 44 | "json", 45 | "node" 46 | ], 47 | "globals": { 48 | "ts-jest": { 49 | "tsConfigFile": "/Users/jimmydaddy/study/myown/rgb/tsconfig.test.json" 50 | } 51 | } 52 | }, 53 | "babel": { 54 | "presets": [ 55 | "react-app" 56 | ] 57 | }, 58 | "eslintConfig": { 59 | "extends": "react-app" 60 | }, 61 | "proxy": "http://localhost:7001" 62 | } 63 | -------------------------------------------------------------------------------- /front_end/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/graphql/projects/article/model.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'koa'; 2 | import BaseModel from '../../base_model'; 3 | import DataLoader from '../../data_loader'; 4 | export default class ArticleModel extends BaseModel { 5 | dataLoader: DataLoader; 6 | constructor(ctx: Context) { 7 | super(ctx); 8 | this.dataLoader = new DataLoader((keys) => new Promise((resolve, reject) => { 9 | this.ctx.connector.BlogArticle.find({ 10 | _id: { 11 | $in: keys, 12 | }, 13 | }).then((res) => { 14 | resolve(res); 15 | }).catch((err) => { 16 | return reject(err); 17 | }); 18 | })); 19 | } 20 | async findAll() { 21 | const result = await this.ctx.connector.BlogArticle.find(); 22 | return result; 23 | } 24 | 25 | async addArticle (article: any) { 26 | const { BlogArticle } = this.ctx.connector; 27 | const articleInfo = new BlogArticle(article); 28 | const result = await articleInfo.save(); 29 | // const result = await this.getCommentById(user.userName); 30 | return result; 31 | } 32 | 33 | async insertComment(articleId, commentId) { 34 | const article = await this.findById(articleId); 35 | 36 | const ids = article.commentIds || []; 37 | 38 | ids.push(commentId); 39 | 40 | article.commentIds = ids; 41 | 42 | const result = await this.ctx.connector.BlogArticle.findByIdAndUpdate({ 43 | _id: article._id, 44 | }, article); 45 | 46 | return result; 47 | } 48 | 49 | async findByTitle(title: string) { 50 | const result = await this.ctx.connector.BlogArticle.find({ 51 | title, 52 | }); 53 | return result; 54 | } 55 | 56 | async findById(id: string) { 57 | const result = await this.getArticleById(id); 58 | return result; 59 | } 60 | 61 | async count() { 62 | const result = await this.ctx.connector.BlogArticle.count(); 63 | return result; 64 | } 65 | 66 | async getByIds(ids: [string]) { 67 | const result = await this.dataLoader.loadManyItems(ids); 68 | return result; 69 | } 70 | 71 | async getArticleById(id: string) { 72 | const result = await this.dataLoader.loadOne(id); 73 | return result; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /front_end/config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right