Compare commits

..

No commits in common. "develop" and "master" have entirely different histories.

47 changed files with 8162 additions and 18271 deletions

View File

@ -1,25 +0,0 @@
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
View File

@ -1,36 +0,0 @@
# 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

View File

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

16
.vscode/settings.json vendored
View File

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

View File

@ -14,6 +14,5 @@ db:
port: 6379
password:
prefix: blog
etcd:
hosts:
- 'http://192.168.31.194:2379'
workspaces:
root: '/Users/ivanli/Projects/fennec/workspaces'

View File

@ -1,20 +0,0 @@
version: "3.9"
services:
postgres:
image: 'postgres:14'
restart: always
environment:
- POSTGRES_HOST_AUTH_METHOD=trust
ports:
- '${PG_PORT}:5432'
redis:
image: 'redis:6'
restart: always
ports:
- '${REDIS_PORT}:6379'
networks:
default:
name: 'blog-dev'
driver: bridge

View File

@ -1,61 +0,0 @@
import execa from 'execa';
import { URL } from 'url';
import { findFreePorts } from 'find-free-ports';
import YAML from 'js-yaml';
import { readFile, writeFile } from 'fs/promises';
const [PG_PORT, REDIS_PORT] = await findFreePorts(2);
await execa(
'docker-compose',
[
'-f',
new URL('./docker-compose.dev.yml', import.meta.url).pathname,
'up',
'-d',
],
{
env: {
PG_PORT,
REDIS_PORT,
},
stdout: process.stdout,
stderr: process.stderr,
},
);
console.log(`✅ Postgres is running on port ${PG_PORT}`);
console.log(`✅ Redis is running on port ${REDIS_PORT}`);
const config = await YAML.load(
await readFile(
new URL('../config.yml.example', import.meta.url).pathname,
'utf-8',
),
);
config.db.postgres = {
host: 'localhost',
port: PG_PORT,
database: 'postgres',
username: 'postgres',
password: '',
};
config.db.redis = {
host: 'localhost',
port: REDIS_PORT,
};
const configOutputPath = new URL('../config.yml', import.meta.url).pathname;
await writeFile(configOutputPath, YAML.dump(config), 'utf-8');
console.log(`✅ Config file is written to ${configOutputPath}`);
await execa.command('npm run typeorm -- migration:run', {
cwd: new URL('../', import.meta.url).pathname,
stdout: process.stdout,
stderr: process.stderr,
});
console.log(`✅ Database Initiated!`);

View File

@ -1,14 +0,0 @@
module.exports = {
apps: [
{
name: 'blog-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,
},
],
};

View File

@ -1,20 +0,0 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class articleAndTagMigration1635661602570 implements MigrationInterface {
name = 'articleAndTagMigration1635661602570'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "article" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP, "title" character varying NOT NULL, "content" text NOT NULL, "publishedAt" TIMESTAMP, "tags" character varying array NOT NULL, CONSTRAINT "PK_40808690eb7b915046558c0f81b" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_f330baf6be412e8dd60ff7f78e" ON "article" ("publishedAt") `);
await queryRunner.query(`CREATE TABLE "tag" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP, "name" character varying(100) NOT NULL, CONSTRAINT "PK_8e4052373c579afc1471f526760" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_6a9775008add570dc3e5a0bab7" ON "tag" ("name") `);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "public"."IDX_6a9775008add570dc3e5a0bab7"`);
await queryRunner.query(`DROP TABLE "tag"`);
await queryRunner.query(`DROP INDEX "public"."IDX_f330baf6be412e8dd60ff7f78e"`);
await queryRunner.query(`DROP TABLE "article"`);
}
}

View File

@ -1,22 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const yaml = require('js-yaml');
const fs = require('fs');
const path = require('path');
const config = yaml.load(
fs.readFileSync(path.join(__dirname, './config.yml'), 'utf8'),
);
module.exports = {
type: 'postgres',
host: config.db.postgres.host,
port: config.db.postgres.port,
username: config.db.postgres.username,
password: config.db.postgres.password,
database: config.db.postgres.database,
migrations: ['migrations/**/*.ts'],
entities: ['src/**/*.entity.ts'],
cli: {
migrationsDir: 'migrations',
},
};

25330
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "blog-be",
"version": "0.1.0",
"name": "fennec-be",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
@ -13,74 +13,67 @@
"start:dev": "DEBUG=fennec:* nest start --watch",
"start:debug": "DEBUG=fennec:* nest start --debug --watch",
"start:prod": "node dist/main",
"init:dev": "node docker/init-dev.mjs",
"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",
"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js"
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@fennec/configuration": "^0.0.2",
"@nestjs-lib/auth": "^0.2.3",
"@nestjs-lib/etcd3": "^0.0.1",
"@nestjs/common": "^8.1.1",
"@nestjs/config": "^1.0.3",
"@nestjs/core": "^8.1.1",
"@nestjs/graphql": "^9.1.1",
"@nestjs/platform-express": "^8.1.1",
"@nestjs/typeorm": "^8.0.2",
"apollo-server-express": "^3.4.0",
"bcrypt": "^5.0.1",
"@nestjs/bull": "^0.3.1",
"@nestjs/common": "^7.5.1",
"@nestjs/config": "^0.6.2",
"@nestjs/core": "^7.5.1",
"@nestjs/graphql": "^7.9.8",
"@nestjs/platform-express": "^7.5.1",
"@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",
"class-transformer": "^0.4.0",
"bull": "^3.20.1",
"class-transformer": "^0.3.2",
"class-validator": "^0.13.1",
"debug": "^4.3.2",
"graphql": "^15.6.1",
"graphql-tools": "^8.2.0",
"graphql-type-json": "^0.3.2",
"highlight.js": "^11.3.1",
"ioredis": "^4.28.0",
"js-yaml": "^4.1.0",
"marked": "^3.0.7",
"nestjs-redis": "^1.3.3",
"observable-to-async-generator": "^1.0.2",
"pg": "^8.7.1",
"debug": "^4.3.1",
"graphql": "^15.5.0",
"graphql-tools": "^7.0.2",
"ioredis": "^4.25.0",
"js-yaml": "^4.0.0",
"nestjs-redis": "^1.2.8",
"observable-to-async-generator": "^1.0.1-rc",
"pg": "^8.5.1",
"ramda": "^0.27.1",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.4.0",
"simple-git": "^2.47.0",
"typeorm": "^0.2.38"
"rxjs": "^6.6.3",
"simple-git": "^2.35.0",
"typeorm": "^0.2.30"
},
"devDependencies": {
"@nestjs/cli": "^8.1.4",
"@nestjs/schematics": "^8.0.4",
"@nestjs/testing": "^8.1.1",
"@types/express": "^4.17.13",
"@types/highlight.js": "^10.1.0",
"@types/jest": "^27.0.2",
"@types/marked": "^3.0.2",
"@types/node": "^16.11.2",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.1.0",
"@typescript-eslint/parser": "^5.1.0",
"apollo-server-testing": "^2.23.0",
"eslint": "^8.0.1",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"execa": "^5.1.1",
"find-free-ports": "^3.0.0",
"jest": "^27.3.1",
"prettier": "^2.4.1",
"supertest": "^6.1.6",
"ts-jest": "^27.0.7",
"ts-loader": "^9.2.6",
"ts-node": "^10.3.0",
"tsconfig-paths": "^3.11.0",
"typescript": "^4.4.4"
"@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",
"@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",
"eslint-config-prettier": "7.2.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"
},
"jest": {
"moduleFileExtensions": [
@ -96,9 +89,6 @@
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"moduleNameMapper": {
"^jose/(.*)$": "<rootDir>/../node_modules/jose/dist/node/cjs/$1"
},
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}

View File

@ -6,19 +6,18 @@ import { AppController } from './app.controller';
import { AppResolver } from './app.resolver';
import { AppService } from './app.service';
import configuration from './commons/config/configuration';
import { RedisModule } from 'nestjs-redis';
import { ParseBodyMiddleware } from './commons/middleware/parse-body.middleware';
import { ArticlesModule } from './articles/articles.module';
import { EtcdModule } from '@nestjs-lib/etcd3';
import { CommonsModule } from './commons/commons.module';
import { TagsModule } from './tags/tags.module';
import { BullModule } from '@nestjs/bull';
import { PubSubModule } from './commons/pub-sub/pub-sub.module';
@Module({
imports: [
ConfigModule.forRoot({
load: [configuration],
isGlobal: true,
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get<string>('db.postgres.host'),
@ -26,12 +25,13 @@ import { TagsModule } from './tags/tags.module';
username: configService.get<string>('db.postgres.username'),
password: configService.get<string>('db.postgres.password'),
database: configService.get<string>('db.postgres.database'),
synchronize: false,
synchronize: true,
autoLoadEntities: true,
}),
inject: [ConfigService],
}),
GraphQLModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
debug: configService.get<string>('env') !== 'prod',
playground: true,
@ -40,15 +40,38 @@ import { TagsModule } from './tags/tags.module';
}),
inject: [ConfigService],
}),
EtcdModule.forRootAsync({
BullModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
hosts: configService.get<string>('db.etcd.hosts', 'localhost:2379'),
redis: {
host: configService.get<string>('db.redis.host', 'localhost'),
port: configService.get<number>('db.redis.port', undefined),
password: configService.get<string>('db.redis.password', undefined),
},
}),
inject: [ConfigService],
}),
PubSubModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
redis: {
host: configService.get<string>('db.redis.host', 'localhost'),
port: configService.get<number>('db.redis.port', undefined),
password: configService.get<string>('db.redis.password', undefined),
},
}),
inject: [ConfigService],
}),
RedisModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
host: configService.get<string>('db.redis.host', 'localhost'),
port: configService.get<number>('db.redis.port', 6379),
password: configService.get<string>('db.redis.password', ''),
keyPrefix: configService.get<string>('db.redis.prefix', 'blog') + ':',
}),
inject: [ConfigService],
}),
CommonsModule,
ArticlesModule,
TagsModule,
],
controllers: [AppController],
providers: [AppService, AppResolver],

View File

@ -1,12 +0,0 @@
import { CommonsModule } from './../commons/commons.module';
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]), CommonsModule],
providers: [ArticlesResolver, ArticlesService],
})
export class ArticlesModule {}

View File

@ -1,30 +0,0 @@
import { JwtService } from '@nestjs-lib/auth';
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: {},
},
{
provide: JwtService,
useValue: {},
},
],
}).compile();
resolver = module.get<ArticlesResolver>(ArticlesResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -1,90 +0,0 @@
import {
Resolver,
Query,
Mutation,
Args,
Int,
ResolveField,
Parent,
} 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';
import * as marked from 'marked';
import highlight from 'highlight.js';
import { AccountRole, Roles } from '@nestjs-lib/auth';
import { ArticleHistory } from './models/article-history.model';
@Resolver(() => Article)
export class ArticlesResolver {
constructor(private readonly articlesService: ArticlesService) {}
@Roles(AccountRole.admin, AccountRole.super)
@Mutation(() => Article)
createArticle(
@Args('createArticleInput') createArticleInput: CreateArticleInput,
) {
return this.articlesService.create(createArticleInput);
}
@Query(() => [Article], { name: 'articles' })
async findAll() {
return await this.articlesService.findAll();
}
@Query(() => Article, { name: 'article' })
findOne(@Args('id', { type: () => String }) id: string) {
return this.articlesService.findOne(id);
}
@Roles(AccountRole.admin, AccountRole.super)
@Mutation(() => Article)
async updateArticle(
@Args('updateArticleInput') updateArticleInput: UpdateArticleInput,
) {
const article = await this.articlesService.findOne(updateArticleInput.id);
return this.articlesService.update(article, updateArticleInput);
}
@Roles(AccountRole.admin, AccountRole.super)
@Mutation(() => Int)
removeArticle(@Args('id', { type: () => String }) id: string) {
return this.articlesService.remove(id);
}
@ResolveField(() => String)
async html(@Parent() article: Article) {
const tokens = marked.lexer(article.content);
const index = tokens.findIndex((token) => ['heading'].includes(token.type));
if (index !== -1) {
tokens.splice(index, 1);
}
return marked.parser(tokens, {
gfm: true,
smartLists: true,
smartypants: true,
langPrefix: 'hljs language-',
highlight: (code, language) => {
return highlight.highlight(code, {
language: highlight.getLanguage(language) ? language : 'plaintext',
}).value;
},
});
}
@ResolveField(() => String, { nullable: true })
async description(@Parent() article: Article) {
const tokens = marked.lexer(article.content);
const token = tokens.find((token) =>
['blockquote', 'paragraph'].includes(token.type),
) as { text: string };
return token?.text;
}
@ResolveField(() => [ArticleHistory])
async histories(@Parent() article: Article) {
return article.histories;
}
}

View File

@ -1,27 +0,0 @@
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

@ -1,42 +0,0 @@
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

@ -1,7 +0,0 @@
import { ObjectType, OmitType } from '@nestjs/graphql';
import { Article } from '../entities/article.entity';
@ObjectType()
export class ArticleListItemDto extends OmitType(Article, [
'content',
] as const) {}

View File

@ -1,20 +0,0 @@
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

@ -1,8 +0,0 @@
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

@ -1,25 +0,0 @@
import { ArticleHistory } from './../models/article-history.model';
import { HideField, 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[];
@HideField()
@Column({ type: 'jsonb', default: [] })
histories: ArticleHistory[];
}

View File

@ -1,15 +0,0 @@
import { Column } from 'typeorm';
import { ObjectType, Field } from '@nestjs/graphql';
import { Article } from '../entities/article.entity';
@ObjectType()
export class ArticleHistory {
@Field(() => Object)
payload: Partial<Article>;
updatedAt: Date;
automatic: boolean;
published: boolean;
}

View File

@ -1,12 +1,10 @@
import { ObjectScalar } from './scalars/object.scalar';
import { Module } from '@nestjs/common';
import { PasswordConverter } from './services/password-converter';
import { PubSubModule } from './pub-sub/pub-sub.module';
import { AuthModule } from '@nestjs-lib/auth';
@Module({
imports: [PubSubModule, AuthModule],
providers: [PasswordConverter, ObjectScalar],
exports: [PasswordConverter, AuthModule],
providers: [PasswordConverter],
exports: [PasswordConverter],
imports: [PubSubModule],
})
export class CommonsModule {}

View File

@ -1,9 +1,9 @@
import { readConfiguration } from '@fennec/configuration';
import { readFileSync } from 'fs';
import * as yaml from 'js-yaml';
import { join } from 'path';
export default () => {
return readConfiguration({
etcd: {
hosts: '192.168.31.2:2379',
},
});
return yaml.load(
readFileSync(join(__dirname, '../../../config.yml'), 'utf8'),
) as unknown;
};

View File

@ -12,7 +12,7 @@ export class AppBaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@CreateDateColumn({ select: false })
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn({ select: false })

View File

@ -4,7 +4,7 @@ import { PubSub } from './pub-sub';
debug.enable('app:pubsub:*');
describe.skip('PubSub', () => {
describe('PubSub', () => {
let instance: PubSub;
beforeEach(async () => {

View File

@ -1,13 +0,0 @@
import { Scalar, CustomScalar } from '@nestjs/graphql';
import { GraphQLJSONObject } from 'graphql-type-json';
@Scalar('Object', (type) => Object)
export class ObjectScalar implements CustomScalar<number, Object> {
description = GraphQLJSONObject.description;
parseValue = GraphQLJSONObject.parseValue;
serialize = GraphQLJSONObject.serialize;
parseLiteral = GraphQLJSONObject.parseLiteral;
}

View File

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

View File

@ -1,4 +1,3 @@
import { ServiceRegister } from '@fennec/configuration';
import { ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
@ -16,11 +15,6 @@ async function bootstrap() {
}),
);
app.useGlobalFilters(new HttpExceptionFilter());
const server = await app.listen(configService.get<number>('http.port', 0));
const port = server.address().port;
const register = new ServiceRegister({ etcd: { hosts: 'http://rpi:2379' } });
register.register('blog/api', `http://localhost:${port}`);
register.register('admin.blog/api', `http://localhost:${port}`);
register.register('api.blog', `http://localhost:${port}`);
await app.listen(configService.get<number>('http.port'));
}
bootstrap();

View File

@ -1,9 +0,0 @@
import { InputType } from '@nestjs/graphql';
import { IsString, Length } from 'class-validator';
@InputType()
export class CreateTagInput {
@IsString()
@Length(1, 100)
name: string;
}

View File

@ -1,9 +0,0 @@
import { CreateTagInput } from './create-tag.input';
import { InputType, PartialType } from '@nestjs/graphql';
import { IsUUID } from 'class-validator';
@InputType()
export class UpdateTagInput extends PartialType(CreateTagInput) {
@IsUUID()
id: string;
}

View File

@ -1,11 +0,0 @@
import { AppBaseEntity } from './../../commons/entities/app-base-entity';
import { ObjectType } from '@nestjs/graphql';
import { Column, Index, Entity } from 'typeorm';
@Entity()
@ObjectType()
export class Tag extends AppBaseEntity {
@Index({ unique: true })
@Column({ length: 100 })
name: string;
}

View File

@ -1,12 +0,0 @@
import { Tag } from './entities/tag.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CommonsModule } from './../commons/commons.module';
import { Module } from '@nestjs/common';
import { TagsService } from './tags.service';
import { TagsResolver } from './tags.resolver';
@Module({
imports: [CommonsModule, TypeOrmModule.forFeature([Tag])],
providers: [TagsResolver, TagsService],
})
export class TagsModule {}

View File

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

View File

@ -1,36 +0,0 @@
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { TagsService } from './tags.service';
import { Tag } from './entities/tag.entity';
import { CreateTagInput } from './dto/create-tag.input';
import { UpdateTagInput } from './dto/update-tag.input';
@Resolver(() => Tag)
export class TagsResolver {
constructor(private readonly tagsService: TagsService) {}
@Mutation(() => Tag)
createTag(@Args('createTagInput') createTagInput: CreateTagInput) {
return this.tagsService.create(createTagInput);
}
@Query(() => [Tag], { name: 'tags' })
findAll() {
return this.tagsService.findAll();
}
@Query(() => Tag, { name: 'tag' })
findOne(@Args('id', { type: () => String }) id: string) {
return this.tagsService.findOne(id);
}
@Mutation(() => Tag)
async updateTag(@Args('updateTagInput') updateTagInput: UpdateTagInput) {
const tag = await this.tagsService.findOne(updateTagInput.id);
return this.tagsService.update(tag, updateTagInput);
}
@Mutation(() => Tag)
removeTag(@Args('id', { type: () => String }) id: string) {
return this.tagsService.remove(id);
}
}

View File

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

View File

@ -1,45 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { BaseDbService } from './../commons/services/base-db.service';
import { Injectable } from '@nestjs/common';
import { CreateTagInput } from './dto/create-tag.input';
import { UpdateTagInput } from './dto/update-tag.input';
import { Tag } from './entities/tag.entity';
@Injectable()
export class TagsService extends BaseDbService<Tag> {
readonly uniqueFields: Array<keyof Tag> = ['name'];
constructor(@InjectRepository(Tag) readonly repository: Repository<Tag>) {
super(repository);
}
/**
* create or recover a tag
*/
async create(createTagInput: CreateTagInput): Promise<Tag> {
const old = await this.repository.findOne({ name: createTagInput.name });
return this.repository.save(
old
? this.repository.merge(old, createTagInput)
: this.repository.create(createTagInput),
);
}
async findAll() {
return await this.repository.find({
order: { createdAt: 'DESC' },
});
}
async update(tag: Tag, updateTagInput: UpdateTagInput) {
await this.isDuplicateEntityForUpdate(tag.id, updateTagInput);
return await this.repository.save(
this.repository.merge(tag, updateTagInput),
);
}
async remove(id: string) {
await this.canRemove([id]);
return await this.repository.softDelete({ id }).then((d) => d.affected);
}
}

View File

@ -1,49 +1,24 @@
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('ArticleResolver (e2e)', () => {
describe('AppController (e2e)', () => {
let app: INestApplication;
let apolloClient: ApolloServerTestClient;
beforeAll(async () => {
beforeEach(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!',
},
});
});
afterAll(async () => {
await app?.close();
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

View File

@ -1,33 +0,0 @@
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();
});
});

8
test/data/bad-work.js Normal file
View File

@ -0,0 +1,8 @@
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

@ -0,0 +1,115 @@
{
"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

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

View File

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

View File

@ -6,12 +6,10 @@
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"lib": ["es2020"],
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true
"incremental": true
}
}