Compare commits
No commits in common. "develop" and "master" have entirely different histories.
25
.eslintrc.js
25
.eslintrc.js
@ -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
36
.gitignore
vendored
@ -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
|
|
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "all"
|
|
||||||
}
|
|
16
.vscode/settings.json
vendored
16
.vscode/settings.json
vendored
@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"cSpell.words": [
|
|
||||||
"boardcat",
|
|
||||||
"execa",
|
|
||||||
"gitea",
|
|
||||||
"lpush",
|
|
||||||
"lrange",
|
|
||||||
"metatype",
|
|
||||||
"pmessage",
|
|
||||||
"psubscribe",
|
|
||||||
"QLJSON",
|
|
||||||
"Repos",
|
|
||||||
"rpop",
|
|
||||||
"rpush"
|
|
||||||
]
|
|
||||||
}
|
|
@ -14,6 +14,5 @@ db:
|
|||||||
port: 6379
|
port: 6379
|
||||||
password:
|
password:
|
||||||
prefix: blog
|
prefix: blog
|
||||||
etcd:
|
workspaces:
|
||||||
hosts:
|
root: '/Users/ivanli/Projects/fennec/workspaces'
|
||||||
- 'http://192.168.31.194:2379'
|
|
@ -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
|
|
@ -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!`);
|
|
@ -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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
@ -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"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
22
ormconfig.js
22
ormconfig.js
@ -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
25330
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": "blog-be",
|
"name": "fennec-be",
|
||||||
"version": "0.1.0",
|
"version": "0.0.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@ -13,74 +13,67 @@
|
|||||||
"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": {
|
||||||
"@fennec/configuration": "^0.0.2",
|
"@nestjs/bull": "^0.3.1",
|
||||||
"@nestjs-lib/auth": "^0.2.3",
|
"@nestjs/common": "^7.5.1",
|
||||||
"@nestjs-lib/etcd3": "^0.0.1",
|
"@nestjs/config": "^0.6.2",
|
||||||
"@nestjs/common": "^8.1.1",
|
"@nestjs/core": "^7.5.1",
|
||||||
"@nestjs/config": "^1.0.3",
|
"@nestjs/graphql": "^7.9.8",
|
||||||
"@nestjs/core": "^8.1.1",
|
"@nestjs/platform-express": "^7.5.1",
|
||||||
"@nestjs/graphql": "^9.1.1",
|
"@nestjs/typeorm": "^7.1.5",
|
||||||
"@nestjs/platform-express": "^8.1.1",
|
"@types/bull": "^3.15.0",
|
||||||
"@nestjs/typeorm": "^8.0.2",
|
"apollo-server-express": "^2.19.2",
|
||||||
"apollo-server-express": "^3.4.0",
|
"bcrypt": "^5.0.0",
|
||||||
"bcrypt": "^5.0.1",
|
|
||||||
"body-parser": "^1.19.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",
|
"class-validator": "^0.13.1",
|
||||||
"debug": "^4.3.2",
|
"debug": "^4.3.1",
|
||||||
"graphql": "^15.6.1",
|
"graphql": "^15.5.0",
|
||||||
"graphql-tools": "^8.2.0",
|
"graphql-tools": "^7.0.2",
|
||||||
"graphql-type-json": "^0.3.2",
|
"ioredis": "^4.25.0",
|
||||||
"highlight.js": "^11.3.1",
|
"js-yaml": "^4.0.0",
|
||||||
"ioredis": "^4.28.0",
|
"nestjs-redis": "^1.2.8",
|
||||||
"js-yaml": "^4.1.0",
|
"observable-to-async-generator": "^1.0.1-rc",
|
||||||
"marked": "^3.0.7",
|
"pg": "^8.5.1",
|
||||||
"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": "^7.4.0",
|
"rxjs": "^6.6.3",
|
||||||
"simple-git": "^2.47.0",
|
"simple-git": "^2.35.0",
|
||||||
"typeorm": "^0.2.38"
|
"typeorm": "^0.2.30"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^8.1.4",
|
"@nestjs/cli": "^7.5.7",
|
||||||
"@nestjs/schematics": "^8.0.4",
|
"@nestjs/schematics": "^7.1.3",
|
||||||
"@nestjs/testing": "^8.1.1",
|
"@nestjs/testing": "^7.5.1",
|
||||||
"@types/express": "^4.17.13",
|
"@types/body-parser": "^1.19.0",
|
||||||
"@types/highlight.js": "^10.1.0",
|
"@types/debug": "^4.1.5",
|
||||||
"@types/jest": "^27.0.2",
|
"@types/express": "^4.17.8",
|
||||||
"@types/marked": "^3.0.2",
|
"@types/ioredis": "^4.22.3",
|
||||||
"@types/node": "^16.11.2",
|
"@types/jest": "^26.0.15",
|
||||||
"@types/supertest": "^2.0.11",
|
"@types/node": "^14.14.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.1.0",
|
"@types/supertest": "^2.0.10",
|
||||||
"@typescript-eslint/parser": "^5.1.0",
|
"@typescript-eslint/eslint-plugin": "^4.6.1",
|
||||||
"apollo-server-testing": "^2.23.0",
|
"@typescript-eslint/parser": "^4.6.1",
|
||||||
"eslint": "^8.0.1",
|
"eslint": "^7.12.1",
|
||||||
"eslint-config-prettier": "8.3.0",
|
"eslint-config-prettier": "7.2.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^3.1.4",
|
||||||
"execa": "^5.1.1",
|
"jest": "^26.6.3",
|
||||||
"find-free-ports": "^3.0.0",
|
"prettier": "^2.1.2",
|
||||||
"jest": "^27.3.1",
|
"supertest": "^6.0.0",
|
||||||
"prettier": "^2.4.1",
|
"ts-jest": "^26.4.3",
|
||||||
"supertest": "^6.1.6",
|
"ts-loader": "^8.0.8",
|
||||||
"ts-jest": "^27.0.7",
|
"ts-node": "^9.0.0",
|
||||||
"ts-loader": "^9.2.6",
|
"tsconfig-paths": "^3.9.0",
|
||||||
"ts-node": "^10.3.0",
|
"typescript": "^4.0.5"
|
||||||
"tsconfig-paths": "^3.11.0",
|
|
||||||
"typescript": "^4.4.4"
|
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
@ -96,9 +89,6 @@
|
|||||||
"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,19 +6,18 @@ 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 { ArticlesModule } from './articles/articles.module';
|
import { BullModule } from '@nestjs/bull';
|
||||||
import { EtcdModule } from '@nestjs-lib/etcd3';
|
import { PubSubModule } from './commons/pub-sub/pub-sub.module';
|
||||||
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'),
|
||||||
@ -26,12 +25,13 @@ import { TagsModule } from './tags/tags.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: false,
|
synchronize: true,
|
||||||
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,15 +40,38 @@ import { TagsModule } from './tags/tags.module';
|
|||||||
}),
|
}),
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
}),
|
}),
|
||||||
EtcdModule.forRootAsync({
|
BullModule.forRootAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
useFactory: (configService: ConfigService) => ({
|
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],
|
inject: [ConfigService],
|
||||||
}),
|
}),
|
||||||
CommonsModule,
|
|
||||||
ArticlesModule,
|
|
||||||
TagsModule,
|
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService, AppResolver],
|
providers: [AppService, AppResolver],
|
||||||
|
@ -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 {}
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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) {}
|
|
@ -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[];
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -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[];
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -1,12 +1,10 @@
|
|||||||
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({
|
||||||
imports: [PubSubModule, AuthModule],
|
providers: [PasswordConverter],
|
||||||
providers: [PasswordConverter, ObjectScalar],
|
exports: [PasswordConverter],
|
||||||
exports: [PasswordConverter, AuthModule],
|
imports: [PubSubModule],
|
||||||
})
|
})
|
||||||
export class CommonsModule {}
|
export class CommonsModule {}
|
||||||
|
@ -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 () => {
|
export default () => {
|
||||||
return readConfiguration({
|
return yaml.load(
|
||||||
etcd: {
|
readFileSync(join(__dirname, '../../../config.yml'), 'utf8'),
|
||||||
hosts: '192.168.31.2:2379',
|
) as unknown;
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
@ -12,7 +12,7 @@ export class AppBaseEntity {
|
|||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@CreateDateColumn({ select: false })
|
@CreateDateColumn()
|
||||||
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.skip('PubSub', () => {
|
describe('PubSub', () => {
|
||||||
let instance: PubSub;
|
let instance: PubSub;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
@ -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;
|
|
||||||
}
|
|
@ -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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
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';
|
||||||
@ -16,11 +15,6 @@ async function bootstrap() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
app.useGlobalFilters(new HttpExceptionFilter());
|
app.useGlobalFilters(new HttpExceptionFilter());
|
||||||
const server = await app.listen(configService.get<number>('http.port', 0));
|
await app.listen(configService.get<number>('http.port'));
|
||||||
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();
|
||||||
|
@ -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;
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -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 {}
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,49 +1,24 @@
|
|||||||
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('ArticleResolver (e2e)', () => {
|
describe('AppController (e2e)', () => {
|
||||||
let app: INestApplication;
|
let app: INestApplication;
|
||||||
let apolloClient: ApolloServerTestClient;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeEach(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!',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
it('/ (GET)', () => {
|
||||||
await app?.close();
|
return request(app.getHttpServer())
|
||||||
|
.get('/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Hello World!');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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
8
test/data/bad-work.js
Normal 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);
|
115
test/data/gitea-hook-payload.json.bin
Normal file
115
test/data/gitea-hook-payload.json.bin
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
7
test/data/one-second-work.js
Normal file
7
test/data/one-second-work.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
let timer;
|
||||||
|
let count = 0;
|
||||||
|
setTimeout(() => clearInterval(timer), 1_000);
|
||||||
|
|
||||||
|
timer = setInterval(() => {
|
||||||
|
console.log(++count * 10);
|
||||||
|
}, 95);
|
@ -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,
|
|
||||||
);
|
|
||||||
};
|
|
@ -5,12 +5,5 @@
|
|||||||
"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,12 +6,10 @@
|
|||||||
"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