Asignatura: Desarrollo de sistemas informáticos
Nombres:
Leonardo Alfonso Cruz Rodríguez
Eduardo González Pérez
Jose Orlando Nina Orellana
Correos:
alu0101233093@ull.edu.es
alu0101319001@ull.edu.es
alu0101322308@ull.edu.es
GitHub Page: Enlace
- Creación del directorio de trabajo y tareas previas
- Debugger TypeScript en VSC
- Documentación con TypeDoc
- Modelos de datos
- Implementación de routers
- Creacion de clúster en MongoDB Atlas
- Creación del servidor y conexión al clúster de MongoDB Atlas
- Peticiones con la extensión Thunder Client
Antes de empezar el proyecto es necesario instalar diversos paquetes para tener una estructura de directorios adecuada. Para ello el primer paso es crear el directorio principal:
$mkdir P12
$cd P12/
Una vez dentro, ejecutaremos los siguientes comandos:
$npm init --yes
$npm install -D eslint eslint-config-google @typescript-eslint/eslint-plugin @typescript-eslint/parser
$tsc --init
$mkdir src
$mkdir tests
$touch README.md
Tras ejecutarlos habremos inicializado el Node Project Manager con la herramienta Eslint y el compilador de TypeScript. Además de crear los directorios donde estará almacenado el código y el fichero README.md
.
El siguiente paso es configurar el fichero tsconfig.json
descomentando las siguientes lineas dentro del fichero:
rootDirs
se debe indicar el directoriosrc
para almacenar el código principal, la carpetatests
será para almacenar las pruebas a la hora de programar en TDD.
"rootDirs": ["./src","./tests"]
declaration
se requerirá para usar el debugger.
"declaration": true
sourceMap
se necesita cuando se exportan funciones.
"sourceMap": true
outDir
para almacenar los archivos compilados en un directorio concreto.
"outDir": "./dist"
Por último, faltaría iniciar el directorio git. Pero antes, crearemos el fichero .gitignore
para evitar que git tenga en seguimiento lo que introduzcamos en dicho archivo.
$touch .gitignore
$cat .gitignore
node_modules/
dist/
package-lock.json
.vscode/
Ahora si, y para finalizar, iniciaremos el repositorio git y añadiremos el remoto:
$git init
$git remote add origin git@github.com:ULL-ESIT-INF-DSI-2122/ull-esit-inf-dsi-21-22-prct11-async-sockets-alu0101233093.git
Para utilizar el debugger en nuestro proyecto pincharemos en el icono de la barra situada a la izquierda:
A continuación pinchamos en cree un archivo launch.json
y se abrirá el siguiente menu desplegable:
Seleccionaremos Node.js
y se abrirá el fichero launch.json
. En él solo habrá que cambiar la dirección de outFiles
, quedaría de la siguiente manera:
{
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/src/ejemplo.ts",
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
]
}
]
}
Para debuggear un archivo, debe compilarse previamente y darle al botón verde en la parte superior.
El primer paso para realizar la documentación con la herramienta TypeDoc
sería instalar la librería correspondiente:
$npm install -D typedoc
A continuación se debe crear el archivo typedoc.json
para escribir la configuración con los parámetros necesarios, el contenido del fichero quedaría como se ve a continuación:
{
"entryPoints": [
"./src/ejemplo-1",
"./src/ejemplo-2"
],
"out": "./docs"
}
Cabe destacar que en el parámetro entryPoints
deben ir los ficheros que se van a documentar uno por uno.
El paso siguiente sería escribir la documentación en nuestro código. Para ello debemos escribir /**
encima de una función y nos aparecerá lo siguiente:
Teclearemos enter y se nos generará automáticamente una plantilla por defecto para escribir la documentación a cerca de la función.
El siguiente paso sería rellenarla, por ejemplo de la siguiente manera:
/**
* Saluda al mundo un número de veces determinado
* @param veces Almacena el número de veces que se saludará
* @returns La cadena con los saludos concatenados
*/
function hello(veces: number): string {
let hi = "";
for(let i = 0; i < veces; i++)
hi += "¡Hello world! ";
return hi;
}
Por último, debemos añadir al fichero package.json
un parámetro que nos permitirá ejecutar la documentación con el comando npm run doc
y se guardaría en ./docs
.
El fichero quedaría de la siguiente manera:
{
"name": "p4",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "mocha",
"doc": "typedoc"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/chai": "^4.3.0",
"@types/mocha": "^9.1.0",
"chai": "^4.3.6",
"mocha": "^9.2.1",
"ts-node": "^10.7.0",
"typedoc": "^0.22.13"
}
}
Primero crearemos la interfaz ArtistDocumentInterface
que hereda de Document
con los atributos:
name:
nombre del artistagenres:
géneros del artistaMonthlyListeners:
número de oyentes mensualessongs:
canciones del artista
Después crearemos el el esquema ArtistSchema
usando como argumento el tipo ArtistDocumentInterface.
-
name:
- Tipo: String
- Único: True
- Requerido: True
- Trim (Quitar espacios al final y principio de la cadena): True
- Validate: Comprueba que los caracteres pertenecen al conjunto ascii o al alfabeto del lenguaje español
-
genres:
- Tipo: String[]
- Requerido: True
- Trim (Quitar espacios al final y principio de la cadena): True
- Validate: Comprueba que los caracteres pertenecen al conjunto ascii o al alfabeto del lenguaje español
-
songs:
- Tipo: String[]
- Único: True
- Requerido: True
- Trim (Quitar espacios al final y principio de la cadena): True
- Validate: Comprueba que los caracteres pertenecen al conjunto ascii o al alfabeto del lenguaje español
-
monthlyListeners:
- Tipo: String
- Único: True
- Requerido: True
- Trim (Quitar espacios al final y principio de la cadena): True
- Validate: Comprueba que los caracteres pertenecen al conjunto ascii o al alfabeto del lenguaje español
Por último, se exporta el modelo como Artist
.
export const Artist = model('Artist', ArtistSchema);
Primero crearemos la interfaz SongDocumentInterface
que hereda de Document
con los atributos:
name:
nombre de la canciónautor:
nombre el autor de la canciónduration:
duración de la cancióngenres:
géneros de la canciónsingle:
si es single o nonumberReproductions:
número de reproducciones
Después crearemos el el esquema SongSchema
usando como argumento el tipo SongDocumentInterface.
-
name:
- Tipo: String
- Requerido: True
- Trim (Quitar espacios al final y principio de la cadena): True
- Validate: Comprueba que los caracteres pertenecen al conjunto ascii o al alfabeto del lenguaje español
-
autor:
- Tipo: String
- Requerido: True
- Trim (Quitar espacios al final y principio de la cadena): True
- Validate: Comprueba que los caracteres pertenecen al conjunto ascii o al alfabeto del lenguaje español
-
duration:
- Tipo: Number
- Requerido: True
- Valor mínimo: 0.0
- Valor por defecto: 0.0
- Validate: Comprueba que el número sea de tipo
Float
-
genres:
- Tipo: String[]
- Requerido: True
- Trim (Quitar espacios al final y principio de la cadena): True
- Validate: Comprueba que los caracteres pertenecen al conjunto ascii o al alfabeto del lenguaje español
-
single:
- Tipo: Boolean
- Requerido: True
-
numberReproductions:
- Tipo: Number
- Requerido: True
- Trim (Quitar espacios al final y principio de la cadena): True
- Valor mínimo: 0
- Valor por defecto: 0
Por último, se exporta el modelo como Song
.
export const Song = model('Song', SongSchema);;
Primero crearemos la interfaz PlaylistDocumentInterface
que hereda de Document
con los atributos:
name:
nombre de la playlistsongs:
canciones que contiene la playlistduration:
duración de la playlistgenres:
géneros de la playlist
Después crearemos el el esquema PlaylistSchema
usando como argumento el tipo PlaylistDocumentInterface.
-
name:
- Tipo: String
- Único: True
- Requerido: True
- Trim (Quitar espacios al final y principio de la cadena): True
- Validate: Comprueba que los caracteres pertenecen al conjunto ascii o al alfabeto del lenguaje español
-
songs:
- Tipo: String[]
- Único: True
- Requerido: True
- Trim (Quitar espacios al final y principio de la cadena): True
- Validate: Comprueba que los caracteres pertenecen al conjunto ascii o al alfabeto del lenguaje español
-
duration:
- Tipo: Number
- Requerido: True
- Valor mínimo: 0.0
- Valor por defecto: 0.0
- Validate: Comprueba que el número sea de tipo
Float
-
genres:
- Tipo: String[]
- Requerido: True
- Trim (Quitar espacios al final y principio de la cadena): True
- Validate: Comprueba que los caracteres pertenecen al conjunto ascii o al alfabeto del lenguaje español
Por último, se exporta el modelo como Playlist
.
export const Playlist = model('Playlist', PlaylistSchema);
Para que nuestra API Rest funcione, es indispensable implementar routers
que gestionen las peticiones.
Para cada modelo de dato, existe la siguiente lista de routers:
-
POST: Permite la creación de documentos en la base de datos. Se le pasarían los atributos por medio de un JSON en el
body
de la petición, con esa información crearíamos un documento según el modelo definido y a continuación se llamaría al métodosave()
que nos devolverá una promesa. Si todo funciona correctamente devolverá el código estado201
, en caso de error se devolvería el código de estado400
. -
GET: Existen dos variantes de peticiones GET:
-
Petición GET por id: Buscará en la base de datos utilizando la
id
del documento. Para ello, recoge la id que se pasará como parámetro, por medio del atributoreq.params.id
. Con él, se le aplica sobre el módulo de datos concreto, la funciónfindById()
, que mediante promesas, obtenemos los datos que devuelve. Si son undefined, devolverá el estádo400
, en otro caso, devolverá los datos con el estado200
. En caso de que la propia función encuentre un error en su ejecución, se devolverá el estado500
, junto con el propio error retornado. -
Petición GET por nombre: Buscará en la base de datos utilizando el atributo
name
. Este atributo lo recibiremos como una query string, por lo que primero comprobamos su existencia. En caso de existir, usaremos como filtro de búsqueda un objeto que contiene la propiedadname
, con el valor correspondiente pasado por el query string. En caso contrario, obtendremos un objeto vacío como filtro, que resultará en mostrar todos los objetos registrados en la base de datos. Esto será resultado de la funciónfind()
, a la que le pasaremos el filtro obtenido y como promesa retornará los datos obtenidos. Si son vacios, se devolverá el estado404
, en otro caso, se enviarán los datos correspondientes junto el estado200
. En caso de fallar la función, se retornará el estado500
, junto al error en cuestión.
-
-
DELETE: Existen dos variantes de la petición DELETE:
- Petición DELETE por id:
Buscará y eliminara de la base de datos el objeto correspondiente con el atributo
id
indicado. Para ello, le pasaremos dichaid
como parámetro de la búsqueda, y será utilizado como filtro de búsqueda en la funciónfindByIdAndDelete()
, que retornará como promesa los datos correspondientes. En caso de datos vacíos, se enviará el estado404
; si es correcto, se enviará los datos eliminados de la base de datos, junto al estado200
; en caso de un error en la función, se enviará el estado400
, junto a la información del error. - Petición DELETE por nombre:
Buscará y eliminará de la base de datos el objeto correspondiente con el atributo
name
indicado. Para ello, le pasaremos el atributoname
por una query string. En caso de que no se le pase dicho atributo, se retornará un estado400
, junto con un objeto con la propiedaderror
, que contiene un mensaje indicando que se debe proporcionar un nombre. Si así sucede, se realizará la funciónfindOneAndDelete()
, pasandole como un filtro un objeto con la propiedadname
y el valor correspondiente obtenido de la query string. Como promesa, se obtendrá los datos devueltos: si son vacíos, se retornará el estado404
; en otro caso, se retornará el estado200
, junto con los datos que se han eliminado. En caso de que la función de un error, se enviará el estado400
junto al error en cuestión.
- Petición DELETE por id:
Buscará y eliminara de la base de datos el objeto correspondiente con el atributo
-
PATCH: Existen dos variantes de la petición PATCH:
-
Petición PATCH por id: Buscará el objeto de la base de datos correspondiente mediante su
id
y realizará las modificaciones en el mismo en base a la información pasada en elbody
. De esta forma, se comprobará que las propiedades del objeto pasado en elbody
, corresponden con las propiedas existentes en el modelo que se está modificando. De no ser el caso, se enviará un estado400
junto a un objeto que contiene el mensaje "Update is not permitted". Si las propiedades introducidas son válidas para el modelo de datos correspondiente, se realizará la funciónfindByIdAndUpdate()
, a la que le pasamos el parámetro obtenido con laid
, que actuará como filtro; los datos a actualizar contenidos enbody
, y un objeto en el que indicamos las opcionesnew
como true, ya que deseamos obtener los datos correspondientes tras la actualización, yrunValidators
como true, que ejecutará los validadores definidos en el modelo de datos correspondiente con los nuevos datos pasados. De esta forma, como promesa, obtendremos los datos devueltos por dicha función. Si estos datos son vacios, retornaremos el estado404
; en otro caso, devolveremos el estado200
, junto con los datos correspondientes. Si está función llegase a fallar, se devolverá el estado400
, junto con el error en cuestión. -
Petición PATCH por nombre: Buscará el objeto de la base de datos correspondiente mediante su
name
y realizará las modificaciones en el mismo en base a la información pasada en elbody
. De esta forma, se comprobará que las propiedades del objeto pasado en elbody
, corresponden con las propiedas existentes en el modelo que se está modificando. De no ser el caso, se enviará un estado400
junto a un objeto que contiene el mensaje "Update is not permitted". A parte, se deberá pasar el nombre a buscar como una query string; en caso contrario, se retornará el estado400
, junto al objeto con propiedaderror
con el mensaje pertinente. Si las propiedades introducidas son válidas para el modelo de datos correspondiente, se realizará la funciónfindByIdAndUpdate()
, a la que le pasamos el parámetro obtenido con laname
, que actuará como filtro; los datos a actualizar contenidos enbody
, y un objeto en el que indicamos las opcionesnew
como true, ya que deseamos obtener los datos correspondientes tras la actualización, yrunValidators
como true, que ejecutará los validadores definidos en el modelo de datos correspondiente con los nuevos datos pasados. De esta forma, como promesa, obtendremos los datos devueltos por dicha función. Si estos datos son vacios, retornaremos el estado404
; en otro caso, devolveremos el estado200
, junto con los datos correspondientes. Si está función llegase a fallar, se devolverá el estado400
, junto con el error en cuestión.
-
-
DEFAULT: En caso de introducir una ruta diferente a la esperada, se activará este router, que devolverá como respuesta el estado
501
.
Primero nos registraremos en la página de MongDB Atlas con el correo institucional.
A continuación crearemos un clúster. Escogeremos un clúster compartido ya que es gratuito. Dejamos todas la opciones por defecto.
Haremos click en Network Access que es una lista de IPs desde la cuáles se puede acceder al clúster. Pulsaremos en Add IP Address y seleccionaremos Allow Access from anywhere, de este modo se podrá acceder al clúster desde cualquier IP.
Luego de esto nos dirigiremos Database Access que nos permite filtrar los usuarios, sus permisos y tipos de autenticación a los clústers. Pulsaremos en Add New Database User, ya dentro seleccionaremos como modo de autenticación Password, crearemos un nuevo usuario llamado admin y crearemos una contraseña para ese usuario.
Le podemos asignar roles o especificar privilegios, pero en nuestro caso solo le asignaremos el role Atlas admin. Para terminar pulsaremos en Add User.
Una vez que vayamos realizando peticiones y adiciones a la base de datos, finalmente, podremos visualizar en la sección de Collections, el resultado de dichas peticiones y el almacenamiento permanente de nuestros datos:
Antes de todo crearemos el fichero .env
que contendrá las variables de entorno MONGODB_URL
, que es la URL para conectarse al
clúster de MongoDB Atlas, y PORT
, el puerto 8000.
Declararemos la variable dbURI
que puede ser o la variable de entorno MONGODB_URL
o una base de datos local llamada music-app
.
Nos intentaremos conectar al clúster de MongoDB Atlas usando connect
pasandole como parámetros la variable dbURI
y una serie de opciones. Esto devuelve una promesa, que en caso de éxito mostraremos por pantalla un mensaje indicando que se ha conectado con el servidor y en caso de error mostraremos un mensajes indicando que no se ha podido conectar con el servidor.
import { connect } from 'mongoose';
require("dotenv").config();
const dbURI = process.env.MONGODB_URL || "mongodb://127.0.0.1:27017/music-app";
connect(dbURI, {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true,
useFindAndModify: false,
}).then(() => {
console.log('Connection to MongoDB server established');
}).catch(() => {
console.log('Unable to connect to MongoDB server');
});
Primero que nada importar el fichero ./db/mongoose creado anteriormente para poder realizar la conexión con el clúster. Crearemos el servidor app
con exprress e indicaremos que usaremos JSON y todos los routers de la carpeta routers (SongRouter, ArtistRouter, PlaylistRouter, DefaultRouter). Crearemos una variable port
que será la variable de entorno PORT
o 3000. Por último escucharemos con listen
en el puerto indicado.
import * as express from 'express';
import './db/mongoose';
import { SongRouter } from './routers/song';
import { ArtistRouter } from './routers/artist';
import { PlaylistRouter } from './routers/playlist';
import { DefaultRouter } from './routers/default';
const app = express.default();
app.use(express.json());
app.use(SongRouter);
app.use(ArtistRouter);
app.use(PlaylistRouter);
app.use(DefaultRouter);
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log('Server has started at port ', port);
});
Tipo: GET
URL: https://grupo-f-music-app.herokuapp.com/
Resultado:
URL: https://grupo-f-music-app.herokuapp.com/artist
Body:
{
"name": "Frank Sinatra",
"genres": [
"Jazz"
],
"monthlyListeners": 10938478,
"songs": [
"Fly Me To The Moon",
"That's Life",
"My Way",
"Theme From New York, New York"
]
}
{
"genres": [
"Jazz"
],
"songs": [
"Fly Me To The Moon",
"That's Life",
"My Way",
"Theme From New York, New York"
],
"monthlyListeners": 10938478,
"_id": "628a4d3dd8426d0016a16189",
"name": "Frank Sinatra",
"__v": 0
}
URL: https://grupo-f-music-app.herokuapp.com/song?name=Lazy song
Resultado:
[
{
"duration": 3.09,
"genres": [
"Pop"
],
"numberReproductions": 579789618,
"_id": "628920dc8f4b200016efdfa8",
"name": "Lazy song",
"author": "Bruno Mars",
"single": true,
"__v": 0
}
]
URL: https://grupo-f-music-app.herokuapp.com/artist?name=David Bis
Resultado:
URL: https://grupo-f-music-app.herokuapp.com/playlist/628927043731ab00162b462q
Resultado:
{
"stringValue": "\"628927043731ab00162b462q\"",
"valueType": "string",
"kind": "ObjectId",
"value": "628927043731ab00162b462q",
"path": "_id",
"reason": {},
"name": "CastError",
"message": "Cast to ObjectId failed for value \"628927043731ab00162b462q\" (type string) at path \"_id\" for model \"Playlist\""
}
URL: https://grupo-f-music-app.herokuapp.com/artist?name=Frank Sinatra
Body:
{
"name": "Frank Sinatra",
"genres": [
"Jazz"
],
"monthlyListeners": 10938578,
"songs": [
"Fly Me To The Moon",
"That's Life",
"My Way",
"Theme From New York, New York"
]
}
{
"genres": [
"Jazz"
],
"songs": [
"Fly Me To The Moon",
"That's Life",
"My Way",
"Theme From New York, New York"
],
"monthlyListeners": 10938578,
"_id": "628a4d3dd8426d0016a16189",
"name": "Frank Sinatra",
"__v": 0
}
URL: https://grupo-f-music-app.herokuapp.com/artist?name=Frank Sinatra
Body:
{
"error": "error",
"name": "Frank Sinatra",
"genres": [
"Jazz"
],
"monthlyListeners": 10938478,
"songs": [
"Fly Me To The Moon",
"That's Life",
"My Way",
"Theme From New York, New York"
]
}
{
"stringValue": "\"fa12\"",
"valueType": "string",
"kind": "Number",
"value": "fa12",
"path": "monthlyListeners",
"reason": {
"generatedMessage": true,
"code": "ERR_ASSERTION",
"actual": false,
"expected": true,
"operator": "=="
},
"name": "CastError",
"message": "Cast to Number failed for value \"fa12\" (type string) at path \"monthlyListeners\""
}
URL: https://grupo-f-music-app.herokuapp.com/playlist?name=Playlist N2
Resultado:
URL: https://grupo-f-music-app.herokuapp.com/artist?name=Frank Sinatra
Resultado:
{
"genres": [
"Jazz"
],
"songs": [
"Fly Me To The Moon",
"That's Life",
"My Way",
"Theme From New York, New York"
],
"monthlyListeners": 10938578,
"_id": "628a4d3dd8426d0016a16189",
"name": "Frank Sinatra",
"__v": 0
}
URL: https://grupo-f-music-app.herokuapp.com/song
Resultado:
{
"error": "A name must be provided"
}
URL: https://grupo-f-music-app.herokuapp.com/artist?name=David
Resultado: