Skip to content

Commit

Permalink
Merge pull request #29 from Artur-Poffo/feat-implement-controllers
Browse files Browse the repository at this point in the history
Feat(Controllers): Implement main controllers
  • Loading branch information
Artur-Poffo committed Feb 24, 2024
2 parents cb64635 + fa9eb38 commit aeb9788
Show file tree
Hide file tree
Showing 198 changed files with 6,468 additions and 511 deletions.
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

0 comments on commit aeb9788

Please sign in to comment.