diff --git a/package.json b/package.json index ebdbffe..3b5c2fb 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { + "start": "cd semana17/LabenuSystem && npm install && npm start", "lint": "tsc && eslint ." }, "devDependencies": { diff --git a/semana17/LabenuSystem/.eslintrc.json b/semana17/LabenuSystem/.eslintrc.json new file mode 100644 index 0000000..c6cf55d --- /dev/null +++ b/semana17/LabenuSystem/.eslintrc.json @@ -0,0 +1,252 @@ +{ + "root": true, + "env": { + "browser": true, + "es2020": true, + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react-hooks/recommended" + ], + "settings": { + "react": { + "version": "detect" + } + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 11, + "sourceType": "module" + }, + "plugins": ["react", "@typescript-eslint", "react-hooks"], + "rules": { + "no-cond-assign": ["error", "always"], + "no-console": "warn", + "no-constant-condition": "error", + "no-debugger": "error", + "no-dupe-else-if": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-empty": "warn", + "no-empty-character-class": "error", + "no-extra-boolean-cast": "warn", + "no-extra-parens": "off", + "@typescript-eslint/no-extra-parens": ["warn", "all", { "ignoreJSX": "multi-line" }], + "no-extra-semi": "warn", + "no-func-assign": "warn", + "no-import-assign": "warn", + "no-inner-declarations": ["warn", "both"], + "no-invalid-regexp": "error", + "no-irregular-whitespace": ["warn", { + "skipRegExps": true, + "skipTemplates": true + }], + "no-loss-of-precision": "warn", + "no-misleading-character-class": "warn", + "no-obj-calls": "warn", + "no-prototype-builtins": "error", + "no-sparse-arrays": "error", + "no-template-curly-in-string": "warn", + "no-unexpected-multiline": "warn", + "no-unreachable": "error", + "no-unreachable-loop": "error", + "no-unsafe-finally": "error", + "no-unsafe-negation": "error", + "use-isnan": "warn", + "valid-typeof": "error", + + "curly": ["warn", "multi", "consistent"], + "no-empty-function": "warn", + + "array-bracket-newline": ["warn", { "multiline": true }], + "array-bracket-spacing": ["warn", "always"], + "array-element-newline": ["warn", { "multiline": true, "minItems": 3 }], + "block-spacing": "warn", + "brace-style": ["warn", "1tbs", { "allowSingleLine": true }], + "capitalized-comments": "warn", + "camelcase": "off", + "comma-dangle": ["warn", "never"], + "comma-spacing": ["warn", { "before": false, "after": true }], + "comma-style": ["warn", "last"], + "computed-property-spacing": "warn", + "eol-last": ["warn", "always"], + "func-call-spacing": ["error", "never"], + "func-names": ["error", "always"], + "func-style": ["error", "declaration", { "allowArrowFunctions": true }], + "function-call-argument-newline": ["warn", "consistent"], + "function-paren-newline": ["warn", "multiline"], + "id-length": ["warn", { "min": 3, "exceptions": ["id", "_"] }], + "implicit-arrow-linebreak": ["warn", "beside"], + "indent": ["warn", 2, { + "SwitchCase": 1, + "VariableDeclarator": "first", + "MemberExpression": 1, + "FunctionDeclaration": {"parameters": "first"}, + "FunctionExpression": {"parameters": "first"}, + "CallExpression": {"arguments": "first"}, + "ArrayExpression": "first", + "ObjectExpression": "first", + "ImportDeclaration": "first", + "flatTernaryExpressions": false, + "offsetTernaryExpressions": true, + "ignoreComments": false + }], + "jsx-quotes": ["warn", "prefer-double"], + "key-spacing": ["warn", { + "beforeColon": false, + "afterColon": true, + "mode": "strict" , + "align": "value" + }], + "keyword-spacing": "warn", + "linebreak-style": ["warn", "unix"], + "lines-around-comment": ["warn", { + "beforeBlockComment": true, + "afterBlockComment": true, + "beforeLineComment": true, + "afterLineComment": false + }], + "lines-between-class-members": ["warn", "always"], + "max-depth": ["warn", 4], + "max-len": ["warn", { "code": 85 }], + "max-lines-per-function": ["warn", { + "max": 100, + "skipBlankLines": true, + "skipComments": true + }], + "max-nested-callbacks": ["warn", 3], + "max-params": ["warn", 6], + "max-statements-per-line": ["warn", { "max": 2 }], + "multiline-comment-style": ["warn", "bare-block"], + "multiline-ternary": ["warn", "always-multiline"], + "newline-per-chained-call": ["warn", { "ignoreChainWithDepth": 3 }], + "no-lonely-if": "warn", + "no-mixed-spaces-and-tabs": "warn", + "no-multi-assign": "warn", + "no-multiple-empty-lines": ["warn", { "max": 1 }], + "no-negated-condition": "warn", + "no-nested-ternary": "warn", + "no-new-object": "error", + "no-tabs": "warn", + "no-trailing-spaces": "warn", + "no-unneeded-ternary": "warn", + "no-whitespace-before-property": "warn", + "nonblock-statement-body-position": ["warn", "below"], + "object-curly-newline": ["warn", { + "ObjectExpression": { "multiline": true, "minProperties": 4 }, + "ObjectPattern": { "multiline": true, "minProperties": 4 }, + "ImportDeclaration": { "multiline": true, "minProperties": 4 }, + "ExportDeclaration": { "multiline": true, "minProperties": 4 } + }], + "object-curly-spacing": ["warn", "always"], + "object-property-newline": "warn", + "one-var": ["warn", { "initialized": "never" }], + "one-var-declaration-per-line": ["warn", "initializations"], + "operator-assignment": ["warn", "always"], + "operator-linebreak": ["warn", "before"], + "padded-blocks": ["warn", "never", { "allowSingleLineBlocks": true } ], + "prefer-exponentiation-operator": "warn", + "prefer-object-spread": "warn", + "quote-props": ["warn", "as-needed"], + "quotes": ["warn", "double"], + "semi": ["warn", "always"], + "semi-spacing": ["warn", {"before": false, "after": true}], + "semi-style": ["warn", "last"], + "space-before-blocks": "warn", + "space-before-function-paren": ["warn", {"anonymous": "always", "named": "never", "asyncArrow": "always"}], + "space-in-parens": ["warn", "never"], + "space-infix-ops": "warn", + "space-unary-ops": [ "warn", {"words": true, "nonwords": false}], + "spaced-comment": ["warn", "never"], + "switch-colon-spacing": "warn", + "template-tag-spacing": "warn", + "wrap-regex": "warn", + + "arrow-body-style": ["warn", "as-needed"], + "arrow-parens": ["warn", "always"], + "arrow-spacing": "warn", + "constructor-super": "error", + "generator-star-spacing": ["warn", "before"], + "no-class-assign": "error", + "no-confusing-arrow": "warn", + "no-const-assign": "error", + "no-duplicate-imports": ["error", { "includeExports": true }], + "no-new-symbol": "error", + "no-useless-computed-key": "warn", + "no-useless-constructor": "error", + "no-useless-rename": "warn", + "no-var": "warn", + "prefer-arrow-callback": "warn", + "prefer-const": "warn", + "prefer-numeric-literals": "warn", + "prefer-rest-params": "error", + "prefer-spread": "warn", + "prefer-template": "warn", + "require-yield": "error", + "rest-spread-spacing": ["warn", "never"], + "symbol-description": "error", + "template-curly-spacing": "warn", + "yield-star-spacing": ["warn", "before"], + + "react/no-array-index-key": "error", + "react/no-deprecated": "error", + "react/self-closing-comp": ["warn", { + "component": true, + "html": true + }], + + "react/jsx-child-element-spacing": "warn", + "react/jsx-closing-bracket-location": "warn", + "react/jsx-closing-tag-location": "warn", + "react/jsx-curly-brace-presence": "warn", + "react/jsx-curly-newline": "warn", + "react/jsx-curly-spacing": "warn", + "react/jsx-equals-spacing": ["warn", "never"], + "react/jsx-filename-extension": ["warn", { "extensions": [".tsx", ".jsx", ".js"] }], + "react/jsx-first-prop-new-line": ["warn", "multiline"], + "react/jsx-fragments": ["warn", "syntax"], + "react/jsx-indent": ["warn", 2, { + "checkAttributes": true, + "indentLogicalExpressions": true + }], + "react/jsx-indent-props": ["warn", 2], + "react/jsx-key": ["warn", { "checkFragmentShorthand": true }], + "react/jsx-max-depth": ["warn", { "max": 5 }], + "react/jsx-max-props-per-line": ["warn", { + "maximum": 1, + "when": "multiline" + }], + "react/jsx-no-comment-textnodes": "warn", + "react/jsx-no-duplicate-props": "error", + "react/jsx-no-script-url": "error", + "react/jsx-no-undef": "warn", + "react/jsx-one-expression-per-line": ["warn", { "allow": "single-child" }], + "react/jsx-pascal-case": "warn", + "react/jsx-props-no-multi-spaces": "warn", + "react/jsx-tag-spacing": ["warn", { + "beforeSelfClosing": "never", + "closingSlash": "never", + "afterOpening": "never", + "beforeClosing": "never" + }], + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error", + "react/prop-types": "off", + + "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }] + }, + "overrides": [ + { + "files": ["*.jsx", "*.js"], + "rules": { + "@typescript-eslint/explicit-module-boundary-types": "off" + } + } + ] +} diff --git a/semana17/LabenuSystem/.gitignore b/semana17/LabenuSystem/.gitignore new file mode 100644 index 0000000..853f700 --- /dev/null +++ b/semana17/LabenuSystem/.gitignore @@ -0,0 +1,5 @@ +node_module +build +.env +package-lock.json +yarn.lock diff --git a/semana17/LabenuSystem/package.json b/semana17/LabenuSystem/package.json new file mode 100644 index 0000000..9493716 --- /dev/null +++ b/semana17/LabenuSystem/package.json @@ -0,0 +1,27 @@ +{ + "name": "aula47", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "npx tsc && node build/index.js", + "lint": "npx eslint --fix ./src" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/express": "^4.17.13", + "@types/node": "^16.4.0", + "@types/uuid": "^8.3.1", + "cors": "^2.8.5", + "dotenv": "^10.0.0", + "express": "^4.17.1", + "knex": "^0.95.7", + "mysql": "^2.18.1", + "typescript": "^4.3.5", + "uuid": "^8.3.2", + "yup": "^0.32.9" + } +} diff --git a/semana17/LabenuSystem/src/@types/index.d.ts b/semana17/LabenuSystem/src/@types/index.d.ts new file mode 100644 index 0000000..f83ee1d --- /dev/null +++ b/semana17/LabenuSystem/src/@types/index.d.ts @@ -0,0 +1,125 @@ +import { Knex } from "knex"; + +export type ID = string; + +export interface Turma { + id: ID; + nome: string; + modulo: number; + dataInicio: Date; + dataFinal: Date; + tipo: "Integral" | "Noturno"; +} + +export interface TurmaDatabase { + id: ID; + nome: string; + modulo: number; + data_inicio: Date; + data_final: Date; + tipo: "Integral" | "Noturno"; +} + +export interface Estudante { + id: ID; + nome: string; + email: string; + dataDeNascimento: string; + turmaID: ID; +} + +export interface EstudanteDatabase { + id: ID; + nome: string; + email: string; + data_de_nascimento: string; + turma_id: ID; +} + +export interface Passatempo { + id: ID; + nome: string; +} + +export interface EstudantePassatempo { + estudanteID: ID; + passatempoID: ID; +} + +export interface EstudantePassatempoDatabase { + estudante_id: ID; + passatempo_id: ID; +} + +export type Professor = Estudante; + +export type ProfessorDatabase = EstudanteDatabase; + +export interface Especialidade { + id: ID; + nome: string; +} + +export interface ProfessorEspecialidade { + especialidadeID: ID; + professorID: ID; +} + +export interface ProfessorEspecialidadeDatabase { + especialidade_id: ID; + professor_id: ID; +} + +//Veja https://knexjs.org/#typescript-support +declare module "knex/types/tables" { + interface Tables { + LabenuSystem_Turma: TurmaDatabase; + LabenuSystem_Turma_composite: Knex.CompositeTableType< + TurmaDatabase, + TurmaDatabase, + Omit + >; + + LabenuSystem_Estudante: EstudanteDatabase; + LabenuSystem_Estudante_composite: Knex.CompositeTableType< + EstudanteDatabase, + EstudanteDatabase, + Omit + >; + + LabenuSystem_Passatempo: Passatempo; + LabenuSystem_Passatempo_composite: Knex.CompositeTableType< + Passatempo, + Passatempo, + Omit + >; + + LabenuSystem_Estudante_Passatempo: EstudantePassatempoDatabase; + LabenuSystem_Estudante_Passatempo_composite: Knex.CompositeTableType< + EstudantePassatempoDatabase, + EstudantePassatempoDatabase, + Omit + >; + + LabenuSystem_Professor: ProfessorDatabase; + LabenuSystem_Professor_composite: Knex.CompositeTableType< + ProfessorDatabase, + ProfessorDatabase, + Omit + >; + + LabenuSystem_Especialidade: Especialidade; + LabenuSystem_Especialidade_composite: Knex.CompositeTableType< + Especialidade, + Especialidade, + Omit + >; + + LabenuSystem_Professor_Especialidade: ProfessorEspecialidadeDatabase; + LabenuSystem_Professor_Especialidade_composite: Knex.CompositeTableType< + ProfessorEspecialidadeDatabase, + ProfessorEspecialidadeDatabase, + Omit + >; + } +} diff --git a/semana17/LabenuSystem/src/database/mysql/connection.ts b/semana17/LabenuSystem/src/database/mysql/connection.ts new file mode 100644 index 0000000..9a2cb91 --- /dev/null +++ b/semana17/LabenuSystem/src/database/mysql/connection.ts @@ -0,0 +1,13 @@ +import { knex } from "knex"; + +export const connection = knex({ + client: process.env.DATABASE_TYPE, + connection: { + host: process.env.DATABASE_HOST, + port: Number(process.env.DATABASE_PORT), + database: process.env.DATABASE_SCHEMA, + user: process.env.DATABASE_USER, + password: process.env.DATABASE_PASSWORD, + multipleStatements: true + } +}); diff --git a/semana17/LabenuSystem/src/database/mysql/estudante/index.ts b/semana17/LabenuSystem/src/database/mysql/estudante/index.ts new file mode 100644 index 0000000..c155cf1 --- /dev/null +++ b/semana17/LabenuSystem/src/database/mysql/estudante/index.ts @@ -0,0 +1,79 @@ +import { v1 as uuidV1 } from "uuid"; +import { Estudante, EstudanteDatabase, ID } from "../../../@types"; +import { TURMA_ZERO_ID } from "../../../handlers"; +import { connection } from "../connection"; + +export async function criarEstudante(estudante: Omit) +: Promise { + const estudanteNovo: EstudanteDatabase = { + id: uuidV1(), + nome: estudante.nome, + email: estudante.email, + turma_id: estudante.turmaID, + data_de_nascimento: estudante.dataDeNascimento + }; + + await connection("LabenuSystem_Estudante").insert(estudanteNovo); + + return { + ...estudante, + id: estudanteNovo.id + }; +} + +export async function verEstudante(id: ID): Promise { + const database = await connection("LabenuSystem_Estudante") + .select("*") + .where({ id }) + .first(); + + return database + ? { + id: database.id, + nome: database.nome, + email: database.email, + dataDeNascimento: database.data_de_nascimento, + turmaID: database.turma_id + } + : undefined; +} + +export async function verEstudantesNaTurma(turmaID: ID) +: Promise[]> { + const database = await connection("LabenuSystem_Estudante") + .select("id", "nome", "email", "data_de_nascimento") + .where({ turma_id: turmaID }); + + return database.map(({ + id, nome, data_de_nascimento, email + }) => ({ + id, + email, + nome, + dataDeNascimento: data_de_nascimento + })); +} + +export async function adicionarEstudanteNaTurma(estudanteID: ID, turmaID: ID) +: Promise { + return connection("LabenuSystem_Estudante") + .update({ turma_id: turmaID }) + .where({ id: estudanteID }); +} + +export async function removerEstudanteDaTurma(id: ID): Promise { + return connection("LabenuSystem_Estudante") + .update({ turma_id: TURMA_ZERO_ID }) + .where({ id }); +} + +export async function removerEstudante(id: ID): Promise { + connection("LabenuSystem_Estudante_Passatempo") + .del() + .where({ estudante_id: id }); + + return connection("LabenuSystem_Estudante") + .del() + .where({ id }); +} + diff --git a/semana17/LabenuSystem/src/database/mysql/index.ts b/semana17/LabenuSystem/src/database/mysql/index.ts new file mode 100644 index 0000000..297e8d7 --- /dev/null +++ b/semana17/LabenuSystem/src/database/mysql/index.ts @@ -0,0 +1,3 @@ +export * from "./turma"; +export * from "./estudante"; +export * from "./professor"; diff --git a/semana17/LabenuSystem/src/database/mysql/professor/index.ts b/semana17/LabenuSystem/src/database/mysql/professor/index.ts new file mode 100644 index 0000000..eb92ba7 --- /dev/null +++ b/semana17/LabenuSystem/src/database/mysql/professor/index.ts @@ -0,0 +1,79 @@ +import { v1 as uuidV1 } from "uuid"; +import { ID, Professor, ProfessorDatabase } from "../../../@types"; +import { TURMA_ZERO_ID } from "../../../handlers"; +import { connection } from "../connection"; + +export async function criarProfessor(professor: Omit) +: Promise { + const professorNovo: ProfessorDatabase = { + id: uuidV1(), + nome: professor.nome, + email: professor.email, + turma_id: professor.turmaID, + data_de_nascimento: professor.dataDeNascimento + }; + + await connection("LabenuSystem_Professor").insert(professorNovo); + + return { + ...professor, + id: professorNovo.id + }; +} + +export async function verProfessor(id: ID): Promise { + const database = await connection("LabenuSystem_Professor") + .select("*") + .where({ id }) + .first(); + + return database + ? { + id: database.id, + nome: database.nome, + email: database.email, + dataDeNascimento: database.data_de_nascimento, + turmaID: database.turma_id + } + : undefined; +} + +export async function verProfessoresNaTurma(turmaID: ID) +: Promise[]> { + const database = await connection("LabenuSystem_Professor") + .select("id", "nome", "email", "data_de_nascimento") + .where({ turma_id: turmaID }); + + return database.map(({ + id, nome, data_de_nascimento, email + }) => ({ + id, + email, + nome, + dataDeNascimento: data_de_nascimento + })); +} + +export async function adicionarProfessorNaTurma(professorID: ID, turmaID: ID) +: Promise { + return connection("LabenuSystem_Professor") + .update({ turma_id: turmaID }) + .where({ id: professorID }); +} + +export async function removerProfessorDaTurma(id: ID): Promise { + return connection("LabenuSystem_Professor") + .update({ turma_id: TURMA_ZERO_ID }) + .where({ id }); +} + +export async function removerProfessor(id: ID): Promise { + connection("LabenuSystem_Professor_Especialidade") + .del() + .where({ professor_id: id }); + + return connection("LabenuSystem_Professor") + .del() + .where({ id }); +} + diff --git a/semana17/LabenuSystem/src/database/mysql/turma/index.ts b/semana17/LabenuSystem/src/database/mysql/turma/index.ts new file mode 100644 index 0000000..d99293f --- /dev/null +++ b/semana17/LabenuSystem/src/database/mysql/turma/index.ts @@ -0,0 +1,25 @@ +import { v1 as uuidV1 } from "uuid"; +import { connection } from "../connection"; +import { ID, Turma, TurmaDatabase } from "../../../@types"; + +export async function criarTurma(turma: Omit): Promise { + const turmaNova: TurmaDatabase = { + id: uuidV1(), + nome: turma.nome, + data_final: turma.dataFinal, + data_inicio: turma.dataInicio, + modulo: turma.modulo, + tipo: turma.tipo + }; + + await connection("LabenuSystem_Turma").insert(turmaNova); + + return { + ...turma, + id: turmaNova.id + }; +} + +export async function mudarModulo(id: ID, modulo:Turma["modulo"]): Promise { + return connection("LabenuSystem_Turma").update({ modulo }).where({ id }); +} diff --git a/semana17/LabenuSystem/src/env.ts b/semana17/LabenuSystem/src/env.ts new file mode 100644 index 0000000..7ff27f7 --- /dev/null +++ b/semana17/LabenuSystem/src/env.ts @@ -0,0 +1,2 @@ +import dotenv from "dotenv"; +dotenv.config(); diff --git a/semana17/LabenuSystem/src/handlers/estudante/index.ts b/semana17/LabenuSystem/src/handlers/estudante/index.ts new file mode 100644 index 0000000..9b8fcf2 --- /dev/null +++ b/semana17/LabenuSystem/src/handlers/estudante/index.ts @@ -0,0 +1,178 @@ +import express, { Request, Response } from "express"; +import { criarEstudanteSchema, estudanteAdicionarTurmaSchema } from "../../validate"; +import { + adicionarEstudanteNaTurma, + criarEstudante as criarEstudanteDatabase, + verEstudantesNaTurma, + verEstudante as verEstudanteDatabase, + removerEstudanteDaTurma, + removerEstudante as removerEstudanteDatabase +} from "../../database/mysql"; +import { TURMA_ZERO_ID } from "../turma"; +import { validate as validarUUID } from "uuid"; + +const erros = { + inesperado: "Aconteceu um erro inesperado", + emailExiste: "Email já existe", + estudanteNaoEcontrada: "Estudante não econtrada", + turmaNaoEcontrada: "Turma não econtrada", + turmaIDInvalido: "O turmaID precisa ser um id válido", + IDInvalido: "O estudanteID precisa ser um id válido" +}; + +export const estudanteRouter = express.Router(); + +estudanteRouter.post("/", criarEstudante); +estudanteRouter.put("/turma", adicionarTurma); +estudanteRouter.get("/turma/:turmaID", verTurma); +estudanteRouter.get("/:id", verEstudante); +estudanteRouter.delete("/:id", removerEstudante); +estudanteRouter.delete("/:id/turma", removerDaTurma); + +async function criarEstudante(request: Request, response: Response): Promise { + const { + nome, + email, + dataDeNascimento, + turmaID + } = request.body; + + const estudante = { + nome, + email, + dataDeNascimento, + turmaID: turmaID || TURMA_ZERO_ID + }; + + try { + await criarEstudanteSchema.validate(estudante, { abortEarly: false }); + + const estudanteNova = await criarEstudanteDatabase(estudante); + + response.status(201).send(estudanteNova); + } catch (erro) { + if (erro.name === "ValidationError") { + response.status(400).send(erro.errors); + return; + } + + if (erro.code === "ER_DUP_ENTRY") { + response.status(409).send(erros.emailExiste); + return; + } + + response.status(500).send(erros.inesperado); + } +} + +async function adicionarTurma(request: Request, response: Response) +: Promise { + const { turmaID, estudanteID } = request.body; + + try { + await estudanteAdicionarTurmaSchema.validate({ + turmaID, + estudanteID + }, { abortEarly: false }); + + const adicionado = await adicionarEstudanteNaTurma(estudanteID, turmaID); + if (!adicionado) { + response.status(404).send(erros.estudanteNaoEcontrada); + return; + } + + response.send("Estudante adicionado na turma com sucesso"); + } catch (erro) { + if (erro.name === "ValidationError") { + response.status(400).send(erro.errors); + return; + } + + if (erro.code.includes("ER_NO_REFERENCED_ROW")) { + response.status(404).send(erros.turmaNaoEcontrada); + return; + } + + response.status(500).send(erros.inesperado); + } +} + +async function verTurma(request: Request, response: Response): Promise { + const { turmaID } = request.params; + if (!validarUUID(turmaID)) { + response.status(400).send(erros.turmaIDInvalido); + return; + } + + try { + const estudantes = await verEstudantesNaTurma(turmaID); + response.send({ + quantidate: estudantes.length, + estudantes + }); + } catch (erro) { + response.status(500).send(erros.inesperado); + } +} + +async function verEstudante(request: Request, response: Response): Promise { + const { id } = request.params; + if (!validarUUID(id)) { + response.status(400).send(erros.IDInvalido); + return; + } + + try { + const professor = await verEstudanteDatabase(id); + if (!professor) { + response.status(404).send(erros.estudanteNaoEcontrada); + return; + } + + response.send(professor); + } catch (erro) { + response.status(500).send(erros.inesperado); + } +} + +async function removerDaTurma(request: Request, response: Response): Promise { + const { id } = request.params; + if (!validarUUID(id)) { + response.status(400).send(erros.IDInvalido); + return; + } + + try { + const professor = await removerEstudanteDaTurma(id); + if (!professor) { + response.status(404).send(erros.estudanteNaoEcontrada); + return; + } + + response.send("Estudante removido da turma"); + } catch (erro) { + response.status(500).send(erros.inesperado); + } +} + +async function removerEstudante(request: Request, response: Response) +: Promise { + const { id } = request.params; + if (!validarUUID(id)) { + response.status(400).send(erros.IDInvalido); + return; + } + + try { + const professor = await removerEstudanteDatabase(id); + if (!professor) { + response.status(404).send(erros.estudanteNaoEcontrada); + return; + } + + response.send("Estudante removido"); + } catch (erro) { + response.status(500).send(erros.inesperado); + } +} + diff --git a/semana17/LabenuSystem/src/handlers/index.ts b/semana17/LabenuSystem/src/handlers/index.ts new file mode 100644 index 0000000..7a3427c --- /dev/null +++ b/semana17/LabenuSystem/src/handlers/index.ts @@ -0,0 +1,2 @@ +export * from "./turma"; +export * from "./estudante"; diff --git a/semana17/LabenuSystem/src/handlers/professor/index.ts b/semana17/LabenuSystem/src/handlers/professor/index.ts new file mode 100644 index 0000000..302da8f --- /dev/null +++ b/semana17/LabenuSystem/src/handlers/professor/index.ts @@ -0,0 +1,176 @@ +import express, { Request, Response } from "express"; +import { criarProfessorSchema, professorAdicionarTurmaSchema } from "../../validate"; +import { + adicionarProfessorNaTurma, + criarProfessor as criarProfessorDatabase, + verProfessoresNaTurma, + verProfessor as verProfessorDatabase, + removerProfessorDaTurma, + removerProfessor as removerProfessorDatabase +} from "../../database/mysql"; +import { TURMA_ZERO_ID } from "../turma"; +import { validate as validarUUID } from "uuid"; + +const erros = { + inesperado: "Aconteceu um erro inesperado", + emailExiste: "Email já existe", + professorNaoEcontrada: "Professor não econtrada", + turmaNaoEcontrada: "Turma não econtrada", + turmaIDInvalido: "O turmaID precisa ser um id válido", + IDInvalido: "O professorID precisa ser um id válido" +}; + +export const professorRouter = express.Router(); + +professorRouter.post("/", criarProfessor); +professorRouter.put("/turma", adicionarTurma); +professorRouter.get("/turma/:turmaID", verTurma); +professorRouter.get("/:id", verProfessor); +professorRouter.delete("/:id", removerProfessor); +professorRouter.delete("/:id/turma", removerDaTurma); + +async function criarProfessor(request: Request, response: Response): Promise { + const { + nome, + email, + dataDeNascimento, + turmaID + } = request.body; + + const professor = { + nome, + email, + dataDeNascimento, + turmaID: turmaID || TURMA_ZERO_ID + }; + + try { + await criarProfessorSchema.validate(professor, { abortEarly: false }); + + const professorNova = await criarProfessorDatabase(professor); + + response.status(201).send(professorNova); + } catch (erro) { + if (erro.name === "ValidationError") { + response.status(400).send(erro.errors); + return; + } + + if (erro.code === "ER_DUP_ENTRY") { + response.status(409).send(erros.emailExiste); + return; + } + + response.status(500).send(erros.inesperado); + } +} + +async function adicionarTurma(request: Request, response: Response): Promise { + const { turmaID, professorID } = request.body; + + try { + await professorAdicionarTurmaSchema.validate({ + turmaID, + professorID + }, { abortEarly: false }); + + const adicionado = await adicionarProfessorNaTurma(professorID, turmaID); + if (!adicionado) { + response.status(404).send(erros.professorNaoEcontrada); + return; + } + + response.send("Professor adicionado na turma com sucesso"); + } catch (erro) { + if (erro.name === "ValidationError") { + response.status(400).send(erro.errors); + return; + } + + if (erro.code.includes("ER_NO_REFERENCED_ROW")) { + response.status(404).send(erros.turmaNaoEcontrada); + return; + } + + response.status(500).send(erros.inesperado); + } +} + +async function verTurma(request: Request, response: Response): Promise { + const { turmaID } = request.params; + if (!validarUUID(turmaID)) { + response.status(400).send(erros.turmaIDInvalido); + return; + } + + try { + const professores = await verProfessoresNaTurma(turmaID); + response.send({ + quantidate: professores.length, + professores + }); + } catch (erro) { + response.status(500).send(erros.inesperado); + } +} + +async function verProfessor(request: Request, response: Response): Promise { + const { id } = request.params; + if (!validarUUID(id)) { + response.status(400).send(erros.IDInvalido); + return; + } + + try { + const professor = await verProfessorDatabase(id); + if (!professor) { + response.status(404).send(erros.professorNaoEcontrada); + return; + } + + response.send(professor); + } catch (erro) { + response.status(500).send(erros.inesperado); + } +} + +async function removerDaTurma(request: Request, response: Response): Promise { + const { id } = request.params; + if (!validarUUID(id)) { + response.status(400).send(erros.IDInvalido); + return; + } + + try { + const professor = await removerProfessorDaTurma(id); + if (!professor) { + response.status(404).send(erros.professorNaoEcontrada); + return; + } + + response.send("Professor removido da turma"); + } catch (erro) { + response.status(500).send(erros.inesperado); + } +} + +async function removerProfessor(request: Request, response: Response) +: Promise { + const { id } = request.params; + if (!validarUUID(id)) { + response.status(400).send(erros.IDInvalido); + return; + } + + try { + const professor = await removerProfessorDatabase(id); + if (!professor) { + response.status(404).send(erros.professorNaoEcontrada); + return; + } + + response.send("Professor removido"); + } catch (erro) { + response.status(500).send(erros.inesperado); + } +} diff --git a/semana17/LabenuSystem/src/handlers/turma/index.ts b/semana17/LabenuSystem/src/handlers/turma/index.ts new file mode 100644 index 0000000..47ab3f4 --- /dev/null +++ b/semana17/LabenuSystem/src/handlers/turma/index.ts @@ -0,0 +1,86 @@ +import express, { Request, Response } from "express"; +import { criarTurmaSchema, mudarModuloSchema } from "../../validate"; +import { + criarTurma as criarTurmaDatabase, + mudarModulo as mudarModuloDatabase +} from "../../database/mysql"; + +const erros = { + inesperado: "Aconteceu um erro inesperado", + nomeExiste: "Nome da turma já existe", + turmaNaoEcontrada: "Turma não econtrada" +}; + +export const turmaRouter = express.Router(); + +turmaRouter.post("/", criarTurma); +turmaRouter.put("/modulo", mudarModulo); + +export const TURMA_ZERO_ID = "00000000-0000-0000-0000-000000000000"; + +const PADRAO_TIPO = "Integral"; + +async function criarTurma(request: Request, response: Response): Promise { + const { + nome, + dataInicio, + dataFinal, + modulo, + tipo + } = request.body; + + const turma = { + nome, + dataInicio, + dataFinal, + modulo, + tipo: tipo || PADRAO_TIPO + }; + + try { + await criarTurmaSchema.validate(turma, { abortEarly: false }); + + const turmaNova = await criarTurmaDatabase(turma); + + response.status(201).send(turmaNova); + } catch (erro) { + if (erro.name === "ValidationError") { + response.status(400).send(erro.errors); + return; + } + + if (erro.code === "ER_DUP_ENTRY") { + response.status(409).send(erros.nomeExiste); + return; + } + + response.status(500).send(erros.inesperado); + } +} + +async function mudarModulo(request: Request, response: Response): Promise { + const { id, modulo } = request.body; + + try { + await mudarModuloSchema.validate({ + id, + modulo + }, { abortEarly: false }); + + const turma = await mudarModuloDatabase(id, modulo); + if (!turma) { + response.status(404).send(erros.turmaNaoEcontrada); + return; + } + + response.send("Modulo atualizado com sucesso"); + } catch (erro) { + if (erro.name === "ValidationError") { + response.status(400).send(erro.errors); + return; + } + + response.status(500).send(erros.inesperado); + } +} + diff --git a/semana17/LabenuSystem/src/index.ts b/semana17/LabenuSystem/src/index.ts new file mode 100644 index 0000000..f08d162 --- /dev/null +++ b/semana17/LabenuSystem/src/index.ts @@ -0,0 +1,25 @@ +import "./env"; +import express from "express"; +import cors from "cors"; +import { estudanteRouter, turmaRouter } from "./handlers"; +import { professorRouter } from "./handlers/professor"; + +const serverPort = process.env.PORT || process.env.NODE_PORT || 3003; + +const server = express(); +server.use(express.json()); +server.use(cors()); + +server.use("/turma", turmaRouter); +server.use("/estudante", estudanteRouter); +server.use("/professor", professorRouter); + +const serverListener = server.listen(serverPort, () => { + if (serverListener) + //eslint-disable-next-line no-console + console.log(`Server is running in http://localhost:${serverPort}`); + else + //eslint-disable-next-line no-console + console.error("Failure upon starting server."); +}); + diff --git a/semana17/LabenuSystem/src/queries.sql b/semana17/LabenuSystem/src/queries.sql new file mode 100644 index 0000000..338fc26 --- /dev/null +++ b/semana17/LabenuSystem/src/queries.sql @@ -0,0 +1,84 @@ + +SHOW DATABASES; + +SHOW TABLES; + +SELECT * FROM information_schema.columns WHERE TABLE_NAME LIKE "LabenuSystem%"; + +SELECT TABLE_NAME, COLUMN_NAME, COLUMN_DEFAULT, COLUMN_TYPE, COLUMN_KEY FROM information_schema.columns WHERE TABLE_NAME LIKE "LabenuSystem%"; + +SELECT * FROM LabenuSystem_Turma; + +SELECT * FROM LabenuSystem_Estudante; + +SELECT * FROM LabenuSystem_Professor; + +INSERT INTO LabenuSystem_Turma (id, nome, data_inicio, data_final, modulo, tipo) +VALUES ("00000000-0000-0000-0000-000000000000", "Turma Zero", "2017-01-01", "9999-12-31", "0", "Integral"); + +DROP TABLE LabenuSystem_Professor_Especialidade; + +DROP TABLE LabenuSystem_Estudante_Passatempo; + +DROP TABLE LabenuSystem_Especialidade; + +DROP TABLE LabenuSystem_Passatempo; + +DROP TABLE LabenuSystem_Professor; + +DROP TABLE LabenuSystem_Estudante; + +DROP TABLE LabenuSystem_Turma; + +CREATE TABLE LabenuSystem_Turma ( + id CHAR(36) PRIMARY KEY NOT NULL, + nome VARCHAR(255) UNIQUE NOT NULL, + data_inicio DATE NOT NULL, + data_final DATE NOT NULL, + modulo TINYINT UNSIGNED NOT NULL DEFAULT 0, + tipo ENUM("Integral", "Noturno") NOT NULL +); + +CREATE TABLE LabenuSystem_Estudante ( + id CHAR(36) PRIMARY KEY NOT NULL, + nome VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + data_de_nascimento DATE NOT NULL, + turma_id CHAR(36) NOT NULL, + FOREIGN KEY (turma_id) REFERENCES LabenuSystem_Turma(id) +); + +CREATE TABLE LabenuSystem_Passatempo ( + id CHAR(36) PRIMARY KEY NOT NULL, + nome VARCHAR(255) UNIQUE NOT NULL +); + +CREATE TABLE LabenuSystem_Estudante_Passatempo ( + estudante_id CHAR(36) NOT NULL, + passatempo_id CHAR(36) NOT NULL, + FOREIGN KEY (estudante_id) REFERENCES LabenuSystem_Estudante(id), + FOREIGN KEY (passatempo_id) REFERENCES LabenuSystem_Passatempo(id) +); + +CREATE TABLE LabenuSystem_Professor ( + id CHAR(36) PRIMARY KEY NOT NULL, + nome VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + data_de_nascimento DATE NOT NULL, + turma_id CHAR(36) NOT NULL, + FOREIGN KEY (turma_id) REFERENCES LabenuSystem_Turma(id) +); + +CREATE TABLE LabenuSystem_Especialidade ( + id CHAR(36) PRIMARY KEY NOT NULL, + nome ENUM("React", "Redux", "CSS", "Testes", "Typescript", "POO", "Backend") + NOT NULL +); + +CREATE TABLE LabenuSystem_Professor_Especialidade ( + especialidade_id CHAR(36) NOT NULL, + professor_id CHAR(36) NOT NULL, + FOREIGN KEY (especialidade_id) REFERENCES LabenuSystem_Especialidade(id), + FOREIGN KEY (professor_id) REFERENCES LabenuSystem_Professor(id) +); + diff --git a/semana17/LabenuSystem/src/validate/index.ts b/semana17/LabenuSystem/src/validate/index.ts new file mode 100644 index 0000000..b29099b --- /dev/null +++ b/semana17/LabenuSystem/src/validate/index.ts @@ -0,0 +1,71 @@ +import * as yup from "yup"; +import { Estudante, Professor, Turma } from "../@types"; + +const MIN_CARACTER = 6; +const MAX_CARACTER = 255; + +const MIN_MODULO = 0; +const MAX_MODULO = 7; + +//@ts-expect-error issue an yup https://github.com/jquense/yup/issues/1183 +export const turmaSchema: yup.SchemaOf = yup.object({ + id: yup.string().uuid().defined(), + nome: yup.string() + .min(MIN_CARACTER) + .max(MAX_CARACTER) + .defined(), + modulo: yup.number() + .min(MIN_MODULO) + .max(MAX_MODULO) + .defined(), + dataFinal: yup.date().defined(), + dataInicio: yup.date().defined(), + tipo: yup.mixed().oneOf([ "Integral", "Noturno" ]).defined() +}); + +export const criarTurmaSchema: yup.SchemaOf> + = turmaSchema.omit([ "id" ]); + +export const mudarModuloSchema: yup.SchemaOf> + = turmaSchema.pick([ "id", "modulo" ]); + +//@ts-expect-error issue an yup https://github.com/jquense/yup/issues/1183 +export const estudanteSchema: yup.SchemaOf = yup.object({ + id: yup.string().uuid().defined(), + nome: yup.string() + .min(MIN_CARACTER) + .max(MAX_CARACTER) + .defined(), + email: yup.string().email().defined(), + turmaID: yup.string().uuid().defined(), + dataDeNascimento: yup.date().defined() +}); + +export const criarEstudanteSchema: yup.SchemaOf> + = estudanteSchema.omit([ "id" ]).shape({ turmaID: yup.string().uuid() }); + +export const professorAdicionarTurmaSchema = yup.object({ + turmaID: yup.string().uuid().defined(), + professorID: yup.string().uuid().defined() +}); + +//@ts-expect-error issue an yup https://github.com/jquense/yup/issues/1183 +export const professorSchema: yup.SchemaOf = yup.object({ + id: yup.string().uuid().defined(), + nome: yup.string() + .min(MIN_CARACTER) + .max(MAX_CARACTER) + .defined(), + email: yup.string().email().defined(), + turmaID: yup.string().uuid().defined(), + dataDeNascimento: yup.date().defined() +}); + +export const criarProfessorSchema: yup.SchemaOf> + = professorSchema.omit([ "id" ]).shape({ turmaID: yup.string().uuid() }); + +export const estudanteAdicionarTurmaSchema = yup.object({ + turmaID: yup.string().uuid().defined(), + estudanteID: yup.string().uuid().defined() +}); + diff --git a/semana17/LabenuSystem/tsconfig.json b/semana17/LabenuSystem/tsconfig.json new file mode 100644 index 0000000..cd88c2c --- /dev/null +++ b/semana17/LabenuSystem/tsconfig.json @@ -0,0 +1,73 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./build", /* Redirect output structure to the directory. */ + "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + "noUnusedLocals": true, /* Report errors on unused locals. */ + "noUnusedParameters": true, /* Report errors on unused parameters. */ + "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ + // "nPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + "typeRoots": ["./nodemodules/@types", "./src/@types"], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + "resolveJsonModule": true + } +}