Compare commits

..

3 Commits

Author SHA1 Message Date
7700100a7d build: add pm2 ecosystem config. 2021-05-02 21:11:34 +08:00
9120332051 fix(articles): 修复返回值不匹配的问题。 2021-05-01 17:17:43 +08:00
fba867e0b5 backup 2021-04-24 14:47:45 +08:00
25 changed files with 18280 additions and 3364 deletions

25
.eslintrc.js Normal file
View File

@ -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',
},
};

36
.gitignore vendored Normal file
View File

@ -0,0 +1,36 @@
# 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
/config.yml

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

14
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,14 @@
{
"cSpell.words": [
"Repos",
"boardcat",
"gitea",
"lpush",
"lrange",
"metatype",
"pmessage",
"psubscribe",
"rpop",
"rpush"
]
}

14
ecosystem.config.js Normal file
View File

@ -0,0 +1,14 @@
module.exports = {
apps: [
{
name: 'fennec-be',
script: 'npm',
args: 'run start:prod',
watch: false,
ignore_watch: ['node_modules'],
log_date_format: 'MM-DD HH:mm:ss.SSS Z',
env: {},
max_restarts: 5,
},
],
};

21091
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -22,13 +22,12 @@
},
"dependencies": {
"@nestjs/bull": "^0.3.1",
"@nestjs/common": "^7.5.1",
"@nestjs/common": "^7.6.15",
"@nestjs/config": "^0.6.2",
"@nestjs/core": "^7.5.1",
"@nestjs/core": "^7.6.15",
"@nestjs/graphql": "^7.9.8",
"@nestjs/platform-express": "^7.5.1",
"@nestjs/platform-express": "^7.6.15",
"@nestjs/typeorm": "^7.1.5",
"@types/bull": "^3.15.0",
"apollo-server-express": "^2.19.2",
"bcrypt": "^5.0.0",
"body-parser": "^1.19.0",
@ -46,34 +45,32 @@
"ramda": "^0.27.1",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^6.6.3",
"rxjs": "^6.6.7",
"simple-git": "^2.35.0",
"typeorm": "^0.2.30"
},
"devDependencies": {
"@nestjs/cli": "^7.5.7",
"@nestjs/schematics": "^7.1.3",
"@nestjs/testing": "^7.5.1",
"@types/body-parser": "^1.19.0",
"@types/debug": "^4.1.5",
"@nestjs/schematics": "^7.3.1",
"@nestjs/testing": "^7.6.15",
"@types/express": "^4.17.8",
"@types/ioredis": "^4.22.3",
"@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",
"@types/jest": "^26.0.22",
"@types/node": "^14.14.41",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"apollo-server-testing": "^2.23.0",
"eslint": "^7.24.0",
"eslint-config-prettier": "7.2.0",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-prettier": "^3.4.0",
"jest": "^26.6.3",
"prettier": "^2.1.2",
"supertest": "^6.0.0",
"ts-jest": "^26.4.3",
"ts-loader": "^8.0.8",
"ts-jest": "^26.5.5",
"ts-loader": "^8.1.0",
"ts-node": "^9.0.0",
"tsconfig-paths": "^3.9.0",
"typescript": "^4.0.5"
"typescript": "^4.2.4"
},
"jest": {
"moduleFileExtensions": [

View File

@ -10,6 +10,7 @@ import { RedisModule } from 'nestjs-redis';
import { ParseBodyMiddleware } from './commons/middleware/parse-body.middleware';
import { BullModule } from '@nestjs/bull';
import { PubSubModule } from './commons/pub-sub/pub-sub.module';
import { ArticlesModule } from './articles/articles.module';
@Module({
imports: [
@ -72,6 +73,7 @@ import { PubSubModule } from './commons/pub-sub/pub-sub.module';
}),
inject: [ConfigService],
}),
ArticlesModule,
],
controllers: [AppController],
providers: [AppService, AppResolver],

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { ArticlesService } from './articles.service';
import { ArticlesResolver } from './articles.resolver';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Article } from './entities/article.entity';
@Module({
imports: [TypeOrmModule.forFeature([Article])],
providers: [ArticlesResolver, ArticlesService],
})
export class ArticlesModule {}

View File

@ -0,0 +1,25 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ArticlesResolver } from './articles.resolver';
import { ArticlesService } from './articles.service';
describe('ArticlesResolver', () => {
let resolver: ArticlesResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ArticlesResolver,
{
provide: ArticlesService,
useValue: {},
},
],
}).compile();
resolver = module.get<ArticlesResolver>(ArticlesResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -0,0 +1,40 @@
import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
import { ArticlesService } from './articles.service';
import { Article } from './entities/article.entity';
import { CreateArticleInput } from './dto/create-article.input';
import { UpdateArticleInput } from './dto/update-article.input';
@Resolver(() => Article)
export class ArticlesResolver {
constructor(private readonly articlesService: ArticlesService) {}
@Mutation(() => Article)
createArticle(
@Args('createArticleInput') createArticleInput: CreateArticleInput,
) {
return this.articlesService.create(createArticleInput);
}
@Query(() => [Article], { name: 'articles' })
findAll() {
return this.articlesService.findAll();
}
@Query(() => Article, { name: 'article' })
findOne(@Args('id', { type: () => String }) id: string) {
return this.articlesService.findOne(id);
}
@Mutation(() => Article)
async updateArticle(
@Args('updateArticleInput') updateArticleInput: UpdateArticleInput,
) {
const article = await this.articlesService.findOne(updateArticleInput.id);
return this.articlesService.update(article, updateArticleInput);
}
@Mutation(() => Int)
removeArticle(@Args('id', { type: () => String }) id: string) {
return this.articlesService.remove(id);
}
}

View File

@ -0,0 +1,27 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ArticlesService } from './articles.service';
import { Article } from './entities/article.entity';
describe('ArticlesService', () => {
let service: ArticlesService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ArticlesService,
{
provide: getRepositoryToken(Article),
useValue: new Repository(),
},
],
}).compile();
service = module.get<ArticlesService>(ArticlesService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,42 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { BaseDbService } from '../commons/services/base-db.service';
import { CreateArticleInput } from './dto/create-article.input';
import { UpdateArticleInput } from './dto/update-article.input';
import { Article } from './entities/article.entity';
@Injectable()
export class ArticlesService extends BaseDbService<Article> {
readonly uniqueFields: Array<keyof Article> = ['title'];
constructor(
@InjectRepository(Article)
readonly repository: Repository<Article>,
) {
super(repository);
}
async create(createArticleInput: CreateArticleInput) {
await this.isDuplicateEntity(createArticleInput);
return await this.repository.save(
this.repository.create(createArticleInput),
);
}
async findAll() {
return await this.repository.find({
order: { createdAt: 'DESC' },
});
}
async update(article: Article, updateArticleInput: UpdateArticleInput) {
await this.isDuplicateEntityForUpdate(article.id, updateArticleInput);
return await this.repository.save(
this.repository.merge(article, updateArticleInput),
);
}
async remove(id: string) {
await this.canRemove([id]);
return await this.repository.softDelete({ id }).then((d) => d.affected);
}
}

View File

@ -0,0 +1,20 @@
import { InputType } from '@nestjs/graphql';
import { IsDate, IsOptional, IsString, Length } from 'class-validator';
@InputType()
export class CreateArticleInput {
@IsString()
@Length(1, 100)
title: string;
@IsString()
@Length(2, 100000)
content: string;
@IsOptional()
@IsDate()
publishedAt?: Date;
@IsString({ each: true })
tags: string[];
}

View File

@ -0,0 +1,8 @@
import { CreateArticleInput } from './create-article.input';
import { InputType, Field, PartialType } from '@nestjs/graphql';
@InputType()
export class UpdateArticleInput extends PartialType(CreateArticleInput) {
@Field(() => String)
id: string;
}

View File

@ -0,0 +1,20 @@
import { ObjectType } from '@nestjs/graphql';
import { Column, Entity, Index } from 'typeorm';
import { AppBaseEntity } from '../../commons/entities/app-base-entity';
@Entity()
@ObjectType()
export class Article extends AppBaseEntity {
@Column()
title: string;
@Column({ type: 'text' })
content: string;
@Index()
@Column({ nullable: true })
publishedAt?: Date;
@Column({ type: 'varchar', array: true })
tags: string[];
}

View File

@ -112,7 +112,7 @@ export class BaseDbService<Entity extends AppBaseEntity> extends TypeormHelper {
}
}
async canYouRemoveWithIds(ids: string[]): Promise<void> {
async canRemove(ids: string[]): Promise<void> {
return;
}

View File

@ -1,24 +1,49 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
import { GraphQLModule } from '@nestjs/graphql';
import {
ApolloServerTestClient,
createTestClient,
} from 'apollo-server-testing';
import { gql } from 'apollo-server-express';
describe('AppController (e2e)', () => {
describe('ArticleResolver (e2e)', () => {
let app: INestApplication;
let apolloClient: ApolloServerTestClient;
beforeEach(async () => {
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
const module: GraphQLModule = moduleFixture.get<GraphQLModule>(
GraphQLModule,
);
// apolloServer is protected, we need to cast module to any to get it
apolloClient = createTestClient((module as any).apolloServer);
});
it('QUERY hello', async () => {
const res = await apolloClient.query({
query: gql`
query {
hello {
message
}
}
`,
variables: {},
});
expect(res.data).toEqual({
hello: {
message: 'Hello, World!',
},
});
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
afterAll(async () => {
await app?.close();
});
});

33
test/article.e2e-spec.ts Normal file
View File

@ -0,0 +1,33 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { GraphQLModule } from '@nestjs/graphql';
import {
ApolloServerTestClient,
createTestClient,
} from 'apollo-server-testing';
import { gql } from 'apollo-server-express';
describe('AppController (e2e)', () => {
let app: INestApplication;
let apolloClient: ApolloServerTestClient;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
const module: GraphQLModule = moduleFixture.get<GraphQLModule>(
GraphQLModule,
);
// apolloServer is protected, we need to cast module to any to get it
apolloClient = createTestClient((module as any).apolloServer);
});
afterAll(async () => {
await app?.close();
});
});

View File

View File

@ -1,8 +0,0 @@
for (let i = 1; i <= 5; i++) {
console.log(i * 10);
}
console.error('Error Message');
console.error('Error Message 2');
console.log('Bye-bye');
process.exit(1);

View File

@ -1,115 +0,0 @@
{
"secret": "boardcat",
"ref": "refs/heads/master",
"before": "429de1eaedf1da83f1e0e3ac3d8b20e771b7051c",
"after": "429de1eaedf1da83f1e0e3ac3d8b20e771b7051c",
"compare_url": "",
"commits": [
{
"id": "429de1eaedf1da83f1e0e3ac3d8b20e771b7051c",
"message": "test(pipeline-tasks): pass test cases.\n",
"url": "https://git.ivanli.cc/Fennec/fennec-be/commit/429de1eaedf1da83f1e0e3ac3d8b20e771b7051c",
"author": {
"name": "Ivan",
"email": "ivanli@live.cn",
"username": ""
},
"committer": {
"name": "Ivan",
"email": "ivanli@live.cn",
"username": ""
},
"verification": null,
"timestamp": "0001-01-01T00:00:00Z",
"added": null,
"removed": null,
"modified": null
}
],
"head_commit": null,
"repository": {
"id": 3,
"owner": {
"id": 3,
"login": "Fennec",
"full_name": "",
"email": "",
"avatar_url": "https://git.ivanli.cc/user/avatar/Fennec/-1",
"language": "",
"is_admin": false,
"last_login": "1970-01-01T08:00:00+08:00",
"created": "2021-01-30T16:46:11+08:00",
"username": "Fennec"
},
"name": "fennec-be",
"full_name": "Fennec/fennec-be",
"description": "Fennec CI/CD Back-End",
"empty": false,
"private": false,
"fork": false,
"template": false,
"parent": null,
"mirror": false,
"size": 1897,
"html_url": "https://git.ivanli.cc/Fennec/fennec-be",
"ssh_url": "ssh://gitea@git.ivanli.cc:7018/Fennec/fennec-be.git",
"clone_url": "https://git.ivanli.cc/Fennec/fennec-be.git",
"original_url": "",
"website": "",
"stars_count": 1,
"forks_count": 0,
"watchers_count": 1,
"open_issues_count": 0,
"open_pr_counter": 0,
"release_counter": 0,
"default_branch": "master",
"archived": false,
"created_at": "2021-01-31T09:58:38+08:00",
"updated_at": "2021-03-27T15:57:00+08:00",
"permissions": {
"admin": false,
"push": false,
"pull": false
},
"has_issues": true,
"internal_tracker": {
"enable_time_tracker": true,
"allow_only_contributors_to_track_time": true,
"enable_issue_dependencies": true
},
"has_wiki": true,
"has_pull_requests": true,
"has_projects": true,
"ignore_whitespace_conflicts": false,
"allow_merge_commits": true,
"allow_rebase": true,
"allow_rebase_explicit": true,
"allow_squash_merge": true,
"avatar_url": "",
"internal": false
},
"pusher": {
"id": 1,
"login": "Ivan",
"full_name": "Ivan Li",
"email": "ivan@noreply.%(DOMAIN)s",
"avatar_url": "https://git.ivanli.cc/user/avatar/Ivan/-1",
"language": "zh-CN",
"is_admin": true,
"last_login": "2021-03-26T22:28:05+08:00",
"created": "2021-01-23T18:15:30+08:00",
"username": "Ivan"
},
"sender": {
"id": 1,
"login": "Ivan",
"full_name": "Ivan Li",
"email": "ivan@noreply.%(DOMAIN)s",
"avatar_url": "https://git.ivanli.cc/user/avatar/Ivan/-1",
"language": "zh-CN",
"is_admin": true,
"last_login": "2021-03-26T22:28:05+08:00",
"created": "2021-01-23T18:15:30+08:00",
"username": "Ivan"
}
}

View File

@ -1,7 +0,0 @@
let timer;
let count = 0;
setTimeout(() => clearInterval(timer), 1_000);
timer = setInterval(() => {
console.log(++count * 10);
}, 95);

15
test/graphql-e2e.ts Normal file
View File

@ -0,0 +1,15 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const transformer = require('@nestjs/graphql/plugin');
module.exports.name = 'nestjs-graphql-transformer';
// you should change the version number anytime you change the configuration below - otherwise, jest will not detect changes
module.exports.version = 1;
module.exports.factory = (cs) => {
return transformer.before(
{
// @nestjs/graphql/plugin options (can be empty)
},
cs.tsCompiler.program,
);
};

View File

@ -5,5 +5,12 @@
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"globals": {
"ts-jest": {
"astTransformers": {
"before": ["<rootDir>/graphql-e2e.ts"]
}
}
}
}