Archived
Merge branch 'main' of github.com:GRZ4NA/city-tracker into main
This commit is contained in:
commit
9123ed86af
15 files changed
+326
-10
No files matched your search
@@ -13,10 +13,16 @@ import { IssuesService } from './issues/issues.service';
|
|||||||
import { IssueType } from './issues/entities/issue-type.enum';
|
import { IssueType } from './issues/entities/issue-type.enum';
|
||||||
import { CreateIssueDto } from './issues/dto/create-issue.dto';
|
import { CreateIssueDto } from './issues/dto/create-issue.dto';
|
||||||
import { CreateIssueCommentDto } from './issues/dto/create-issue-comment.dto';
|
import { CreateIssueCommentDto } from './issues/dto/create-issue-comment.dto';
|
||||||
|
import { SolutionsService } from './solutions/solutions.service';
|
||||||
|
import { CreateSolutionDto } from './solutions/dto/create-solution.dto';
|
||||||
|
import { CreateSolutionCommentDto } from './solutions/dto/create-solution-comment.dto';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(private readonly issues: IssuesService) {}
|
constructor(
|
||||||
|
private readonly issues: IssuesService,
|
||||||
|
private readonly solutions: SolutionsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Get('/issues/problems')
|
@Get('/issues/problems')
|
||||||
listProblems() {
|
listProblems() {
|
||||||
@@ -35,13 +41,27 @@ export class AppController {
|
|||||||
@Get('/issues/:id')
|
@Get('/issues/:id')
|
||||||
async getIssue(@Param('id', ParseUUIDPipe) id: string) {
|
async getIssue(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
const data = await this.issues.findOne(id).then((i) => i.toDto());
|
const data = await this.issues.findOne(id).then((i) => i.toDto());
|
||||||
const comments = await this.issues.getCommentsForIssue(id);
|
const comments = await this.issues
|
||||||
|
.getCommentsForIssue(id)
|
||||||
|
.then((c) => c.map((c) => c.toDto()));
|
||||||
|
const solution =
|
||||||
|
(await this.solutions.getForIssue(id).catch(() => undefined))?.toList() ??
|
||||||
|
undefined;
|
||||||
|
return { data, comments, solution };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/issues/:id/solution')
|
||||||
|
async getSolution(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
const data = await this.solutions.getForIssue(id).then((i) => i.toDto());
|
||||||
|
const comments = await this.solutions
|
||||||
|
.getComments(id)
|
||||||
|
.then((c) => c.map((c) => c.toDto()));
|
||||||
return { data, comments };
|
return { data, comments };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/issues')
|
@Post('/issues')
|
||||||
createIssue(@Body() dto: CreateIssueDto) {
|
createIssue(@Body() dto: CreateIssueDto) {
|
||||||
return this.issues.create(dto);
|
return this.issues.create(dto).then((i) => i.toDto());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/issues/:id/comments')
|
@Post('/issues/:id/comments')
|
||||||
@@ -52,6 +72,22 @@ export class AppController {
|
|||||||
return this.issues.addComment(id, dto).then((c) => c.toDto());
|
return this.issues.addComment(id, dto).then((c) => c.toDto());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('/issues/:id/solution')
|
||||||
|
createSolution(
|
||||||
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
|
@Body() dto: CreateSolutionDto,
|
||||||
|
) {
|
||||||
|
return this.solutions.create(id, dto).then((r) => r.toDto());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/issues/:id/solution/comments')
|
||||||
|
addSolutionComment(
|
||||||
|
@Body() dto: CreateSolutionCommentDto,
|
||||||
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
|
) {
|
||||||
|
return this.solutions.addComment(id, dto).then((c) => c.toDto());
|
||||||
|
}
|
||||||
|
|
||||||
@Post('/issues/:id/like')
|
@Post('/issues/:id/like')
|
||||||
@HttpCode(HttpStatus.ACCEPTED)
|
@HttpCode(HttpStatus.ACCEPTED)
|
||||||
async addLike(@Param('id', ParseUUIDPipe) id: string) {
|
async addLike(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
@@ -64,6 +100,18 @@ export class AppController {
|
|||||||
await this.issues.dislike(id);
|
await this.issues.dislike(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('/issues/:id/solution/like')
|
||||||
|
@HttpCode(HttpStatus.ACCEPTED)
|
||||||
|
async addLikeToSolution(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
await this.solutions.like(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/issues/:id/solution/dislike')
|
||||||
|
@HttpCode(HttpStatus.ACCEPTED)
|
||||||
|
async addDislikeToSolution(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
await this.solutions.dislike(id);
|
||||||
|
}
|
||||||
|
|
||||||
@Delete('/issues/:id/like')
|
@Delete('/issues/:id/like')
|
||||||
@HttpCode(HttpStatus.ACCEPTED)
|
@HttpCode(HttpStatus.ACCEPTED)
|
||||||
async removeLike(@Param('id', ParseUUIDPipe) id: string) {
|
async removeLike(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
@@ -75,4 +123,16 @@ export class AppController {
|
|||||||
async removeDislike(@Param('id', ParseUUIDPipe) id: string) {
|
async removeDislike(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
await this.issues.dislike(id, true);
|
await this.issues.dislike(id, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Delete('/issues/:id/solution/like')
|
||||||
|
@HttpCode(HttpStatus.ACCEPTED)
|
||||||
|
async removeLikeToSolution(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
await this.solutions.like(id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('/issues/:id/solution/dislike')
|
||||||
|
@HttpCode(HttpStatus.ACCEPTED)
|
||||||
|
async removeDislikeToSolution(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
await this.solutions.dislike(id, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { IssuesModule } from './issues/issues.module';
|
import { IssuesModule } from './issues/issues.module';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
|
import { SolutionsModule } from './solutions/solutions.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
@@ -17,6 +18,7 @@ import { AppController } from './app.controller';
|
|||||||
synchronize: process.env.NODE_ENV !== 'production',
|
synchronize: process.env.NODE_ENV !== 'production',
|
||||||
}),
|
}),
|
||||||
IssuesModule,
|
IssuesModule,
|
||||||
|
SolutionsModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
@@ -20,9 +20,9 @@ export class IssuesService {
|
|||||||
return this.repository.save(issue);
|
return this.repository.save(issue);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addComment(id: string, { content }: CreateIssueCommentDto) {
|
async addComment(id: string, dto: CreateIssueCommentDto) {
|
||||||
const issue = await this.findOne(id);
|
const issue = await this.findOne(id);
|
||||||
const comment = this.comments.create({ content });
|
const comment = this.comments.create(dto);
|
||||||
comment.issue = Promise.resolve(issue);
|
comment.issue = Promise.resolve(issue);
|
||||||
return this.comments.save(comment);
|
return this.comments.save(comment);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { IsString, MaxLength, MinLength } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateSolutionCommentDto {
|
||||||
|
@IsString()
|
||||||
|
@MinLength(3)
|
||||||
|
@MaxLength(16)
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@MinLength(1)
|
||||||
|
@MaxLength(255)
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { IsString, MaxLength, MinLength } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateSolutionDto {
|
||||||
|
@IsString()
|
||||||
|
@MinLength(1)
|
||||||
|
@MaxLength(32)
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@MinLength(1)
|
||||||
|
@MaxLength(255)
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Solution } from './solution.entity';
|
||||||
|
|
||||||
|
@Entity('solution_comments')
|
||||||
|
export class SolutionComment {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
content: string;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@ManyToOne(() => Solution, { lazy: true, onDelete: 'CASCADE' })
|
||||||
|
solution: Promise<Solution>;
|
||||||
|
|
||||||
|
toDto() {
|
||||||
|
return {
|
||||||
|
username: this.username,
|
||||||
|
content: this.content,
|
||||||
|
createdAt: this.createdAt.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Issue } from 'src/issues/entities/issue.entity';
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
OneToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('solutions')
|
||||||
|
export class Solution {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@OneToOne(() => Issue, { lazy: true, onDelete: 'CASCADE' })
|
||||||
|
issue: Promise<Issue>;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
likes: number;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
dislikes: number;
|
||||||
|
|
||||||
|
async toDto() {
|
||||||
|
return {
|
||||||
|
...this.toList(),
|
||||||
|
description: this.description,
|
||||||
|
issue: (await this.issue).toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toList() {
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
username: this.username,
|
||||||
|
likes: this.likes,
|
||||||
|
dislikes: this.dislikes,
|
||||||
|
createdAt: this.createdAt.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { SolutionsService } from './solutions.service';
|
||||||
|
import { IssuesModule } from 'src/issues/issues.module';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { Solution } from './entities/solution.entity';
|
||||||
|
import { SolutionComment } from './entities/solution-comment.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
IssuesModule,
|
||||||
|
TypeOrmModule.forFeature([Solution, SolutionComment]),
|
||||||
|
],
|
||||||
|
providers: [SolutionsService],
|
||||||
|
exports: [SolutionsService],
|
||||||
|
})
|
||||||
|
export class SolutionsModule {}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
ConflictException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { CreateSolutionDto } from './dto/create-solution.dto';
|
||||||
|
import { IssuesService } from 'src/issues/issues.service';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Solution } from './entities/solution.entity';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { CreateSolutionCommentDto } from './dto/create-solution-comment.dto';
|
||||||
|
import { SolutionComment } from './entities/solution-comment.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SolutionsService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Solution)
|
||||||
|
private readonly repository: Repository<Solution>,
|
||||||
|
@InjectRepository(SolutionComment)
|
||||||
|
private readonly comments: Repository<SolutionComment>,
|
||||||
|
private readonly issues: IssuesService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async create(issueId: string, createSolutionDto: CreateSolutionDto) {
|
||||||
|
if ((await this.getForIssue(issueId)) !== null)
|
||||||
|
throw new ConflictException('This issue already has a solution');
|
||||||
|
const solution = this.repository.create({ ...createSolutionDto });
|
||||||
|
const issue = await this.issues.findOne(issueId);
|
||||||
|
solution.issue = Promise.resolve(issue);
|
||||||
|
return this.repository.save(solution);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getForIssue(issueId: string): Promise<Solution> {
|
||||||
|
const ent = await this.repository.findOneBy({ issue: { id: issueId } });
|
||||||
|
if (ent === null) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
'Solution for the given issue has not been found',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ent;
|
||||||
|
}
|
||||||
|
|
||||||
|
async like(issueId: string, remove?: boolean) {
|
||||||
|
const ent = await this.getForIssue(issueId);
|
||||||
|
ent.likes = remove ? ent.likes - 1 : ent.likes + 1;
|
||||||
|
return this.repository.save(ent);
|
||||||
|
}
|
||||||
|
|
||||||
|
async dislike(issueId: string, remove?: boolean) {
|
||||||
|
const ent = await this.getForIssue(issueId);
|
||||||
|
ent.dislikes = remove ? ent.dislikes - 1 : ent.dislikes + 1;
|
||||||
|
return this.repository.save(ent);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addComment(issueId: string, dto: CreateSolutionCommentDto) {
|
||||||
|
const ent = await this.getForIssue(issueId);
|
||||||
|
const comment = this.comments.create(dto);
|
||||||
|
comment.solution = Promise.resolve(ent);
|
||||||
|
return this.comments.save(comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getComments(id: string) {
|
||||||
|
return this.comments.findBy({ solution: { id } });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { CommentModel } from './comment.model';
|
import { CommentModel } from './comment.model';
|
||||||
|
|
||||||
export interface SolutionModel {
|
export interface SolutionModel {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
comments: CommentModel[];
|
comments: CommentModel[];
|
||||||
id: string;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
@@ -1 +1,4 @@
|
|||||||
<p>solutions works!</p>
|
<div *ngFor="let solution of solutions$ | async">
|
||||||
|
{{solution.title}}
|
||||||
|
</div>
|
||||||
|
gv gvghv
|
||||||
@@ -1,10 +1,22 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { EMPTY, Observable } from 'rxjs';
|
||||||
|
import { SolutionModel } from 'src/app/models/solution.model';
|
||||||
|
import { receiveAllIssuesAction } from 'src/app/store/actions/issue.actions';
|
||||||
|
import { selectAllSolutions } from 'src/app/store/selectors/issue.selectors';
|
||||||
|
import { SolutionsModule } from './solutions.module';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-solutions',
|
selector: 'app-solutions',
|
||||||
templateUrl: './solutions.component.html',
|
templateUrl: './solutions.component.html',
|
||||||
styleUrls: ['./solutions.component.scss']
|
styleUrls: ['./solutions.component.scss']
|
||||||
})
|
})
|
||||||
export class SolutionsComponent {
|
export class SolutionsComponent implements OnInit {
|
||||||
|
solutions$: Observable<SolutionModel[]> = EMPTY
|
||||||
|
constructor(private store: Store){}
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.store.dispatch(receiveAllIssuesAction())
|
||||||
|
this.solutions$ = this.store.select(selectAllSolutions)
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { map } from 'rxjs';
|
||||||
|
import { SolutionModel } from '../models/solution.model';
|
||||||
|
import { CommentModel } from '../models/comment.model';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class IssueService {
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
getSolutions(id: string) {
|
||||||
|
return this.http.get<SolutionDto[]>('/issue/'+id+'/solution').pipe(
|
||||||
|
map<SolutionDto[], SolutionModel[]>((solution) =>
|
||||||
|
solution.map((solution) => ({
|
||||||
|
title: solution.title,
|
||||||
|
description: solution.description,
|
||||||
|
comments: solution.comments,
|
||||||
|
createdAt: solution.createdAt
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SolutionDto {
|
||||||
|
title: string,
|
||||||
|
description: string,
|
||||||
|
comments: CommentModel[],
|
||||||
|
createdAt: Date
|
||||||
|
}
|
||||||
@@ -35,6 +35,12 @@ const initialState: IssueState = {
|
|||||||
photos: [],
|
photos: [],
|
||||||
status: 'new',
|
status: 'new',
|
||||||
type: 'problem',
|
type: 'problem',
|
||||||
|
solution: {
|
||||||
|
comments: [],
|
||||||
|
createdAt: new Date(),
|
||||||
|
description: "eoooo",
|
||||||
|
title: "title"
|
||||||
|
},
|
||||||
comments: [],
|
comments: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
||||||
import { IssueState } from '../state/issue.state';
|
import { IssueState } from '../state/issue.state';
|
||||||
|
import { SolutionModel } from 'src/app/models/solution.model';
|
||||||
|
import { IssueModel } from 'src/app/models/issue.model';
|
||||||
|
|
||||||
const selectIssueFeatureState = createFeatureSelector<IssueState>('issueState');
|
const selectIssueFeatureState = createFeatureSelector<IssueState>('issueState');
|
||||||
|
|
||||||
@@ -12,3 +14,5 @@ export const selectIssue = (id: string) =>
|
|||||||
createSelector(selectAllIssues, (issues) =>
|
createSelector(selectAllIssues, (issues) =>
|
||||||
issues.find((issue) => issue.id === id)
|
issues.find((issue) => issue.id === id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const selectAllSolutions = createSelector<any, IssueModel[], SolutionModel[]>(selectAllIssues, issues => issues.map(issue => issue.solution).filter(solution => solution !== undefined) as SolutionModel[])
|
||||||
Reference in new issue
Block a user