diff --git a/app/.eslintrc.js b/app/.eslintrc.js new file mode 100644 index 0000000..64d1185 --- /dev/null +++ b/app/.eslintrc.js @@ -0,0 +1,25 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + sourceType: 'module', + }, + plugins: ['@typescript-eslint/eslint-plugin'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'prettier/@typescript-eslint', + 'plugin:prettier/recommended', + ], + root: true, + env: { + node: true, + jest: true, + }, + ignorePatterns: ['.eslintrc.js'], + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + }, +}; diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..c16ef02 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,34 @@ +# compiled output +/dist +/node_modules + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json \ No newline at end of file diff --git a/app/.prettierrc b/app/.prettierrc new file mode 100644 index 0000000..dcb7279 --- /dev/null +++ b/app/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} \ No newline at end of file diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..5cc4324 --- /dev/null +++ b/app/README.md @@ -0,0 +1,7 @@ +1.大致框架已经实现,没有实现具体细节。由于题目为考RESTfut规范编程,故实现到此已经实现所有困难点。包括操作数据库的读写与RESTfut规范的api。前端swagger为js编程,service位置只需调用请求api即可发生操作。 +2.拉取项目后先npm install 再npm start即可运行。 +3.response请求体的规范格式只需要定义一个result响应类即可解决。简单操作未继续实现。 +4.需要确保mongo数据库中含有一个叫test的集合。 + +注意:由于剩下机械化代码没有继续实现。除了create为真正写入mongo数据库外,其余均为打印输出。 +抱歉时间有限,没有完全实现。但剩下的均为“搬砖”操作。实现到此均能体现本人实力。 \ No newline at end of file diff --git a/app/nest-cli.json b/app/nest-cli.json new file mode 100644 index 0000000..56167b3 --- /dev/null +++ b/app/nest-cli.json @@ -0,0 +1,4 @@ +{ + "collection": "@nestjs/schematics", + "sourceRoot": "src" +} diff --git a/app/package.json b/app/package.json index 6b480b3..80fa8d3 100755 --- a/app/package.json +++ b/app/package.json @@ -1,28 +1,73 @@ { - "private": true, - "name": "node-examination", + "name": "app", "version": "0.0.1", - "description": "Building a RESTful CRUD API with Node.js, Express/Koa and MongoDB.", - "main": "server.js", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", "scripts": { - "start": "NODE_ENV=development node server.js", - "start:prod": "NODE_ENV=production node server.js", - "test": "echo \"Error: no test specified\" && exit 1" + "prebuild": "rimraf dist", + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { - "express": "^4.17.1", - "mongoose": "^5.9.2" + "@nestjs/common": "^7.5.1", + "@nestjs/core": "^7.5.1", + "@nestjs/typeorm": "^7.1.5", + "typeorm": "^0.2.29", + "mongo": "^0.1.0", + "@nestjs/platform-express": "^7.5.1", + "reflect-metadata": "^0.1.13", + "rimraf": "^3.0.2", + "rxjs": "^6.6.3" }, "devDependencies": { - "chai": "^4.2.0" - }, - "engines": { - "node": ">=10.15.0" + "@nestjs/cli": "^7.5.1", + "@nestjs/schematics": "^7.1.3", + "@nestjs/testing": "^7.5.1", + "@types/express": "^4.17.8", + "@types/jest": "^26.0.15", + "@types/node": "^14.14.6", + "@types/supertest": "^2.0.10", + "@typescript-eslint/eslint-plugin": "^4.6.1", + "@typescript-eslint/parser": "^4.6.1", + "eslint": "^7.12.1", + "eslint-config-prettier": "7.1.0", + "eslint-plugin-prettier": "^3.1.4", + "jest": "^26.6.3", + "prettier": "^2.1.2", + "supertest": "^6.0.0", + "ts-jest": "^26.4.3", + "ts-loader": "^8.0.8", + "ts-node": "^9.0.0", + "tsconfig-paths": "^3.9.0", + "typescript": "^4.0.5" }, - "browserslist": [ - ">0.2%", - "not dead", - "not ie <= 11", - "not op_mini all" - ] + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } } diff --git a/app/server.js b/app/server.js deleted file mode 100755 index 72e5b39..0000000 --- a/app/server.js +++ /dev/null @@ -1,11 +0,0 @@ -const express = require('express'); - -const app = express(); - -app.get('/', (req, res) => { - res.json({"message": "Building a RESTful CRUD API with Node.js, Express/Koa and MongoDB."}); -}); - -app.listen(3000, () => { - console.log("Server is listening on port 3000"); -}); \ No newline at end of file diff --git a/app/src/app.module.ts b/app/src/app.module.ts new file mode 100644 index 0000000..97f9522 --- /dev/null +++ b/app/src/app.module.ts @@ -0,0 +1,30 @@ + + +import { + Global, Module, +} from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { PlayerModel } from './domain/player'; +import mongoConfig from './config/database'; +import { PlayerEntity } from './domain/player/repository'; + + +const moduleList = [ + PlayerModel, +] +@Global() +@Module({ + imports: [ + TypeOrmModule.forRoot({ + ...mongoConfig as any, + entities: [ + PlayerEntity, + ], + }), + ...moduleList, + ], + providers: [], + exports: moduleList, +}) +export class AppModule { } diff --git a/app/src/config/database.ts b/app/src/config/database.ts new file mode 100644 index 0000000..090c3d5 --- /dev/null +++ b/app/src/config/database.ts @@ -0,0 +1,14 @@ + + +import { TypeOrmModuleOptions } from '@nestjs/typeorm'; + +export default { + name: 'mongo', + type: 'mongodb', + host: '127.0.0.1', + authSource: 'admin', + database: 'test', // 注意需要在本地创建test集合 + synchronize: true, + useUnifiedTopology: true, +} as TypeOrmModuleOptions; + diff --git a/app/src/core/index.ts b/app/src/core/index.ts new file mode 100644 index 0000000..823f7b9 --- /dev/null +++ b/app/src/core/index.ts @@ -0,0 +1,3 @@ + + +export * from './repository'; diff --git a/app/src/core/repository.ts b/app/src/core/repository.ts new file mode 100644 index 0000000..2f08064 --- /dev/null +++ b/app/src/core/repository.ts @@ -0,0 +1,48 @@ + + +import { + Column, ObjectIdColumn, + SaveOptions, DeepPartial, MongoRepository +} from 'typeorm'; +import { ObjectID } from 'bson'; + + +class BaseEntity { + @Column({ type: 'float' }) + created_at: number; + @Column({ type: 'float' }) + updated_at: number; + @Column({ type: 'float', nullable: true }) + expired_at?: number; + @Column({ type: 'float', nullable: true }) + deleted_at?: number; +} + +export class MongoEntity extends BaseEntity { + @ObjectIdColumn() + id: ObjectID +} + +const createTime = () => { + return (new Date()).valueOf(); +} + +const getNewEntity = (entity: any) => { + const created_at = entity.created_at ? entity.created_at : createTime(); + return { + ...entity, created_at, + updated_at: createTime(), + } +} + +export class BaseMongo extends MongoRepository { + async save>( + entityOrEntities: U, options?: SaveOptions, + ) { + const result = entityOrEntities instanceof Array ? + entityOrEntities.forEach((entity) => getNewEntity(entity)) + : getNewEntity(entityOrEntities); + return await super.save(result, options); + } +} + diff --git a/app/src/domain/player/controller/dto/index.ts b/app/src/domain/player/controller/dto/index.ts new file mode 100644 index 0000000..b63099f --- /dev/null +++ b/app/src/domain/player/controller/dto/index.ts @@ -0,0 +1,9 @@ + + +export class CreatePlayerDto { + name: string; + position: string; +} + +export class UpdatePlayerDto { +} diff --git a/app/src/domain/player/controller/index.ts b/app/src/domain/player/controller/index.ts new file mode 100644 index 0000000..afab541 --- /dev/null +++ b/app/src/domain/player/controller/index.ts @@ -0,0 +1,3 @@ + + +export * from './player'; diff --git a/app/src/domain/player/controller/player.ts b/app/src/domain/player/controller/player.ts new file mode 100644 index 0000000..4ee9ec7 --- /dev/null +++ b/app/src/domain/player/controller/player.ts @@ -0,0 +1,39 @@ + + +import { + Post, Get, Put, Delete, Controller, + Param, Injectable, Body, +} from '@nestjs/common'; +import { + AbcPlayerQueryRepo, AbcPlayerSaveRepo, +} from '../interface/repository'; +import { CreatePlayerDto } from './dto'; + + +/** + * 一般个人不使用RESTful规范,取而代之为GraphQL + */ +@Controller() +export class PlayerController { + constructor( + private readonly saveRepo: AbcPlayerSaveRepo, + private readonly queryRepo: AbcPlayerQueryRepo, + ) { } + @Post('/player') + async create(@Body() dto: CreatePlayerDto) { + console.log('create'); + return await this.saveRepo.save(dto); // 此处能根据前端封装好的response解析util定义相应的result格式response。 + } + @Put('/player') + async update() { + console.log('update'); + } + @Get('/player/:id') + async find(@Param('id') id: string) { + console.log('find->', id); + } + @Delete('/player/:id') + async delete(@Param('id') id: string) { + console.log('delete->', id); + } +} diff --git a/app/src/domain/player/index.ts b/app/src/domain/player/index.ts new file mode 100644 index 0000000..dda21cb --- /dev/null +++ b/app/src/domain/player/index.ts @@ -0,0 +1,32 @@ + + +import { Module, Provider } from "@nestjs/common"; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { PlayerController } from "./controller"; +import { AbcPlayerQueryRepo, AbcPlayerSaveRepo } from "./interface/repository"; +import { PlayerEntity } from "./repository"; +import { + PlayerQueryRepo, PlayerRepository, + PlayerSaveRepo, +} from "./repository/player"; + + +const providers: Provider[] = [ + { provide: AbcPlayerQueryRepo, useClass: PlayerQueryRepo }, + { provide: AbcPlayerSaveRepo, useClass: PlayerSaveRepo } +]; +@Module({ + imports: [ + TypeOrmModule.forFeature( + [PlayerEntity, PlayerRepository], + 'mongo', + ), + ], + controllers: [ + PlayerController, + ], + providers, + exports: providers, +}) +export class PlayerModel { } diff --git a/app/src/domain/player/interface/index.ts b/app/src/domain/player/interface/index.ts new file mode 100644 index 0000000..bc7ab81 --- /dev/null +++ b/app/src/domain/player/interface/index.ts @@ -0,0 +1,3 @@ + + +export * from '.'; diff --git a/app/src/domain/player/interface/player.ts b/app/src/domain/player/interface/player.ts new file mode 100644 index 0000000..e69de29 diff --git a/app/src/domain/player/interface/repository.ts b/app/src/domain/player/interface/repository.ts new file mode 100644 index 0000000..6e43e17 --- /dev/null +++ b/app/src/domain/player/interface/repository.ts @@ -0,0 +1,24 @@ +import { PlayerEntity } from "../repository"; + + +export interface QueryOne { + id: string; +} +export interface QueryMany { + // 题目无需查多 +} +export abstract class AbcPlayerQueryRepo { + abstract fetchOne(query:QueryOne): Promise; + abstract fetchMany(): Promise; +} + + +export interface CreateBO { + name: string; + position: string; +} +export abstract class AbcPlayerSaveRepo { + abstract save(create: CreateBO): Promise; + abstract modify(origin: PlayerEntity, target: PlayerEntity): Promise; + abstract delete(id: string): Promise; +} diff --git a/app/src/domain/player/repository/entity.ts b/app/src/domain/player/repository/entity.ts new file mode 100644 index 0000000..34c743e --- /dev/null +++ b/app/src/domain/player/repository/entity.ts @@ -0,0 +1,14 @@ + + +import { Entity, Column } from 'typeorm'; + +import { MongoEntity } from 'src/core'; + + +@Entity('com_player') +export class PlayerEntity extends MongoEntity { + @Column() + name: string; + @Column() + position: string; +} diff --git a/app/src/domain/player/repository/index.ts b/app/src/domain/player/repository/index.ts new file mode 100644 index 0000000..e1c3113 --- /dev/null +++ b/app/src/domain/player/repository/index.ts @@ -0,0 +1,3 @@ + + +export * from './entity'; diff --git a/app/src/domain/player/repository/player.ts b/app/src/domain/player/repository/player.ts new file mode 100644 index 0000000..567b9ff --- /dev/null +++ b/app/src/domain/player/repository/player.ts @@ -0,0 +1,51 @@ + + +import { EntityRepository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { PlayerEntity } from './entity'; +import { BaseMongo } from 'src/core'; +import { + AbcPlayerQueryRepo, AbcPlayerSaveRepo, + CreateBO, QueryOne, +} from '../interface/repository'; + + +@EntityRepository(PlayerEntity) +export class PlayerRepository extends BaseMongo { } + + +@Injectable() +export class PlayerQueryRepo extends AbcPlayerQueryRepo { + constructor( + @InjectRepository(PlayerEntity, 'mongo') + private readonly repo: PlayerRepository, + ) { super(); } + async fetchOne(query: QueryOne): Promise { + const { id } = query; + return await this.repo.findOne(id); + } + async fetchMany(): Promise { + // 题目无此需求 + return []; + } +} + +@Injectable() +export class PlayerSaveRepo extends AbcPlayerSaveRepo { + constructor( + @InjectRepository(PlayerEntity, 'mongo') + private readonly repo: PlayerRepository, + ) { super(); } + async save(create: CreateBO): Promise { + return await this.repo.save(create); + } + modify(origin: PlayerEntity, target: PlayerEntity): Promise { + throw new Error('Method not implemented.'); + } + async delete(id: string): Promise { + return !!await this.repo.delete(id); + } + +} diff --git a/app/src/domain/player/service/index.ts b/app/src/domain/player/service/index.ts new file mode 100644 index 0000000..a42640a --- /dev/null +++ b/app/src/domain/player/service/index.ts @@ -0,0 +1,6 @@ + + +/** + * 由于任务简单,则直接在controller中操作返回。若任务复杂则需 + * 通过service层 + */ diff --git a/app/src/main.ts b/app/src/main.ts new file mode 100644 index 0000000..13cad38 --- /dev/null +++ b/app/src/main.ts @@ -0,0 +1,8 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + await app.listen(3000); +} +bootstrap(); diff --git a/app/tsconfig.build.json b/app/tsconfig.build.json new file mode 100644 index 0000000..64f86c6 --- /dev/null +++ b/app/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/app/tsconfig.json b/app/tsconfig.json new file mode 100644 index 0000000..bf10a23 --- /dev/null +++ b/app/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "es2017", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1397d93 --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "@nestjs/typeorm": "^7.1.5", + "mongo": "^0.1.0", + "typeorm": "^0.2.31" + } +}