Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
e413163993 | |||
4b8bb47800 | |||
ece8ccf27a | |||
|
6cf4d1b748 | ||
|
6f6018501b | ||
|
6de2f85b24 | ||
|
d0f6b8f9a6 | ||
|
51d5ac6ee6 | ||
|
76862b738d | ||
|
2b7e344931 | ||
|
c4a0f3e405 | ||
|
7700100a7d | ||
|
9120332051 | ||
|
fba867e0b5 |
25
.eslintrc.js
Normal file
25
.eslintrc.js
Normal 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
36
.gitignore
vendored
Normal 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
4
.prettierrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
16
.vscode/settings.json
vendored
Normal file
16
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"boardcat",
|
||||||
|
"execa",
|
||||||
|
"gitea",
|
||||||
|
"lpush",
|
||||||
|
"lrange",
|
||||||
|
"metatype",
|
||||||
|
"pmessage",
|
||||||
|
"psubscribe",
|
||||||
|
"QLJSON",
|
||||||
|
"Repos",
|
||||||
|
"rpop",
|
||||||
|
"rpush"
|
||||||
|
]
|
||||||
|
}
|
@ -14,5 +14,6 @@ db:
|
|||||||
port: 6379
|
port: 6379
|
||||||
password:
|
password:
|
||||||
prefix: blog
|
prefix: blog
|
||||||
workspaces:
|
etcd:
|
||||||
root: '/Users/ivanli/Projects/fennec/workspaces'
|
hosts:
|
||||||
|
- 'http://192.168.31.194:2379'
|
20
docker/docker-compose.dev.yml
Normal file
20
docker/docker-compose.dev.yml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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
|
61
docker/init-dev.mjs
Normal file
61
docker/init-dev.mjs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
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!`);
|
14
ecosystem.config.js
Normal file
14
ecosystem.config.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
20
migrations/1635661602570-articleAndTagMigration.ts
Normal file
20
migrations/1635661602570-articleAndTagMigration.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
22
ormconfig.js
Normal file
22
ormconfig.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/* 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',
|
||||||
|
},
|
||||||
|
};
|
25262
package-lock.json
generated
25262
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
108
package.json
108
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fennec-be",
|
"name": "blog-be",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@ -13,67 +13,74 @@
|
|||||||
"start:dev": "DEBUG=fennec:* nest start --watch",
|
"start:dev": "DEBUG=fennec:* nest start --watch",
|
||||||
"start:debug": "DEBUG=fennec:* nest start --debug --watch",
|
"start:debug": "DEBUG=fennec:* nest start --debug --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "node dist/main",
|
||||||
|
"init:dev": "node docker/init-dev.mjs",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"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"
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
|
"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/bull": "^0.3.1",
|
"@fennec/configuration": "^0.0.2",
|
||||||
"@nestjs/common": "^7.5.1",
|
"@nestjs-lib/auth": "^0.2.3",
|
||||||
"@nestjs/config": "^0.6.2",
|
"@nestjs-lib/etcd3": "^0.0.1",
|
||||||
"@nestjs/core": "^7.5.1",
|
"@nestjs/common": "^8.1.1",
|
||||||
"@nestjs/graphql": "^7.9.8",
|
"@nestjs/config": "^1.0.3",
|
||||||
"@nestjs/platform-express": "^7.5.1",
|
"@nestjs/core": "^8.1.1",
|
||||||
"@nestjs/typeorm": "^7.1.5",
|
"@nestjs/graphql": "^9.1.1",
|
||||||
"@types/bull": "^3.15.0",
|
"@nestjs/platform-express": "^8.1.1",
|
||||||
"apollo-server-express": "^2.19.2",
|
"@nestjs/typeorm": "^8.0.2",
|
||||||
"bcrypt": "^5.0.0",
|
"apollo-server-express": "^3.4.0",
|
||||||
|
"bcrypt": "^5.0.1",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"bull": "^3.20.1",
|
"class-transformer": "^0.4.0",
|
||||||
"class-transformer": "^0.3.2",
|
|
||||||
"class-validator": "^0.13.1",
|
"class-validator": "^0.13.1",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.2",
|
||||||
"graphql": "^15.5.0",
|
"graphql": "^15.6.1",
|
||||||
"graphql-tools": "^7.0.2",
|
"graphql-tools": "^8.2.0",
|
||||||
"ioredis": "^4.25.0",
|
"graphql-type-json": "^0.3.2",
|
||||||
"js-yaml": "^4.0.0",
|
"highlight.js": "^11.3.1",
|
||||||
"nestjs-redis": "^1.2.8",
|
"ioredis": "^4.28.0",
|
||||||
"observable-to-async-generator": "^1.0.1-rc",
|
"js-yaml": "^4.1.0",
|
||||||
"pg": "^8.5.1",
|
"marked": "^3.0.7",
|
||||||
|
"nestjs-redis": "^1.3.3",
|
||||||
|
"observable-to-async-generator": "^1.0.2",
|
||||||
|
"pg": "^8.7.1",
|
||||||
"ramda": "^0.27.1",
|
"ramda": "^0.27.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rxjs": "^6.6.3",
|
"rxjs": "^7.4.0",
|
||||||
"simple-git": "^2.35.0",
|
"simple-git": "^2.47.0",
|
||||||
"typeorm": "^0.2.30"
|
"typeorm": "^0.2.38"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^7.5.7",
|
"@nestjs/cli": "^8.1.4",
|
||||||
"@nestjs/schematics": "^7.1.3",
|
"@nestjs/schematics": "^8.0.4",
|
||||||
"@nestjs/testing": "^7.5.1",
|
"@nestjs/testing": "^8.1.1",
|
||||||
"@types/body-parser": "^1.19.0",
|
"@types/express": "^4.17.13",
|
||||||
"@types/debug": "^4.1.5",
|
"@types/highlight.js": "^10.1.0",
|
||||||
"@types/express": "^4.17.8",
|
"@types/jest": "^27.0.2",
|
||||||
"@types/ioredis": "^4.22.3",
|
"@types/marked": "^3.0.2",
|
||||||
"@types/jest": "^26.0.15",
|
"@types/node": "^16.11.2",
|
||||||
"@types/node": "^14.14.6",
|
"@types/supertest": "^2.0.11",
|
||||||
"@types/supertest": "^2.0.10",
|
"@typescript-eslint/eslint-plugin": "^5.1.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.6.1",
|
"@typescript-eslint/parser": "^5.1.0",
|
||||||
"@typescript-eslint/parser": "^4.6.1",
|
"apollo-server-testing": "^2.23.0",
|
||||||
"eslint": "^7.12.1",
|
"eslint": "^8.0.1",
|
||||||
"eslint-config-prettier": "7.2.0",
|
"eslint-config-prettier": "8.3.0",
|
||||||
"eslint-plugin-prettier": "^3.1.4",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"jest": "^26.6.3",
|
"execa": "^5.1.1",
|
||||||
"prettier": "^2.1.2",
|
"find-free-ports": "^3.0.0",
|
||||||
"supertest": "^6.0.0",
|
"jest": "^27.3.1",
|
||||||
"ts-jest": "^26.4.3",
|
"prettier": "^2.4.1",
|
||||||
"ts-loader": "^8.0.8",
|
"supertest": "^6.1.6",
|
||||||
"ts-node": "^9.0.0",
|
"ts-jest": "^27.0.7",
|
||||||
"tsconfig-paths": "^3.9.0",
|
"ts-loader": "^9.2.6",
|
||||||
"typescript": "^4.0.5"
|
"ts-node": "^10.3.0",
|
||||||
|
"tsconfig-paths": "^3.11.0",
|
||||||
|
"typescript": "^4.4.4"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
@ -89,6 +96,9 @@
|
|||||||
"collectCoverageFrom": [
|
"collectCoverageFrom": [
|
||||||
"**/*.(t|j)s"
|
"**/*.(t|j)s"
|
||||||
],
|
],
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^jose/(.*)$": "<rootDir>/../node_modules/jose/dist/node/cjs/$1"
|
||||||
|
},
|
||||||
"coverageDirectory": "../coverage",
|
"coverageDirectory": "../coverage",
|
||||||
"testEnvironment": "node"
|
"testEnvironment": "node"
|
||||||
}
|
}
|
||||||
|
@ -6,18 +6,19 @@ import { AppController } from './app.controller';
|
|||||||
import { AppResolver } from './app.resolver';
|
import { AppResolver } from './app.resolver';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import configuration from './commons/config/configuration';
|
import configuration from './commons/config/configuration';
|
||||||
import { RedisModule } from 'nestjs-redis';
|
|
||||||
import { ParseBodyMiddleware } from './commons/middleware/parse-body.middleware';
|
import { ParseBodyMiddleware } from './commons/middleware/parse-body.middleware';
|
||||||
import { BullModule } from '@nestjs/bull';
|
import { ArticlesModule } from './articles/articles.module';
|
||||||
import { PubSubModule } from './commons/pub-sub/pub-sub.module';
|
import { EtcdModule } from '@nestjs-lib/etcd3';
|
||||||
|
import { CommonsModule } from './commons/commons.module';
|
||||||
|
import { TagsModule } from './tags/tags.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
load: [configuration],
|
load: [configuration],
|
||||||
|
isGlobal: true,
|
||||||
}),
|
}),
|
||||||
TypeOrmModule.forRootAsync({
|
TypeOrmModule.forRootAsync({
|
||||||
imports: [ConfigModule],
|
|
||||||
useFactory: (configService: ConfigService) => ({
|
useFactory: (configService: ConfigService) => ({
|
||||||
type: 'postgres',
|
type: 'postgres',
|
||||||
host: configService.get<string>('db.postgres.host'),
|
host: configService.get<string>('db.postgres.host'),
|
||||||
@ -25,13 +26,12 @@ import { PubSubModule } from './commons/pub-sub/pub-sub.module';
|
|||||||
username: configService.get<string>('db.postgres.username'),
|
username: configService.get<string>('db.postgres.username'),
|
||||||
password: configService.get<string>('db.postgres.password'),
|
password: configService.get<string>('db.postgres.password'),
|
||||||
database: configService.get<string>('db.postgres.database'),
|
database: configService.get<string>('db.postgres.database'),
|
||||||
synchronize: true,
|
synchronize: false,
|
||||||
autoLoadEntities: true,
|
autoLoadEntities: true,
|
||||||
}),
|
}),
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
}),
|
}),
|
||||||
GraphQLModule.forRootAsync({
|
GraphQLModule.forRootAsync({
|
||||||
imports: [ConfigModule],
|
|
||||||
useFactory: (configService: ConfigService) => ({
|
useFactory: (configService: ConfigService) => ({
|
||||||
debug: configService.get<string>('env') !== 'prod',
|
debug: configService.get<string>('env') !== 'prod',
|
||||||
playground: true,
|
playground: true,
|
||||||
@ -40,38 +40,15 @@ import { PubSubModule } from './commons/pub-sub/pub-sub.module';
|
|||||||
}),
|
}),
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
}),
|
}),
|
||||||
BullModule.forRootAsync({
|
EtcdModule.forRootAsync({
|
||||||
imports: [ConfigModule],
|
|
||||||
useFactory: (configService: ConfigService) => ({
|
useFactory: (configService: ConfigService) => ({
|
||||||
redis: {
|
hosts: configService.get<string>('db.etcd.hosts', 'localhost:2379'),
|
||||||
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],
|
inject: [ConfigService],
|
||||||
}),
|
}),
|
||||||
|
CommonsModule,
|
||||||
|
ArticlesModule,
|
||||||
|
TagsModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService, AppResolver],
|
providers: [AppService, AppResolver],
|
||||||
|
12
src/articles/articles.module.ts
Normal file
12
src/articles/articles.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
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 {}
|
30
src/articles/articles.resolver.spec.ts
Normal file
30
src/articles/articles.resolver.spec.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
90
src/articles/articles.resolver.ts
Normal file
90
src/articles/articles.resolver.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
27
src/articles/articles.service.spec.ts
Normal file
27
src/articles/articles.service.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
42
src/articles/articles.service.ts
Normal file
42
src/articles/articles.service.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
7
src/articles/dto/article-list-item.dto.ts
Normal file
7
src/articles/dto/article-list-item.dto.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { ObjectType, OmitType } from '@nestjs/graphql';
|
||||||
|
import { Article } from '../entities/article.entity';
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class ArticleListItemDto extends OmitType(Article, [
|
||||||
|
'content',
|
||||||
|
] as const) {}
|
20
src/articles/dto/create-article.input.ts
Normal file
20
src/articles/dto/create-article.input.ts
Normal 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[];
|
||||||
|
}
|
8
src/articles/dto/update-article.input.ts
Normal file
8
src/articles/dto/update-article.input.ts
Normal 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;
|
||||||
|
}
|
25
src/articles/entities/article.entity.ts
Normal file
25
src/articles/entities/article.entity.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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[];
|
||||||
|
}
|
15
src/articles/models/article-history.model.ts
Normal file
15
src/articles/models/article-history.model.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
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;
|
||||||
|
}
|
@ -1,10 +1,12 @@
|
|||||||
|
import { ObjectScalar } from './scalars/object.scalar';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { PasswordConverter } from './services/password-converter';
|
import { PasswordConverter } from './services/password-converter';
|
||||||
import { PubSubModule } from './pub-sub/pub-sub.module';
|
import { PubSubModule } from './pub-sub/pub-sub.module';
|
||||||
|
import { AuthModule } from '@nestjs-lib/auth';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [PasswordConverter],
|
imports: [PubSubModule, AuthModule],
|
||||||
exports: [PasswordConverter],
|
providers: [PasswordConverter, ObjectScalar],
|
||||||
imports: [PubSubModule],
|
exports: [PasswordConverter, AuthModule],
|
||||||
})
|
})
|
||||||
export class CommonsModule {}
|
export class CommonsModule {}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { readFileSync } from 'fs';
|
import { readConfiguration } from '@fennec/configuration';
|
||||||
import * as yaml from 'js-yaml';
|
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return yaml.load(
|
return readConfiguration({
|
||||||
readFileSync(join(__dirname, '../../../config.yml'), 'utf8'),
|
etcd: {
|
||||||
) as unknown;
|
hosts: '192.168.31.2:2379',
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -12,7 +12,7 @@ export class AppBaseEntity {
|
|||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn({ select: false })
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
@UpdateDateColumn({ select: false })
|
@UpdateDateColumn({ select: false })
|
||||||
|
@ -4,7 +4,7 @@ import { PubSub } from './pub-sub';
|
|||||||
|
|
||||||
debug.enable('app:pubsub:*');
|
debug.enable('app:pubsub:*');
|
||||||
|
|
||||||
describe('PubSub', () => {
|
describe.skip('PubSub', () => {
|
||||||
let instance: PubSub;
|
let instance: PubSub;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
13
src/commons/scalars/object.scalar.ts
Normal file
13
src/commons/scalars/object.scalar.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
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;
|
||||||
|
}
|
@ -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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { ServiceRegister } from '@fennec/configuration';
|
||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
@ -15,6 +16,11 @@ async function bootstrap() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
app.useGlobalFilters(new HttpExceptionFilter());
|
app.useGlobalFilters(new HttpExceptionFilter());
|
||||||
await app.listen(configService.get<number>('http.port'));
|
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}`);
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
9
src/tags/dto/create-tag.input.ts
Normal file
9
src/tags/dto/create-tag.input.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { InputType } from '@nestjs/graphql';
|
||||||
|
import { IsString, Length } from 'class-validator';
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class CreateTagInput {
|
||||||
|
@IsString()
|
||||||
|
@Length(1, 100)
|
||||||
|
name: string;
|
||||||
|
}
|
9
src/tags/dto/update-tag.input.ts
Normal file
9
src/tags/dto/update-tag.input.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
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;
|
||||||
|
}
|
11
src/tags/entities/tag.entity.ts
Normal file
11
src/tags/entities/tag.entity.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
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;
|
||||||
|
}
|
12
src/tags/tags.module.ts
Normal file
12
src/tags/tags.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
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 {}
|
25
src/tags/tags.resolver.spec.ts
Normal file
25
src/tags/tags.resolver.spec.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
36
src/tags/tags.resolver.ts
Normal file
36
src/tags/tags.resolver.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
27
src/tags/tags.service.spec.ts
Normal file
27
src/tags/tags.service.spec.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
45
src/tags/tags.service.ts
Normal file
45
src/tags/tags.service.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,24 +1,49 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { INestApplication } from '@nestjs/common';
|
import { INestApplication } from '@nestjs/common';
|
||||||
import * as request from 'supertest';
|
|
||||||
import { AppModule } from './../src/app.module';
|
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 app: INestApplication;
|
||||||
|
let apolloClient: ApolloServerTestClient;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeAll(async () => {
|
||||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
imports: [AppModule],
|
imports: [AppModule],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
app = moduleFixture.createNestApplication();
|
app = moduleFixture.createNestApplication();
|
||||||
await app.init();
|
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)', () => {
|
afterAll(async () => {
|
||||||
return request(app.getHttpServer())
|
await app?.close();
|
||||||
.get('/')
|
|
||||||
.expect(200)
|
|
||||||
.expect('Hello World!');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
33
test/article.e2e-spec.ts
Normal file
33
test/article.e2e-spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
0
test/data/article.e2e-spec.ts
Normal file
0
test/data/article.e2e-spec.ts
Normal 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);
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
15
test/graphql-e2e.ts
Normal 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,
|
||||||
|
);
|
||||||
|
};
|
@ -5,5 +5,12 @@
|
|||||||
"testRegex": ".e2e-spec.ts$",
|
"testRegex": ".e2e-spec.ts$",
|
||||||
"transform": {
|
"transform": {
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"ts-jest": {
|
||||||
|
"astTransformers": {
|
||||||
|
"before": ["<rootDir>/graphql-e2e.ts"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,12 @@
|
|||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"lib": ["es2020"],
|
||||||
"target": "es2017",
|
"target": "es2017",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"incremental": true
|
"incremental": true,
|
||||||
|
"skipLibCheck": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user