Add lessons on the API. (#78)
This commit is contained in:
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = tab
|
||||||
6
.env.example
Normal file
6
.env.example
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
POSTGRES_USER=
|
||||||
|
POSTGRES_PASSWORD=
|
||||||
|
POSTGRES_NAME=
|
||||||
|
POSTGRES_HOST=
|
||||||
|
DATABASE_URL=
|
||||||
|
JWT_SECRET=
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -5,4 +5,7 @@ pyvenv.cfg
|
|||||||
include
|
include
|
||||||
.env
|
.env
|
||||||
prisma/migrations/*
|
prisma/migrations/*
|
||||||
.vscode
|
.vscode
|
||||||
|
output.xml
|
||||||
|
report.html
|
||||||
|
log.html
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
FROM node:17
|
FROM node:17
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ./package.json ./
|
|
||||||
RUN npm install
|
|
||||||
COPY . .
|
|
||||||
CMD npx prisma generate ; npx prisma migrate dev ; npm run start:dev
|
CMD npx prisma generate ; npx prisma migrate dev ; npm run start:dev
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/nest-cli",
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
"collection": "@nestjs/schematics",
|
"collection": "@nestjs/schematics",
|
||||||
"sourceRoot": "src"
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"plugins": [{
|
||||||
|
"name": "@nestjs/swagger",
|
||||||
|
"options": {
|
||||||
|
"introspectComments": true,
|
||||||
|
"dtoFileNameSuffix": []
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
back/package-lock.json
generated
35
back/package-lock.json
generated
@@ -27,7 +27,8 @@
|
|||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rxjs": "^7.2.0"
|
"rxjs": "^7.2.0",
|
||||||
|
"swagger-ui-express": "^4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^8.0.0",
|
"@nestjs/cli": "^8.0.0",
|
||||||
@@ -8194,6 +8195,25 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/swagger-ui-dist": {
|
||||||
|
"version": "4.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.14.0.tgz",
|
||||||
|
"integrity": "sha512-TBzhheU15s+o54Cgk9qxuYcZMiqSm/SkvKnapoGHOF66kz0Y5aGjpzj5BT/vpBbn6rTPJ9tUYXQxuDWfsjiGMw=="
|
||||||
|
},
|
||||||
|
"node_modules/swagger-ui-express": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-DHk3zFvsxrkcnurGvQlAcLuTDacAVN1JHKDgcba/gr2NFRE4HGwP1YeHIXMiGznkWR4AeS7X5vEblNn4QljuNA==",
|
||||||
|
"dependencies": {
|
||||||
|
"swagger-ui-dist": ">=4.11.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= v0.10.32"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"express": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/symbol-observable": {
|
"node_modules/symbol-observable": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
|
||||||
@@ -15400,6 +15420,19 @@
|
|||||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"swagger-ui-dist": {
|
||||||
|
"version": "4.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.14.0.tgz",
|
||||||
|
"integrity": "sha512-TBzhheU15s+o54Cgk9qxuYcZMiqSm/SkvKnapoGHOF66kz0Y5aGjpzj5BT/vpBbn6rTPJ9tUYXQxuDWfsjiGMw=="
|
||||||
|
},
|
||||||
|
"swagger-ui-express": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-DHk3zFvsxrkcnurGvQlAcLuTDacAVN1JHKDgcba/gr2NFRE4HGwP1YeHIXMiGznkWR4AeS7X5vEblNn4QljuNA==",
|
||||||
|
"requires": {
|
||||||
|
"swagger-ui-dist": ">=4.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"symbol-observable": {
|
"symbol-observable": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
|
||||||
|
|||||||
@@ -39,7 +39,8 @@
|
|||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rxjs": "^7.2.0"
|
"rxjs": "^7.2.0",
|
||||||
|
"swagger-ui-express": "^4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^8.0.0",
|
"@nestjs/cli": "^8.0.0",
|
||||||
|
|||||||
26
back/prisma/migrations/20220924080202_/migration.sql
Normal file
26
back/prisma/migrations/20220924080202_/migration.sql
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "DifficultyPoint" AS ENUM ('TwoHands', 'Rhythm', 'NoteCombo', 'Arpeggio', 'Distance', 'LeftHand', 'RightHand', 'LeadHandChange', 'ChordComplexity', 'ChordTiming', 'Length', 'PedalPoint', 'Precision');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "User" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"username" TEXT NOT NULL,
|
||||||
|
"password" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Lesson" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"description" TEXT NOT NULL,
|
||||||
|
"requiredLvl" INTEGER NOT NULL,
|
||||||
|
"difficulyPoint" "DifficultyPoint" NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Lesson_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||||
28
back/prisma/migrations/20220924081654_/migration.sql
Normal file
28
back/prisma/migrations/20220924081654_/migration.sql
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `difficulyPoint` on the `Lesson` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `requiredLvl` on the `Lesson` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `mainSkill` to the `Lesson` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `requiredLevel` to the `Lesson` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Lesson" DROP COLUMN "difficulyPoint",
|
||||||
|
DROP COLUMN "requiredLvl",
|
||||||
|
ADD COLUMN "mainSkill" "DifficultyPoint" NOT NULL,
|
||||||
|
ADD COLUMN "requiredLevel" INTEGER NOT NULL;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "LessonHistory" (
|
||||||
|
"lessonID" INTEGER NOT NULL,
|
||||||
|
"userID" INTEGER NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "LessonHistory_pkey" PRIMARY KEY ("lessonID","userID")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "LessonHistory" ADD CONSTRAINT "LessonHistory_userID_fkey" FOREIGN KEY ("userID") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "LessonHistory" ADD CONSTRAINT "LessonHistory_lessonID_fkey" FOREIGN KEY ("lessonID") REFERENCES "Lesson"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
10
back/prisma/migrations/20220926065731_/migration.sql
Normal file
10
back/prisma/migrations/20220926065731_/migration.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `title` on the `Lesson` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `name` to the `Lesson` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Lesson" DROP COLUMN "title",
|
||||||
|
ADD COLUMN "name" TEXT NOT NULL;
|
||||||
3
back/prisma/migrations/migration_lock.toml
Normal file
3
back/prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "postgresql"
|
||||||
@@ -10,8 +10,45 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id Int @default(autoincrement()) @id
|
id Int @id @default(autoincrement())
|
||||||
username String @unique
|
username String @unique
|
||||||
password String
|
password String
|
||||||
email String
|
email String
|
||||||
|
LessonHistory LessonHistory[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Lesson {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String
|
||||||
|
description String
|
||||||
|
requiredLevel Int
|
||||||
|
mainSkill Skill
|
||||||
|
LessonHistory LessonHistory[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model LessonHistory {
|
||||||
|
lesson Lesson @relation(fields: [lessonID], references: [id])
|
||||||
|
lessonID Int
|
||||||
|
user User @relation(fields: [userID], references: [id])
|
||||||
|
userID Int
|
||||||
|
|
||||||
|
@@id([lessonID, userID])
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Skill {
|
||||||
|
TwoHands
|
||||||
|
Rhythm
|
||||||
|
NoteCombo
|
||||||
|
Arpeggio
|
||||||
|
Distance
|
||||||
|
LeftHand
|
||||||
|
RightHand
|
||||||
|
LeadHandChange
|
||||||
|
ChordComplexity
|
||||||
|
ChordTiming
|
||||||
|
Length
|
||||||
|
PedalPoint
|
||||||
|
Precision
|
||||||
|
|
||||||
|
@@map("DifficultyPoint")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import { PrismaService } from './prisma/prisma.service';
|
|||||||
import { UsersModule } from './users/users.module';
|
import { UsersModule } from './users/users.module';
|
||||||
import { PrismaModule } from './prisma/prisma.module';
|
import { PrismaModule } from './prisma/prisma.module';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
|
import { LessonModule } from './lesson/lesson.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [UsersModule, PrismaModule, AuthModule],
|
imports: [UsersModule, PrismaModule, AuthModule, LessonModule],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService, PrismaService],
|
providers: [AppService, PrismaService],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { UsersService } from '../users/users.service';
|
import { UsersService } from '../users/users.service';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import * as bcrypt from 'bcryptjs';
|
import * as bcrypt from 'bcryptjs';
|
||||||
import PayloadInterface from './interface/payload.interface';
|
import PayloadInterface from './interface/payload.interface';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private userService: UsersService,
|
private userService: UsersService,
|
||||||
private jwtService: JwtService
|
private jwtService: JwtService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async validateUser(username: string, password: string): Promise<PayloadInterface> {
|
async validateUser(
|
||||||
const user = await this.userService.user({username});
|
username: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<PayloadInterface | null> {
|
||||||
|
const user = await this.userService.user({ username });
|
||||||
if (user && bcrypt.compareSync(password, user.password)) {
|
if (user && bcrypt.compareSync(password, user.password)) {
|
||||||
return {
|
return {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
id: user.id
|
id: user.id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -26,7 +28,7 @@ export class AuthService {
|
|||||||
const payload = { username: user.username, id: user.id };
|
const payload = { username: user.username, id: user.id };
|
||||||
const access_token = this.jwtService.sign(payload);
|
const access_token = this.jwtService.sign(payload);
|
||||||
return {
|
return {
|
||||||
access_token
|
access_token,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { Strategy } from 'passport-local';
|
import { Strategy } from 'passport-local';
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
@@ -7,15 +6,18 @@ import PayloadInterface from './interface/payload.interface';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LocalStrategy extends PassportStrategy(Strategy) {
|
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||||
constructor(private authService: AuthService) {
|
constructor(private authService: AuthService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
async validate(username: string, password: string): Promise<PayloadInterface> {
|
async validate(
|
||||||
const user = await this.authService.validateUser(username, password);
|
username: string,
|
||||||
if (!user) {
|
password: string,
|
||||||
throw new UnauthorizedException();
|
): Promise<PayloadInterface> {
|
||||||
}
|
const user = await this.authService.validateUser(username, password);
|
||||||
return user;
|
if (!user) {
|
||||||
}
|
throw new UnauthorizedException();
|
||||||
}
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
18
back/src/lesson/lesson.controller.spec.ts
Normal file
18
back/src/lesson/lesson.controller.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { LessonController } from './lesson.controller';
|
||||||
|
|
||||||
|
describe('LessonController', () => {
|
||||||
|
let controller: LessonController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [LessonController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<LessonController>(LessonController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
103
back/src/lesson/lesson.controller.ts
Normal file
103
back/src/lesson/lesson.controller.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Res,
|
||||||
|
Query,
|
||||||
|
Req,
|
||||||
|
Request,
|
||||||
|
Param,
|
||||||
|
ParseIntPipe,
|
||||||
|
DefaultValuePipe,
|
||||||
|
BadRequestException,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Delete,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Plage } from 'src/models/plage';
|
||||||
|
import { LessonService } from './lesson.service';
|
||||||
|
import { ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { Prisma, Skill } from '@prisma/client';
|
||||||
|
|
||||||
|
export class Lesson {
|
||||||
|
@ApiProperty()
|
||||||
|
id: number;
|
||||||
|
@ApiProperty()
|
||||||
|
name: string;
|
||||||
|
@ApiProperty()
|
||||||
|
description: string;
|
||||||
|
@ApiProperty()
|
||||||
|
requiredLevel: number;
|
||||||
|
@ApiProperty()
|
||||||
|
mainSkill: Skill;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiTags('lessons')
|
||||||
|
@Controller('lesson')
|
||||||
|
export class LessonController {
|
||||||
|
constructor(private lessonService: LessonService) {}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Get all lessons',
|
||||||
|
})
|
||||||
|
@Get()
|
||||||
|
async getAll(
|
||||||
|
@Req() request: Request,
|
||||||
|
@Query() filter: Prisma.LessonWhereInput,
|
||||||
|
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||||
|
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||||
|
): Promise<Plage<Lesson>> {
|
||||||
|
try {
|
||||||
|
const ret = await this.lessonService.getAll({
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
where: {
|
||||||
|
...filter,
|
||||||
|
requiredLevel: filter.requiredLevel
|
||||||
|
? +filter.requiredLevel
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return new Plage(ret, request);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
throw new BadRequestException(null, e?.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Get a particular lessons',
|
||||||
|
})
|
||||||
|
@Get(':id')
|
||||||
|
async get(@Param('id', ParseIntPipe) id: number): Promise<Lesson> {
|
||||||
|
const ret = await this.lessonService.get(id);
|
||||||
|
if (!ret) throw new NotFoundException();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Create a lessons',
|
||||||
|
})
|
||||||
|
@Post()
|
||||||
|
async post(@Body() lesson: Lesson): Promise<Lesson> {
|
||||||
|
try {
|
||||||
|
return await this.lessonService.create(lesson);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
throw new BadRequestException(null, e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Delete a lessons',
|
||||||
|
})
|
||||||
|
@Delete(':id')
|
||||||
|
async delete(@Param('id', ParseIntPipe) id: number): Promise<Lesson> {
|
||||||
|
try {
|
||||||
|
return await this.lessonService.delete(id);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
throw new BadRequestException(null, e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
back/src/lesson/lesson.module.ts
Normal file
11
back/src/lesson/lesson.module.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||||
|
import { LessonController } from './lesson.controller';
|
||||||
|
import { LessonService } from './lesson.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
controllers: [LessonController],
|
||||||
|
providers: [LessonService],
|
||||||
|
})
|
||||||
|
export class LessonModule {}
|
||||||
18
back/src/lesson/lesson.service.spec.ts
Normal file
18
back/src/lesson/lesson.service.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { LessonService } from './lesson.service';
|
||||||
|
|
||||||
|
describe('LessonService', () => {
|
||||||
|
let service: LessonService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [LessonService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<LessonService>(LessonService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
41
back/src/lesson/lesson.service.ts
Normal file
41
back/src/lesson/lesson.service.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Lesson, Prisma } from '@prisma/client';
|
||||||
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LessonService {
|
||||||
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async getAll(params: {
|
||||||
|
skip?: number;
|
||||||
|
take?: number;
|
||||||
|
cursor?: Prisma.LessonWhereUniqueInput;
|
||||||
|
where?: Prisma.LessonWhereInput;
|
||||||
|
orderBy?: Prisma.LessonOrderByWithRelationInput;
|
||||||
|
}): Promise<Lesson[]> {
|
||||||
|
const { skip, take, cursor, where, orderBy } = params;
|
||||||
|
return this.prisma.lesson.findMany({
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
cursor,
|
||||||
|
where,
|
||||||
|
orderBy,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id: number): Promise<Lesson | null> {
|
||||||
|
return this.prisma.lesson.findFirst({
|
||||||
|
where: {
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(lesson: Lesson): Promise<Lesson> {
|
||||||
|
return this.prisma.lesson.create({ data: lesson });
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: number): Promise<Lesson> {
|
||||||
|
return this.prisma.lesson.delete({ where: { id: id } });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,21 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { PrismaService } from './prisma/prisma.service';
|
import { PrismaService } from './prisma/prisma.service';
|
||||||
|
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
const prismaService = app.get(PrismaService);
|
const prismaService = app.get(PrismaService);
|
||||||
await prismaService.enableShutdownHooks(app)
|
await prismaService.enableShutdownHooks(app);
|
||||||
|
|
||||||
|
const config = new DocumentBuilder()
|
||||||
|
.setTitle('Chromacase')
|
||||||
|
.setDescription('The chromacase API')
|
||||||
|
.setVersion('1.0')
|
||||||
|
.build();
|
||||||
|
const document = SwaggerModule.createDocument(app, config);
|
||||||
|
SwaggerModule.setup('api', app, document);
|
||||||
|
|
||||||
await app.listen(3000);
|
await app.listen(3000);
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
51
back/src/models/plage.ts
Normal file
51
back/src/models/plage.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Thanks to https://github.com/Arthi-chaud/Meelo/blob/master/src/pagination/models/paginated-response.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class Plage<T> {
|
||||||
|
@ApiProperty()
|
||||||
|
metadata: {
|
||||||
|
this: string;
|
||||||
|
next: string | null;
|
||||||
|
previous: string | null;
|
||||||
|
};
|
||||||
|
@ApiProperty()
|
||||||
|
data: T[];
|
||||||
|
|
||||||
|
constructor(data: T[], request: Request | any) {
|
||||||
|
this.data = data;
|
||||||
|
let take = Number(request.query['take'] ?? 20).valueOf();
|
||||||
|
if (take == 0) take = 20;
|
||||||
|
let skipped: number = Number(request.query['skip'] ?? 0).valueOf();
|
||||||
|
if (skipped % take) {
|
||||||
|
skipped += take - (skipped % take);
|
||||||
|
}
|
||||||
|
this.metadata = {
|
||||||
|
this: this.buildUrl(request.path, request.query),
|
||||||
|
next:
|
||||||
|
data.length >= take
|
||||||
|
? this.buildUrl(request.path, {
|
||||||
|
...request.query,
|
||||||
|
skip: skipped + take,
|
||||||
|
})
|
||||||
|
: null,
|
||||||
|
previous: skipped
|
||||||
|
? this.buildUrl(request.path, {
|
||||||
|
...request.query,
|
||||||
|
skip: Math.max(0, skipped - take),
|
||||||
|
})
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildUrl(route: string, queryParameters: any) {
|
||||||
|
if (queryParameters.skip == 0) delete queryParameters.skip;
|
||||||
|
const builtQueryParameters = new URLSearchParams(
|
||||||
|
queryParameters,
|
||||||
|
).toString();
|
||||||
|
if (builtQueryParameters.length) return `${route}?${builtQueryParameters}`;
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,55 +7,54 @@ import * as bcrypt from 'bcryptjs';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersService {
|
export class UsersService {
|
||||||
[x: string]: any;
|
constructor(private prisma: PrismaService) {}
|
||||||
constructor(private prisma: PrismaService) {}
|
|
||||||
|
|
||||||
async user(
|
async user(
|
||||||
userWhereUniqueInput: Prisma.UserWhereUniqueInput,
|
userWhereUniqueInput: Prisma.UserWhereUniqueInput,
|
||||||
): Promise<User | null> {
|
): Promise<User | null> {
|
||||||
return this.prisma.user.findUnique({
|
return this.prisma.user.findUnique({
|
||||||
where: userWhereUniqueInput,
|
where: userWhereUniqueInput,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async users(params: {
|
async users(params: {
|
||||||
skip?: number;
|
skip?: number;
|
||||||
take?: number;
|
take?: number;
|
||||||
cursor?: Prisma.UserWhereUniqueInput;
|
cursor?: Prisma.UserWhereUniqueInput;
|
||||||
where?: Prisma.UserWhereInput;
|
where?: Prisma.UserWhereInput;
|
||||||
orderBy?: Prisma.UserOrderByWithRelationInput;
|
orderBy?: Prisma.UserOrderByWithRelationInput;
|
||||||
}): Promise<User[]> {
|
}): Promise<User[]> {
|
||||||
const { skip, take, cursor, where, orderBy } = params;
|
const { skip, take, cursor, where, orderBy } = params;
|
||||||
return this.prisma.user.findMany({
|
return this.prisma.user.findMany({
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
cursor,
|
cursor,
|
||||||
where,
|
where,
|
||||||
orderBy,
|
orderBy,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createUser(data: Prisma.UserCreateInput): Promise<User> {
|
async createUser(data: Prisma.UserCreateInput): Promise<User> {
|
||||||
data.password = await bcrypt.hash(data.password, 8)
|
data.password = await bcrypt.hash(data.password, 8);
|
||||||
return this.prisma.user.create({
|
return this.prisma.user.create({
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUser(params: {
|
async updateUser(params: {
|
||||||
where: Prisma.UserWhereUniqueInput;
|
where: Prisma.UserWhereUniqueInput;
|
||||||
data: Prisma.UserUpdateInput;
|
data: Prisma.UserUpdateInput;
|
||||||
}): Promise<User> {
|
}): Promise<User> {
|
||||||
const { where, data } = params;
|
const { where, data } = params;
|
||||||
return this.prisma.user.update({
|
return this.prisma.user.update({
|
||||||
data,
|
data,
|
||||||
where,
|
where,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> {
|
async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> {
|
||||||
return this.prisma.user.delete({
|
return this.prisma.user.delete({
|
||||||
where,
|
where,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
80
back/test/robot/lesson/lesson.robot
Normal file
80
back/test/robot/lesson/lesson.robot
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
*** Settings ***
|
||||||
|
Documentation Tests of the /lesson route.
|
||||||
|
... Ensures that the lesson CRUD works corectly.
|
||||||
|
|
||||||
|
Resource ../rest.resource
|
||||||
|
|
||||||
|
|
||||||
|
*** Test Cases ***
|
||||||
|
Post a lesson
|
||||||
|
[Documentation] Get a lesson
|
||||||
|
&{res}= POST
|
||||||
|
... /lesson
|
||||||
|
... {"name": "toto", "requiredLevel": 3, "mainSkill": "TwoHands", "description": "What am i doing"}
|
||||||
|
Output
|
||||||
|
Integer response status 201
|
||||||
|
[Teardown] DELETE /lesson/${res.body.id}
|
||||||
|
|
||||||
|
Get a lesson
|
||||||
|
[Documentation] Get a lesson
|
||||||
|
&{res}= POST
|
||||||
|
... /lesson
|
||||||
|
... {"name": "toto", "requiredLevel": 3, "mainSkill": "TwoHands", "description": "What am i doing"}
|
||||||
|
Output
|
||||||
|
Integer response status 201
|
||||||
|
&{get}= GET /lesson/${res.body.id}
|
||||||
|
Output
|
||||||
|
Should Be Equal ${res.body} ${get.body}
|
||||||
|
[Teardown] DELETE /lesson/${res.body.id}
|
||||||
|
|
||||||
|
Get a non-lesson
|
||||||
|
[Documentation] Get a lesson
|
||||||
|
&{get}= GET /lesson/toto
|
||||||
|
Output
|
||||||
|
Integer response status 400
|
||||||
|
|
||||||
|
Get a not-existing-lesson
|
||||||
|
[Documentation] Get a lesson
|
||||||
|
&{get}= GET /lesson/99999999
|
||||||
|
Output
|
||||||
|
Integer response status 404
|
||||||
|
|
||||||
|
Get all lessons
|
||||||
|
[Documentation] Get a lesson
|
||||||
|
&{res}= POST
|
||||||
|
... /lesson
|
||||||
|
... {"name": "toto", "requiredLevel": 3, "mainSkill": "TwoHands", "description": "What am i doing"}
|
||||||
|
Output
|
||||||
|
Integer response status 201
|
||||||
|
&{res2}= POST
|
||||||
|
... /lesson
|
||||||
|
... {"name": "tata", "requiredLevel": 3, "mainSkill": "TwoHands", "description": "What am i doing"}
|
||||||
|
Output
|
||||||
|
Integer response status 201
|
||||||
|
&{get}= GET /lesson
|
||||||
|
Output
|
||||||
|
Should Contain ${get.body.data} ${res.body}
|
||||||
|
Should Contain ${get.body.data} ${res2.body}
|
||||||
|
|
||||||
|
[Teardown] Run Keywords DELETE /lesson/${res.body.id}
|
||||||
|
... AND DELETE /lesson/${res2.body.id}
|
||||||
|
|
||||||
|
Get all lessons filtered
|
||||||
|
[Documentation] Get a lesson
|
||||||
|
&{res}= POST
|
||||||
|
... /lesson
|
||||||
|
... {"name": "toto", "requiredLevel": 3, "mainSkill": "TwoHands", "description": "What am i doing"}
|
||||||
|
Output
|
||||||
|
Integer response status 201
|
||||||
|
&{res2}= POST
|
||||||
|
... /lesson
|
||||||
|
... {"name": "tata", "requiredLevel": 3, "mainSkill": "Distance", "description": "What am i doing"}
|
||||||
|
Output
|
||||||
|
Integer response status 201
|
||||||
|
&{get}= GET /lesson?mainSkill=Distance
|
||||||
|
Output
|
||||||
|
Should Not Contain ${get.body.data} ${res.body}
|
||||||
|
Should Contain ${get.body.data} ${res2.body}
|
||||||
|
|
||||||
|
[Teardown] Run Keywords DELETE /lesson/${res.body.id}
|
||||||
|
... AND DELETE /lesson/${res2.body.id}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
*** Settings ***
|
*** Settings ***
|
||||||
Documentation Tests of the /users route.
|
Documentation Tests of the /users route.
|
||||||
... Ensures that the users CRUD works corectly.
|
... Ensures that the users CRUD works corectly.
|
||||||
|
|
||||||
Resource ../rest.resource
|
Resource ../rest.resource
|
||||||
|
|
||||||
|
|
||||||
*** Keywords ***
|
|
||||||
*** Test Cases ***
|
*** Test Cases ***
|
||||||
Create a user
|
Create a user
|
||||||
[Documentation] Create a user
|
[Documentation] Create a user
|
||||||
POST /users {"username": "i-don-t-exist", "password": "pass", "email": "wow@gmail.com"}
|
&{res}= POST /users {"username": "louis-boufon", "password": "pass", "email": "wow@gmail.com"}
|
||||||
Output
|
Output
|
||||||
Integer response status 201
|
Integer response status 201
|
||||||
[Teardown] DELETE /users/1
|
[Teardown] DELETE /users/${res.body.id}
|
||||||
|
|||||||
@@ -12,10 +12,10 @@
|
|||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strictNullChecks": false,
|
"strictNullChecks": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"strictBindCallApply": false,
|
"strictBindCallApply": false,
|
||||||
"forceConsistentCasingInFileNames": false,
|
"forceConsistentCasingInFileNames": false,
|
||||||
"noFallthroughCasesInSwitch": false
|
"noFallthroughCasesInSwitch": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
node_modules/.yarn-integrity
generated
vendored
12
node_modules/.yarn-integrity
generated
vendored
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"systemParams": "linux-x64-108",
|
|
||||||
"modulesFolders": [
|
|
||||||
"node_modules"
|
|
||||||
],
|
|
||||||
"flags": [],
|
|
||||||
"linkedModules": [],
|
|
||||||
"topLevelPatterns": [],
|
|
||||||
"lockfileEntries": {},
|
|
||||||
"files": [],
|
|
||||||
"artifacts": {}
|
|
||||||
}
|
|
||||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "Chromacase",
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user