Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat(Controllers): Implement main controllers #29

Merged
merged 54 commits into from
Feb 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
9421b09
Feat(User, Route): Register user route
Artur-Poffo Feb 19, 2024
85815ee
Feat(User, Route): Get user info route
Artur-Poffo Feb 19, 2024
8a74ea8
Feat(User, Authentication): Authenticate user route
Artur-Poffo Feb 19, 2024
501c963
Feat(User): Edit user details route
Artur-Poffo Feb 19, 2024
2f71bf4
Feat(User, Route): Delete user route
Artur-Poffo Feb 20, 2024
cb6d515
Feat(Course, Route): Register course route
Artur-Poffo Feb 20, 2024
24215a4
Feat(Course, Route): Get course info route
Artur-Poffo Feb 20, 2024
82438ae
Feat(Course, Route): Query courses by name route
Artur-Poffo Feb 20, 2024
81d8ff4
Feat(Course, Route): Query courses by tags route
Artur-Poffo Feb 20, 2024
a99f1b0
Feat(Course, Route): Edit course details route
Artur-Poffo Feb 20, 2024
180f3b4
Feat(Course, Route): Delete course route
Artur-Poffo Feb 20, 2024
c089e0a
Feat(Course, Route): Get course metrics route
Artur-Poffo Feb 20, 2024
c8c5bd8
Feat(Course, Route): Get course stats route
Artur-Poffo Feb 20, 2024
fd44dfa
Feat(Course, Route): Fetch recent courses route
Artur-Poffo Feb 20, 2024
c8c4a85
Feat(File, Route): First Upload!!! Just for testing
Artur-Poffo Feb 21, 2024
2b3079c
Feat(Image, Route): Upload image route
Artur-Poffo Feb 21, 2024
79f23b2
Feat(Video, Route): Upload video route - Initial
Artur-Poffo Feb 21, 2024
4c3c32d
Refactor(Video, Image, Upload): Refactor upload event
Artur-Poffo Feb 22, 2024
98339fe
Feat(Module, Route): Register module for course route
Artur-Poffo Feb 22, 2024
696409e
Feat(Course, Module, Route): Fetch course modules route
Artur-Poffo Feb 22, 2024
7e95923
Feat(Course, Class, Route): Fetch course classes route
Artur-Poffo Feb 22, 2024
90ef024
Feat(Module, Class, Route): Fetch module classes route
Artur-Poffo Feb 22, 2024
73d7f0f
Feat(Module, Route): Edit module details route
Artur-Poffo Feb 22, 2024
b3e2f6a
Feat(Module, Route): Delete module route
Artur-Poffo Feb 22, 2024
65adf59
Feat(Class, Module, Route): Add class to module route
Artur-Poffo Feb 22, 2024
5f09bca
Feat(Class, Route): Edit class details route
Artur-Poffo Feb 22, 2024
ccd5a01
Feat(Class, Route): Delete class route
Artur-Poffo Feb 22, 2024
3e948f5
Feat(Tag, Route): Register tag route
Artur-Poffo Feb 22, 2024
ffd54ab
Feat(Tag, Route): Fetch recent tags route
Artur-Poffo Feb 22, 2024
94fdd98
Feat(CourseTag, Course, Route): Attach tag to course route
Artur-Poffo Feb 22, 2024
bee7a15
Feat(CourseTag, Course, Route): Remove tag from course route
Artur-Poffo Feb 22, 2024
092c5a7
Refactor(Video, Image, Upload): Refactor upload routes
Artur-Poffo Feb 23, 2024
8169f69
Feat(CourseTag, Course, Route): Fetch course tags route
Artur-Poffo Feb 23, 2024
69c6584
Feat(Evaluation, Class, Route): Evaluate class route
Artur-Poffo Feb 23, 2024
13a3c85
Feat(Evaluation, Course, Route): Get course evaluations average route
Artur-Poffo Feb 23, 2024
1a81411
Feat(Evaluation, Route): Edit evaluation value route
Artur-Poffo Feb 23, 2024
18e3b9d
Feat(Enrollments, Route): Enroll to course route
Artur-Poffo Feb 23, 2024
1f0b26d
Feat(Enrollments, Route): Cancel enrollment route
Artur-Poffo Feb 23, 2024
dcdb2e7
Feat(Video, Route): Get video details route
Artur-Poffo Feb 23, 2024
a33705d
Feat(Image, Route): Get image details route
Artur-Poffo Feb 23, 2024
c21e644
Feat(Certificate, Route): Register certificate for course route
Artur-Poffo Feb 23, 2024
88bb885
Feat(Certificate, StudentCertificate, Route): Issue certificate route
Artur-Poffo Feb 23, 2024
0cc96f9
Refactor(Upload, Video, Image, File): Refactor all upload flow
Artur-Poffo Feb 23, 2024
fabc655
Feat(Course, Enrollment, Route): Fetch course students route
Artur-Poffo Feb 23, 2024
67a2bf7
Feat(Student, Enrollment, Route): Fetch student courses route
Artur-Poffo Feb 23, 2024
8ac5ec1
Feat(Course, Instructor, Route): Get course instructor details route
Artur-Poffo Feb 23, 2024
b16b2f3
Feat(Course, Instructor, Route): Fetch instructor courses route
Artur-Poffo Feb 23, 2024
02e8ae0
Feat(Class, Enrollment, Route): Mark class as completed route
Artur-Poffo Feb 23, 2024
51a59d0
Feat(Module, Enrollment, Route): Mark module as completed route
Artur-Poffo Feb 23, 2024
a079985
Feat(Course, Enrollment, Route): Mark course as completed route
Artur-Poffo Feb 23, 2024
786f328
Feat(Class, Enrollment, Route): Fetch enrollment completed classes route
Artur-Poffo Feb 24, 2024
137f60c
Feat(Module, Enrollment, Route): Fetch enrollment completed modules r…
Artur-Poffo Feb 24, 2024
db70462
Feat(Enrollment, Course, Route): Get student progress in a course route
Artur-Poffo Feb 24, 2024
fa9eb38
Fix(Enrollment, Course, Route): Fetch correct completed course module…
Artur-Poffo Feb 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"@typescript-eslint/no-unused-expressions": "off",
"no-new": "off",
"no-self-assign": "off",
"@typescript-eslint/no-unsafe-argument": "off"
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-floating-promises": "off",
"no-constant-condition": "off"
}
}
11 changes: 11 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"cSpell.words": [
"accesstoken",
"authtoken",
"codespark",
"dtos",
"fastify",
"originalname"
],
"CodeGPT.apiKey": "CodeGPT Plus Beta"
}
109 changes: 60 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
- [x] CRUDs for all main entities: course, module, class, user, etc.

- [ ] Return information about a course with student progress in your modules and classes.
- [ ] Video streaming to watch the classes.
- [x] Video streaming to watch the classes.

## Business Rules

Expand All @@ -59,11 +59,11 @@

## Non-Functional Requirements

- [ ] File upload/storage on Cloudflare R2.
- [x] File upload/storage on Cloudflare R2.
- [x] User's password must be encrypted.
- [x] Application data must be persisted in a PostgreSQL database with Docker.
- [ ] User must be identified by JWT.
- [ ] JWT must use the RS256 algorithm.
- [x] User must be identified by JWT.
- [x] JWT must use the RS256 algorithm.

## Initial Entities (Domain)

Expand Down Expand Up @@ -182,78 +182,85 @@
## Initial Routes (must have changes)

### Users
- [ ] GET /users/:userId - Get user details
- [ ] POST /users - Register user
- [ ] PUT /users/:userId - Update user
- [ ] DELETE /users/:userId - Delete user
- [x] GET /users/:userId - Get user details
- [x] POST /users - Register user
- [x] PUT /users/:userId - Update user
- [x] DELETE /users/:userId - Delete user

### Sessions
- [ ] POST /sessions - User authentication
- [x] POST /sessions - User authentication

### Students
- [ ] GET /courses/:courseId/students - Get students enrolled in course
- [ ] GET /students/:studentId/enrollments - Get student courses with instructor and evaluations
- [x] GET /courses/:courseId/students - Get students enrolled in course
- [x] GET /students/:studentId/enrollments - Get student courses with instructor and evaluations

### Instructors
- [ ] GET /courses/:courseId/instructor - Get course instructor details
- [ ] GET /instructors/:instructorId/courses - Get instructor courses with instructor and evaluations
- [x] GET /courses/:courseId/instructors - Get course instructor details
- [x] GET /instructors/:instructorId/courses - Get instructor courses with instructor and evaluations

### Courses
- [ ] GET /courses/:courseId - Get course details
- [ ] GET /courses/:courseId/enrollments/:enrollmentId/progress - Get course details with student progress
- [ ] GET /courses/:courseId/stats - Get course statistics, like duration and number of classes
- [ ] GET /courses/:courseId/metrics - Get course metrics for a dashboard
- [ ] GET /courses - Get recent courses with instructor and evaluation average
- [ ] GET /courses/filter - Filter courses by name or tags
- [ ] POST /courses - Register course
- [ ] PUT /courses/:courseId - Update course details
- [ ] DELETE /courses/:courseId - Delete course
- [x] GET /courses/:courseId - Get course details
- [x] GET /courses/:courseId/stats - Get course statistics, like duration and number of classes
- [x] GET /courses/:courseId/metrics - Get course metrics for a dashboard
- [x] GET /courses - Get recent courses with instructor and evaluation average
- [x] GET /courses/filter/name?q="" - Filter courses by name
- [x] GET /courses/filter/tags?q="" - Filter courses by tags
- [x] POST /courses - Register course
- [x] PUT /courses/:courseId - Update course details
- [x] DELETE /courses/:courseId - Delete course

### Certificate
- [ ] POST /courses/:courseId/certificates - Add certificate to course
- [ ] DELETE /courses/:courseId/certificates - Remove certificate from course
- [x] POST /courses/:courseId/certificates - Add certificate to course
- [x] DELETE /courses/:courseId/certificates - Remove certificate from course

### StudentCertificate
- [ ] GET /enrollments/:enrollmentId/certificate - Issue student certificate
- [x] POST /enrollments/:enrollmentId/certificates/issue - Issue student certificate

### Modules
- [ ] GET /courses/:courseId/modules - Get course modules
- [ ] POST /modules - Register module
- [ ] GET /modules/:moduleId/classes - Get classes from a module
- [ ] PUT /modules/:moduleId - Update module details
- [ ] DELETE /modules/:moduleId - Delete module
- [x] GET /courses/:courseId/modules - Get course modules
- [x] POST /modules - Register module
- [x] GET /modules/:moduleId/classes - Get classes from a module
- [x] PUT /modules/:moduleId - Update module details
- [x] DELETE /modules/:moduleId - Delete module

### Classes
- [ ] GET /courses/:courseId/classes - Get course classes
- [ ] POST /classes - Register class
- [ ] PUT /classes/:classId - Update class details
- [ ] DELETE /classes/:classId - Delete class
- [x] GET /courses/:courseId/classes - Get course classes
- [x] POST /modules/:moduleId/classes/video/:videoId - Register class
- [x] PUT /classes/:classId - Update class details
- [x] DELETE /classes/:classId - Delete class

### Tags
- [ ] GET /tags - Get recent tags
- [ ] POST /tags - Register tag
- [x] GET /tags - Get recent tags
- [x] POST /tags - Register tag

### CourseTags
- [ ] GET /courses/:courseId/tags - Get course tags
- [ ] POST /courses/:courseId/tags/:tagId - Attach tag to course
- [ ] POST /courses/:courseId/tags/tag:id - Remove tag to course
- [x] GET /courses/:courseId/tags - Get course tags
- [x] POST /courses/:courseId/tags/:tagId - Attach tag to course
- [x] POST /courses/:courseId/tags/:tagId - Remove tag to course

### Evaluations
- [ ] GET /courses/:courseId/evaluation - Get course evaluation average
- [ ] POST /evaluations - Register evaluation
- [ ] PUT /evaluations/:evaluationId - Update evaluation
- [x] GET /courses/:courseId/evaluations/average - Get course evaluation average
- [x] POST /evaluations - Register evaluation
- [x] PUT /evaluations/:evaluationId - Update evaluation

### Enrollments
- [ ] POST /enrollments - Register enrollment
- [ ] GET /enrollments/students/:studentId/courses/:courseId - Get enrollment of a student on a course
- [ ] DELETE /enrollments/students/:studentId/courses/:courseId - Cancel enrollment
- [x] POST /courses/:courseId/enroll - Enroll to course
- [x] POST /enrollments/:enrollmentId/modules/:moduleId/complete - Mark module as completed
- [x] POST /enrollments/:enrollmentId/classes/:classId/complete - Mark class as completed
- [x] POST /enrollments/:enrollmentId/complete - Mark enrollment as completed
- [x] GET /enrollments/:enrollmentId/progress - Get student enrollment progress
- [x] GET /courses/:courseId/students/:studentId/enrollments - Get enrollment of a student on a course
- [x] GET /enrollments/:enrollmentId/classes/completed - Fetch enrollment completed classes
- [x] GET /enrollments/:enrollmentId/modules/completed - Fetch enrollment completed modules
- [x] DELETE /enrollments/:enrollmentId - Cancel enrollment

### Video
- [ ] POST /videos - Upload video
- [ ] GET /videos/:videoId/stream - Video streaming
- [x] POST /videos - Upload video
- [x] GET /videos/:videoId - Get video details

### Image
- [ ] POST /images - Upload image
- [x] POST /images - Upload image
- [x] GET /images/:imageId - Get image details

## Potential Refactoring or Updates:

Expand All @@ -271,4 +278,8 @@
- [x] Fix infinite calls to Prisma repositories in some mapper usage scenarios.
- [x] Introduce domain events for Prisma repositories.
- [ ] Implement mappers for mapping domain entities to DTOs.
- [ ] Implement pagination.
- [ ] Implement pagination.
- [ ] Implement E2E tests.
- [ ] Implement register user validations, like: email and cpf. - Could it be a value object?
- [ ] Refactor error handling in controllers
- [ ] Refactor the event handler class instance
19 changes: 14 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,36 +14,45 @@
"prisma:init": "prisma migrate deploy && prisma generate",
"test:unit": "vitest run",
"test:unit:watch": "vitest",
"test:e2e": "vitest run --config ./vitest.config.e2e.ts",
"test:e2e:watch": "vitest --config ./vitest.config.e2e.ts",
"test:e2e": "vitest run --config ./vitest.config.e2e.mts",
"test:e2e:watch": "vitest --config ./vitest.config.e2e.mts",
"test:cov": "vitest run --coverage",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix"
},
"keywords": [],
"author": "Artur Poffo",
"license": "ISC",
"dependencies": {
"@prisma/client": "^5.9.1",
"@aws-sdk/client-s3": "^3.515.0",
"@fastify/cookie": "^9.3.1",
"@fastify/cors": "^9.0.1",
"@fastify/jwt": "^8.0.0",
"@prisma/client": "^5.10.2",
"bcryptjs": "^2.4.3",
"buffer-to-stream": "^1.0.0",
"dotenv": "^16.3.1",
"fastify": "^4.25.2",
"fastify-multer": "^2.0.3",
"get-video-duration": "^4.1.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@faker-js/faker": "^8.3.1",
"@types/bcryptjs": "^2.4.6",
"@types/buffer-to-stream": "^1.0.3",
"@types/node": "^20.11.5",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"eslint": "^8.0.1",
"eslint-config-standard-with-typescript": "^43.0.1",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-n": "^15.0.0 || ^16.0.0 ",
"eslint-plugin-promise": "^6.0.0",
"prisma": "^5.9.1",
"prisma": "^5.10.2",
"supertest": "^6.3.4",
"tsup": "^8.0.1",
"tsx": "^4.7.0",
"typescript": "*",
"vite-tsconfig-paths": "^4.3.1",
"vitest": "^1.2.1"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- DropForeignKey
ALTER TABLE "videos" DROP CONSTRAINT "videos_file_key_fkey";

-- AlterTable
ALTER TABLE "videos" ALTER COLUMN "file_key" DROP NOT NULL;

-- AddForeignKey
ALTER TABLE "videos" ADD CONSTRAINT "videos_file_key_fkey" FOREIGN KEY ("file_key") REFERENCES "files"("key") ON DELETE SET NULL ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "files" ALTER COLUMN "key" DROP NOT NULL;
13 changes: 13 additions & 0 deletions prisma/migrations/20240222232146_create_image_model/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE "images" (
"id" TEXT NOT NULL,
"file_key" TEXT,

CONSTRAINT "images_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "images_file_key_key" ON "images"("file_key");

-- AddForeignKey
ALTER TABLE "images" ADD CONSTRAINT "images_file_key_fkey" FOREIGN KEY ("file_key") REFERENCES "files"("key") ON DELETE SET NULL ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:

- Made the column `key` on table `files` required. This step will fail if there are existing NULL values in that column.

*/
-- AlterTable
ALTER TABLE "files" ALTER COLUMN "key" SET NOT NULL;
15 changes: 13 additions & 2 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,9 @@ model File {
key String @unique()
size Decimal
storedAt DateTime @default(now()) @map("stored_at")
video Video?

video Video?
Image Image?

@@map("files")
}
Expand All @@ -190,7 +192,16 @@ model Video {

classes Class[]

fileKey String @unique() @map("file_key")
fileKey String? @unique() @map("file_key")

@@map("videos")
}

model Image {
id String @id @default(uuid())
file File? @relation(fields: [fileKey], references: [key])

fileKey String? @unique() @map("file_key")

@@map("images")
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export interface EncrypterProps {
role: 'STUDENT' | 'INSTRUCTOR'
sub: string
}

export interface Encrypter {
encrypt: (payload: Record<string, unknown>) => Promise<string>
encrypt: (payload: EncrypterProps) => Promise<string>
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { type EnrollmentCompletedItem } from '../../enterprise/entities/enrollme

export interface EnrollmentCompletedItemsRepository {
findById: (id: string) => Promise<EnrollmentCompletedItem | null>
findByEnrollmentIdAndItemId: (enrollmentId: string, itemId: string) => Promise<EnrollmentCompletedItem | null>
findManyCompletedClassesByEnrollmentId: (enrollmentId: string) => Promise<EnrollmentCompletedItem[]>
findManyCompletedModulesByEnrollmentId: (enrollmentId: string) => Promise<EnrollmentCompletedItem[]>
findAllByEnrollmentId: (enrollmentId: string) => Promise<EnrollmentCompletedItem[]>
create: (completedItem: EnrollmentCompletedItem) => Promise<EnrollmentCompletedItem>
delete: (completedItem: EnrollmentCompletedItem) => Promise<void>
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export interface EnrollmentsRepository {
findManyByCourseId: (courseId: string) => Promise<Enrollment[]>
findManyByStudentId: (studentId: string) => Promise<Enrollment[]>
findManyStudentsByCourseId: (courseId: string) => Promise<Student[]>
markItemAsCompleted: (completedItemId: string, enrollment: Enrollment) => Promise<Enrollment | null>
markAsCompleted: (enrollment: Enrollment) => Promise<Enrollment | null>
countEnrollmentsByYear: (year: number) => Promise<number>
create: (enrollment: Enrollment) => Promise<Enrollment | null>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { OnFileUploaded } from '@/domain/course-management/application/subscribers/on-file-uploaded'
import { GetVideoDuration } from '@/infra/storage/utils/get-video-duration'
import { InMemoryVideosRepository } from '../../../../../test/repositories/in-memory-videos-repository'
import { waitFor } from '../../../../../test/utils/wait-for'
import { InMemoryFilesRepository } from './../../../../../test/repositories/in-memory-files-repository'
import { InMemoryImagesRepository } from './../../../../../test/repositories/in-memory-images-repository'
import { FakeUploader } from './../../../../../test/storage/fake-uploader'
import { UploadFileUseCase } from './../../../storage/application/use-cases/upload-file'

let inMemoryFilesRepository: InMemoryFilesRepository
let inMemoryImagesRepository: InMemoryImagesRepository
let inMemoryVideosRepository: InMemoryVideosRepository
let fakeUploader: FakeUploader
let getVideoDuration: GetVideoDuration
let uploadFileUseCase: UploadFileUseCase

describe('On file uploaded event', () => {
beforeEach(() => {
inMemoryFilesRepository = new InMemoryFilesRepository()
inMemoryImagesRepository = new InMemoryImagesRepository()
inMemoryVideosRepository = new InMemoryVideosRepository()
fakeUploader = new FakeUploader()
getVideoDuration = new GetVideoDuration()
uploadFileUseCase = new UploadFileUseCase(
inMemoryFilesRepository,
fakeUploader
)

new OnFileUploaded(
inMemoryImagesRepository,
inMemoryVideosRepository,
getVideoDuration
)
})

it('should be able to upload a file and persist your respective entity, image or video', async () => {
const result = await uploadFileUseCase.exec({
fileName: 'file.jpg',
body: Buffer.from('image body'),
size: 1024,
fileType: 'image/jpeg'
})

expect(result.isRight()).toBe(true)
expect(inMemoryFilesRepository.items[0]).toMatchObject({
fileName: 'file.jpg'
})
expect(fakeUploader.files[0]).toMatchObject({
fileName: 'file.jpg'
})
await waitFor(() => {
expect(inMemoryImagesRepository.items[0]).toMatchObject({
imageName: 'file.jpg'
})
}, 5000)
})
})
Loading