diff --git a/docs/develop/basic/angular17.md b/docs/develop/basic/angular17.md index 7c0b59e..ea61d74 100644 --- a/docs/develop/basic/angular17.md +++ b/docs/develop/basic/angular17.md @@ -14,7 +14,22 @@ Si abrimos el proyecto con el IDE que tengamos (Visual Studio Code en el caso de * `app.html` → contiene la plantilla inicial del proyecto escrita en HTML. * `app.scss` → contiene los estilos CSS privados de la plantilla inicial. -Vamos a modificar este código inicial para ver como funciona. Abrimos el fichero `app.component.ts` y modificamos la línea donde se asigna un valor a la variable `title`. +!!! info + Comprueba el fichero `tsconfig.json` del proyecto, puede que estés usando el modo `strict` o tengas `strictNullChecks` a true. + Con estas propiedades activadas se obliga al compilador a tratar los tipos `null` y `undefined` de manera más estricta. + + **Comportamiento sin strictNullChecks (false):** + - `null` y `undefined` se consideran valores válidos para cualquier tipo + - Puedes asignar `null` o `undefined` a variables de cualquier tipo sin errores + - Mayor flexibilidad pero menos seguridad de tipos + + **Comportamiento con strictNullChecks (true):** + - Solo los tipos `null` y `undefined` pueden contener esos valores explícitamente + - Debes usar tipos union para permitir nulidad: `string | null`, `number | undefined` + - El compilador previene acceso a propiedades/métodos en valores potencialmente nulos + - Mayor seguridad de tipos y detección de errores en tiempo de compilación + +Vamos a modificar el código inicial para ver como funciona. Abrimos el fichero `app.component.ts` y modificamos la línea donde se asigna un valor a la variable `title`. === "app.component.ts" ``` TypeScript @@ -265,7 +280,7 @@ Ahora vamos a construir la pantalla. Para manejar la información del listado, n === "category.ts" ``` Typescript - export class Category { + export interface Category { id: number; name: string; } @@ -685,7 +700,14 @@ Ahora vamos a darle forma al formulario de editar y crear. Para ello vamos al ht } onSave() { - const category: Category = { id: this.id(), name: this.name() }; + const id = this.id(); + const name = this.name(); + + if(!name) { + return; + } + + const category = { id, name } as Category; this.categoryService.saveCategory(category).subscribe(() => { this.dialogRef.close(true); }); @@ -700,6 +722,13 @@ Ahora vamos a darle forma al formulario de editar y crear. Para ello vamos al ht Si te fijas en el código TypeScript, hemos añadido en el método `onSave` una llamada al servicio de `CategoryService` que aunque no realice ninguna operación de momento, por lo menos lo dejamos preparado para conectar con el servidor. +!!! Cast de tipo para resolver incompatibilidad de tipos + Se utiliza "as Category" porque el tipo obtenido del servidor siempre llegará con id, + sin embargo al crear una nueva categoría no dispondremos de valor hasta que lo genere el backend + + ⚠️ Nota: Usar con cuidado, ya que bypasea la validación de tipos. + Considera validar los datos en tiempo de ejecución si es crítico (cómo se hace con la propiedad `name`) + Además, como siempre, al utilizar componentes `matInput`, `matForm`, `matError` hay que añadir los módulos correspondientes como dependencias en el atributo imports. Ahora podemos navegar y abrir el cuadro de diálogo mediante el botón `Nueva categoría` para ver como queda nuestro formulario. @@ -839,7 +868,14 @@ Y los Dialog: } onSave() { - const category: Category = { id: this.id(), name: this.name() }; + const id = this.id(); + const name = this.name(); + + if(!name) { + return; + } + + const category = { id, name } as Category; this.categoryService.saveCategory(category).subscribe(() => { this.dialogRef.close(true); }); diff --git a/docs/develop/filtered/angular17.md b/docs/develop/filtered/angular17.md index 3ae9c6b..734bef9 100644 --- a/docs/develop/filtered/angular17.md +++ b/docs/develop/filtered/angular17.md @@ -27,7 +27,7 @@ Lo primero que vamos a hacer es crear el modelo en `game/model/Game.ts` con toda import { Author } from "../../author/model/Author"; import { Category } from "../../category/model/Category"; - export class Game { + export interface Game { id: number; title: string; age: number; @@ -588,6 +588,7 @@ Ahora sí que tenemos todo listo para implementar el cuadro de diálogo para dar import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; + import { validateFields } from '../../core/helpers/validation.helper'; @Component({ selector: 'app-game-edit', @@ -632,13 +633,26 @@ Ahora sí que tenemos todo listo para implementar el cuadro de diálogo para dar } onSave() { - const game: Game = { - id: this.id(), - title: this.title(), - age: this.age(), - category: this.categories().find(c => c.id === this.categoryId()) ?? null, - author: this.authors().find(a => a.id === this.authorId()) ?? null, - }; + const id = this.id(); + const title = this.title(); + const age = this.age(); + const categoryId = this.categoryId(); + const authorId = this.authorId(); + + const requiredFields = ["title", "age", "categoryId", "authorId"] as const + const data = { title, age, categoryId, authorId } + + if (!validateFields(data, requiredFields)) { + return; + } + + const game = { + id, + title, + age, + category: this.categories().find(c => c.id === categoryId) ?? null, + author: this.authors().find(a => a.id === authorId) ?? null, + } as Game; this.gameService.saveGame(game).subscribe(() => { this.dialogRef.close(true); }); @@ -648,7 +662,6 @@ Ahora sí que tenemos todo listo para implementar el cuadro de diálogo para dar this.dialogRef.close(); } } - ``` Como puedes ver, para rellenar los componentes seleccionables de dropdown, hemos realizado una consulta al servicio para recuperar todos los autores y categorías, y en la respuesta de cada uno de ellos, hemos buscado en los resultados cuál es el que coincide con el ID enviado desde el listado, y ese es el que hemos fijado en el objeto `Game`. diff --git a/docs/develop/paginated/angular17.md b/docs/develop/paginated/angular17.md index b557a42..a089edf 100644 --- a/docs/develop/paginated/angular17.md +++ b/docs/develop/paginated/angular17.md @@ -25,7 +25,7 @@ Creamos el modelo en `author/model/Author.ts` con las propiedades necesarias par === "Author.ts" ``` TypeScript - export class Author { + export interface Author { id: number; name: string; nationality: string; @@ -85,7 +85,7 @@ Así que necesitamos poder enviar y recuperar esa información desde Angular, no === "SortPage.ts" ``` TypeScript - export class SortPage { + export interface SortPage { property: string; direction: string; } @@ -94,7 +94,7 @@ Así que necesitamos poder enviar y recuperar esa información desde Angular, no ``` TypeScript import { SortPage } from './SortPage'; - export class Pageable { + export interface Pageable { pageNumber: number; pageSize: number; sort: SortPage[]; @@ -104,7 +104,7 @@ Así que necesitamos poder enviar y recuperar esa información desde Angular, no ``` TypeScript import { Pageable } from "src/app/core/model/page/Pageable"; - export class PaginatedData { + export interface PaginatedData { content: TData[]; pageable: Pageable; totalElements: number; @@ -441,6 +441,7 @@ El último paso es definir la pantalla de diálogo que realizará el alta y modi import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; + import { validateFields } from '../../core/helpers/validation.helper'; @Component({ selector: 'app-author-edit', @@ -469,7 +470,23 @@ El último paso es definir la pantalla de diálogo que realizará el alta y modi } onSave() { - this.authorService.saveAuthor(this.author).subscribe(() => { + const id = this.id(); + const name = this.name(); + const nationality = this.nationality(); + + const requiredFields = ["name", "nationality"] as const + const data = { name, nationality } + + if (!validateFields(data, requiredFields)) { + return; + } + + const author = { + id, + name, + nationality, + } as Author; + this.authorService.saveAuthor(author).subscribe(() => { this.dialogRef.close(true); }); } @@ -480,6 +497,9 @@ El último paso es definir la pantalla de diálogo que realizará el alta y modi } ``` +!!! info + Podemos usar el helper `validateFields` cuando haya varios campos para validar que sean requeridos + Que debería quedar algo así: ![step4-angular2](../../assets/images/step4-angular2.png) diff --git a/docs/install/angular.md b/docs/install/angular.md index f329f5f..b2b077c 100644 --- a/docs/install/angular.md +++ b/docs/install/angular.md @@ -65,14 +65,21 @@ Con esto, scoop ya nos instalará todo lo necesario. ### Angular CLI -El siguiente pasó será instalar una capa de gestión por encima de Nodejs que nos ayudará en concreto con la funcionalidad de Angular. Si no indicamos nada se instalará la última versión del CLI, pero si queremos podemos elegir una versión en concreto añadiendo '@' y el número de la versión correspondiente. Para poder instalarlo, tan solo hay que abrir una consola de msdos y ejecutar el comando y Nodejs ya hará el resto: +El siguiente pasó será instalar una capa de gestión por encima de Nodejs que nos ayudará en concreto con la funcionalidad de Angular. Para poder instalarlo, tan solo hay que abrir una consola de msdos y ejecutar el comando y Nodejs ya hará el resto: ``` npm install -g @angular/cli +``` +Si en el comando no indicamos la versión se instalará la última del CLI, pero si queremos podemos elegir una versión en concreto añadiendo '@' y el número de la versión correspondiente. +``` npm install -g @angular/cli@16 ``` +!!! MUY IMPORTANTE + Si vas a realizar el tutorial de `Angular 17+` deberás instalar la última versión, en el caso de que por alguna razón optes por el tutorial `Angular` deberás instalar la versión 16. + + Y con esto ya tendremos todo instalado, listo para empezar a crear los proyectos. !!! note "Aviso para navegantes corporativos" diff --git a/site/develop/basic/angular17/index.html b/site/develop/basic/angular17/index.html index acbd44b..511afe0 100644 --- a/site/develop/basic/angular17/index.html +++ b/site/develop/basic/angular17/index.html @@ -2578,7 +2578,21 @@

Primeros pasos

  • app.html → contiene la plantilla inicial del proyecto escrita en HTML.
  • app.scss → contiene los estilos CSS privados de la plantilla inicial.
  • -

    Vamos a modificar este código inicial para ver como funciona. Abrimos el fichero app.component.ts y modificamos la línea donde se asigna un valor a la variable title.

    +
    +

    Info

    +

    Comprueba el fichero tsconfig.json del proyecto, puede que estés usando el modo strict o tengas strictNullChecks a true. +Con estas propiedades activadas se obliga al compilador a tratar los tipos null y undefined de manera más estricta.

    +

    Comportamiento sin strictNullChecks (false): +- null y undefined se consideran valores válidos para cualquier tipo +- Puedes asignar null o undefined a variables de cualquier tipo sin errores +- Mayor flexibilidad pero menos seguridad de tipos

    +

    Comportamiento con strictNullChecks (true): +- Solo los tipos null y undefined pueden contener esos valores explícitamente +- Debes usar tipos union para permitir nulidad: string | null, number | undefined +- El compilador previene acceso a propiedades/métodos en valores potencialmente nulos +- Mayor seguridad de tipos y detección de errores en tiempo de compilación

    +
    +

    Vamos a modificar el código inicial para ver como funciona. Abrimos el fichero app.component.ts y modificamos la línea donde se asigna un valor a la variable title.

    @@ -2821,7 +2835,7 @@

    Código de la pantalla

    -
    export class Category {
    +
    export interface Category {
         id: number;
         name: string;
     }
    @@ -3232,7 +3246,14 @@ 
         }
     
         onSave() {
    -        const category: Category = { id: this.id(), name: this.name() };
    +        const id = this.id();
    +        const name = this.name();
    +
    +        if(!name) {
    +            return;
    +        }
    +
    +        const category = { id, name } as Category;
             this.categoryService.saveCategory(category).subscribe(() => {
                 this.dialogRef.close(true);
             });
    @@ -3247,6 +3268,13 @@ 
     

    Si te fijas en el código TypeScript, hemos añadido en el método onSave una llamada al servicio de CategoryService que aunque no realice ninguna operación de momento, por lo menos lo dejamos preparado para conectar con el servidor.

    +
    +

    Cast

    +

    Se utiliza "as Category" porque el tipo obtenido del servidor siempre llegará con id, +sin embargo al crear una nueva categoría no dispondremos de valor hasta que lo genere el backend

    +

    ⚠️ Nota: Usar con cuidado, ya que bypasea la validación de tipos. +Considera validar los datos en tiempo de ejecución si es crítico (cómo se hace con la propiedad name)

    +

    Además, como siempre, al utilizar componentes matInput, matForm, matError hay que añadir los módulos correspondientes como dependencias en el atributo imports.

    Ahora podemos navegar y abrir el cuadro de diálogo mediante el botón Nueva categoría para ver como queda nuestro formulario.

    Utilizar el diálogo para editar

    @@ -3385,7 +3413,14 @@

    Utilizar el diálogo para editar

    } onSave() { - const category: Category = { id: this.id(), name: this.name() }; + const id = this.id(); + const name = this.name(); + + if(!name) { + return; + } + + const category = { id, name } as Category; this.categoryService.saveCategory(category).subscribe(() => { this.dialogRef.close(true); }); diff --git a/site/develop/filtered/angular17/index.html b/site/develop/filtered/angular17/index.html index 6b1ce54..50aa5b4 100644 --- a/site/develop/filtered/angular17/index.html +++ b/site/develop/filtered/angular17/index.html @@ -2273,7 +2273,7 @@

    Crear el modelo

    import { Author } from "../../author/model/Author";
     import { Category } from "../../category/model/Category";
     
    -export class Game {
    +export interface Game {
         id: number;
         title: string;
         age: number;
    @@ -2815,80 +2815,94 @@ 

    Implementar diálogo de edición

    -
    import { Component, inject, OnInit, signal } from '@angular/core';
    -import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
    -import { GameService } from '../game.service';
    -import { Game } from '../model/Game';
    -import { AuthorService } from '../../author/author.service';
    -import { Author } from '../../author/model/Author';
    -import { CategoryService } from '../../category/category.service';
    -import { Category } from '../../category/model/Category';
    -import { FormsModule, ReactiveFormsModule } from '@angular/forms';
    -import { MatButtonModule } from '@angular/material/button';
    -import { MatFormFieldModule } from '@angular/material/form-field';
    -import { MatInputModule } from '@angular/material/input';
    -import { MatSelectModule } from '@angular/material/select';
    +

    ``` TypeScript +import { Component, inject, OnInit, signal } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { GameService } from '../game.service'; +import { Game } from '../model/Game'; +import { AuthorService } from '../../author/author.service'; +import { Author } from '../../author/model/Author'; +import { CategoryService } from '../../category/category.service'; +import { Category } from '../../category/model/Category'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { validateFields } from '../../core/helpers/validation.helper';

    +

    @Component({ + selector: 'app-game-edit', + standalone: true, + imports: [FormsModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatButtonModule, MatSelectModule ], + templateUrl: './game-edit.component.html', + styleUrl: './game-edit.component.scss', +}) +export class GameEditComponent implements OnInit { + protected readonly id = signal(null); + protected readonly title = signal(null); + protected readonly age = signal(null); + protected readonly categoryId = signal(null); + protected readonly authorId = signal(null); + protected readonly categories = signal([]); + protected readonly authors = signal([]);

    +
    protected readonly dialogRef = inject(MatDialogRef<GameEditComponent>);
    +protected readonly data = inject(MAT_DIALOG_DATA);
    +protected readonly gameService = inject(GameService);
    +protected readonly categoryService = inject(CategoryService);
    +protected readonly authorService = inject(AuthorService);
    +
    +ngOnInit(): void {
    +    this.loadFormData(this.data.game ?? null);
    +}
     
    -@Component({
    -    selector: 'app-game-edit',
    -    standalone: true,
    -    imports: [FormsModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatButtonModule, MatSelectModule ],
    -    templateUrl: './game-edit.component.html',
    -    styleUrl: './game-edit.component.scss',
    -})
    -export class GameEditComponent implements OnInit {
    -    protected readonly id = signal<number | null>(null);
    -    protected readonly title = signal<string | null>(null);
    -    protected readonly age = signal<number | null>(null);
    -    protected readonly categoryId = signal<number | null>(null);
    -    protected readonly authorId = signal<number | null>(null);
    -    protected readonly categories = signal<Category[]>([]);
    -    protected readonly authors = signal<Author[]>([]);
    +loadFormData(initialData: Game | null): void {
    +    this.id.set(initialData?.id ?? null);
    +    this.title.set(initialData?.title ?? null);
    +    this.age.set(initialData?.age ?? null);
     
    -    protected readonly dialogRef = inject(MatDialogRef<GameEditComponent>);
    -    protected readonly data = inject(MAT_DIALOG_DATA);
    -    protected readonly gameService = inject(GameService);
    -    protected readonly categoryService = inject(CategoryService);
    -    protected readonly authorService = inject(AuthorService);
    +    this.categoryService.getCategories().subscribe((cats) => {
    +        this.categories.set(cats);
    +        this.categoryId.set(initialData?.category?.id ?? null);
    +    });
     
    -    ngOnInit(): void {
    -        this.loadFormData(this.data.game ?? null);
    -    }
    +    this.authorService.getAllAuthors().subscribe((auts) => {
    +        this.authors.set(auts);
    +        this.authorId.set(initialData?.author?.id ?? null);
    +    });
    +}
     
    -    loadFormData(initialData: Game | null): void {
    -        this.id.set(initialData?.id ?? null);
    -        this.title.set(initialData?.title ?? null);
    -        this.age.set(initialData?.age ?? null);
    +onSave() {
    +    const id = this.id();
    +    const title = this.title(); 
    +    const age = this.age(); 
    +    const categoryId = this.categoryId(); 
    +    const authorId = this.authorId();
     
    -        this.categoryService.getCategories().subscribe((cats) => {
    -            this.categories.set(cats);
    -            this.categoryId.set(initialData?.category?.id ?? null);
    -        });
    +    const requiredFields = ["title", "age", "categoryId", "authorId"] as const
    +    const data = { title, age, categoryId, authorId }
     
    -        this.authorService.getAllAuthors().subscribe((auts) => {
    -            this.authors.set(auts);
    -            this.authorId.set(initialData?.author?.id ?? null);
    -        });
    +    if (!validateFields(data, requiredFields)) {
    +        return;
         }
     
    -    onSave() {
    -        const game: Game = {
    -            id: this.id(),
    -            title: this.title(),
    -            age: this.age(),
    -            category: this.categories().find(c => c.id === this.categoryId()) ?? null,
    -            author: this.authors().find(a => a.id === this.authorId()) ?? null,
    -        };
    -        this.gameService.saveGame(game).subscribe(() => {
    -            this.dialogRef.close(true);
    -        });
    -    }
    +    const game = {
    +        id,
    +        title,
    +        age,
    +        category: this.categories().find(c => c.id === categoryId) ?? null,
    +        author: this.authors().find(a => a.id === authorId) ?? null,
    +    } as Game;
    +    this.gameService.saveGame(game).subscribe(() => {
    +        this.dialogRef.close(true);
    +    });
    +}
     
    -    onClose() {
    -        this.dialogRef.close();
    -    }
    +onClose() {
    +    this.dialogRef.close();
     }
     
    + +

    }

    diff --git a/site/develop/paginated/angular17/index.html b/site/develop/paginated/angular17/index.html index 9f02b43..596a4e7 100644 --- a/site/develop/paginated/angular17/index.html +++ b/site/develop/paginated/angular17/index.html @@ -2252,7 +2252,7 @@

    Crear el modelo

    -
    export class Author {
    +
    export interface Author {
         id: number;
         name: string;
         nationality: string;
    @@ -2315,7 +2315,7 @@ 

    Implementar servicio

    -
    export class SortPage {
    +
    export interface SortPage {
         property: string;
         direction: string;
     }
    @@ -2324,7 +2324,7 @@ 

    Implementar servicio

    import { SortPage } from './SortPage';
     
    -export class Pageable {
    +export interface Pageable {
         pageNumber: number;
         pageSize: number;
         sort: SortPage[];
    @@ -2334,7 +2334,7 @@ 

    Implementar servicio

    import { Pageable } from "src/app/core/model/page/Pageable";
     
    -export class PaginatedData <TData>{
    +export interface PaginatedData <TData>{
         content: TData[];
         pageable: Pageable;
         totalElements: number;
    @@ -2670,6 +2670,7 @@ 

    Implementar diálogo edición

    import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; +import { validateFields } from '../../core/helpers/validation.helper'; @Component({ selector: 'app-author-edit', @@ -2698,7 +2699,23 @@

    Implementar diálogo edición

    } onSave() { - this.authorService.saveAuthor(this.author).subscribe(() => { + const id = this.id(); + const name = this.name(); + const nationality = this.nationality(); + + const requiredFields = ["name", "nationality"] as const + const data = { name, nationality } + + if (!validateFields(data, requiredFields)) { + return; + } + + const author = { + id, + name, + nationality, + } as Author; + this.authorService.saveAuthor(author).subscribe(() => { this.dialogRef.close(true); }); } @@ -2711,6 +2728,10 @@

    Implementar diálogo edición

    +
    +

    Info

    +

    Podemos usar el helper validateFields cuando haya varios campos para validar que sean requeridos

    +

    Que debería quedar algo así:

    step4-angular2

    Conectar con Backend

    diff --git a/site/install/angular/index.html b/site/install/angular/index.html index 233c2bf..5892a94 100644 --- a/site/install/angular/index.html +++ b/site/install/angular/index.html @@ -2384,11 +2384,16 @@

    Nodejs

    Con esto, scoop ya nos instalará todo lo necesario.

    Angular CLI

    -

    El siguiente pasó será instalar una capa de gestión por encima de Nodejs que nos ayudará en concreto con la funcionalidad de Angular. Si no indicamos nada se instalará la última versión del CLI, pero si queremos podemos elegir una versión en concreto añadiendo '@' y el número de la versión correspondiente. Para poder instalarlo, tan solo hay que abrir una consola de msdos y ejecutar el comando y Nodejs ya hará el resto:

    +

    El siguiente pasó será instalar una capa de gestión por encima de Nodejs que nos ayudará en concreto con la funcionalidad de Angular. Para poder instalarlo, tan solo hay que abrir una consola de msdos y ejecutar el comando y Nodejs ya hará el resto:

    npm install -g @angular/cli
    -
    -npm install -g @angular/cli@16
     
    +

    Si en el comando no indicamos la versión se instalará la última del CLI, pero si queremos podemos elegir una versión en concreto añadiendo '@' y el número de la versión correspondiente. +

    npm install -g @angular/cli@16
    +

    +
    +

    Muy

    +

    Si vas a realizar el tutorial de Angular 17+ deberás instalar la última versión, en el caso de que por alguna razón optes por el tutorial Angular deberás instalar la versión 16.

    +

    Y con esto ya tendremos todo instalado, listo para empezar a crear los proyectos.

    Aviso para navegantes corporativos

    diff --git a/site/search/search_index.json b/site/search/search_index.json index 3cd3d5c..4143789 100644 --- a/site/search/search_index.json +++ b/site/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["es"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Bienvenido!","text":"

    Si est\u00e1s leyendo esto es porque tienes mucha fuerza de voluntad y unas enormes ganas de aprender a desarrollar con el stack tecnol\u00f3gico de CCA (Java Spring Boot, Nodejs, Angular, React, Vue) o porque te han mandando hacer este tutorial en tu etapa de formaci\u00f3n. En cualquier caso, te agradecemos el esfuerzo que est\u00e1s haciendo y te deseamos suerte .

    Por favor, si detectas que hay algo incorrecto en el tutorial, que no funciona o que est\u00e1 mal escrito, contacta con nosotros para que podamos solventarlo para futuras lecturas. Escr\u00edbenos un issue aqu\u00ed.

    "},{"location":"#que-vamos-a-hacer","title":"\u00bfQue vamos a hacer?","text":"

    Durante este tutorial, vamos a crear una aplicaci\u00f3n web paso a paso con Spring Boot o Nodejs para la parte servidora y con Angular o React para la parte frontal. Intentar\u00e9 comentar todo lo m\u00e1s detallado posible, pero si echas en falta alguna explicaci\u00f3n por favor, escr\u00edbenos un issue aqu\u00ed para que podamos a\u00f1adirla.

    "},{"location":"#como-lo-vamos-a-hacer","title":"\u00bfComo lo vamos a hacer?","text":"

    En primer lugar te comentar\u00e9 brevemente las herramientas que usaremos en el tutorial y la forma de instalarlas (altamente recomendado). Luego veremos un vistazo general de lo que vamos a construir para que tengas un contexto general de la aplicaci\u00f3n. Y por \u00faltimo desarrollaremos paso a paso el backend y el frontend de la aplicaci\u00f3n.

    Durante todo el tutorial intentar\u00e9 dar unas pautas y consejos de buenas pr\u00e1cticas que todos deber\u00edamos adoptar, en la medida de lo posible, para homogeneizar el desarrollo de todos los proyectos.

    Adem\u00e1s para cada uno de los cap\u00edtulos que lo requieran, voy a desdoblar el tutorial por cada una de las tecnolog\u00edas disponibles para que vayas construyendo con la que m\u00e1s c\u00f3modo te sientas.

    As\u00ed que antes de empezar debes elegir bien con que tecnolog\u00edas vas a comenzar de las que tengo disponibles. Puedes volver a este tutorial m\u00e1s adelante por si he ido a\u00f1adiendo nuevas tecnolog\u00edas.

    Elige UNA tecnolog\u00eda de backend y UNA tecnolog\u00eda de frontend y completa el tutorial con esas dos tecnolog\u00edas. No mezcles ni hagas todas las tecnolog\u00edas a la vez ya que si no, te vas a volver loco.

    "},{"location":"#hay-pre-requisitos","title":"\u00bfHay pre-requisitos?","text":"

    No es obligado tener ning\u00fan conocimiento previo, pero es altamente recomendable que al menos conozcas lo b\u00e1sico de las tecnolog\u00edas que vamos a ver en el tutorial. Si no tienes ni idea, ni has oido hablar de las tecnolog\u00edas que has seleccionado para el tutorial, te sugiero que visites los itinerarios formativos y realices los cursos de nivel Esencial. De momento tenemos estos itinerarios:

    • \ud83d\udd35 Frontend - Angular
    • \ud83d\udd35 Frontend - React
    • \ud83d\udd35 Frontend - Vue
    • \ud83d\udfe2 Backend - SpringBoot
    • \ud83d\udfe2 Backend - Nodejs

    Una vez hayas hecho los cursos esenciales, ya puedes volver y continuar con este tutorial. Repito que no es obligado, si ya tienes conocimientos previos de las tecnolog\u00edas no es necesario que hagas los cursos. Cuando termines el tutorial, ya puedes realizar el resto de cursos de otros niveles.

    "},{"location":"#y-luego-que","title":"\u00bfY luego qu\u00e9?","text":"

    Pues al final del tutorial, expondremos unos ejercicios pr\u00e1cticos para que los resuelvas tu mismo, aplicando los conocimientos adquiridos en el tutorial. Para ver si has comprendido correctamente todo lo aqu\u00ed descrito.

    No te preocupes, no es un examen

    "},{"location":"#recomendaciones","title":"Recomendaciones","text":"

    Te recomiendo que leas todo el tutorial, que no te saltes ning\u00fan punto y si se hace referencia a los anexos, que los visites y los leas tambi\u00e9n. Si tan solo copias y pegas, no ser\u00e1s capaz de hacer el \u00faltimo ejercicio por ti mismo. Debes leer y comprender lo que se est\u00e1 haciendo.

    Adem\u00e1s, los anexos est\u00e1n ah\u00ed por algo, sirven para completar informaci\u00f3n y para que conozcas los motivos por los que estamos programando as\u00ed el tutorial. Por favor, \u00e9chales un ojo tambi\u00e9n cuando te lo indique.

    "},{"location":"#por-ultimo-no-te-olvides","title":"Por \u00faltimo, \u00a1no te olvides!","text":"

    Cuando lo tengas todo listo, por favor no te olvides de subir los proyectos a alg\u00fan repositorio Github propio y av\u00edsanos para que podamos echarle un ojo y darte sugerencias y feedback .

    "},{"location":"exercise/","title":"Ahora hazlo tu!","text":"

    Ahora vamos a ver si has comprendido bien el tutorial. Voy a poner dos ejercicios uno m\u00e1s sencillo que el otro para ver si eres capaz de llevarlos a cabo. \u00a1Vamos alla, mucha suerte!

    Nuestro amigo Ernesto Esvida ya tiene disponible su web para gestionar su cat\u00e1logo de juegos, autores y categor\u00edas, pero todav\u00eda le falta un poco m\u00e1s para poder hacer buen uso de su ludoteca. As\u00ed que nos ha pedido dos funcionalidades extra.

    Ten en cuenta

    Solo te pedimos que realices las funcionalidades que se indican en los requisitos. No es necesario que completes los anexos antes de que revisemos los resultados.

    "},{"location":"exercise/#gestion-de-clientes","title":"Gesti\u00f3n de clientes","text":""},{"location":"exercise/#requisitos","title":"Requisitos","text":"

    Por un lado necesita poder tener una base de datos de sus clientes. Para ello nos ha pedido que si podemos crearle una pantalla de CRUD sencilla, al igual que hicimos con las categor\u00edas donde \u00e9l pueda dar de alta a sus clientes.

    Nos ha pasado un esquema muy sencillo de lo que quiere, tan solo quiere guardar un listado de los nombres de sus clientes para tenerlos fichados, y nos ha hecho un par de pantallas sencillas muy similares a Categor\u00edas.

    Un listado sin filtros de ning\u00fan tipo ni paginaci\u00f3n.

    Un formulario de edici\u00f3n / alta, cuyo \u00fanico dato editable sea el nombre. Adem\u00e1s, la \u00fanica restricci\u00f3n que nos ha pedido es que NO podamos dar de alta a un cliente con el mismo nombre que otro existente. As\u00ed que deberemos comprobar el nombre, antes de guardar el cliente.

    "},{"location":"exercise/#consejos","title":"Consejos","text":"

    Para empezar te dar\u00e9 unos consejos:

    • Recuerda crear la tabla de la BBDD y sus datos
    • Intenta primero hacer el listado completo, en el orden que m\u00e1s te guste: frontend o backend.
    • Completa el listado conectando ambas capas.
    • Termina el caso de uso haciendo las funcionalidades de edici\u00f3n, nuevo y borrado. Presta atenci\u00f3n a la validaci\u00f3n a la hora de guardar un cliente, NO se puede guardar si el nombre ya existe.
    "},{"location":"exercise/#gestion-de-prestamos","title":"Gesti\u00f3n de pr\u00e9stamos","text":""},{"location":"exercise/#requisitos_1","title":"Requisitos","text":"

    Por otro lado, quiere hacer uso de su cat\u00e1logo de juegos y de sus clientes, y quiere saber que juegos ha prestado a cada cliente. Para ello nos ha pedido una p\u00e1gina bastante compleja donde se podr\u00e1 consultar diferente informaci\u00f3n y se permitir\u00e1 realizar el pr\u00e9stamo de los juegos.

    Nos ha pasado el siguiente boceto y requisitos:

    Atenci\u00f3n

    Aunque no aparezca en el boceto, como en todas las listas que hemos visto, esperamos que puedan editarse los registros. El bot\u00f3n de \"Filtrar\" tendr\u00e1 el mismo aspecto visual que el bot\u00f3n de \"Nuevo pr\u00e9stamo\".

    La pantalla tendr\u00e1 dos zonas:

    • Una zona de filtrado donde se permitir\u00e1 filtrar por:
      • T\u00edtulo del juego, que deber\u00e1 ser un combo seleccionable con los juegos del cat\u00e1logo de la Ludoteca.
      • Cliente, que deber\u00e1 ser un combo seleccionable con los clientes dados de alta en la aplicaci\u00f3n.
      • Fecha, que deber\u00e1 ser de tipo Datepicker y que permitir\u00e1 elegir una fecha de b\u00fasqueda. Al elegir un d\u00eda nos deber\u00e1 mostrar que juegos est\u00e1n prestados para dicho d\u00eda. OJO que los pr\u00e9stamos son con fecha de inicio y de fin, si elijo un d\u00eda intermedio deber\u00eda aparecer el elemento en la tabla.
    • Una zona de listado paginado que deber\u00e1 mostrar
      • El identificador del pr\u00e9stamo
      • El nombre del juego prestado
      • El nombre del cliente que lo solicit\u00f3
      • La fecha de inicio del pr\u00e9stamo
      • La fecha de fin del pr\u00e9stamo
      • Un bot\u00f3n que permite eliminar el pr\u00e9stamo

    Al pulsar el bot\u00f3n de Nuevo pr\u00e9stamo se abrir\u00e1 una pantalla donde se podr\u00e1 ingresar la siguiente informaci\u00f3n, toda ella obligatoria:

    • Identificador, inicialmente vac\u00edo y en modo lectura
    • Nombre del cliente, mediante un combo seleccionable
    • Nombre del juego, mediante un combo seleccionable
    • Fechas del pr\u00e9stamo, donde se podr\u00e1 introducir dos fechas, de inicio y fin del pr\u00e9stamo.

    Las validaciones son sencillas aunque laboriosas:

    • La fecha de fin NO podr\u00e1 ser anterior a la fecha de inicio
    • El periodo de pr\u00e9stamo m\u00e1ximo solo podr\u00e1 ser de 14 d\u00edas. Si el usuario quiere un pr\u00e9stamo para m\u00e1s de 14 d\u00edas la aplicaci\u00f3n no debe permitirlo mostrando una alerta al intentar guardar.
    • El mismo juego no puede estar prestado a dos clientes distintos en un mismo d\u00eda. OJO que los pr\u00e9stamos tienen fecha de inicio y fecha fin, el juego no puede estar prestado a m\u00e1s de un cliente para ninguno de los d\u00edas que contemplan las fechas actuales del rango.
    • Un mismo cliente no puede tener prestados m\u00e1s de 2 juegos en un mismo d\u00eda. OJO que los pr\u00e9stamos tienen fecha de inicio y fecha fin, el cliente no puede tener m\u00e1s de dos pr\u00e9stamos para ninguno de los d\u00edas que contemplan las fechas actuales del rango.
    "},{"location":"exercise/#consejos_1","title":"Consejos","text":"

    Para empezar te dar\u00e9 unos consejos:

    • Recuerda crear la tabla de la BBDD y sus datos
    • Intenta primero hacer el listado paginado sin filtros, en el orden que m\u00e1s te guste: frontend o backend. Recuerda que se trata de un listado paginado, as\u00ed que deber\u00e1s utilizar el obtejo Page.
    • Completa el listado conectando ambas capas.
    • Ahora implementa los filtros, presta atenci\u00f3n al filtro de fecha, es el m\u00e1s complejo.
    • Para la paginaci\u00f3n filtrada solo tienes que mezclar los conceptos que hemos visto en los puntos del tutorial anteriores.
      • Si hiciste el backend en Springboot recuerda revisar Baeldung por si tienes dudas sobre las queries y recuerda que las Specifications son muy \u00fatiles, pero en este caso deber\u00e1s implementar otro tipo de operaciones, no te sirve solo con la operaci\u00f3n de igualdad :, que ya vimos en el tutorial.
    • Implementa la pantalla de alta de pr\u00e9stamo, sin ninguna validaci\u00f3n.
    • Cuando ya te funcione, intenta ir a\u00f1adiendo una a una las validaciones. Algunas de ellas pueden hacerse en frontend, mientras que otras deber\u00e1n validarse en backend
    • Os recordamos que han de poder crearse y editarse pr\u00e9stamos seg\u00fan las reglas de validaci\u00f3n indicadas anteriormente. Aplican las mismas reglas para ambas operaciones.
    • El Backend ha de validar siempre, independientemente de que el Frontend ya lo haya validado. Nunca conf\u00edes de manera exclusiva en terceras partes (Frontend o en otro Backend).
    "},{"location":"exercise/#ya-has-terminado","title":"\u00bfYa has terminado?","text":"

    Si has llegado a este punto es porque ya tienes terminado el tutorial. Por favor no te olvides de subir los proyectos a alg\u00fan repositorio Github propio (puedes revisar el anexo Tutorial b\u00e1sico de Git) y av\u00edsarnos para que podamos echarle un ojo y darte sugerencias y feedback .

    "},{"location":"thanks/","title":"Agradecimientos!","text":"

    Antes de empezar quer\u00edamos dar las gracias a todos los que hab\u00e9is participado de manera directa o indirecta en la elaboraci\u00f3n de este tutorial, y a todos aquellos que lo hab\u00e9is sufrido haciendolo.

    De verdad

                                    G R A C I A S\n
    "},{"location":"thanks/#colaboradores","title":"Colaboradores","text":"

    Menci\u00f3n especial a las personas que han participado en el tutorial ya sea como testers, como promotores o como desarrolladores, por orden temporal de colaboraci\u00f3n:

    • Felipe Garcia (@fgarciafer)
    • Armen Mirzoyan (@armirzoya)
    • Carlos Aguilar (@caaguila)
    • Jhonatan Core (@corevill)
    • Carlos Navarro (@DarkWarlord)
    • Cesar Cardona (@Cazs03)
    • Marina Valls (@mvalemany)
    • Jaume Segarra (@jaumesegarra)
    • Laura Medina (@larulirea)
    • Yolanda Ubeda
    • Pablo Jimenez (@pajimene)
    "},{"location":"usecases/","title":"Contexto de la aplicaci\u00f3n","text":""},{"location":"usecases/#introduccion","title":"Introducci\u00f3n","text":"

    Nuestro amigo Ernesto Esvida es muy aficionado a los juegos de mesa y desde muy peque\u00f1o ha ido coleccionando muchos juegos. Hasta tal punto que ha decidido regentar una Ludoteca.

    Como la colecci\u00f3n de juegos era suya personal, toda la informaci\u00f3n del cat\u00e1logo de juegos la ten\u00eda perfectamente clasificado en fichas de cart\u00f3n. Pero ahora que va abrir su propio negocio, necesita digitalizar esa informaci\u00f3n y hacerla m\u00e1s accesible.

    Como es un buen amigo de la infancia, hemos decidido ayudar a Ernesto y colaborar haciendo una peque\u00f1a aplicaci\u00f3n web que le sirva de cat\u00e1logo de juegos. Es m\u00e1s o menos el mismo sistema que estaba utilizando, pero esta vez en digital.

    Por cierto, la Ludoteca al final se va a llamar Ludoteca T\u00e1n.

    Info

    Las im\u00e1genes que aparecen a continuaci\u00f3n son mockups o dise\u00f1os de alambre de las pantallas que vamos a desarrollar durante el tutorial. No quiere decir que el estilo final de las pantallas deba ser as\u00ed, ni mucho menos. Es simplemente una forma sencilla de ejemplificar como debe quedar m\u00e1s o menos una pantalla.

    "},{"location":"usecases/#estructura-de-un-proyecto-web","title":"Estructura de un proyecto Web","text":"

    En todas las aplicaciones web modernas y los proyectos en los que trabajamos se pueden diferenciar, de forma general, tres grandes bloques funcionales, como se muestra en la imagen inferior.

    El funcionamiento es muy sencillo y difiere de las aplicaciones instalables que se ejecuta todo en una misma m\u00e1quina o servidor.

    • Con esta estructura, el usuario accede a la aplicaci\u00f3n mediante un navegador web instalado en su m\u00e1quina local.
    • Este navegador solicita informaci\u00f3n mediante una URL a un servidor de recursos est\u00e1ticos. Esto es lo que denominaremos un servidor frontend. Para programar servidores frontend se pueden usar muchas tecnolog\u00edas, en este tutorial lo desarrollaremos en Angular o en React. Este c\u00f3digo frontend se descarga y se ejecuta dentro del navegador, y contiene la representaci\u00f3n visual de las pantallas y ciertos comportamientos y navegaci\u00f3n entre componentes. Sin embargo, por lo general, no tiene datos ni ejecuta l\u00f3gica de negocio.
    • Para estas labores de obtener datos o ejecutar l\u00f3gica de negocio, el c\u00f3digo frontend necesita invocar endpoints de la segunda capa, el backend. Al igual que antes, el backend, puede estar desarrollado en muchas tecnolog\u00edas, en este tutorial se puede elegir entre Java-Springboot o Nodejs. Lo importante de esta capa es que es necesario exponer unos endpoints que sean invocados por la capa de frontend. T\u00edpicamente estos endpoints son operaciones API Rest que veremos m\u00e1s adelante.
    • Por \u00faltimo, el servidor backend / capa backend, necesitar\u00e1 leer y guardar datos de alg\u00fan sitio. Esto se hace utilizando la tercera capa, la capa de datos. Normalmente esta capa de datos ser\u00e1 una BBDD instalada en alg\u00fan servidor externo, aunque a veces como es el caso del tutorial de Springboot, podemos embeber el servidor en memoria de backend. Pero por norma general, esta capa es externa.

    As\u00ed pues el flujo normal de una aplicaci\u00f3n ser\u00eda el siguiente:

    • El usuario abre el navegador y solicita una web mediante una URL
    • El servidor frontend, le sirve los recursos (p\u00e1ginas web, javascript, im\u00e1genes, ...) y se cargan en el navegador
    • El navegador renderiza las p\u00e1ginas web, ejecuta los procesos javascript y realiza las navegaciones
    • Si en alg\u00fan momento se requiere invocar una operaci\u00f3n, el navegador lanzar\u00e1 una petici\u00f3n contra una URL del backend
    • El backend estar\u00e1 escuchando las peticiones y las ejecutar\u00e1 en el momento que le invoquen devulviendo un resultado al navegador
    • Si hiciera falta leer o guardar datos, el backend lo realizar\u00e1 lanzando consultas SQL contra la BBDD

    Dicho esto, por lo general necesitaremos un m\u00ednimo de dos proyectos para desarrollar una aplicaci\u00f3n:

    • Por un lado tendremos un proyecto Frontend que se ejecutar\u00e1 en un servidor web de ficheros est\u00e1ticos, tipo Apache. Este proyecto ser\u00e1 c\u00f3digo javascript, css y html, que se renderizar\u00e1 en el navegador Web y que realizar\u00e1 ciertas operaciones sencillas y validaciones en local y llamadas a nuestro servidor backend para ejecutar las operaciones de negocio.

    • Por otro lado tendremos un proyecto Backend que se ejecutar\u00e1 en un servidor de aplicaciones, tipo Tomcat o Node. Este proyecto tendr\u00e1 la l\u00f3gica de negocio de las operaciones, el acceso a los datos de la BBDD y cualquier integraci\u00f3n con servicios de terceros. La forma de exponer estas operaciones de negocio ser\u00e1 mediante endpoints de acceso, en concreto llamadas tipo REST.

    Pueden haber otros tipos de proyectos dentro de la aplicaci\u00f3n, sobretodo si est\u00e1n basados en microservicios o tienen componentes batch, pero estos proyectos no vamos a verlos en el tutorial.

    A partir de ahora, para que sea m\u00e1s sencillo acceder al tutorial, diferenciaremos las tecnolog\u00edas en el men\u00fa mediante los siguientes colores:

    • \ud83d\udd35 Frontend
    • \ud83d\udfe2 Backend

    Consejo

    Como norma cada uno de los proyectos que componen la aplicaci\u00f3n, deber\u00eda estar conectado a un repositorio de c\u00f3digo diferente para poder evolucionar y trabajar con cada uno de ellos de forma aislada sin afectar a los dem\u00e1s. As\u00ed adem\u00e1s podemos tener equipos aislados que trabajen con cada uno de los proyectos por separado.

    Info

    Durante todo el tutorial, voy a intentar separar la construcci\u00f3n del proyecto Frontend de la construcci\u00f3n del proyecto Backend. Elige una tecnolog\u00eda para cada una de las capas y utiliza siempre la misma en todos los apartados del tutorial.

    "},{"location":"usecases/#diseno-de-bd","title":"Dise\u00f1o de BD","text":"

    Para el proyecto que vamos a crear vamos a modelizar y gestionar 3 entidades: CATEGORY, AUTHOR y GAME.

    La entidad CATEGORY estar\u00e1 compuesta por los siguientes campos:

    • id (lo mismo que en GAME)
    • name

    La entidad AUTHOR estar\u00e1 compuesta por los siguientes campos:

    • id (lo mismo que en GAME)
    • name
    • nationality

    Para la entidad GAME, Ernesto nos ha comentado que la informaci\u00f3n que est\u00e1 guardando en sus fichas es la siguiente:

    • id (este dato no estaba originalmente en las fichas pero nos ser\u00e1 muy util para indexar y realizar b\u00fasquedas)
    • title
    • age
    • category
    • author

    Comenzaremos con un caso b\u00e1sico que cumpla las siguientes premisas: un juego pertenece a una categor\u00eda y ha sido creado por un \u00fanico autor.

    Modelando este contexto quedar\u00eda algo similar a esto:

    "},{"location":"usecases/#diseno-de-pantallas","title":"Dise\u00f1o de pantallas","text":"

    Deber\u00edamos construir tres pantallas de mantenimiento CRUD (Create, Read, Update, Delete) y una pantalla de Login general para activar las acciones de administrador. M\u00e1s o menos las pantallas deber\u00edan quedar as\u00ed:

    "},{"location":"usecases/#listado-de-categorias","title":"Listado de categor\u00edas","text":""},{"location":"usecases/#edicion-de-categoria","title":"Edici\u00f3n de categor\u00eda","text":""},{"location":"usecases/#listado-de-autores","title":"Listado de autores","text":""},{"location":"usecases/#edicion-de-autor","title":"Edici\u00f3n de autor","text":""},{"location":"usecases/#listado-de-juegos","title":"Listado de juegos","text":""},{"location":"usecases/#edicion-de-juego","title":"Edici\u00f3n de juego","text":""},{"location":"usecases/#diseno-funcional","title":"Dise\u00f1o funcional","text":"

    Por \u00faltimo vamos a definir un poco la funcionalidad b\u00e1sica que Ernesto necesita para iniciar su negocio.

    "},{"location":"usecases/#aspectos-generales","title":"Aspectos generales","text":"
    • El sistema tan solo tendr\u00e1 dos roles: ** usuario b\u00e1sico es el usuario an\u00f3nimo que accede a la web sin registrar. Solo tiene permisos para mostrar listados ** usuario administrador es el usuario que se registra en la aplicaci\u00f3n. Puede realizar las operaciones de alta, edici\u00f3n y borrado

    Por defecto cuando entras en la aplicaci\u00f3n tendr\u00e1s los privilegios de un usuario b\u00e1sico hasta que el usuario haga un login correcto con el usuario / password admin / admin. En ese momento pasara a ser un usuario administrador y podr\u00e1 realizar operaciones de alta, baja y modificaci\u00f3n.

    La estructura general de la aplicaci\u00f3n ser\u00e1:

    • Una cabecer\u00e1 superior que contendr\u00e1:
    • el logo y el nombre de la tienda
    • un enlace a cada uno de los CRUD del sistema
    • un bot\u00f3n de Sign in
    • Zona de trabajo, donde cargaremos las pantallas que el usuario vaya abriendo

    Al pulsar sobre la funcionalidad de Sign in aparecer\u00e1 una ventana modal que preguntar\u00e1 usuario y password. Esto realizar\u00e1 una llamada al backend, donde se validar\u00e1 si el usuario es correcto.

    • En caso de ser correcto, devolver\u00e1 un token jwt de acceso, que el cliente web deber\u00e1 guardar en sessionStorage para futuras peticiones
    • En caso de no ser correcto, devolver\u00e1 un error de Usuario y/o password incorrectos

    Todas las operaciones del backend que permitan crear, modificar o borrar datos, deber\u00e1n estar securizadas para que no puedan ser accedidas sin haberse autenticado previamente.

    "},{"location":"usecases/#crud-de-categorias","title":"CRUD de Categor\u00edas","text":"

    Al acceder a esta pantalla se mostrar\u00e1 un listado de las categor\u00edas que tenemos en la BD. La tabla no tiene filtros, puesto que tiene muy pocos registros. Tampoco estar\u00e1 paginada.

    En la tabla debe aparecer:

    • identificador de la categor\u00eda
    • nombre de la categor\u00eda
    • bot\u00f3n de editar (solo en el caso de que el usuario tenga permisos)
    • bot\u00f3n de borrar (solo en el caso de que el usuario tenga permisos)

    Debajo de la tabla aparecer\u00e1 un bot\u00f3n para crear nuevas categor\u00edas (solo en el caso de que el usuario tenga permisos).

    Crear

    Al pulsar el bot\u00f3n de crear se deber\u00e1 abrir una ventana modal con dos inputs:

    • Identificador. Este input deber\u00e1 ser de solo lectura y deber\u00e1 aparecer vac\u00edo, sin ning\u00fan valor. Con el placeholder de Identificador
    • Nombre. Este input es obligatorio, ser\u00e1 de escritura y deber\u00e1 aparecer vac\u00edo, sin ning\u00fan valor. Con el placeholder de Nombre

    Todos los datos obligatorios se deber\u00e1n comprobar que son v\u00e1lidos antes de guardarlo en BD. Dos botones en la parte inferior de la ventana permitir\u00e1n al usuario cerrar la ventana o guardar los datos en la BD.

    Editar

    Al pulsar el icono de editar se deber\u00e1 abrir una ventana modal utilizando el mismo componente que la ventana de Crear pero con los dos campos rellenados con los datos de BD.

    Borrar

    Si el usuario pulsa el bot\u00f3n de borrar, se deber\u00e1 comprobar si esa categor\u00eda tiene alg\u00fan Juego asociado. En caso de tenerlo se le informar\u00e1 al usuario de que dicha categor\u00eda no se puede eliminar por tener asociado un juego. En caso de no estar asociada, se le preguntar\u00e1 al usuario mediante un mensaje de confirmaci\u00f3n si desea eliminar la categor\u00eda. Solo en caso de que la respuesta sea afirmativa, se lanzar\u00e1 el borrado f\u00edsico de la categor\u00eda en BD.

    "},{"location":"usecases/#crud-de-autores","title":"CRUD de Autores","text":"

    Al acceder a esta pantalla se mostrar\u00e1 un listado de los autores que tenemos en la BD. La tabla no tiene filtros pero deber\u00e1 estar paginada en servidor.

    En la tabla debe aparecer:

    • identificador del autor
    • nombre del autor
    • nacionalidad del autor
    • bot\u00f3n de editar (solo en el caso de que el usuario tenga permisos)
    • bot\u00f3n de borrar (solo en el caso de que el usuario tenga permisos)

    Debajo de la tabla aparecer\u00e1 un bot\u00f3n para crear nuevos autores (solo en el caso de que el usuario tenga permisos).

    Crear

    Al pulsar el bot\u00f3n de crear se deber\u00e1 abrir una ventana modal con tres inputs:

    • Identificador. Este input deber\u00e1 ser de solo lectura y deber\u00e1 aparecer vac\u00edo, sin ning\u00fan valor. Con el placeholder de Identificador
    • Nombre. Este input es obligatorio, ser\u00e1 de escritura y deber\u00e1 aparecer vac\u00edo, sin ning\u00fan valor. Con el placeholder de Nombre
    • Nacionalidad. Este input es obligatorio, ser\u00e1 de escritura y deber\u00e1 aparecer vac\u00edo, sin ning\u00fan valor. Con el placeholder de Nacionalidad

    Todos los datos obligatorios se deber\u00e1n comprobar que son v\u00e1lidos antes de guardarlo en BD. Dos botones en la parte inferior de la ventana permitir\u00e1n al usuario cerrar la ventana o guardar los datos en la BD.

    Editar

    Al pulsar el icono de editar se deber\u00e1 abrir una ventana modal utilizando el mismo componente que la ventana de Crear pero con los tres campos rellenados con los datos de BD.

    Borrar

    Si el usuario pulsa el bot\u00f3n de borrar, se deber\u00e1 comprobar si ese autor tiene alg\u00fan Juego asociado. En caso de tenerlo se le informar\u00e1 al usuario de que dicho autor no se puede eliminar por tener asociado un juego. En caso de no estar asociado, se le preguntar\u00e1 al usuario mediante un mensaje de confirmaci\u00f3n si desea eliminar el autor. Solo en caso de que la respuesta sea afirmativa, se lanzar\u00e1 el borrado f\u00edsico de la categor\u00eda en BD.

    "},{"location":"usecases/#crud-de-juegos","title":"CRUD de Juegos","text":"

    Al acceder a esta pantalla se mostrar\u00e1 un listado de los juegos disponibles en el cat\u00e1logo de la BD. Esta tabla debe contener filtros en la parte superior, pero no debe estar paginada.

    Se debe poder filtrar por:

    • nombre del juego. Donde el usuario podr\u00e1 poner cualquier texto y el filtrado ser\u00e1 todos aquellos juegos que contengan el texto buscado
    • categor\u00eda del juego. Donde aparecer\u00e1 un desplegable que el usuario seleccionar de entre todas las categor\u00edas de juego que existan en la BD.

    Dos botones permitir\u00e1n realizar el filtrado de juegos (lanzando una nueva consulta a BD) o limpiar los filtros seleccionados (lanzando una consulta con los filtros vac\u00edos).

    En la tabla debe aparecer a modo de fichas. No hace falta que sea exactamente igual a la maqueta, no es un requisito determinar un ancho general de ficha por lo que pueden caber 2,3 o x fichas en una misma fila, depender\u00e1 del programador. Pero todas las fichas deben tener el mismo ancho:

    • Un espacio destinado a una foto (de momento no pondremos nada en ese espacio)
    • Una columna con la siguiente informaci\u00f3n:
      • T\u00edtulo del juego, resaltado de alguna forma
      • Edad recomendada
      • Categor\u00eda del juego, mostraremos su nombre
      • Autor del juego, mostraremos su nombre
      • Nacionalidad del juego, mostraremos la nacionalidad del autor del juego

    Los juegos no se pueden eliminar, pero si se puede editar si el usuario pulsa en alguna de las fichas (solo en el caso de que el usuario tenga permisos).

    Debajo de la tabla aparecer\u00e1 un bot\u00f3n para crear nuevos juegos (solo en el caso de que el usuario tenga permisos).

    Crear

    Al pulsar el bot\u00f3n de crear se deber\u00e1 abrir una ventana modal con cinco inputs:

    • Identificador. Este input deber\u00e1 ser de solo lectura y deber\u00e1 aparecer vac\u00edo, sin ning\u00fan valor. Con el placeholder de Identificador
    • T\u00edtulo. Este input es obligatorio, ser\u00e1 de escritura y deber\u00e1 aparecer vac\u00edo, sin ning\u00fan valor. Con el placeholder de T\u00edtulo
    • Edad. Este input es obligatorio, es de tipo num\u00e9rico de 0 a 99, ser\u00e1 de escritura y deber\u00e1 aparecer vac\u00edo, sin ning\u00fan valor. Con el placeholder de Edad
    • Categor\u00eda. Este input es obligatorio, ser\u00e1 un campo seleccionable donde aparecer\u00e1n todas las categor\u00edas de la BD, aparecer\u00e1 vac\u00edo por defecto. Con el placeholder de Categor\u00eda
    • Autor. Este input es obligatorio, ser\u00e1 un campo seleccionable donde aparecer\u00e1n todos los autores de la BD, aparecer\u00e1 vac\u00edo por defecto. Con el placeholder de Autor

    Todos los datos obligatorios se deber\u00e1n comprobar que son v\u00e1lidos antes de guardarlo en BD. Dos botones en la parte inferior de la ventana permitir\u00e1n al usuario cerrar la ventana o guardar los datos en la BD.

    Editar

    Al pulsar en una de las fichas con un click simple, se deber\u00e1 abrir una ventana modal utilizando el mismo componente que la ventana de Crear pero con los cinco campos rellenados con los datos de BD.

    "},{"location":"appendix/aws/","title":"AWS CLI","text":"

    AWS CLI (Command Line Interface) es una herramienta oficial proporcionada por Amazon Web Services que permite gestionar y automatizar servicios en la nube directamente desde la terminal, sin necesidad de acceder a la consola web. Con AWS CLI, los desarrolladores, administradores de sistemas y arquitectos pueden ejecutar comandos para crear, configurar y controlar recursos como instancias EC2, buckets S3, funciones Lambda, entre otros. Su uso es esencial en entornos donde se requiere eficiencia, repetibilidad y automatizaci\u00f3n, como en scripts de despliegue, tareas programadas o integraciones CI/CD. Adem\u00e1s, facilita el trabajo remoto y la administraci\u00f3n de m\u00faltiples cuentas o regiones de AWS de forma centralizada y segura.

    "},{"location":"appendix/aws/#pre-requisitos","title":"Pre-requisitos","text":"

    Vamos a instalar AWS CLI desde un sistema Linux, as\u00ed que lo primero que deber\u00e1s tener instalado en tu m\u00e1quina es un Linux o un WSL. Si no lo tienes, puedes revisar el tutorial de Subsistema de Linux para Windows.

    "},{"location":"appendix/aws/#instalacion-de-aws-cli","title":"Instalaci\u00f3n de AWS CLI","text":"

    Una vez tenemos Linux, si entramos en su consola de comandos, para instalar AWS CLI tan solo es necesario lanzar estos comandos:

    1. Update your Ubuntu packages

      sudo apt update\nsudo apt upgrade -y\n

    2. Install unzip if you don't have it already

      sudo apt install -y unzip curl\n

    3. Download the AWS CLI v2 installation package

      curl \"https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip\" -o \"awscliv2.zip\"\n

    4. Unzip the installer

      unzip awscliv2.zip\n

    5. Run the install program

      sudo ./aws/install\n

    6. Verify the installation

      aws --version\n

    Y eso es todo, ya tienes AWS CLI instalado.

    "},{"location":"appendix/dates/","title":"Fechas","text":"

    Uno de los elementos m\u00e1s problem\u00e1ticos en las aplicaciones son las fechas. M\u00e1s concretamente, c\u00f3mo las gestionamos.

    Pero podemos afrontarlo de una manera m\u00e1s clara. Vamos a empezar con unas definiciones b\u00e1sicas, veremos por qu\u00e9 hay problemas y la soluci\u00f3n que proponemos para que no te supongan un problema en tus aplicaciones.

    "},{"location":"appendix/dates/#definiciones-basicas","title":"Definiciones b\u00e1sicas","text":"

    Antes de nada, vamos a definir unos conceptos b\u00e1sicos que nos ayudar\u00e1n a entender mejor el problema.

    • Fecha (Date): Una fecha es una representaci\u00f3n de un d\u00eda concreto en el calendario. Por ejemplo, \"15 de junio de 2024\".
    • Hora (Time): La hora representa un momento espec\u00edfico dentro de un d\u00eda.
    • Zona horaria (Timezone): La zona horaria es una regi\u00f3n geogr\u00e1fica que tiene la misma hora est\u00e1ndar. Por ejemplo, \"CET\" (Central European Time) o \"GMT\" (Greenwich Mean Time).
    • Fecha y hora (DateTime): La combinaci\u00f3n de fecha y hora representa un momento espec\u00edfico en el tiempo, incluyendo la zona horaria. Por ejemplo, \"15 de junio de 2024 a las 14:30 CET\".
    • Timestamp: Un timestamp es una representaci\u00f3n num\u00e9rica de un momento espec\u00edfico en el tiempo, generalmente expresado en segundos o milisegundos desde una fecha de referencia (por ejemplo, el 1 de enero de 1970, conocido como la \"\u00e9poca Unix\").
    "},{"location":"appendix/dates/#problemas-comunes","title":"Problemas comunes","text":"

    Al trabajar con fechas y horas, pueden surgir varios problemas comunes:

    • Conversi\u00f3n de zonas horarias: Si una aplicaci\u00f3n maneja usuarios en diferentes zonas horarias, es crucial convertir correctamente las fechas y horas para evitar confusiones.
    • Formato inconsistente: Diferentes regiones utilizan diferentes formatos de fecha y hora, lo que puede llevar a errores de interpretaci\u00f3n. Ej. DD/MM/AAAA (Europa) vs MM/DD/AAAA (EE.UU.).
    • Diferencias en el horario de verano: Las fechas y horas pueden verse afectadas por el horario de verano, lo que puede causar confusiones si no se maneja correctamente.
    • Errores de c\u00e1lculo: Al realizar c\u00e1lculos con fechas y horas (por ejemplo, sumar d\u00edas o restar horas), es f\u00e1cil cometer errores si no se consideran todos los factores relevantes.
    • Me quita un d\u00eda: Es la m\u00e1s com\u00fan al principio, \u00bfpor qu\u00e9 ocurre esto? La respuesta est\u00e1 en c\u00f3mo se manejan las zonas horarias y el horario de verano. Cuando le pasamos una fecha, estamos indic\u00e1ndole un d\u00eda sin zona horaria, por lo que se detecta nuestra zona horaria local y se aplica el desfase correspondiente. GMT+1 en horario est\u00e1ndar y GMT+2 en horario de verano.

    Lo importante es recordar que un d\u00eda en una zona horaria puede no ser el mismo d\u00eda en otra zona horaria. Por ejemplo, el 15 de junio de 2024 en CET puede ser el 14 de junio de 2024 en GMT, que es por lo que ocurre un descuento de d\u00eda.

    "},{"location":"appendix/dates/#solucion-propuesta","title":"Soluci\u00f3n propuesta","text":"

    Para evitar estos problemas, os proponemos adheriros al est\u00e1ndar ISO 8601 para representar fechas y horas en vuestras aplicaciones. Este est\u00e1ndar define un formato claro y consistente para las fechas y horas, que incluye la zona horaria.

    El formato ISO 8601 para una fecha y hora completa con zona horaria es el siguiente:

    AAAA-MM-DDTHH:MM:SS.mmmZ\u00b1HH:MM\n

    Por ejemplo, \"2024-06-15T14:30:00.000Z+02:00\" representa el 15 de junio de 2024 a las 14:30 en la zona horaria GMT+2.

    "},{"location":"appendix/dates/#backend","title":"Backend","text":"

    En Java, para manejar fechas con ISO 8601 de forma segura, recomendamos el uso de Date, as\u00ed, siempre que le pasen una fecha en formato ISO 8601, se gestionar\u00e1 correctamente la zona horaria.

    import java.util.Date;\n\n// ...\n\n    private Date fecha;\n
    "},{"location":"appendix/dates/#frontend","title":"Frontend","text":"

    El uso de fechas lo gestionaremos con ISO Date

    // Para leer fechas en formato ISO 8601\nconst fecha = new Date(`2024-06-15T14:30:00.000Z`);\n\n// Para enviar fechas en formato ISO 8601\nconst fechaISO = fecha.toISOString();\n
    "},{"location":"appendix/dates/#buenas-practicas","title":"Buenas pr\u00e1cticas","text":"
    • Siempre utiliza el formato ISO 8601 para representar fechas y horas en tus aplicaciones.
    • Aseg\u00farate de manejar correctamente las zonas horarias al mostrar fechas y horas a los usuarios.
    • Realiza pruebas exhaustivas para verificar que las fechas y horas se manejan correctamente en diferentes escenarios y zonas horarias.
    • Nunca elimines las horas ni la zona horaria al manejar fechas. Siempre trabaja con fechas completas en formato ISO 8601.

    Si te cuestionan por qu\u00e9 pasan un d\u00eda y el servidor entiende otro, expl\u00edcales que es por la zona horaria y que la soluci\u00f3n es usar siempre ISO 8601. De esta manera la consistencia en los datos estar\u00e1 garantizada.

    "},{"location":"appendix/git/","title":"Tutorial b\u00e1sico de Git","text":"

    Cada vez se tiende m\u00e1s a utilizar repositorios de c\u00f3digo Git y, aunque no sea objeto de este tutorial Springboot-Angular, queremos hacer un resumen muy b\u00e1sico y sencillo de como utilizar Git.

    En el mercado existen multitud de herramientas para gestionar repositorios Git, podemos utilizar cualquiera de ellas, aunque desde devonfw se recomienda utilizar Git SCM. Adem\u00e1s, existen tambi\u00e9n multitud de servidores de c\u00f3digo que implementan repositorios Git, como podr\u00edan ser GitHub, GitLab, Bitbucket, etc. Todos ellos trabajan de la misma forma, as\u00ed que este resumen servir\u00e1 para todos ellos.

    Info

    Este anexo muestra un resumen muy sencillo y b\u00e1sico de los comandos m\u00e1s comunes que se utilizan en Git. Para ver detalles m\u00e1s avanzados o un tutorial completo te recomiendo que leas la guia de Atlassian.

    "},{"location":"appendix/git/#funcionamiento-basico","title":"Funcionamiento b\u00e1sico","text":"

    Existen dos conceptos en Git que debes tener muy claros: las ramas y los repositorios. Vamos a ver como funciona cada uno de ellos.

    "},{"location":"appendix/git/#ramas","title":"Ramas","text":"

    Por un lado tenemos las ramas de Git. El repositorio puede tener tantas ramas como se quiera, pero por lo general debe existir una rama maestra a menudo llamada develop o master, y luego muchas ramas con cada una de las funcionalidades desarrolladas.

    Las ramas siempre se deben crear a partir de una rama (en el ejemplo llamaremos develop), con una foto concreta y determinada de esa rama. Esta rama deber\u00e1 tener un nombre que describa lo que va a contener esa rama (en el ejemplo feature/xxx). Y por lo general, esa rama se mergear\u00e1 con otra rama del repositorio, que puede ser la rama de origen o cualquier otra (en el ejemplo ser\u00e1 con la rama origen develop).

    As\u00ed pues, podemos tener algo as\u00ed:

    Las acciones de crear ramas y mergear ramas est\u00e1n explicadas m\u00e1s abajo. En este punto solo es necesario que seas conocedor de:

    • existen ramas maestras --> que contienen el c\u00f3digo completo de la aplicaci\u00f3n
    • existen ramas de desarrollo --> que generalmente se crean de una rama maestra en un punto temporal concreto
    • en alg\u00fan momento esas ramas de desarrollo se deben mergear en una rama maestra
    • ojo cuidado, cuando hay varias personas en el equipo trabajando, habr\u00e1n varias ramas de desarrollo que nazcan de diferentes puntos temporales y que habr\u00e1 que tener en cuenta para posibles conflictos. Recuerda que no est\u00e1s solo programando, hay m\u00e1s gente modificando el c\u00f3digo.
    "},{"location":"appendix/git/#repositorios","title":"Repositorios","text":"

    El otro concepto que debe queda claro, es el de repositorios. Por defecto, en Git, se trabaja con el repositorio local, en el que puedes crear ramas, modificar c\u00f3digo, mergear, etc. pero todos esos cambios que se hagan, ser\u00e1n todos en local, nadie m\u00e1s tendr\u00e1 acceso.

    Tambi\u00e9n existe el repositorio remoto, tambi\u00e9n llamado origin. Este repositorio es el que todos los integrantes del equipo utilizan como referencia. Existen acciones de Git que permite sincronizar los repositorios.

    En este punto solo es necesario que seas conocedor de:

    • Los cambios que realices en local (en tu repositorio local) solo ser\u00e1n visibles por ti. Puedes crear ramas y borrarlas, pero solo tu las ver\u00e1s.
    • Los cambios que se suban al repositorio remoto ser\u00e1n visibles para todos. Pueden haber ramas protegidas para que no se puedan modificar desde el equipo, t\u00edpicamente las ramas maestras. Estas ramas solo pueden modificarse previa validaci\u00f3n y pull request o merge request (depende de la aplicaci\u00f3n usada para Git).
    • Existen acciones que permiten subir tus cambios de local a remoto. Recuerda que pueden existir ramas protegidas.
    • Existen acciones que permiten actualizar tus ramas locales con los cambios remotos.
    • Recuerda que no trabajas solo, es posible que tu repositorio local no est\u00e9 sincronizado, tus compa\u00f1eros han podido subir c\u00f3digo y deber\u00edas sincronizarte frecuentemente.
    "},{"location":"appendix/git/#acciones-mas-tipicas","title":"Acciones m\u00e1s t\u00edpicas","text":"

    En la Gu\u00eda r\u00e1pida puedes ver m\u00e1s detalle de estas acciones pero por lo general:

    • Lo primero es descargarse una copia del repositorio con todas sus ramas. Se descargar\u00eda de remoto a local. A partir de este momento se trabaja en local.
    • Cada nueva funcionalidad deber\u00eda tener su rama asociada, por tanto, lo l\u00f3gico es crear una rama de desarrollo (t\u00edpicamente feature/xxx) a partir de una rama maestra (t\u00edpicamente develop o master).
    • Se trabajar\u00eda de forma local con esa rama. Es buena pr\u00e1ctica que si llevas mucho tiempo con la rama creada, de vez en cuando, sincronices tu repositorio local con lo que exista en el repositorio remoto. Adem\u00e1s, como es posible que la rama maestra de la que part\u00eda haya cambiado, esos cambios deber\u00edas llevarlos tambi\u00e9n a tu rama en desarrollo. Con esto consigues que tu punto temporal sea m\u00e1s moderno y tengas menos conflictos. Recuerda que no est\u00e1s solo trabajando.
    • Cuando lo tengas listo y antes de subir nada, deber\u00edas realizar una \u00faltima sincronizaci\u00f3n de remoto a local. Despu\u00e9s deber\u00edas hacer un merge de tus ramas locales de desarrollo con las ramas maestras locales de las que partieron, por los posibles cambios que alguien hubiera podido subir.
    • Por \u00faltimo, una vez tengas todo actualizado, ya puedes subir el c\u00f3digo al repositorio remoto (tu rama de desarrollo), y solicitar un pull request o merge request contra la rama maestra que quieras modificar.
    • Alguien, diferente a ti, debe revisar esa solicitud y aprobarla antes de que se realice todo el merge correcto en remoto. Y vuelta a empezar.
    "},{"location":"appendix/git/#funcionamiento-avanzado","title":"Funcionamiento avanzado","text":"

    A continuaci\u00f3n vamos a describir estos mismos conceptos y acciones que hemos visto, pero m\u00e1s en profundidad para que veas como trabaja internamente Git. No es necesario que leas este punto, aunque es recomendable.

    "},{"location":"appendix/git/#estructuras-y-flujo-de-trabajo","title":"Estructuras y flujo de trabajo","text":"

    Lo primero que debes conocer de Git es su funcionamiento b\u00e1sico de flujo de trabajo. Tu repositorio local est\u00e1 compuesto por tres \"estructuras\" que contienen los archivos y los cambios de los ficheros del repositorio.

    • Working directory - Contiene los archivos con los que est\u00e1s trabajando localmente.
    • Staging Area - Es un \u00e1rea intermedia donde vamos a\u00f1adiendo ficheros para ir agrupando modificaciones.
    • Local Repository - Es el repositorio local donde tendr\u00e9mos el registro de todos los commits que hemos realizado. Por defecto apunta a HEAD que es el \u00faltimo commit registrado.

    Existen operaciones que nos permiten a\u00f1adir o borrar ficheros dentro de cada una de las estructuras desde otra estructura.

    As\u00ed pues, los comandos b\u00e1sicos dentro de nuestro repositorio local son los siguientes.

    "},{"location":"appendix/git/#add-y-commmit","title":"add y commmit","text":"

    Puedes registrar los cambios realizados en tu working directory y a\u00f1adirlos al staging area usando el comando

    git add <filename>\n
    o si quieres a\u00f1adir todos los ficheros modificados
    git add .\n

    Este es el primer paso en el flujo de trabajo b\u00e1sico. Una vez tenemos los cambios registrados en el staging area podemos hacer un commit y persistirlos dentro del local repository mediante el comando

    git commit -m \"<Commit message>\"\n

    A partir de ese momento, los ficheros modificados y a\u00f1adidos al local repository se han persistido y se han a\u00f1adido a tu HEAD, aunque todav\u00eda siguen estando el local, no lo has enviado a ning\u00fan repositorio remoto.

    "},{"location":"appendix/git/#reset","title":"reset","text":"

    De la misma manera que se han a\u00f1adido ficheros a staging area o a local repository, podemos retirarlos de estas estructuras y volver a recuperar los ficheros que ten\u00edamos anteriormente en el working directory. Por ejemplo, si nos hemos equivocado al incluir ficheros en un commit o simplemente queremos deshacer los cambios que hemos realizado bastar\u00eda con lanzar el comando

    git reset --hard\n
    o si queremos volver a un commit concreto
    git reset <COMMIT>\n

    "},{"location":"appendix/git/#trabajo-con-ramas","title":"Trabajo con ramas","text":"

    Para complicarlo todo un poco m\u00e1s, el trabajo con git siempre se realiza mediante ramas. Estas ramas nos sirven para desarrollar funcionalidades aisladas unas de otras y poder hacer mezclas de c\u00f3digo de unas ramas a otras. Las ramas m\u00e1s comunes dentro de git suelen ser:

    • master Esta ser\u00e1 la rama que contenga el c\u00f3digo fuente que tenemos en producci\u00f3n.
    • release Esta ser\u00e1 la rama que contenga el c\u00f3digo fuente de cada una de las entregas parciales, no tiene porqu\u00e9 coincidir con la rama master.
    • develop Esta ser\u00e1 la rama que contenga el c\u00f3digo fuente estable que est\u00e1 actualmente en desarrollo.
    • feature/xxxx Estas ser\u00e1nn la rama que contengan el c\u00f3digo fuente de desarrollo de cada una de las funcionalidades. Generalmente estas ramas las crea cada desarrollador, las mantiene en local, hasta que las sube a remoto para realizar un merge a la rama develop.

    Siempre que trabajes con ramas debes tener en cuenta que al empezar tu desarrollo debes partir de una versi\u00f3n actualizada de la rama develop, y al terminar tu desarrollo debes solicitar un merge contra develop, para que tu funcionalidad est\u00e9 incorporada en la rama de desarrollo.

    "},{"location":"appendix/git/#crear-ramas","title":"Crear ramas","text":"

    Crear ramas en local es tan sencillo como ejecutar este comando:

    git checkout -b <NOMBRE_RAMA>\n

    Eso nos crear\u00e1 una rama con el nombre que le hayamos dicho y mover\u00e1 el Working Directory a dicha rama.

    "},{"location":"appendix/git/#cambiar-de-rama","title":"Cambiar de rama","text":"

    Para cambiar de una rama a otra en local tan solo debemos ejecutar el comando:

    git checkout <NOMBRE_RAMA>\n

    La rama debe existir, sino se quejar\u00e1 de que no encuentra la rama. Este comando nos mover\u00e1 el Working Directory a la rama que le hayamos indicado. Si tenemos cambios en el Staging Area que no hayan sido movidos al Local Repository NO nos permitir\u00e1 movernos a la rama ya que perder\u00edamos los cambios. Antes de poder movernos debemos resetear los cambios o bien commitearlos.

    "},{"location":"appendix/git/#remote-repository","title":"Remote repository","text":"

    Hasta aqu\u00ed es todo m\u00e1s o menos sencillo, trabajamos con nuestro repositorio local, creamos ramas, commiteamos o reseteamos cambios de c\u00f3digo, pero todo esto lo hacemos en local. Ahora necesitamos que esos cambios se distribuyan y puedan leerlos el resto de integrantes de nuestro equipo.

    Aqu\u00ed es donde entra en juego los repositorios remotos.

    Aqu\u00ed debemos tener MUY en cuenta que el c\u00f3digo que vamos a publicar en remoto SOLO es posible publicarlo desde el Local Repository. Es decir que para poder subir c\u00f3digo a remote antes debemos a\u00f1adirlo a Staging Area y hacer un commit para persistirlo en el Local Repository.

    "},{"location":"appendix/git/#clone","title":"clone","text":"

    Antes de empezar a tocar c\u00f3digo del proyecto podemos crear un Local Repository vac\u00edo o bien bajarnos un proyecto que ya exista en un Remote Repository. Esta \u00faltima opci\u00f3n es la m\u00e1s normal.

    Para bajarnos un proyecto desde remoto tan solo hay que ejecutar el comando:

    git clone <REMOTE_URL>\n

    Esto te crear\u00e1 una carpeta con el nombre del proyecto y dentro se descargar\u00e1 la estructura completa del repositorio y te mover\u00e1 al Working Directory todo el c\u00f3digo de la rama por defecto para ese repositorio.

    "},{"location":"appendix/git/#envio-de-cambios","title":"env\u00edo de cambios","text":"

    El env\u00edo de datos a un Remote Repository tan solo es posible realizarlo desde Local Repository (por lo que antes deber\u00e1s commitear cambios all\u00ed), y se debe ejecutar el comando:

    git push origin\n
    "},{"location":"appendix/git/#actualizar-y-fusionar","title":"actualizar y fusionar","text":"

    En ocasiones (bastante habitual) ser\u00e1 necesario descargarse los cambios de un Remote Repository para poder trabajar con la \u00faltima versi\u00f3n. Para ello debemos ejecutar el comando:

    git pull\n

    El propio git realizar\u00e1 la fusi\u00f3n local del c\u00f3digo remoto con el c\u00f3digo de tu Working Directory. Pero en ocasiones, si se ha modificado el mismo fichero en remoto y en local, se puede producir un Conflicto. No pasa nada, tan solo tendr\u00e1s que abrir dicho fichero en conflicto y resolverlo manualmente dejando el c\u00f3digo mezclado correcto.

    Tambi\u00e9n es posible que el c\u00f3digo que queramos actualizar est\u00e9 en otra rama, si lo que necesitamos es fusionar el c\u00f3digo de otra rama con la rama actual, nos situaremos en la rama destino y ejecutaremos el comando:

    git merge <RAMA_ORIGEN>\n

    Esto har\u00e1 lo mismo que un pull en local y fusionar\u00e1 el c\u00f3digo de una rama en otra. Tambi\u00e9n es posible que se produzcan conflictos que deber\u00e1s resolver de forma manual.

    "},{"location":"appendix/git/#merge-request","title":"Merge Request","text":"

    Ya por \u00faltimo, como estamos trabajando con ramas, lo \u00fanico que hacemos es subir y bajar ramas, pero en alg\u00fan momento alguien debe fusionar el contenido de una rama en la rama develop, release o master, que son las ramas principales.

    Se podr\u00eda directamente usar el comando merge para eso, pero en la mayor\u00eda de los repositorios no esta permitido subir el c\u00f3digo de una rama principal, por lo que no podr\u00e1s hacer un merge y subirlo. Para eso existe otra opci\u00f3n que es la de Merge Request.

    Esta opci\u00f3n permite a un usuario solicitar que otro usuario verifique y valide que el c\u00f3digo de su rama es correcto y lo puede fusionar en Remote Repository con una rama principal. Al ser una operaci\u00f3n delicada, tan solo es posible ejecutarla a trav\u00e9s de la web del repositorio git.

    Por lo general existir\u00e1 una opci\u00f3n / bot\u00f3n que permitir\u00e1 hacer un Merge Request con una rama origen y una rama destino (generalmente una de las principales). A esa petici\u00f3n se le asignar\u00e1 un validador y se enviar\u00e1. El usuario validador verificar\u00e1 si es correcto o no y validar\u00e1 o rechazar\u00e1 la petici\u00f3n. En caso de validarla se fusionar\u00e1 autom\u00e1ticamente en remoto y todos los usuarios podr\u00e1n descargar los nuevos cambios desde la rama.

    \u00a1Cuidado!

    Siempre antes de solicitar un Merge Request debes comprobar que tienes actualizada la rama comparandola con la rama remota que queremos mergear, en nuestro ejemplo ser\u00e1 develop.

    Para actualizarla tu rama hay que seguir tres pasos muy sencillos:

    • Cambias a la rama develop y descargarnos los cambios del repositorio remoto (git pull)
    • Cambias a tu rama y ejecutar un merge desde develop hacia nuestra rama (git merge develop)
    • Subes tus cambios a remoto (git add, git commit y git push) y ya puedes solcitar el Merge Request
    "},{"location":"appendix/git/#guia-rapida","title":"Gu\u00eda r\u00e1pida","text":"

    Los pasos b\u00e1sicos de utilizaci\u00f3n de git son sencillos.

    • Primero nos bajamos el repositorio o lo creamos en local mediante los comandos
      git clone\n    o \ngit init\n
    • Una vez estamos trabajando con nuestro repositorio local, cada vez que vayamos a comenzar una funcionalidad nueva, debemos crear una rama nueva siempre partiendo desde una rama maestra mediante el comando: (en nuestro ejemplo la rama maestra ser\u00e1 develop)
      git checkout -b <rama>\n
    • Cuando tengamos implementados los cambios que queremos realizar, hay que subirlos al staging y luego persistirlos en nuestro repositorio local. Esto lo hacemos con el comando
      git add .\ngit commit -m \"<Commit message>\"\n
    • Siempre antes de subir los cambios al repositorio remoto, hay que comprobar que tenemos actualizada nuestra rama comparandola con la rama remota que queremos mergear, en nuestro ejemplo ser\u00e1 develop. Por tanto tenemos que cambiar a la rama develop, descargarnos los cambios del repositorio remoto, volver a cambiar a nuestra rama y ejecutar un merge desde develop hacia nuestra rama, ejecutando estos comandos
      git checkout develop\ngit pull\ngit checkout <rama>\ngit merge develop\n
    • Ahora que ya tenemos actualizadas las ramas, tan solo nos basta subir nuestra rama a remoto, con el comando
      git push --set-upstream origin <rama>\n
    • Por \u00faltimo accedemos al cliente web del repositorio y solicitamos un merge request contra develop. Para que sea validado y aprobado por otro compa\u00f1ero del equipo.
    • Si en alg\u00fan momento necesitamos modificar nuestro c\u00f3digo del merge request antes de que haya sido aprobado, nos basta con repetir los pasos anteriores
      git add .\ngit commit -m \"<Commit message>\"\ngit push origin\n
    • Una vez hayamos terminado el desarrollo y vayamos a empezar una nueva funcionalidad, volveremos al punto 2 de este listado y comenzaremos de nuevo los comando. Debemos recordad que tenemos que partir siempre de la rama develop y adem\u00e1s debe estar actualizada git pull.
    "},{"location":"appendix/jpa/","title":"Funcionamiento Spring Data","text":"

    Este anexo no pretende explicar el funcionamiento interno de Spring Data, simplemente conocer un poco como utilizarlo y algunos peque\u00f1os tips que pueden ser interesantes.

    "},{"location":"appendix/jpa/#funcionamiento-basico","title":"Funcionamiento b\u00e1sico","text":"

    Lo primero que deber\u00edas tener claro, es que hagas lo que hagas, al final todo termina lanzando una query nativa sobre la BBDD. Da igual que uses cualquier tipo de acelerador (luego veremos alguno), ya que al final Spring Data termina convirtiendo lo que hayas programado en una query nativa.

    Cuanta m\u00e1s informaci\u00f3n le proporciones a Spring Data, tendr\u00e1s m\u00e1s control sobre la query final, pero m\u00e1s dificil ser\u00e1 de mantener. Lo mejor es utilizar, siempre que se pueda, todos los automatismos y automagias posibles y dejar que Spring haga su faena. Habr\u00e1 ocasiones en que esto no nos sirva, en ese momento tendremos que decidir si queremos bajar el nivel de implementaci\u00f3n o queremos utilizar otra alternativa como procesos por streams.

    "},{"location":"appendix/jpa/#derived-query-methods","title":"Derived Query Methods","text":"

    Para la realizaci\u00f3n de consultas a la base de datos, Spring Data nos ofrece un sencillo mecanismo que consiste en crear definiciones de m\u00e9todos con una sintaxis especifica, para luego traducirlas autom\u00e1ticamente a consultas nativas, por parte de Spring Data.

    Esto es muy \u00fatil, ya que convierte a la aplicaci\u00f3n en agn\u00f3sticos de la tecnolog\u00eda de BBDD utilizada y podemos migrar con facilidad entre las muchas soluciones disponibles en el mercado, delegando esta tarea en Spring.

    Esta es la opci\u00f3n m\u00e1s indicada en la mayor\u00eda de los casos, siempre que puedas deber\u00edas utilizar esta forma de realizar las consultas. Como parte negativa, en algunos casos en consultas m\u00e1s complejas la definici\u00f3n de los m\u00e9todos puede extenderse demasiado dificultando la lectura del c\u00f3digo.

    De esto tenemos alg\u00fan ejemplo por el tutorial, en el repositorio de GameRepository.

    Siguiendo el ejemplo del tutorial, si tuvieramos que recuperar los Game por el nombre del juego, se podr\u00eda crear un m\u00e9todo en el GameRepository de esta forma:

    List<Game> findByName(String name);\n

    Spring Data entender\u00eda que quieres recuperar un listado de Game que est\u00e1n filtrados por su propiedad Name y generar\u00eda la consulta SQL de forma autom\u00e1tica, sin tener que implementar nada.

    Se pueden contruir muchos m\u00e9todos diferentes, te recomiendo que leas un peque\u00f1o tutorial de Baeldung y profundices con la documentaci\u00f3n oficial donde podr\u00e1s ver todas las opciones.

    "},{"location":"appendix/jpa/#anotacion-query","title":"Anotaci\u00f3n @Query","text":"

    Otra forma de realizar consultas, esta vez menos autom\u00e1tica y m\u00e1s cercana a SQL, es la anotaci\u00f3n @Query.

    Existen dos opciones a la hora de usar la anotaci\u00f3n @Query. Esta anotaci\u00f3n ya la hemos usado en el tutorial, dentro del GameRepository.

    En primer lugar tenemos las consultas JPQL. Estas guardan un parecido con el lenguaje SQL pero al igual que en el caso anterior, son traducidas por Spring Data a la consulta final nativa. Su uso no est\u00e1 recomendado ya que estamos a\u00f1adiendo un nivel de concreci\u00f3n y por tanto estamos aumentando la complejidad del c\u00f3digo. Aun as\u00ed, es otra forma de generar consultas.

    Por otra parte, tambi\u00e9n es posible generar consultas nativas directamente dentro de esta anotaci\u00f3n interactuando de forma directa con la base de datos. Esta pr\u00e1ctica es altamente desaconsejable ya que crea acoplamientos con la tecnolog\u00eda de la BBDD utilizada y es una fuente de errores.

    Puedes ver m\u00e1s informaci\u00f3n de esta anotaci\u00f3n desde este peque\u00f1o tutorial de Baeldung.

    "},{"location":"appendix/jpa/#acelerando-las-consultas","title":"Acelerando las consultas","text":"

    En muchas ocasiones necesitamos obtener informaci\u00f3n que no est\u00e1 en una \u00fanica tabla por motivos de dise\u00f1o de la base de datos. Debemos plasmar esta casu\u00edstica con cuidado a nuestro modelo relacional para obtener resultados \u00f3ptimos en cuanto al rendimiento.

    Para ilustrar el caso vamos a recuperar los objetos utilizados en el tutorial Author, Gategory y Game. Si recuerdas, tenemos que un Game tiene asociado un Author y tiene asociada una Gategory.

    Cuando utilizamos el m\u00e9todo de filtrado find que construimos en el GameRepository, vemos que Spring Data traduce la @Query que hab\u00edamos dise\u00f1ado en una query SQL para recuperar los juegos.

    @Query(\"select g from Game g where (:title is null or g.title like '%'||:title||'%') and (:category is null or g.category.id = :category)\")\nList<Game> find(@Param(\"title\") String title, @Param(\"category\") Long category);\n

    Esta @Query es la que utiliza Spring Data para traducir las propiedades a objetos de BBDD y mapear los resultados a objetos Java. Si tenemos activada la property spring.jpa.show-sql=true podremos ver las queries que est\u00e1 generando Spring Data. El resultado es el siguiente.

    Hibernate: select game0_.id as id1_2_, game0_.age as age2_2_, game0_.author_id as author_i4_2_, game0_.category_id as category5_2_, game0_.title as title3_2_ from game game0_ where (? is null or game0_.title like ('%'||?||'%')) and (? is null or game0_.category_id=?)\nHibernate: select author0_.id as id1_0_0_, author0_.name as name2_0_0_, author0_.nationality as national3_0_0_ from author author0_ where author0_.id=?\nHibernate: select category0_.id as id1_1_0_, category0_.name as name2_1_0_ from category category0_ where category0_.id=?\nHibernate: select author0_.id as id1_0_0_, author0_.name as name2_0_0_, author0_.nationality as national3_0_0_ from author author0_ where author0_.id=?\nHibernate: select category0_.id as id1_1_0_, category0_.name as name2_1_0_ from category category0_ where category0_.id=?\nHibernate: select author0_.id as id1_0_0_, author0_.name as name2_0_0_, author0_.nationality as national3_0_0_ from author author0_ where author0_.id=?\nHibernate: select author0_.id as id1_0_0_, author0_.name as name2_0_0_, author0_.nationality as national3_0_0_ from author author0_ where author0_.id=?\nHibernate: select author0_.id as id1_0_0_, author0_.name as name2_0_0_, author0_.nationality as national3_0_0_ from author author0_ where author0_.id=?\n

    Si te fijas ha generado una query SQL para filtrar los Game, pero luego cuando ha intentado construir los objetos Java, ha tenido que lanzar una serie de queries para recuperar los diferentes Author y Category a trav\u00e9s de sus id. Obviamente Spring Data es muy lista y cachea los resultados obtenidos para no tener que recuperarlos n veces, pero aun as\u00ed, lanza unas cuantas consultas. Esto penaliza el rendimiento de nuestra operaci\u00f3n, ya que tiene que lanzar n queries a BBDD que, aunque son muy \u00f3ptimas, incrementan unos milisegundos el tiempo total.

    Para evitar esta circunstancia, disponemos de la anotaci\u00f3n denominada @EnitityGraph la cual proporciona directrices a Spring Data sobre la forma en la que deseamos realizar la consulta, permitiendo que realice agrupaciones y uniones de tablas en una \u00fanica query que, aun siendo mas compleja, en muchos casos el rendimiento es mucho mejor que realizar m\u00faltiples interacciones con la BBDD.

    Siguiendo el ejemplo anterior podr\u00edamos utilizar la anotaci\u00f3n de esta forma:

    @Query(\"select g from Game g where (:title is null or g.title like '%'||:title||'%') and (:category is null or g.category.id = :category)\")\n@EntityGraph(attributePaths = {\"category\", \"author\"})\nList<Game> find(@Param(\"title\") String title, @Param(\"category\") Long category);\n

    Donde le estamos diciendo a Spring Data que cuando realice la query, haga el cruce con las propiedades category y author, que a su vez son entidades y por tanto mapean dos tablas de BBDD. El resultado es el siguiente:

    Hibernate: select game0_.id as id1_2_0_, category1_.id as id1_1_1_, author2_.id as id1_0_2_, game0_.age as age2_2_0_, game0_.author_id as author_i4_2_0_, game0_.category_id as category5_2_0_, game0_.title as title3_2_0_, category1_.name as name2_1_1_, author2_.name as name2_0_2_, author2_.nationality as national3_0_2_ from game game0_ left outer join category category1_ on game0_.category_id=category1_.id left outer join author author2_ on game0_.author_id=author2_.id where (? is null or game0_.title like ('%'||?||'%')) and (? is null or game0_.category_id=?)\n

    Una \u00fanica query, que es m\u00e1s compleja que la anterior, ya que hace dos cruces con tablas de BBDD, pero que nos evita tener que lanzar n queries diferentes para recuperar Author y Category.

    Generalmente, el uso de @EntityGraph acelera mucho los resultados y es muy recomendable utilizarlo para realizar los cruces inline. Se puede utilizar tanto con @Query como con Derived Query Methods. Puedes leer m\u00e1s informaci\u00f3n en este peque\u00f1o tutorial de Baeldung.

    "},{"location":"appendix/jpa/#alternativa-de-streams","title":"Alternativa de Streams","text":"

    A partir de Java 8 disponemos de los Java Streams. Se trata de una herramienta que nos permite multitud de opciones relativas tratamiento y trasformaci\u00f3n de los datos manejados.

    En este apartado \u00fanicamente se menciona debido a que en muchas ocasiones cuando nos enfrentamos a consultas complejas, puede ser beneficioso evitar ofuscar las consultas y realizar las trasformaciones necesarias mediante los Streams.

    Un ejemplo de uso pr\u00e1ctico podr\u00eda ser, evitar usar la cl\u00e1usula IN de SQL en una determinada consulta que podr\u00eda penalizar notablemente el rendimiento de las consultas. En vez de eso se podr\u00eda utilizar el m\u00e9todo de JAVA filter sobre el conjunto de elementos para obtener el mismo resultado.

    Puedes leer m\u00e1s informaci\u00f3n en el tutorial de Baeldung.

    "},{"location":"appendix/jpa/#specifications","title":"Specifications","text":"

    En algunos casos puede ocurrir que con las herramientas descritas anteriormente no tengamos suficiente alcance, bien porque las definiciones de los m\u00e9todos se complican y alargan demasiado o debido a que la consulta es demasiado gen\u00e9rica como para realizarlo de este modo.

    Para este caso se dispone de las Specifications que nos proveen de una forma de escribir consultas reutilizables mediante una API que ofrece una forma fluida de crear y combinar consultas complejas.

    Un ejemplo de caso de uso podr\u00eda ser un CRUD de una determinada entidad que debe poder filtrar por todos los atributos de esta, donde el tipo de filtrado viene especificado en la propia consulta y no siempre es requerido. En este caso no podr\u00edamos construir una consulta basada en definir un determinado m\u00e9todo ya no conocemos de ante mano que filtros ni que atributos vamos a recibir y deberemos recurrir al uso de las Specifications.

    Puedes leer m\u00e1s informaci\u00f3n en el tutorial de Baeldung.

    "},{"location":"appendix/multilanguage/","title":"Multidioma","text":"

    En las aplicaciones reales, un detalle de suma importancia es la capacidad de soportar m\u00faltiples idiomas para llegar a una audiencia m\u00e1s amplia. Afortunadamente, tanto en el frontend como en el backend, existen herramientas y bibliotecas que facilitan la implementaci\u00f3n de esta funcionalidad.

    En especial nos centraremos en el frontend.

    "},{"location":"appendix/multilanguage/#backend","title":"Backend","text":"

    La comunicaci\u00f3n del backend ha de ser agn\u00f3stica al idioma del cliente (frontend). Para conseguir este resultado, nos comunicaremos con c\u00f3digos de error (o \u00e9xito). Estableceremos un est\u00e1ndar de c\u00f3digos que el frontend interpretar\u00e1 y mostrar\u00e1 el mensaje adecuado en funci\u00f3n del idioma seleccionado por el usuario.

    Para cualquier informaci\u00f3n extra, por ejemplo, indicar el n\u00famero m\u00e1ximo de juegos que pueden prestarse. Acordaremos un campo asociado al c\u00f3digo de error que contendr\u00e1 dicha informaci\u00f3n adicional.

    "},{"location":"appendix/multilanguage/#frontend","title":"Frontend","text":"

    Para el frontend, existen varias bibliotecas que facilitan la implementaci\u00f3n de la funcionalidad multiling\u00fce. Usaremos el est\u00e1ndar i18n de Angular, que permite definir archivos de traducci\u00f3n para cada idioma soportado. (Gu\u00eda oficial de Internalizaci\u00f3n)[https://angular.dev/guide/i18n]

    Los t\u00e9rminos que has de tener en cuenta en esta nueva etapa son:

    • i18n: Abreviatura de \"internationalization\" (internacionalizaci\u00f3n), donde 18 representa el n\u00famero de letras entre la 'i' y la 'n'.
    • locale: Configuraci\u00f3n regional que define el idioma y las convenciones culturales (formato de fecha, moneda, etc.) para una regi\u00f3n espec\u00edfica. Ej. es-ES para espa\u00f1ol de Espa\u00f1a, en-US para ingl\u00e9s de Estados Unidos.
    "},{"location":"appendix/multilanguage/#material","title":"Material","text":"

    Angular Material tambi\u00e9n soporta la internacionalizaci\u00f3n. Para ello, es necesario importar los m\u00f3dulos de localizaci\u00f3n correspondientes y configurar el proveedor de localizaci\u00f3n en el m\u00f3dulo principal de la aplicaci\u00f3n.

    import { MAT_DATE_LOCALE } from \"@angular/material/core\";\n@NgModule({\n  providers: [\n    { provide: MAT_DATE_LOCALE, useValue: \"es-ES\" }, // Cambia 'es-ES' por el locale deseado\n  ],\n})\nexport class AppModule {}\n

    Esto evitar\u00e1 que tengamos un Items per page: 10 en ingl\u00e9s en las tablas de paginaci\u00f3n de Angular Material.

    "},{"location":"appendix/multilanguage/#estructura-de-un-fichero-de-traducciones","title":"Estructura de un fichero de traducciones","text":"

    Crearemos ficheros de traducciones que contendr\u00e1n keys (los c\u00f3digos de acceso) y sus correspondientes valores en cada idioma. Por ejemplo, podr\u00edamos tener un fichero en.json para ingl\u00e9s y otro es.json para espa\u00f1ol.

    • translations
      • en.json
      • es.json

    Aunque solemos preferir nombres m\u00e1s expl\u00edcitos:

    • translations
      • en-US.json
      • es-ES.json

    Un fichero de traducciones en Angular suele tener la siguiente estructura:

    {\n  \"VIEWS\": {\n    \"HOME\": {\n      \"TITLE\": \"T\u00edtulo de la aplicaci\u00f3n\",\n      \"WELCOME_MESSAGE\": \"Bienvenido a nuestra aplicaci\u00f3n\",\n      \"LOGIN\": \"Iniciar sesi\u00f3n\",\n      \"LOGOUT\": \"Cerrar sesi\u00f3n\"\n    },\n    \"DASHBOARD\": {\n      \"TITLE\": \"Panel de control\",\n      \"STATISTICS\": \"Estad\u00edsticas\",\n      \"SETTINGS\": \"Configuraciones\"\n    }\n  }\n}\n

    Ahora ya tienes todo lo necesario para crear aplicaciones multidioma. \u00a1Manos a la obra!

    "},{"location":"appendix/rest/","title":"Breve detalle sobre REST","text":"

    Antes de empezar vamos a hablar de operaciones REST. Estas operaciones son el punto de entrada a nuestra aplicaci\u00f3n y se pueden diferenciar dos claros elementos:

    • Ruta hacia el recurso, lo que viene siendo la URL.
    • Acci\u00f3n a realizar sobre el recurso, lo que viene siendo la operaci\u00f3n HTTP o el verbo.
    "},{"location":"appendix/rest/#ruta-del-recurso","title":"Ruta del recurso","text":"

    La ruta del recurso nos indica entre otras cosas, el endpoint y su posible jerarqu\u00eda sobre la que se va a realizar la operaci\u00f3n. Debe tener una ra\u00edz de recurso y si se requiere navegar por el recursos, la jerarqu\u00eda ir\u00e1 separada por barras. La URL nunca deber\u00eda tener verbos o acciones solamente recursos, identificadores o atributos. Por ejemplo en nuestro caso de Categor\u00edas, ser\u00edan correctas las siguientes rutas:

    • /category
    • /category/3
    • /category/?name=Dados

    Sin embargo, no ser\u00edan del todo correctas las rutas:

    • /getCategory
    • /findCategories
    • /saveCategory
    • /category/save

    A menudo, se integran datos identificadores o atributos de b\u00fasqueda dentro de la propia ruta. Podr\u00edamos definir la operaci\u00f3n category/3 para referirse a la Categor\u00eda con ID = 3, o category/?name=Dados para referirse a las categor\u00edas con nombre = Dados. A veces, estos datos tambi\u00e9n pueden ir como atributos en la URL o en el cuerpo de la petici\u00f3n, aunque se recomienda que siempre que sean identificadores vayan determinados en la propia URL.

    Si el dominio categor\u00eda tuviera hijos o relaciones con alg\u00fan otro dominio se podr\u00eda a\u00f1adir esas jerarqu\u00eda a la URL. Por ejemplo podr\u00edamos tener category/3/child/2 para referirnos al hijo de ID = 2 que tiene la Categor\u00eda de ID = 3, y as\u00ed sucesivamente.

    "},{"location":"appendix/rest/#accion-sobre-el-recurso","title":"Acci\u00f3n sobre el recurso","text":"

    La acci\u00f3n sobre el recurso se determina mediante la operaci\u00f3n o verbo HTTP que se utiliza en el endpoint. Los verbos m\u00e1s usados ser\u00edan:

    • GET. Cuando se quiere recuperar un recursos.
    • POST. Cuando se quiere crear un recurso. Aunque a menudo se utiliza para realizar otras acciones de b\u00fasqueda o validaci\u00f3n.
    • PUT. Cuando se quiere actualizar o modificar un recurso. Aunque a menudo se utiliza una sola operaci\u00f3n para crear o actualizar. En ese caso se utilizar\u00eda solamente POST.
    • DELETE. Cuando se quiere eliminar un recurso.

    De esta forma tendr\u00edamos:

    • GET /category/3. Realizar\u00eda un acceso para recuperar la categor\u00eda 3.
    • POST o PUT /category/3. Realizar\u00eda un acceso para crear o modificar la categor\u00eda 3. Los datos a modificar deber\u00edan ir en el body.
    • DELETE /category/3. Realizar\u00eda un acceso para borrar la categor\u00eda 3.
    • GET /category/?name=Dados. Realizar\u00eda un acceso para recuperar las categor\u00edas que tengan nombre = Dados.

    Excepciones a la regla

    A veces hay que ejecutar una operaci\u00f3n que no es 'estandar' en cuanto a verbos HTTP. Para ese caso, deberemos clarificar en la URL la acci\u00f3n que se debe realizar y si vamos a enviar datos deber\u00eda ser de tipo POST mientras que si simplemente se requiere una contestaci\u00f3n sin enviar datos ser\u00e1 de tipo GET. Por ejemplo POST /category/3/validate realizar\u00eda un acceso para ejecutar una validaci\u00f3n sobre los datos enviados en el body de la categor\u00eda 3.

    "},{"location":"appendix/tdd/","title":"TDD (Test Driven Development)","text":"

    Se trata de una pr\u00e1ctica de programaci\u00f3n que consiste en escribir primero las pruebas (generalmente unitarias), despu\u00e9s escribir el c\u00f3digo fuente que pase la prueba satisfactoriamente y, por \u00faltimo, refactorizar el c\u00f3digo escrito.

    Este ciclo se suele representar con la siguiente imagen:

    Con esta pr\u00e1ctica se consigue entre otras cosas: un c\u00f3digo m\u00e1s robusto, m\u00e1s seguro, m\u00e1s mantenible y una mayor rapidez en el desarrollo.

    Los pasos que se siguen son:

    1. Primero hay que escribir el test o los tests que cubran la funcionalidad que voy a implementar. Los test no solo deben probar los casos correctos, sino que deben probar los casos err\u00f3neos e incluso los casos en los que se provoca una excepci\u00f3n. Cuantos m\u00e1s test hagas, mejor probada y m\u00e1s robusta ser\u00e1 tu aplicaci\u00f3n.

      Adem\u00e1s, como efecto colateral, al escribir el test est\u00e1s pensando el dise\u00f1o de c\u00f3mo va a funcionar la aplicaci\u00f3n. En vez de liarte a programar como loco, te est\u00e1s forzando a pensar primero y ver cual es la mejor soluci\u00f3n. Por ejemplo para implementar una operaci\u00f3n de calculadora primero piensas en qu\u00e9 es lo que necesitar\u00e1s: una clase Calculadora con un m\u00e9todo que se llame Suma y que tenga dos par\u00e1metros.

    2. El segundo paso una vez tengo definido el test, que evidentemente fallar\u00e1 (e incluso a menudo ni siquiera compilar\u00e1), es implementar el c\u00f3digo necesario para que los tests funcionen. Aqu\u00ed muchas veces pecamos de querer implementar demasiadas cosas o pensando en que en un futuro necesitaremos modificar ciertas partes y lo dejamos ya preparado para ello. Hay que ir con mucho cuidado con las optimizaciones prematuras, a menudo no son necesarias y solo hacen que dificultar nuestro c\u00f3digo.

      Piensa en construir el m\u00ednimo c\u00f3digo que haga que tus tests funcionen correctamente. Adem\u00e1s, no es necesario que sea un c\u00f3digo demasiado purista y limpio.

    3. El \u00faltimo paso y a menudo el m\u00e1s olvidado es el Refactor. Una vez te has asegurado que tu c\u00f3digo funciona y que los tests funcionan correctamente (ojo no solo los tuyos sino todos los que ya existan en la aplicaci\u00f3n) llega el paso de sacarle brillo a tu c\u00f3digo.

      En este paso tienes que intentar mejorar tu c\u00f3digo, evitar duplicidades, evitar malos olores de programaci\u00f3n, eliminar posibles malos usos del lenguaje, etc. En definitiva que tu c\u00f3digo se lea y se entienda mejor.

    Si seguimos estos pasos a la hora de programar, nuestra aplicaci\u00f3n estar\u00e1 muy bien testada. Cada vez que hagamos un cambio tendremos una certeza muy elevada, de forma r\u00e1pida y sencilla, de si la aplicaci\u00f3n sigue funcionando o hemos roto algo. Y lo mejor de todo, las implementaciones que hagamos estar\u00e1n bien pensadas y dise\u00f1adas y acotadas realmente a lo que necesitamos.

    "},{"location":"appendix/docker/docudocker/","title":"M\u00d3DULO 2: \u00bfQU\u00c9 ES DOCKER?","text":"

    Docker es una plataforma de contenedorizaci\u00f3n de c\u00f3digo abierto que simplifica el despliegue de aplicaciones empaquetando el software y sus dependencias en una unidad estandarizada llamada contenedor. A diferencia de las m\u00e1quinas virtuales tradicionales , los contenedores Docker comparten el n\u00facleo del sistema operativo anfitri\u00f3n, lo que los hace m\u00e1s eficientes y ligeros. Los contenedores garantizan que una aplicaci\u00f3n se ejecute de la misma forma en entornos de desarrollo, pruebas y producci\u00f3n. Esto reduce los problemas de compatibilidad y mejora la portabilidad entre varias plataformas. Debido a su flexibilidad y escalabilidad, Docker se ha convertido en una herramienta crucial en los flujos de trabajo modernos de DevOps y desarrollo nativo en la nube.

    "},{"location":"appendix/docker/docudocker/#modulo-3-arquitectura-docker","title":"M\u00d3DULO 3: ARQUITECTURA DOCKER","text":"

    Docker opera en un modelo cliente-servidor, consistiendo en varios componentes clave que trabajan juntos sin problemas.

    "},{"location":"appendix/docker/docudocker/#motor-de-docker","title":"Motor de Docker","text":"

    En el n\u00facleo de Docker est\u00e1 el Motor de Docker, que incluye:

    • Daemon de Docker: El servicio de fondo que se ejecuta en el host y gestiona la construcci\u00f3n, ejecuci\u00f3n y distribuci\u00f3n de contenedores Docker.
    • CLI de Docker: La interfaz de l\u00ednea de comandos utilizada para interactuar con el daemon de Docker.
    • API REST: Permite que aplicaciones remotas interact\u00faen con el daemon de Docker.
    "},{"location":"appendix/docker/docudocker/#componentes-de-docker","title":"Componentes de Docker","text":"

    Docker utiliza varios objetos para construir y ejecutar aplicaciones:

    • Im\u00e1genes: Plantillas de solo lectura utilizadas para crear contenedores.
    • Contenedores: Instancias ejecutables de im\u00e1genes.
    • Networks: Redes que facilitan la comunicaci\u00f3n entre contenedores y el mundo exterior.
    • Vol\u00famenes: Almacenamiento de datos persistente para contenedores.

    Entender esta arquitectura permite visualizar c\u00f3mo interact\u00faan los diferentes componentes y ayuda en la resoluci\u00f3n de problemas potenciales.

    "},{"location":"appendix/docker/docudocker/#beneficios-de-la-arquitectura-de-docker","title":"Beneficios de la Arquitectura de Docker","text":"
    • Aislamiento: Los contenedores se ejecutan en entornos aislados, asegurando consistencia en diferentes sistemas.
    • Portabilidad: Las im\u00e1genes de Docker pueden ejecutarse en cualquier sistema que soporte Docker, independientemente del SO subyacente.
    • Eficiencia: Los contenedores comparten el kernel del SO host, haci\u00e9ndolos ligeros en comparaci\u00f3n con las VMs tradicionales.
    "},{"location":"appendix/docker/docudocker/#docker-hub","title":"Docker Hub","text":"

    Docker Hub es un servicio de registro basado en la nube donde puedes encontrar y compartir im\u00e1genes de contenedores. Es un excelente recurso para principiantes para explorar varias im\u00e1genes pre-construidas.

    "},{"location":"appendix/docker/docudocker/#modulo-4-docker-vs-vm","title":"M\u00d3DULO 4: DOCKER vs VM","text":"Factores Docker M\u00e1quina virtual Arranque

    En segundos

    En minutos

    Disponibilidad

    Los contenedores docker preconstruidos est\u00e1n f\u00e1cilmente disponibles

    Las m\u00e1quinas virtuales listas para usar son dif\u00edciles de encontrar

    Recursos

    Menor uso de recursos

    M\u00e1s uso de recursos

    Almacenamiento

    Los contenedores son algo m\u00e1s ligeros (KBs/MBs)

    Las m\u00e1quinas virtuales tienen unos pocos GB

    Sistema operativo

    Cada contenedor puede compartir el sistema operativo

    Cada m\u00e1quina virtual tiene un sistema operativo independiente

    Movilidad

    Los contenedores se destruyen y se vuelven a crear en lugar de moverse

    Las m\u00e1quinas virtuales pueden moverse a nuevos hosts f\u00e1cilmente

    Se ejecuta en

    Los dockers hacen uso del motor de ejecuci\u00f3n.

    Las m\u00e1quinas virtuales hacen uso del hipervisor.

    Uso

    Docker tiene un medio de operaci\u00f3n complejo que se compone de herramientas de terceros y administradas por Docker.

    Las herramientas son m\u00e1s sencillas de trabajar y f\u00e1ciles de usar.

    Gesti\u00f3n

    Los contenedores dejan de funcionar con la ejecuci\u00f3n del \"comando stop\"

    Las m\u00e1quinas virtuales siempre est\u00e1n en el estado de ejecuci\u00f3n de funcionamiento

    Control

    Las im\u00e1genes pueden ser interpretation controlled; tienen un registro original llamado Docker Hub.

    La VM no tiene un hub central; no est\u00e1n controlados

    Gesti\u00f3n de memoria Es m\u00e1s eficiente en la memoria. Es menos eficiente en la memoria. Aislamiento No dispone de un sistema de aislamiento, por lo que es muy propenso a los problemas. Cuenta con un eficiente mecanismo de aislamiento. Tiempo Es f\u00e1cil de implementar y lleva menos tiempo en comparaci\u00f3n con las m\u00e1quinas virtuales. Es un proceso largo. Por lo tanto, se necesita mucho tiempo para la implementaci\u00f3n. Facilidad de uso Es un poco dif\u00edcil de usar debido al complejo mecanismo de uso. Es f\u00e1cil de usar."},{"location":"appendix/docker/docudocker/#modulo-5-imagenes-docker-dockerfile","title":"M\u00d3DULO 5: IM\u00c1GENES DOCKER, DOCKERFILE","text":"

    Las im\u00e1genes Docker son los bloques de construcci\u00f3n fundamentales de los contenedores. Son plantillas inmutables, de s\u00f3lo lectura, que contienen todo lo necesario para ejecutar una aplicaci\u00f3n, incluido el sistema operativo, el c\u00f3digo de la aplicaci\u00f3n, el tiempo de ejecuci\u00f3n y las dependencias.

    Las im\u00e1genes se construyen utilizando un Dockerfile, que define las instrucciones para crear una imagen capa a capa.

    Las im\u00e1genes pueden almacenarse y recuperarse de registros de contenedores como Docker Hub.

    Aqu\u00ed tienes algunos comandos de ejemplo para trabajar con im\u00e1genes:

    docker build -t tu-nombre-de-imagen .: Genera una imagen y le da un nombre. docker image ls: Lista todas las im\u00e1genes disponibles en la m\u00e1quina local. docker pull nginx: Obt\u00e9n la \u00faltima imagen de Nginx de Docker Hub. docker rmi -f nginx: Eliminar una imagen de la m\u00e1quina local (forzado).

    "},{"location":"appendix/docker/docudocker/#modulo-6-contenedores-docker","title":"M\u00d3DULO 6: CONTENEDORES DOCKER","text":"

    Un contenedor Docker es una instancia en ejecuci\u00f3n de una imagen Docker. Cada contenedor tiene su propio sistema de archivos, red y espacio de procesos, pero comparte el n\u00facleo anfitri\u00f3n.

    Los contenedores siguen un ciclo de vida sencillo que incluye su creaci\u00f3n, inicio, parada y eliminaci\u00f3n. Aqu\u00ed tienes un desglose de los comandos comunes de gesti\u00f3n de contenedores:

    docker create o docker run: Crear un contenedor. docker start: Poner en marcha un contenedor. docker stop: Detener un contenedor. docker restart: Reiniciar un contenedor. docker rm: Borrar un contenedor. docker ps -a: Listar todos los contenedores.

    "},{"location":"appendix/docker/installdocker/","title":"M\u00d3DULO 1: INSTALACI\u00d3N DOCKER EN WINDOWS","text":"

    Para instalar la versi\u00f3n gratuita y open source de Docker Community Edition (CE) siga estos pasos:

    1. Instalar Ubuntu 24.04.1 LTS desde Microsoft Store: Como no est\u00e1 el WSL, al ejecutar, no funcionar\u00e1 y saldr\u00e1 un error, pero se solucionar\u00e1 en los pasos siguientes.
    2. Instalar PowerShell desde Microsoft Store.
    3. Instalar el Subsistema de Linux para Windows:
    4. Ejecuta el comando wsl --install
    5. Manda una petici\u00f3n para que te lo instalen. Cuando sea aprobada, deber\u00eda dejarte continuar. Ejemplo de lo que os sale, se elige la primera opci\u00f3n
    6. Despu\u00e9s

    7. Verificar la instalaci\u00f3n:

    8. Para ver si est\u00e1 instalado, ejecuta wsl \u2013version (es posible que necesites reiniciar el ordenador)

    9. Prueba wsl --status. Aqu\u00ed deber\u00eda indicar que \"Windows subsystem for Linux has no installed distributions\".

    10. Cuando abras Ubuntu, seguramente no funcione. De manera que tienes que reiniciar el PC y comprobar que ahora Ubuntu s\u00ed funciona.

    11. Introducir los siguientes comandos en Ubuntu.

    12. Si pide un usuario y contrase\u00f1a, poner la vuestra propia.

    13. Pod\u00e9is continuar con estos comandos:

      sudo apt update\nsudo apt install curl apt-transport-https ca-certificates software-properties-common\nsudo apt install docker.io -y\ncurl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg\necho \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null\nsudo apt update\nsudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\nsudo systemctl start docker  \n##? gpasswd -a $USER docker  \nsudo docker run hello-world\n

    14. En este o en el siguiente paso, te dar\u00e1 error por falta de permisos, no hay problema.

    15. Abrir Ubuntu y darle a ejecutar como administrador.
    16. Despu\u00e9s, tendr\u00e9is que pedir una solicitud para los permisos. sudo systemctl start docker sudo docker run hello-world

    17. Entrar en Ubuntu como administrador:

    18. Vuelve a ejecutar en Ubuntu sudo service docker start
    19. Ejecuta docker run hello-world

    20. Verificar Docker:

    21. Cuando ejecutes el comando anterior, deber\u00eda funcionar.
    22. Prueba docker ps -a para ver un listado de contenedores.
    "},{"location":"appendix/docker/summary/","title":"Resumen de la contenerizaci\u00f3n usando Docker","text":""},{"location":"appendix/docker/summary/#que-hemos-ganado-empleando-estas-herramientas","title":"\u00bfQu\u00e9 hemos ganado empleando estas herramientas?","text":"
    • Usar un lenguaje declarativo para facilitar las tareas de despliegue de aplicaciones.
    • Hacer m\u00e1s portables nuestros desarrollos en entorno local. Al menos estamos dando a los DevOps de preproducci\u00f3n, UAT y Producci\u00f3n una informaci\u00f3n adicional que les va a resultar \u00fatil cuando tengan que hacer los despliegues en sus entornos de producci\u00f3n.
    • Incrementar la productividad de los entornos de desarrollo locales a corto (porque es sencillo de usar), medio y largo plazo.
    • Facilitar y acortar la tarea de migraci\u00f3n de nuestros desarrollos a los entornos de alta productividad y econom\u00eda de escala en la nube.
    "},{"location":"appendix/docker/traindocker/","title":"M\u00d3DULO 7: HANDS-ON. \u00a1AHORA HAZLO T\u00da!","text":"

    Ahora vamos a construir im\u00e1genes de servicios y sobre estas im\u00e1genes lanzaremos contenedores en el entorno local del desarrollador.

    Teniendo ya instalados WSL, Docker Community y Docker Compose nos centramos en la parte pr\u00e1ctica.

    "},{"location":"appendix/docker/traindocker/#construir-imagenes-con-un-dockerfile","title":"Construir im\u00e1genes con un Dockerfile","text":"

    Empezamos creando una im\u00e1gen por cada servicio de nuestra aplicaci\u00f3n b\u00e1sica en microservicios basada en un backend con Spring Boot.

    Cada servicio requiere un fichero propio de nombre Dockerfile sin extensi\u00f3n, que queda situado en el directorio ra\u00edz del m\u00f3dulo y al mismo nivel que el fichero POM.

    Para el proyecto server-springboot-micros y m\u00f3dulo server-springboot-eureka un posible Dockerfile ser\u00eda:

    # Use the official Ubuntu 22.04 LTS base image\nFROM ubuntu:22.04\n\n# Install necessary packages\nRUN apt-get update && apt-get install -y \\\nopenjdk-19-jdk \\\nmaven \\\nwget \\\ncurl \\\ngnupg \\\n&& rm -rf /var/lib/apt/lists/*\n\n# Set the working directory\nWORKDIR /app\n\n# Copy the project files to the container\nCOPY . .\n\n# Build the project using Maven\nRUN mvn clean package\n\n# Expose the application port\nEXPOSE 8761\n\n# Run the Spring Boot application\nCMD [\"java\", \"-jar\", \"target/tutorial-eureka-0.0.1-SNAPSHOT.jar\"]\n
    Es un script b\u00e1sico para el service discovery de ejemplo, donde los comentarios de c\u00f3digo nos dan las explicaciones debidas.

    Para un m\u00f3dulo de reglas de negocio del mismo proyecto, su correspondiente Dockerfile b\u00e1sico podr\u00eda ser:

    # Use the official Ubuntu 22.04 LTS base image\nFROM ubuntu:22.04\n\n# Install necessary packages\nRUN apt-get update && apt-get install -y \\\n    openjdk-19-jdk \\\n    maven \\\n    wget \\\n    curl \\\n    gnupg \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Set the working directory\nWORKDIR /app\n\n# Copy the project files to the container\nCOPY . .\n\n# Build the project using Maven\nRUN mvn clean package\n\n# Expose the application port\nEXPOSE 8092\n\n# Run the Spring Boot application\nCMD [\"java\", \"-jar\", \"target/tutorial-author-0.0.1-SNAPSHOT.jar\"]\n
    Y de forma parecida el resto de los m\u00f3dulos del backend.

    Para el frontend, tomar en esta pr\u00e1ctica el m\u00f3dulo client-angular17 cuyo Dockerfile b\u00e1sico podr\u00eda ser:

    # Use the official Node.js image as the base image\nFROM node:18\n\n# Set the working directory inside the container\nWORKDIR /usr/src/app\n\n# Install Angular CLI globally\nRUN npm install -g @angular/cli@17\n\n# Copy package.json and package-lock.json to the working directory\nCOPY package*.json ./\n\n# Install project dependencies\nRUN npm install\n\n# Copy the rest of the application code to the working directory\nCOPY . .\n\n# Expose the port the app runs on\nEXPOSE 4200\n\n# Command to run the application in development mode\nCMD [\"ng\", \"serve\", \"--host\", \"0.0.0.0\"]\n
    "},{"location":"appendix/docker/traindocker/#desplegar-una-imagen-dentro-de-un-contenedor","title":"Desplegar una imagen dentro de un contenedor","text":"

    Teniendo en local las instalaciones hechas, no representa problema alguno, por ejemplo, para el caso del servicio eureka:

    docker build -t i-tutorial-eureka .\ndocker run -d -p 8761:8761 --name c-tutorial-eureka i-tutorial-eureka\n\ndocker logs c-tutorial-eureka\ndocker stop c-tutorial-eureka\ndocker start c-tutorial-eureka\ndocker rm c-tutorial-eureka\n

    que hace lo siguiente: 1. desde terminal situado en la ra\u00edz del m\u00f3dulo junto al Dockerfile del servicio, primero creamos la imagen i-tutorial-eureka, 2. lanzamos la creaci\u00f3n de su correspondiente contenedor de nombre c-tutorial-eureka y su ejecuci\u00f3n detached. 3. Por \u00faltimo, damos unos comandos para inspeccionar su log, pararlo, arrancarlo, eliminarlo cuando dejemos de necesitarlo.

    En el laptop corporativo, puede ocurrir que el build se detenga por timeout cuando descargue la imagen del SO. En tal caso, revise el estado de su VPN.

    Las im\u00e1genes y contenedores son ligeros para un servidor, pero no para un laptop corporativo, elimine los recursos que no est\u00e9 usando para no saturar su equipo.

    "},{"location":"appendix/docker/traindocker/#desplegar-un-conjunto-de-contenedores-que-se-comunican","title":"Desplegar un conjunto de contenedores que se comunican","text":"

    En lo sucesivo notar que en este tutorial estamos usando Version: 28.0.2 de Docker Community y Docker Compose version v2.34.0

    B\u00e1sicamente, si solo utilizamos m\u00f3dulos de negocio, cada uno en su Dockerfile correspondiente, no tenemos garantizado que todos ellos puedan comunicarse entre s\u00ed en la forma deseada.

    Adem\u00e1s de querer tener componentes separados que sean escalables, queremos asegurarnos de que los m\u00f3dulos puedan hablarse entre ellos, si deben hacerlo.

    Otro requerimiento que tenemos en este proyecto simple, es la necesidad de arrancar unos servicios antes que otros, por ejemplo: el service discovery debe iniciar primero, segundo el gateway, y luego el resto de los m\u00f3dulos con l\u00f3gica de negocio. Por \u00faltimo, si todos han arrancado bien y est\u00e1n perfectamente up, arrancamos el frontend.

    Para lograr estas necesidades, por otra parte, muy comunes en los proyectos basados en microservicios, tenemos disponible la herramienta docker-compose.

    docker-compose permite lanzar en un solo script todos los servicios/contenedores estableciendo el orden deseado de arranque y ejecuci\u00f3n, as\u00ed como, la configuraci\u00f3n de un network donde declarar qu\u00e9 m\u00f3dulos pueden hablar entre s\u00ed.

    Para el caso de nuestro proyecto:

    docker network create backend-network\ndocker compose up --build\n\ndocker network ls\ndocker compose ps\ndocker compose down\n
    Primero creamos una red donde puedan comunicarse los contenedores que deben comunicarse entre s\u00ed. Luego, en un comando ordenamos la interpretaci\u00f3n del script docker-compose.yml situado en la ra\u00edz del proyecto general, seguido del build de todas las im\u00e1genes declaradas, seguido del arranque en el orden dado, seguido del establecimiento de las comunicaciones declaradas, seguido del respaldo de los latidos solicitados. El resto son sencillos comandos para ver el estado de la red, de los contenedores, y para desarmar todos los contenedores pertenecientes al compose cuando dejemos de necesitarlos.

    A continuaci\u00f3n damos un posible docker-compose.yml de ejemplo, sigue siendo b\u00e1sico (aunque ya no tanto):

    services:\n  tutorial-eureka:\n    build:\n      context: ./server-springboot-micros/server-springboot-eureka\n      dockerfile: Dockerfile\n    ports:\n      - \"8761:8761\"\n    networks:\n      - backend-network\n    environment:\n      eureka.client.serviceUrl.defaultZone: http://tutorial-eureka:8761/eureka/\n    healthcheck:\n      test: \"curl -f http://tutorial-eureka:8761/actuator/health || exit 1\"\n      interval: 40s\n      timeout: 10s\n      retries: 3\n\n  tutorial-gateway:\n    build:\n      context: ./server-springboot-micros/server-springbbot-gateway\n      dockerfile: Dockerfile\n    ports:\n      - \"8080:8080\"\n    networks:\n      - backend-network\n    depends_on:\n      tutorial-eureka:\n        condition: service_healthy\n    environment:\n      eureka.client.serviceUrl.defaultZone: http://tutorial-eureka:8761/eureka/\n    healthcheck:\n      test: \"curl -f http://tutorial-gateway:8080/actuator/health || exit 1\"\n      interval: 40s\n      timeout: 10s\n      retries: 3\n\n  tutorial-category:\n    build:\n      context: ./server-springboot-micros/server-springboot-category\n      dockerfile: Dockerfile\n    ports:\n      - \"8091:8091\"\n    networks:\n      - backend-network\n    depends_on:\n      tutorial-eureka:\n        condition: service_healthy\n      tutorial-gateway:\n        condition: service_healthy\n    environment:\n      eureka.client.serviceUrl.defaultZone: http://tutorial-eureka:8761/eureka/\n    healthcheck:\n      test: \"curl -f http://tutorial-category:8091/actuator/health || exit 1\"\n      interval: 40s\n      timeout: 10s\n      retries: 3\n\n  tutorial-author:\n    build:\n      context: ./server-springboot-micros/server-springboot-author\n      dockerfile: Dockerfile\n    ports:\n      - \"8092:8092\"\n    networks:\n      - backend-network\n    depends_on:\n      tutorial-eureka:\n        condition: service_healthy\n      tutorial-gateway:\n        condition: service_healthy\n    environment:\n      eureka.client.serviceUrl.defaultZone: http://tutorial-eureka:8761/eureka/\n    healthcheck:\n      test: \"curl -f http://tutorial-author:8092/actuator/health || exit 1\"\n      interval: 40s\n      timeout: 10s\n      retries: 3\n\n  tutorial-game:\n    build:\n      context: ./server-springboot-micros/server-springboot-game\n      dockerfile: Dockerfile\n    ports:\n      - \"8093:8093\"\n    networks:\n      - backend-network\n    depends_on:\n      tutorial-eureka:\n        condition: service_healthy\n      tutorial-gateway:\n        condition: service_healthy\n      tutorial-category:\n        condition: service_healthy\n      tutorial-author:\n        condition: service_healthy\n    environment:\n      eureka.client.serviceUrl.defaultZone: http://tutorial-eureka:8761/eureka/\n    healthcheck:\n      test: \"curl -f http://tutorial-game:8093/actuator/health || exit 1\"\n      interval: 40s\n      timeout: 10s\n      retries: 3\n\n  tutorial-front:\n    build:\n      context: ./client-angular17\n      dockerfile: Dockerfile\n    ports:\n      - \"4200:4200\"\n    networks:\n      - backend-network\n    depends_on:\n      tutorial-eureka:\n        condition: service_healthy\n      tutorial-gateway:\n        condition: service_healthy\n      tutorial-category:\n        condition: service_healthy\n      tutorial-author:\n        condition: service_healthy\n      tutorial-game:\n        condition: service_healthy\n    environment:\n      eureka.client.serviceUrl.defaultZone: http://tutorial-eureka:8761/eureka/\n    healthcheck:\n      test: \"curl -f http://tutorial-front:4200/actuator/health || exit 1\"\n      interval: 40s\n      timeout: 10s\n      retries: 3\n\nnetworks:\n  backend-network:\n    driver: bridge\n
    Para lograr la orquestaci\u00f3n/sincronizaci\u00f3n/comunicaci\u00f3n deseada es mejor que todo est\u00e9 en su sitio con el orden debido.

    "},{"location":"appendix/docker/traindocker/#todo-y-practicar","title":"TODO y practicar","text":"
    1. Como pr\u00e1ctica, queremos duplicar nuestro frontend b\u00e1sico de manera que tengamos dos portales muy similares pero distintos.

    2. Es decir, que el Front1 ejecute en un contenedor y el Front2 ejecute en otro contenedor.

    3. Adem\u00e1s, ambos frontales se comunican con el mismo backend, el nuestro.
    4. Crea agentes \"cliente\" que solo accedan a uno de los frontales, crea un agente \"admin\" que acceda a la network que agrupa al resto de networks.

    5. [ ] Sugerencia: crea tres networks, una para los frontales, otra para el backend, y una tercera que agrupe las dos anteriores.

    "},{"location":"appendix/springbatch/clean/","title":"Limpieza - Spring Batch","text":"

    Ya tenemos todo configurado de los pasos anteriores asi que proseguimos con el \u00faltimo ejemplo.

    "},{"location":"appendix/springbatch/clean/#caso-de-uso","title":"Caso de Uso","text":"

    Este es un caso de uso nuevo para poner en pr\u00e1ctica el uso de Tasklet.

    "},{"location":"appendix/springbatch/clean/#que-vamos-a-hacer","title":"\u00bfQu\u00e9 vamos a hacer?","text":"

    Vamos a implementar un batch que limpie de ficheros un determinado directorio. Esta vez y dado que no necesitamos realizar ning\u00fan tipo de lectura ni trasformaci\u00f3n ni escritura y queremos hacerlo todo al mismo tiempo, es buen momento para utilizar un Tasklet.

    "},{"location":"appendix/springbatch/clean/#como-lo-vamos-a-hacer","title":"\u00bfC\u00f3mo lo vamos a hacer?","text":"

    A diferencia de los casos anteriores seguiremos el esquema de funcionamiento de tasklet de un proceso batch que hemos visto en la parte de introducci\u00f3n:

    • Tasklet: Eliminar\u00e1 todos los ficheros del directorio.
    • Step: El paso que contiene el tasklet que van a realizar la funcionalidad.
    • Job: La tarea que contiene los pasos definidos.
    "},{"location":"appendix/springbatch/clean/#codigo","title":"C\u00f3digo","text":""},{"location":"appendix/springbatch/clean/#tasklet","title":"Tasklet","text":"

    En primer lugar, vamos a crear CleanTasklet dentro del package com.ccsw.tutorialbatch.tasklet.

    CleanTasklet.java
    import java.io.File;\n\npublic class CleanTasklet implements Tasklet, InitializingBean {\n\n    private Resource directory;\n\n    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {\n        File dir = directory.getFile();\n\n        File[] files = dir.listFiles();\n\n        for (File file : files) {\n            boolean deleted = file.delete();\n            if (!deleted) {\n                throw new UnexpectedJobExecutionException(\"Could not delete file \" + file.getPath());\n            }\n        }\n        return RepeatStatus.FINISHED;\n    }\n\n    public void setDirectoryResource(Resource directory) {\n\n        this.directory = directory;\n    }\n\n    public void afterPropertiesSet() throws Exception {\n\n        if (directory == null) {\n            throw new UnexpectedJobExecutionException(\"Directory must be set\");\n        }\n    }\n}\n

    La implementaci\u00f3n de la interface Tasklet consiste en sobreescribir el m\u00e9todo execute de forma muy similar como lo hac\u00edamos en los Processors. En este m\u00e9todo emplazamos nuestra l\u00f3gica de negocio que b\u00e1sicamente consiste en borrar todos los ficheros que se encuentren en el directorio proporcionado como atributo.

    "},{"location":"appendix/springbatch/clean/#step-y-job","title":"Step y Job","text":"

    Posteriormente, como en el caso anterior, emplazamos la configuraci\u00f3n junto al resto de beans dentro del package com.ccsw.tutorialbatch.config.

    CleanBatchConfiguration.java
    package com.ccsw.tutorialbatch.config;\n\nimport com.ccsw.tutorialbatch.tasklet.CleanTasklet;\nimport org.springframework.batch.core.Job;\nimport org.springframework.batch.core.Step;\nimport org.springframework.batch.core.job.builder.JobBuilder;\nimport org.springframework.batch.core.launch.support.RunIdIncrementer;\nimport org.springframework.batch.core.repository.JobRepository;\nimport org.springframework.batch.core.step.builder.StepBuilder;\nimport org.springframework.batch.core.step.tasklet.Tasklet;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.io.FileSystemResource;\nimport org.springframework.transaction.PlatformTransactionManager;\n\n@Configuration\npublic class CleanBatchConfiguration {\n\n    @Bean\n    public Tasklet taskletClean() {\n        CleanTasklet tasklet = new CleanTasklet();\n\n        tasklet.setDirectoryResource(new FileSystemResource(\"target/test-outputs\"));\n\n        return tasklet;\n    }\n\n    @Bean\n    public Step step1Clean(JobRepository jobRepository, PlatformTransactionManager transactionManager, Tasklet taskletClean) {\n        return new StepBuilder(\"step1Clean\", jobRepository)\n                .tasklet(taskletClean, transactionManager)\n                .build();\n    }\n\n    @Bean\n    public Job jobClean(JobRepository jobRepository, Step step1Clean) {\n        return new JobBuilder(\"jobClean\", jobRepository)\n                .incrementer(new RunIdIncrementer())\n                .start(step1Clean)\n                .build();\n    }\n\n}\n
    • Tasklet: El bean del Tasklet que hemos creado anteriormente.
    • Step: La creaci\u00f3n del Step se realiza mediante \u00e9l StepBuilder al que \u00fanicamente le a\u00f1adimos el Tasklet que se va a ejecutar de forma at\u00f3mica.
    • Job: Finalmente, debemos definir \u00e9l Job que ser\u00e1 lo que se ejecute al lanzar nuestro proceso. La creaci\u00f3n se hace mediante el builder correspondiente como en los casos anteriores.
    "},{"location":"appendix/springbatch/clean/#pruebas","title":"Pruebas","text":"

    Ahora ya tenemos varios Jobs en nuestro batch por lo que debemos especificar en el arranque cu\u00e1l queremos ejecutar.

    Como en el caso anterior pasamos como VM option la siguiente propiedad en el arranque de la aplicaci\u00f3n:

    -Dspring.batch.job.name=jobClean\n

    Hecho esto y ejecutado el batch, podremos ver la traza de la ejecuci\u00f3n en nuestro log y que el fichero generado en el target del proyecto de la ejecuci\u00f3n del batch de autores ya no est\u00e1.

    Job: [SimpleJob: [name=jobClean]] launched with the following parameters: [{'run.id':'{value=1, type=class java.lang.Long, identifying=true}'}]\nExecuting step: [step1Clean]\nStep: [step1Clean] executed in 9ms\nJob: [SimpleJob: [name=jobClean]] completed with the following parameters: [{'run.id':'{value=1, type=class java.lang.Long, identifying=true}'}] and the following status: [COMPLETED] in 23ms\n
    "},{"location":"appendix/springbatch/exercise/","title":"Ahora hazlo t\u00fa!","text":"

    Ahora vamos a ver si has comprendido bien el tutorial. \u00a1Vamos alla!

    "},{"location":"appendix/springbatch/exercise/#exportacion-de-juegos-a-fichero","title":"Exportaci\u00f3n de juegos a fichero","text":""},{"location":"appendix/springbatch/exercise/#requisitos","title":"Requisitos","text":"

    En este ejercicio vamos a simular la exportaci\u00f3n de datos desde una tabla de base de datos a fichero.

    El objetivo es que en funci\u00f3n del n\u00famero de stock de un determinado juego, generemos un fichero con su nombre y si el juego est\u00e1 disponible.

    Par ello debemos tener una tabla de juegos con los siguientes atributos:

    • Identificador
    • T\u00edtulo
    • Edad recomendada
    • Stock

    El proceso batch debe consultar los registros y convertirlos a la siguiente estructura:

    • T\u00edtulo: T\u00edtulo del juego (el mismo que en la tabla de BBDD).
    • Disponibilidad: Si el stock es mayor que cero estar\u00e1 disponible y si es cero debera aparecer que no est\u00e1 disponible.

    Una vez realizada la conversion, se debe escribir dicha informaci\u00f3n a fichero y guardarlo en el target del proyecto.

    "},{"location":"appendix/springbatch/exercise/#consejos","title":"Consejos","text":"

    Para empezar te dar\u00e9 unos consejos:

    • Recuerda crear la tabla de la BBDD y sus datos.
    • Intenta re-aprovechar lo que hemos aprendido en los ejemplos.
    • Consulta la documentaci\u00f3n para utilizar un Reader apropiado para la lectura desde BBDD.
    • Date cuenta de que el Processor que necesitas es algo m\u00e1s complejo esta vez y necesitaras m\u00e1s de un modelo diferente.
    "},{"location":"appendix/springbatch/exercise/#ya-has-terminado","title":"\u00bfYa has terminado?","text":"

    Si has llegado a este punto es porque ya tienes terminado el tutorial. Por favor no te olvides de subir los proyectos a alg\u00fan repositorio Github propio (puedes revisar el anexo Tutorial b\u00e1sico de Git) y av\u00edsarnos para que podamos echarle un ojo y darte sugerencias y feedback .

    Si es una formaci\u00f3n ligada a proyecto, que tu responsable nos contacte para que podamos darle prioridad al feedback.

    "},{"location":"appendix/springbatch/filetodb/","title":"Categor\u00eda - Spring Batch","text":"

    Al igual que el tutorial b\u00e1sico de Spring Boot, debemos configurar el entorno y crear el proyecto.

    Para la configuraci\u00f3n del entorno nos remitimos a la gu\u00eda de instalaci\u00f3n donde se detalla el proceso de configuraci\u00f3n del Entorno de desarrollo

    Todos los pasos son exactamente iguales, lo \u00fanico que va a variar es la creaci\u00f3n del proyecto desde Spring Initializr:

    • Tipo de proyecto: Maven
    • Lenguage: Java
    • Versi\u00f3n Spring boot: 3.2.2 (o alguna similar)
    • Group: com.ccsw
    • ArtifactId: tutorial-batch
    • Versi\u00f3n Java: 17 (o similar)
    • Dependencias: Spring Batch, H2 Database

    Esto nos generar\u00e1 un proyecto que ya vendr\u00e1 configurado con Spring Batch y H2 para crear una BBDD en memoria de ejemplo con la que trabajaremos durante el tutorial.

    Esta parte de tutorial es una ampliaci\u00f3n de la parte de backend con Spring Boot, por tanto, no se ve a enfocar en las partes b\u00e1sicas aprendidas previamente, sino que se va a explicar el funcionamiento de los procesos batch.

    "},{"location":"appendix/springbatch/filetodb/#caso-de-uso","title":"Caso de Uso","text":"

    En este ejemplo no podemos seguir los mismos casos de uso que de los ejemplos del tutorial de Spring Boot, ya que sus requisitos no son v\u00e1lidos para implementarse como un proceso batch por lo que vamos a mantener las mismas entidades pero imaginar casos de uso diferentes.

    "},{"location":"appendix/springbatch/filetodb/#que-vamos-a-hacer","title":"\u00bfQu\u00e9 vamos a hacer?","text":"

    Vamos a implementar un batch para leer un fichero de Categorias e insertar los registros le\u00eddos en Base de Datos.

    "},{"location":"appendix/springbatch/filetodb/#como-lo-vamos-a-hacer","title":"\u00bfC\u00f3mo lo vamos a hacer?","text":"

    Seguiremos el esquema de funcionamiento habitual de un proceso batch que hemos visto en la parte de introducci\u00f3n:

    • ItemReader: Se va a leer de un fichero y convertir los registros le\u00eddos al modelo de Category.
    • ItemProcessor: Va a procesar todos los registros convirtiendo los textos a may\u00fasculas.
    • ItemWriter: Va a insertar los registros en la BBDD.
    • Step: El paso que contiene los elementos que van a realizar la funcionalidad.
    • Job: La tarea que contiene los pasos definidos.
    "},{"location":"appendix/springbatch/filetodb/#codigo","title":"C\u00f3digo","text":""},{"location":"appendix/springbatch/filetodb/#modelo","title":"Modelo","text":"

    En primer lugar, vamos a crear el modelo dentro del package com.ccsw.tutorialbatch.model. En este caso no trabajamos con entidades, ya que ahora son simples estructuras de datos.

    Category.java
    package com.ccsw.tutorialbatch.model;\n\npublic class Category {\n\n    private String name;\n    private String type;\n    private String characteristics;\n\n    public Category() {\n    }\n\n    public Category(String name, String type, String characteristics) {\n        this.name = name;\n        this.type = type;\n        this.characteristics = characteristics;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getType() {\n        return type;\n    }\n\n    public void setType(String type) {\n        this.type = type;\n    }\n\n    public String getCharacteristics() {\n        return characteristics;\n    }\n\n    public void setCharacteristics(String characteristics) {\n        this.characteristics = characteristics;\n    }\n\n    @Override\n    public String toString() {\n        return \"Category [name=\" + getName() + \", type=\" + getType() + \", characteristics=\" + getCharacteristics() + \"]\";\n    }\n\n}\n
    "},{"location":"appendix/springbatch/filetodb/#reader","title":"Reader","text":"

    Ahora, emplazamos \u00e9l Reader en la clase donde posteriormente a\u00f1adiremos la configuraci\u00f3n junto al resto de beans, dentro del package com.ccsw.tutorialbatch.config.

    CategoryBatchConfiguration.java
    package com.ccsw.tutorialbatch.config;\n\n...\n\n@Configuration\npublic class CategoryBatchConfiguration {\n\n    @Bean\n    public ItemReader<Category> readerCategory() {\n        return new FlatFileItemReaderBuilder<Category>().name(\"categoryItemReader\")\n                .resource(new ClassPathResource(\"category-list.csv\"))\n                .delimited()\n                .names(new String[] { \"name\", \"type\", \"characteristics\" })\n                .fieldSetMapper(new BeanWrapperFieldSetMapper<>() {{\n                    setTargetType(Category.class);\n                }})\n                .build();\n    }\n\n}\n

    Para la ingesta de datos vamos a hacer uso de FlatFileItemReader que nos proporciona Spring Batch. Como se puede observar se le proporciona el fichero a leer y el mapeo a la clase que deseamos. Aqu\u00ed el cat\u00e1logo de Readers que proporciona Spring Batch.

    "},{"location":"appendix/springbatch/filetodb/#processor","title":"Processor","text":"

    Posteriormente, emplazamos \u00e9l Processor dentro del package com.ccsw.tutorialbatch.processor.

    CategoryItemProcessor.java
    package com.ccsw.tutorialbatch.processor;\n\nimport com.ccsw.tutorialbatch.model.Category;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.batch.item.ItemProcessor;\n\npublic class CategoryItemProcessor implements ItemProcessor<Category, Category> {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(CategoryItemProcessor.class);\n\n    @Override\n    public Category process(final Category category) {\n        String name = category.getName().toUpperCase();\n        String type = category.getType().toUpperCase();\n        String characteristics = category.getCharacteristics().toUpperCase();\n\n        Category transformedCategory = new Category(name, type, characteristics);\n        LOGGER.info(\"Converting ( {} ) into ( {} )\", category, transformedCategory);\n\n        return transformedCategory;\n    }\n}\n

    Hemos implementado un Processor personalizado, esta clase implementa ItemProcessor donde especificamos de qu\u00e9 clase a qu\u00e9 clase se va a realizar la trasformaci\u00f3n.

    En nuestro caso, va a ser de Category a Category donde \u00fanicamente vamos a realizar una trasformaci\u00f3n de pasar los datos le\u00eddos a may\u00fasculas, ya que el Reader que veremos m\u00e1s adelante ya nos habr\u00e1 trasformado los datos del fichero al modelo deseado. Las trasformaciones en s\u00ed se especifican sobreescribiendo el m\u00e9todo process.

    "},{"location":"appendix/springbatch/filetodb/#writer","title":"Writer","text":"

    Posteriormente, a\u00f1adimos el writer a la clase de configuraci\u00f3n CategoryBatchConfiguration donde ya hab\u00edamos a\u00f1adido Reader.

    CategoryBatchConfiguration.java
    package com.ccsw.tutorialbatch.config;\n\n...\n\n@Configuration\npublic class CategoryBatchConfiguration {\n\n    ...\n\n    @Bean\n    public ItemWriter<Category> writerCategory(DataSource dataSource) {\n        return new JdbcBatchItemWriterBuilder<Category>()\n                .itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())\n                .sql(\"INSERT INTO category (name, type, characteristics) VALUES (:name, :type, :characteristics)\")\n                .dataSource(dataSource)\n                .build();\n    }\n\n}\n

    Para la parte de escritura usaremos JdbcBatchItemWriter que nos ayuda a lanzar inserciones en la base de datos de forma sencilla. \u00c9l DataSource se inicializa autom\u00e1ticamente con la instancia de H2 que se carga al arrancar el Batch. Aqu\u00ed el cat\u00e1logo de Writers que proporciona Spring Batch.

    "},{"location":"appendix/springbatch/filetodb/#step-y-job","title":"Step y Job","text":"

    Ahora ya podemos a\u00f1adir la configuraci\u00f3n del Step y del Job dentro de la clase de configuraci\u00f3n. La clase completa deber\u00eda quedar de esta forma:

    CategoryBatchConfiguration.java
    package com.ccsw.tutorialbatch.config;\n\n\nimport com.ccsw.tutorialbatch.model.Category;\nimport com.ccsw.tutorialbatch.processor.CategoryItemProcessor;\nimport com.ccsw.tutorialbatch.listener.JobCategoryCompletionNotificationListener;\nimport org.springframework.batch.core.Job;\nimport org.springframework.batch.core.Step;\nimport org.springframework.batch.core.job.builder.JobBuilder;\nimport org.springframework.batch.core.launch.support.RunIdIncrementer;\nimport org.springframework.batch.core.repository.JobRepository;\nimport org.springframework.batch.core.step.builder.StepBuilder;\nimport org.springframework.batch.item.ItemProcessor;\nimport org.springframework.batch.item.ItemReader;\nimport org.springframework.batch.item.ItemWriter;\nimport org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider;\nimport org.springframework.batch.item.database.builder.JdbcBatchItemWriterBuilder;\nimport org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;\nimport org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.io.ClassPathResource;\nimport org.springframework.transaction.PlatformTransactionManager;\n\nimport javax.sql.DataSource;\n\n@Configuration\npublic class CategoryBatchConfiguration {\n\n    @Bean\n    public ItemReader<Category> readerCategory() {\n        return new FlatFileItemReaderBuilder<Category>().name(\"categoryItemReader\")\n                .resource(new ClassPathResource(\"category-list.csv\"))\n                .delimited()\n                .names(new String[] { \"name\", \"type\", \"characteristics\" })\n                .fieldSetMapper(new BeanWrapperFieldSetMapper<>() {{\n                    setTargetType(Category.class);\n                }})\n                .build();\n    }\n\n    @Bean\n    public ItemProcessor<Category, Category> processorCategory() {\n\n        return new CategoryItemProcessor();\n    }\n\n    @Bean\n    public ItemWriter<Category> writerCategory(DataSource dataSource) {\n        return new JdbcBatchItemWriterBuilder<Category>()\n                .itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())\n                .sql(\"INSERT INTO category (name, type, characteristics) VALUES (:name, :type, :characteristics)\")\n                .dataSource(dataSource)\n                .build();\n    }\n\n    @Bean\n    public Step step1Category(JobRepository jobRepository, PlatformTransactionManager transactionManager, ItemReader<Category> readerCategory, ItemProcessor<Category, Category> processorCategory, ItemWriter<Category> writerCategory) {\n        return new StepBuilder(\"step1Category\", jobRepository)\n                .<Category, Category> chunk(10, transactionManager)\n                .reader(readerCategory)\n                .processor(processorCategory)\n                .writer(writerCategory)\n                .build();\n    }\n\n    @Bean\n    public Job jobCategory(JobRepository jobRepository, JobCategoryCompletionNotificationListener listener, Step step1Category) {\n        return new JobBuilder(\"jobCategory\", jobRepository)\n                .incrementer(new RunIdIncrementer())\n                .listener(listener)\n                .flow(step1Category)\n                .end()\n                .build();\n    }\n\n}\n
    • ItemReader: El bean del Reader que hemos creado anteriormente.
    • ItemProcessor: El bean del Processor que hemos creado anteriormente.
    • ItemWriter: El bean del Writer que hemos creado anteriormente.
    • Step: La creaci\u00f3n del Step se realiza mediante \u00e9l StepBuilder al que le definimos el tama\u00f1o del chunk que es el n\u00famero de elementos procesados por lote y le asignamos los tres beans creados previamente. En este caso solo vamos a tener un \u00fanico Step pero podr\u00edamos tener todos los que quisi\u00e9ramos.
    • Job: Finalmente, debemos definir \u00e9l Job que ser\u00e1 lo que se ejecute al lanzar nuestro proceso. La creaci\u00f3n se hace mediante el builder correspondiente como en el caso anterior. Se asigna el identificador de Job, el conjunto de steps, en este caso solo tenemos uno y finalmente el listener que es opcional y se crea en el siguiente paso.
    "},{"location":"appendix/springbatch/filetodb/#listener","title":"Listener","text":"

    Ahora, para verificar que nuestro proceso se ha ejecutado correctamente vamos a a\u00f1adir un Listener que al final de la ejecuci\u00f3n consultar\u00e1 que los datos se han insertado correctamente. Emplazamos \u00e9l Listener dentro del package com.ccsw.tutorialbatch.listener.

    JobCategoryCompletionNotificationListener.java
    package com.ccsw.tutorialbatch.listener;\n\n\nimport com.ccsw.tutorialbatch.model.Category;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.batch.core.BatchStatus;\nimport org.springframework.batch.core.JobExecution;\nimport org.springframework.batch.core.JobExecutionListener;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.jdbc.core.JdbcTemplate;\nimport org.springframework.stereotype.Component;\n\n@Component\npublic class JobCategoryCompletionNotificationListener implements JobExecutionListener {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(JobCategoryCompletionNotificationListener.class);\n\n    private final JdbcTemplate jdbcTemplate;\n\n    @Autowired\n    public JobCategoryCompletionNotificationListener(JdbcTemplate jdbcTemplate) {\n        this.jdbcTemplate = jdbcTemplate;\n    }\n\n    @Override\n    public void afterJob(JobExecution jobExecution) {\n        if (jobExecution.getStatus() == BatchStatus.COMPLETED) {\n            LOGGER.info(\"!!! JOB FINISHED! Time to verify the results\");\n\n            String query = \"SELECT name, type, characteristics FROM category\";\n            jdbcTemplate.query(query, (rs, row) -> new Category(rs.getString(1), rs.getString(2), rs.getString(3)))\n                .forEach(category -> LOGGER.info(\"Found < {} > in the database.\", category));\n        }\n    }\n}\n

    Para el listener implementamos la interface JobExecutionListener y sobreescribimos el m\u00e9todo afterJob que se ejecutara justo al terminar nuestro Job lanzando una consulta y mostrando el resultado.

    "},{"location":"appendix/springbatch/filetodb/#base-de-datos-y-fichero-carga","title":"Base de Datos y Fichero Carga","text":"

    Finalmente, debemos crear el fichero de inicializaci\u00f3n de base de datos con la tabla de categor\u00edas y crear el fichero que leeremos con los datos de las categor\u00edas que deseamos insertar.

    schema-all.sqlcategory-list.csv
    DROP TABLE category IF EXISTS;\n\nCREATE TABLE category  (\ncategory_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\nname VARCHAR(20),\ntype VARCHAR(20),\ncharacteristics VARCHAR(30)\n);\n
    Eurogames,Mechanics,Hard\nAmeritrash,Thematic,Mid\nFamiliar,Fillers,Easy\n
    "},{"location":"appendix/springbatch/filetodb/#pruebas","title":"Pruebas","text":"

    Ahora si arrancamos la aplicaci\u00f3n como cualquier aplicaci\u00f3n Spring Boot, podremos observar la traza de la ejecuci\u00f3n en nuestro log y comprobar que la ejecuci\u00f3n ha sido correcta y los registros se han insertado.

    Job: [FlowJob: [name=jobCategory]] launched with the following parameters: [{'run.id':'{value=1, type=class java.lang.Long, identifying=true}'}]\nExecuting step: [step1Category]\nConverting ( Category [name=Eurogames, type=Mechanics, characteristics=Hard] ) into ( Category [name=EUROGAMES, type=MECHANICS, characteristics=HARD] )\nConverting ( Category [name=Ameritrash, type=Thematic, characteristics=Mid] ) into ( Category [name=AMERITRASH, type=THEMATIC, characteristics=MID] )\nConverting ( Category [name=Familiar, type=Fillers, characteristics=Easy] ) into ( Category [name=FAMILIAR, type=FILLERS, characteristics=EASY] )\nStep: [step1Category] executed in 55ms\n!!! JOB FINISHED! Time to verify the results\nFound < Category [name=EUROGAMES, type=MECHANICS, characteristics=HARD] > in the database.\nFound < Category [name=AMERITRASH, type=THEMATIC, characteristics=MID] > in the database.\nFound < Category [name=FAMILIAR, type=FILLERS, characteristics=EASY] > in the database.\nJob: [FlowJob: [name=jobCategory]] completed with the following parameters: [{'run.id':'{value=1, type=class java.lang.Long, identifying=true}'}] and the following status: [COMPLETED] in 73ms\n
    "},{"location":"appendix/springbatch/filetofile/","title":"Autor - Spring Batch","text":"

    Ya tenemos todo configurado del paso anterior asi que proseguimos con el siguiente ejemplo.

    "},{"location":"appendix/springbatch/filetofile/#caso-de-uso","title":"Caso de Uso","text":"

    En este caso tambi\u00e9n debemos plantear requisitos diferentes para la parte de Autores.

    "},{"location":"appendix/springbatch/filetofile/#que-vamos-a-hacer","title":"\u00bfQu\u00e9 vamos a hacer?","text":"

    Vamos a implementar un batch para leer un fichero de Autores trasformar la nacionalidad del autor a c\u00f3digo de region y general un fichero con los datos trasformados.

    "},{"location":"appendix/springbatch/filetofile/#como-lo-vamos-a-hacer","title":"\u00bfC\u00f3mo lo vamos a hacer?","text":"

    Al igual que en el caso anterior seguiremos el esquema de funcionamiento habitual de un proceso batch que hemos visto en la parte de introducci\u00f3n:

    • ItemReader: Se va a leer de un fichero y convertir los registros le\u00eddos al modelo de Author.
    • ItemProcessor: Va a procesar todos los registros convirtiendo el c\u00f3digo de nacionalidad al formato xx_XX.
    • ItemWriter: Va a escribir los registros en un fichero.
    • Step: El paso que contiene los elementos que van a realizar la funcionalidad.
    • Job: La tarea que contiene los pasos definidos.
    "},{"location":"appendix/springbatch/filetofile/#codigo","title":"C\u00f3digo","text":""},{"location":"appendix/springbatch/filetofile/#modelo","title":"Modelo","text":"

    En primer lugar, vamos a crear el modelo dentro del package com.ccsw.tutorialbatch.model de la misma forma que en el ejemplo anterior.

    Author.java
    package com.ccsw.tutorialbatch.model;\n\npublic class Author {\n\n    private String name;\n    private String nationality;\n\n    public Author() {\n    }\n\n    public Author(String name, String nationality) {\n        this.name = name;\n        this.nationality = nationality;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getNationality() {\n        return nationality;\n    }\n\n    public void setNationality(String nationality) {\n        this.nationality = nationality;\n    }\n\n    @Override\n    public String toString() {\n        return \"Author [name=\" + getName() + \", nationality=\" + getNationality() + \"]\";\n    }\n\n}\n
    "},{"location":"appendix/springbatch/filetofile/#reader","title":"Reader","text":"

    Ahora, como en el caso anterior, emplazamos \u00e9l Reader en la clase donde posteriormente a\u00f1adiremos la configuraci\u00f3n junto al resto de beans, dentro del package com.ccsw.tutorialbatch.config.

    AuthorBatchConfiguration.java
    package com.ccsw.tutorialbatch.config;\n\n...\n\n@Configuration\npublic class AuthorBatchConfiguration {\n\n    @Bean\n    public ItemReader<Author> readerAuthor() {\n        return new FlatFileItemReaderBuilder<Author>().name(\"authorItemReader\")\n                .resource(new ClassPathResource(\"author-list.csv\"))\n                .delimited()\n                .names(new String[] { \"name\", \"nationality\" })\n                .fieldSetMapper(new BeanWrapperFieldSetMapper<>() {{\n                    setTargetType(Author.class);\n                }})\n                .build();\n    }\n

    Para la ingesta de datos vamos a hacer uso de este FlatFileItemReader que nos proporciona Spring Batch. Como se puede observar se le proporciona el fichero a leer y el mapeo a la clase que deseamos. Aqu\u00ed el cat\u00e1logo de Readers que proporciona Spring Batch.

    "},{"location":"appendix/springbatch/filetofile/#processor","title":"Processor","text":"

    Posteriormente, emplazamos \u00e9l Processor dentro del package com.ccsw.tutorialbatch.processor.

    AuthorItemProcessor.java
    package com.ccsw.tutorialbatch.processor;\n\n\nimport com.ccsw.tutorialbatch.model.Author;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.batch.item.ItemProcessor;\n\n\npublic class AuthorItemProcessor implements ItemProcessor<Author, Author> {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(AuthorItemProcessor.class);\n\n    @Override\n    public Author process(final Author author) {\n        String name = author.getName();\n        String nationality = author.getNationality().toLowerCase() + \"_\" + author.getNationality().toUpperCase();\n\n        Author transformedAuthor = new Author(name, nationality);\n        LOGGER.info(\"Converting ( {} ) into ( {} )\", author, transformedAuthor);\n\n        return transformedAuthor;\n    }\n}\n

    De la misma forma que en el caso anterior hemos implementado un Processor personalizado, esta clase implementa ItemProcessor donde especificamos de qu\u00e9 clase a qu\u00e9 clase se va a realizar la trasformaci\u00f3n.

    En nuestro caso, va a ser de Author a Author donde vamos a implementar la l\u00f3gica requerida para este caso de uso.

    "},{"location":"appendix/springbatch/filetofile/#writer","title":"Writer","text":"

    Posteriormente, a\u00f1adimos el writer a la clase de configuraci\u00f3n AuthorBatchConfiguration donde ya hab\u00edamos a\u00f1adido Reader.

    AuthorBatchConfiguration.java
    package com.ccsw.tutorialbatch.config;\n\n...\n\n@Configuration\npublic class AuthorBatchConfiguration {\n\n    ...\n\n    @Bean\n    public ItemWriter<Author> writerAuthor() {\n        return  new FlatFileItemWriterBuilder<Author>().name(\"writerAuthor\")\n                .resource(new FileSystemResource(\"target/test-outputs/author-output.txt\"))\n                .lineAggregator(new PassThroughLineAggregator<>())\n                .build();\n    }\n\n}\n

    A diferencia del ejemplo anterior utilizamos FlatFileItemWriter diferente que en este caso nos ayuda a crear un fichero con los datos deseados. Aqu\u00ed el cat\u00e1logo de Writers que proporciona Spring Batch.

    "},{"location":"appendix/springbatch/filetofile/#step-y-job","title":"Step y Job","text":"

    Ahora ya podemos a\u00f1adir la configuraci\u00f3n del Step y del Job dentro de la clase de configuraci\u00f3n. La clase completa deber\u00eda quedar de esta forma:

    AuthorBatchConfiguration.java
    package com.ccsw.tutorialbatch.config;\n\n\nimport com.ccsw.tutorialbatch.model.Author;\nimport com.ccsw.tutorialbatch.processor.AuthorItemProcessor;\nimport org.springframework.batch.core.Job;\nimport org.springframework.batch.core.Step;\nimport org.springframework.batch.core.job.builder.JobBuilder;\nimport org.springframework.batch.core.launch.support.RunIdIncrementer;\nimport org.springframework.batch.core.repository.JobRepository;\nimport org.springframework.batch.core.step.builder.StepBuilder;\nimport org.springframework.batch.item.ItemProcessor;\nimport org.springframework.batch.item.ItemReader;\nimport org.springframework.batch.item.ItemWriter;\nimport org.springframework.batch.item.file.FlatFileItemReader;\nimport org.springframework.batch.item.file.FlatFileItemWriter;\nimport org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;\nimport org.springframework.batch.item.file.builder.FlatFileItemWriterBuilder;\nimport org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;\nimport org.springframework.batch.item.file.transform.PassThroughLineAggregator;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.io.ClassPathResource;\nimport org.springframework.core.io.FileSystemResource;\nimport org.springframework.transaction.PlatformTransactionManager;\n\n@Configuration\npublic class AuthorBatchConfiguration {\n\n    @Bean\n    public ItemReader<Author> readerAuthor() {\n        return new FlatFileItemReaderBuilder<Author>().name(\"authorItemReader\")\n                .resource(new ClassPathResource(\"author-list.csv\"))\n                .delimited()\n                .names(new String[] { \"name\", \"nationality\" })\n                .fieldSetMapper(new BeanWrapperFieldSetMapper<>() {{\n                    setTargetType(Author.class);\n                }})\n                .build();\n    }\n\n    @Bean\n    public ItemProcessor<Author, Author> processorAuthor() {\n\n        return new AuthorItemProcessor();\n    }\n\n    @Bean\n    public ItemWriter<Author> writerAuthor() {\n        return  new FlatFileItemWriterBuilder<Author>().name(\"writerAuthor\")\n                .resource(new FileSystemResource(\"target/test-outputs/author-output.txt\"))\n                .lineAggregator(new PassThroughLineAggregator<>())\n                .build();\n    }\n\n    @Bean\n    public Step step1Author(JobRepository jobRepository, PlatformTransactionManager transactionManager, ItemReader<Author> readerAuthor, ItemProcessor<Author, Author> processorAuthor, ItemWriter<Author> writerAuthor) {\n        return new StepBuilder(\"step1Author\", jobRepository)\n                .<Author, Author> chunk(10, transactionManager)\n                .reader(readerAuthor)\n                .processor(processorAuthor)\n                .writer(writerAuthor)\n                .build();\n    }\n\n    @Bean\n    public Job jobAuthor(JobRepository jobRepository, Step step1Author) {\n        return new JobBuilder(\"jobAuthor\", jobRepository)\n                .incrementer(new RunIdIncrementer())\n                .flow(step1Author)\n                .end()\n                .build();\n    }\n\n}\n
    • ItemReader: El bean del Reader que hemos creado anteriormente.
    • ItemProcessor: El bean del Processor que hemos creado anteriormente.
    • ItemWriter: El bean del Writer que hemos creado anteriormente.
    • Step: La creaci\u00f3n del Step se realiza mediante \u00e9l StepBuilder al que le definimos el tama\u00f1o del chunk que es el n\u00famero de elementos procesados por lote y le asignamos los tres beans creados previamente. En este caso solo vamos a tener un \u00fanico Step pero podr\u00edamos tener todos los que quisi\u00e9ramos.
    • Job: Finalmente, debemos definir \u00e9l Job que ser\u00e1 lo que se ejecute al lanzar nuestro proceso. La creaci\u00f3n se hace mediante el builder correspondiente como en el caso anterior. Se asigna el identificador de Job, el conjunto de steps, en este caso solo tenemos uno. En este caso no necesitamos un listener, ya que para verificar el resultado podemos ver el archivo generado.
    "},{"location":"appendix/springbatch/filetofile/#fichero-carga","title":"Fichero Carga","text":"

    Finalmente, debemos crear el fichero que leeremos con los datos de los autores que deseamos procesar.

    author-list.csv
    Alan R. Moon,US\nVital Lacerda,PT\nSimone Luciani,IT\nPerepau Llistosella,ES\nMichael Kiesling,DE\nPhil Walker-Harding,US\n
    "},{"location":"appendix/springbatch/filetofile/#pruebas","title":"Pruebas","text":"

    Ahora ya tenemos dos Jobs en nuestro batch por lo que debemos especificar en el arranque cual queremos ejecutar.

    Esto se realiza pasando una VM option en el arranque de la aplicaci\u00f3n:

    -Dspring.batch.job.name=jobAuthor\n
    \u00f3
    -Dspring.batch.job.name=jobCtegory\n

    Hecho esto y ejecutado el batch, podremos ver la traza de la ejecuci\u00f3n en nuestro log y el fichero generado en el target del proyecto:

    Job: [FlowJob: [name=jobAuthor]] launched with the following parameters: [{'run.id':'{value=1, type=class java.lang.Long, identifying=true}'}]\nExecuting step: [step1Author]\nConverting ( Author [name=Alan R. Moon, nationality=US] ) into ( Author [name=Alan R. Moon, nationality=us_US] )\nConverting ( Author [name=Vital Lacerda, nationality=PT] ) into ( Author [name=Vital Lacerda, nationality=pt_PT] )\nConverting ( Author [name=Simone Luciani, nationality=IT] ) into ( Author [name=Simone Luciani, nationality=it_IT] )\nConverting ( Author [name=Perepau Llistosella, nationality=ES] ) into ( Author [name=Perepau Llistosella, nationality=es_ES] )\nConverting ( Author [name=Michael Kiesling, nationality=DE] ) into ( Author [name=Michael Kiesling, nationality=de_DE] )\nConverting ( Author [name=Phil Walker-Harding, nationality=US] ) into ( Author [name=Phil Walker-Harding, nationality=us_US] )\nStep: [step1Author] executed in 50ms\nJob: [FlowJob: [name=jobAuthor]] completed with the following parameters: [{'run.id':'{value=1, type=class java.lang.Long, identifying=true}'}] and the following status: [COMPLETED] in 67ms\n
    author-output.txt
    Author [name=Alan R. Moon, nationality=us_US]\nAuthor [name=Vital Lacerda, nationality=pt_PT]\nAuthor [name=Simone Luciani, nationality=it_IT]\nAuthor [name=Perepau Llistosella, nationality=es_ES]\nAuthor [name=Michael Kiesling, nationality=de_DE]\nAuthor [name=Phil Walker-Harding, nationality=us_US]\n
    "},{"location":"appendix/springbatch/intro/","title":"Introducci\u00f3n Batch - Spring Batch","text":""},{"location":"appendix/springbatch/intro/#que-son-los-procesos-batch","title":"Que son los procesos batch?","text":"

    El proceso batch o procesamiento por lotes es un proceso por el cual un sistema realiza procesos, muchas veces de forma simult\u00e1nea, de forma continuada y secuencial.

    Normalmente, este tipo de procesos se dividen en peque\u00f1as partes que se realizan de forma cont\u00ednua consiguiendo un mejor rendimiento.

    "},{"location":"appendix/springbatch/intro/#spring-batch","title":"Spring Batch","text":"

    Existente multiples soluciones para implementar procesos batch, en nuestro caso vamos a utilizar la soluci\u00f3n que nos ofrece Spring Framework y que est\u00e1 incluido dentro del m\u00f3dulo Spring Batch.

    Spring Batch es framework de procesos batch ligero y completo dise\u00f1ado para permitir el desarrollo de aplicaciones por lotes robustas, vitales para las operaciones diarias de los sistemas empresariales.

    Proporciona funciones reutilizables que son esenciales en el procesamiento de grandes vol\u00famenes de registros, incluyendo trazabilidad, gesti\u00f3n de transacciones, estad\u00edsticas de procesamiento de trabajos, reinicio de trabajos, omisi\u00f3n y gesti\u00f3n de recursos. Tambi\u00e9n proporciona servicios y funcionalidades m\u00e1s avanzadas que permitir\u00e1n realizar procesos batch de gran volumen y alto rendimiento mediante t\u00e9cnicas de optimizaci\u00f3n y partici\u00f3n.

    "},{"location":"appendix/springbatch/intro/#estructura","title":"Estructura","text":"
    • JobLauncher: Esta pieza es la encargada de la gesti\u00f3n de ejecuciones de los distintos Jobs que componen nuestro sistema. En nuestro ejemplo no vamos a utilizarla, ya que lanzaremos los procesos manualmente para simplificar el c\u00f3digo, pero pod\u00e9is consultar el detalle en la documentaci\u00f3n.
    • JobRepository: Se trata del repositorio que almacena informaci\u00f3n sobre cada Job y los datos de su ejecuci\u00f3n necesario para mantener la trazabilidad del sistema. Para m\u00e1s informaci\u00f3n consultar la documentaci\u00f3n.
    • Job: Se trata de la entidad principal de un proceso batch y es un bloque que contiene uno o varios steps que conforman el proceso a ejecutar.
    • Step: Un Step, como su nombre indica, es un paso en la ejecuci\u00f3n de un Job el cual contiene la l\u00f3gica de negocio de un determinado caso de uso. Un Step habitualmente est\u00e1 formado por un ItemReader, ItemProcessor y ItemWriter o por un Tasklet. La primera opci\u00f3n es relativa a la ejecuci\u00f3n normal de un batch donde asociamos el tama\u00f1o del lote y el procesado es en funci\u00f3n de esta configuraci\u00f3n. Esta es la opci\u00f3n que deber\u00edamos usar en la mayor\u00eda de los casos, mientras que la opci\u00f3n de Tasklet esta reservada para cuando necesitamos realizar operaciones de forma at\u00f3mica.
    • ItemReader: Se trata de la ingesta de datos para un determinado Step. Se puede realizar de forma manual o con los Readers que proporciona Spring Batch.
    • ItemProcessor: En esta pieza se realizan todas las trasformaciones de datos que contenga nuestra l\u00f3gica de negocio.
    • ItemWriter: Es la producci\u00f3n de datos por determinado Step. Se puede realizar de forma manual o con los Writers que proporciona Spring Batch.
    • Tasklet: En los casos que no deseemos realizar ingestas, trasformaci\u00f3n y producci\u00f3n de datos para realizar funcionalidades de forma at\u00f3mica tenemos disponibles los Tasklet.
    "},{"location":"appendix/springbatch/intro/#contexto-de-la-aplicacion","title":"Contexto de la aplicaci\u00f3n","text":"

    Llegados a este punto, \u00bfqu\u00e9 es lo que vamos a hacer en los siguientes pasos?. Bas\u00e1ndonos en el ejemplo del tutorial y en el Contexto de la aplicaci\u00f3n vamos a reinventar nuestros requisitos para poder resolver las problem\u00e1ticas con procesos batch.

    Ya deber\u00edamos tener claros los conceptos y los actores que compondr\u00e1n nuestro sistema, as\u00ed que, all\u00e1 vamos!!!

    "},{"location":"appendix/springbatch/summary/","title":"Resumen Batch - Spring Batch","text":""},{"location":"appendix/springbatch/summary/#que-hemos-hecho","title":"\u00bfQu\u00e9 hemos hecho?","text":"

    Llegados a este punto, ya has podido ver que los procesos batch tienen una filosof\u00eda muy diferente a una aplicaci\u00f3n Spring Boot corriente, ya que el objetivo de los procesos est\u00e1 enfocado en procesado de datos y realizaci\u00f3n de tareas recurrentes.

    En definitiva, lo que hemos implementado ha sido:

    • Lectura de fichero y persistencia en BBDD: Este ha sido el primer ejemplo donde hemos visto la estructura b\u00e1sica de un batch y hemos hecho uso de las herramientas que nos proporciona para realizar tareas complejas de forma sencilla.

    • Lectura de fichero y persistencia en fichero: Ejemplo similar al anterior para ilustrar la existencia de otro Writer y su utilizaci\u00f3n.

    • Limpieza: Puesta en escena de la utilizaci\u00f3n de Tasklet que nos permite realizar operaciones at\u00f3micas que no requieran lectura, procesado y escritura para abarcar todo el espectro de posibles requisitos para implementar un proceso.

    "},{"location":"appendix/springbatch/summary/#consideraciones","title":"Consideraciones","text":"

    En estos ejemplos hemos realizado la implementaci\u00f3n lo m\u00e1s sencilla posible de un proceso batch con Spring Batch y aunque no dista mucho de una implementaci\u00f3n para un proyecto real, aqu\u00ed un par de consideraciones a tener en cuenta:

    • Estructura: A diferencia de Spring Boot no existe un convenio unificado de organizaci\u00f3n de clases y paquetes por lo que se puede ver de muchas formas diferentes. Aqu\u00ed lo importante es que si se utiliza en un determinado proyecto, se debe respetar su estructura por homogeneidad y mantenibilidad del mismo.

    • Ejecuci\u00f3n: La ejecuci\u00f3n de los procesos normalmente se delega en herramientas externas para su programaci\u00f3n y ejecuci\u00f3n. Esto var\u00eda mucho en funci\u00f3n de la arquitectura que tenga implementada un determinado cliente.

    Y como siempre, para tener la informaci\u00f3n m\u00e1s actualizada, acude a la documentaci\u00f3n oficial de Spring Batch.

    "},{"location":"appendix/springbatch/summary/#siguientes-pasos","title":"Siguientes pasos","text":"

    Ahora te propongo hacer un peque\u00f1o ejercicio para poner aprueba si los conceptos se han consolidado. Puedes realizarlo en el punto Ahora hazlo t\u00fa!

    "},{"location":"appendix/springcloud/basic/","title":"Listado simple - Spring Boot","text":"

    A diferencia del tutorial b\u00e1sico de Spring Boot, donde constru\u00edamos una aplicaci\u00f3n monol\u00edtica, ahora vamos a construir multiples servicios por lo que necesitamos crear proyectos separados.

    Para la creaci\u00f3n de proyecto nos remitimos a la gu\u00eda de instalaci\u00f3n donde se detalla el proceso de creaci\u00f3n de nuevo proyecto Entorno de desarrollo

    Todos los pasos son exactamente iguales, lo \u00fanico que va a variar es el nombre de nuestro proyecto, que en este caso se va a llamar tutorial-category. El campo que debemos modificar es artifact en Spring Initilizr, el resto de campos se cambiaran autom\u00e1ticamente.

    "},{"location":"appendix/springcloud/basic/#estructurar-el-codigo-y-buenas-practicas","title":"Estructurar el c\u00f3digo y buenas pr\u00e1cticas","text":"

    Esta parte de tutorial es una ampliaci\u00f3n de la parte de backend con Spring Boot, por tanto, no se ve a enfocar en las partes b\u00e1sicas aprendidas previamente, sino que se va a explicar el funcionamiento de los micro servicios aplicados al mismo caso de uso.

    Para cualquier duda sobre la estructura del c\u00f3digo y buenas pr\u00e1cticas, consultar el apartado de Estructura y buenas pr\u00e1cticas, ya que aplican a este caso en el mismo modo.

    "},{"location":"appendix/springcloud/basic/#codigo","title":"C\u00f3digo","text":"

    Dado de vamos a implementar el micro servicio Spring Boot de Categor\u00edas, vamos a respetar la misma estructura del Listado simple de la version monol\u00edtica.

    "},{"location":"appendix/springcloud/basic/#entity-y-dto","title":"Entity y Dto","text":"

    En primer lugar, vamos a crear la entidad y el DTO dentro del package com.ccsw.tutorialcategory.category.model. Ojo al package que lo hemos renombrado con respecto al listado monol\u00edtico.

    Category.javaCategoryDto.java
    package com.ccsw.tutorialcategory.category.model;\n\nimport jakarta.persistence.*;\n\n/**\n * @author ccsw\n *\n */\n@Entity\n@Table(name = \"category\")\npublic class Category {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"id\", nullable = false)\n    private Long id;\n\n    @Column(name = \"name\", nullable = false)\n    private String name;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return name\n     */\n    public String getName() {\n\n        return this.name;\n    }\n\n    /**\n     * @param name new value of {@link #getName}.\n     */\n    public void setName(String name) {\n\n        this.name = name;\n    }\n\n}\n
    package com.ccsw.tutorialcategory.category.model;\n\n/**\n * @author ccsw\n *\n */\npublic class CategoryDto {\n\n    private Long id;\n\n    private String name;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return name\n     */\n    public String getName() {\n\n        return this.name;\n    }\n\n    /**\n     * @param name new value of {@link #getName}.\n     */\n    public void setName(String name) {\n\n        this.name = name;\n    }\n\n}\n
    "},{"location":"appendix/springcloud/basic/#repository-service-y-controller","title":"Repository, Service y Controller","text":"

    Posteriormente, emplazamos el resto de clases dentro del package com.ccsw.tutorialcategory.category.

    CategoryRepository.javaCategoryService.javaCategoryServiceImpl.javaCategoryController.java
    package com.ccsw.tutorialcategory.category;\n\nimport com.ccsw.tutorialcategory.category.model.Category;\nimport org.springframework.data.repository.CrudRepository;\n\n/**\n * @author ccsw\n *\n */\npublic interface CategoryRepository extends CrudRepository<Category, Long> {\n\n}\n
    package com.ccsw.tutorialcategory.category;\n\n\nimport com.ccsw.tutorialcategory.category.model.Category;\nimport com.ccsw.tutorialcategory.category.model.CategoryDto;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\npublic interface CategoryService {\n\n    /**\n     * Recupera una {@link Category} a partir de su ID\n     *\n     * @param id PK de la entidad\n     * @return {@link Category}\n     */\n    Category get(Long id);\n\n    /**\n     * M\u00e9todo para recuperar todas las {@link Category}\n     *\n     * @return {@link List} de {@link Category}\n     */\n    List<Category> findAll();\n\n    /**\n     * M\u00e9todo para crear o actualizar una {@link Category}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    void save(Long id, CategoryDto dto);\n\n    /**\n     * M\u00e9todo para borrar una {@link Category}\n     *\n     * @param id PK de la entidad\n     */\n    void delete(Long id) throws Exception;\n\n}\n
    package com.ccsw.tutorialcategory.category;\n\nimport com.ccsw.tutorialcategory.category.model.Category;\nimport com.ccsw.tutorialcategory.category.model.CategoryDto;\nimport jakarta.transaction.Transactional;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Service\n@Transactional\npublic class CategoryServiceImpl implements CategoryService {\n\n    @Autowired\n    CategoryRepository categoryRepository;\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public Category get(Long id) {\n\n        return this.categoryRepository.findById(id).orElse(null);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public List<Category> findAll() {\n\n        return (List<Category>) this.categoryRepository.findAll();\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void save(Long id, CategoryDto dto) {\n\n        Category category;\n\n        if (id == null) {\n            category = new Category();\n        } else {\n            category = this.get(id);\n        }\n\n        category.setName(dto.getName());\n\n        this.categoryRepository.save(category);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void delete(Long id) throws Exception {\n\n        if(this.get(id) == null){\n            throw new Exception(\"Not exists\");\n        }\n\n        this.categoryRepository.deleteById(id);\n    }\n\n}\n
    package com.ccsw.tutorialcategory.category;\n\nimport com.ccsw.tutorialcategory.category.model.Category;\nimport com.ccsw.tutorialcategory.category.model.CategoryDto;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.modelmapper.ModelMapper;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * @author ccsw\n *\n */\n@Tag(name = \"Category\", description = \"API of Category\")\n@RequestMapping(value = \"/category\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class CategoryController {\n\n    @Autowired\n    CategoryService categoryService;\n\n    @Autowired\n    ModelMapper mapper;\n\n    /**\n     * M\u00e9todo para recuperar todas las {@link Category}\n     *\n     * @return {@link List} de {@link CategoryDto}\n     */\n    @Operation(summary = \"Find\", description = \"Method that return a list of Categories\"\n    )\n    @RequestMapping(path = \"\", method = RequestMethod.GET)\n    public List<CategoryDto> findAll() {\n\n        List<Category> categories = this.categoryService.findAll();\n\n        return categories.stream().map(e -> mapper.map(e, CategoryDto.class)).collect(Collectors.toList());\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar una {@link Category}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Category\"\n    )\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody CategoryDto dto) {\n\n        this.categoryService.save(id, dto);\n    }\n\n    /**\n     * M\u00e9todo para borrar una {@link Category}\n     *\n     * @param id PK de la entidad\n     */\n    @Operation(summary = \"Delete\", description = \"Method that deletes a Category\")\n    @RequestMapping(path = \"/{id}\", method = RequestMethod.DELETE)\n    public void delete(@PathVariable(\"id\") Long id) throws Exception {\n\n        this.categoryService.delete(id);\n    }\n\n}\n
    "},{"location":"appendix/springcloud/basic/#sql-y-configuracion","title":"SQL y Configuraci\u00f3n","text":"

    Finalmente, debemos crear el mismo fichero de inicializaci\u00f3n de base de datos con solo los datos de categor\u00edas y modificar ligeramente la configuraci\u00f3n inicial para a\u00f1adir un puerto manualmente. Esto es necesario ya que vamos a levantar varios servicios simult\u00e1neamente y necesitaremos levantarlos en puertos diferentes para que no colisionen entre ellos.

    data.sqlapplication.properties
    INSERT INTO category(name) VALUES ('Eurogames');\nINSERT INTO category(name) VALUES ('Ameritrash');\nINSERT INTO category(name) VALUES ('Familiar');\n
    server.port=8091\n\n#Database\nspring.datasource.url=jdbc:h2:mem:testdb\nspring.datasource.username=sa\nspring.datasource.password=sa\nspring.datasource.driver-class-name=org.h2.Driver\n\nspring.jpa.database-platform=org.hibernate.dialect.H2Dialect\nspring.jpa.defer-datasource-initialization=true\nspring.jpa.show-sql=true\n\nspring.h2.console.enabled=true\n
    "},{"location":"appendix/springcloud/basic/#pruebas","title":"Pruebas","text":"

    Ahora si arrancamos la aplicaci\u00f3n server y abrimos el Postman podemos realizar las mismas pruebas del apartado de Listado simple pero esta vez apuntado al puerto 8091.

    "},{"location":"appendix/springcloud/basic/#siguientes-pasos","title":"Siguientes pasos","text":"

    Con esto ya tendr\u00edamos nuestro primer servicio separado. Podr\u00edamos conectar el frontend a este servicio, pero a medida que nuestra aplicaci\u00f3n creciera en n\u00famero de servicios ser\u00eda un poco engorroso todo, as\u00ed que todav\u00eda no lo vamos a conectar hasta que no tengamos toda la infraestructura.

    Vamos a convertir en micro servicio el siguiente listado.

    "},{"location":"appendix/springcloud/filtered/","title":"Listado filtrado - Spring Boot","text":"

    Al igual que en los caos anteriores vamos a crear un nuevo proyecto que contendr\u00e1 un nuevo micro servicio.

    Para la creaci\u00f3n de proyecto nos remitimos a la gu\u00eda de instalaci\u00f3n donde se detalla el proceso de creaci\u00f3n de nuevo proyecto Entorno de desarrollo

    Todos los pasos son exactamente iguales, lo \u00fanico que va a variar, es el nombre de nuestro proyecto, que en este caso se va a llamar tutorial-game. El campo que debemos modificar es artifact en Spring Initilizr, el resto de campos se cambiaran autom\u00e1ticamente.

    "},{"location":"appendix/springcloud/filtered/#codigo","title":"C\u00f3digo","text":"

    Dado de vamos a implementar el micro servicio Spring Boot de Juegos, vamos a respetar la misma estructura del Listado filtrado de la version monol\u00edtica.

    "},{"location":"appendix/springcloud/filtered/#criteria","title":"Criteria","text":"

    En primer lugar, vamos a a\u00f1adir la clase que necesitamos para realizar el filtrado y vimos en la version monol\u00edtica del tutorial en el package com.ccsw.tutorialgame.common.criteria.

    SearchCriteria.java
    package com.ccsw.tutorialgame.common.criteria;\n\npublic class SearchCriteria {\n\n    private String key;\n    private String operation;\n    private Object value;\n\n    public SearchCriteria(String key, String operation, Object value) {\n\n        this.key = key;\n        this.operation = operation;\n        this.value = value;\n    }\n\n    public String getKey() {\n        return key;\n    }\n\n    public void setKey(String key) {\n        this.key = key;\n    }\n\n    public String getOperation() {\n        return operation;\n    }\n\n    public void setOperation(String operation) {\n        this.operation = operation;\n    }\n\n    public Object getValue() {\n        return value;\n    }\n\n    public void setValue(Object value) {\n        this.value = value;\n    }\n\n}\n
    "},{"location":"appendix/springcloud/filtered/#entity-y-dto","title":"Entity y Dto","text":"

    Seguimos con la entidad y el DTO dentro del package com.ccsw.tutorialgame.game.model. En este punto, f\u00edjate que nuestro modelo de Entity no tiene relaci\u00f3n con la tabla Author ni Category ya que estos dos objetos no pertenecen a nuestro dominio y se gestionan desde otro micro servicio. Lo que tendremos ahora ser\u00e1 el identificador del registro que hace referencia a esos objetos. Ya no usaremos @JoinColumn porque en nuestro modelo no existen esas tablas relacionadas.

    Sin embargo el Dto si que utiliza relaciones, ya que son relaciones de negocio (en el Service) y no son relaciones de dominio (en BBDD o Repository)

    Game.javaGameDto.java
    package com.ccsw.tutorialgame.game.model;\n\nimport jakarta.persistence.*;\n\n\n/**\n * @author ccsw\n *\n */\n@Entity\n@Table(name = \"game\")\npublic class Game {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"id\", nullable = false)\n    private Long id;\n\n    @Column(name = \"title\", nullable = false)\n    private String title;\n\n    @Column(name = \"age\", nullable = false)\n    private String age;\n\n    @Column(name = \"category_id\", nullable = false)\n    private Long idCategory;\n\n    @Column(name = \"author_id\", nullable = false)\n    private Long idAuthor;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return title\n     */\n    public String getTitle() {\n\n        return this.title;\n    }\n\n    /**\n     * @param title new value of {@link #getTitle}.\n     */\n    public void setTitle(String title) {\n\n        this.title = title;\n    }\n\n    /**\n     * @return age\n     */\n    public String getAge() {\n\n        return this.age;\n    }\n\n    /**\n     * @param age new value of {@link #getAge}.\n     */\n    public void setAge(String age) {\n\n        this.age = age;\n    }\n\n    /**\n     * @return idCategory\n     */\n    public Long getIdCategory() {\n\n        return this.idCategory;\n    }\n\n    /**\n     * @param idCategory new value of {@link #getIdCategory}.\n     */\n    public void setIdCategory(Long idCategory) {\n\n        this.idCategory = idCategory;\n    }\n\n    /**\n     * @return idAuthor\n     */\n    public Long getIdAuthor() {\n\n        return this.idAuthor;\n    }\n\n    /**\n     * @param idAuthor new value of {@link #getIdAuthor}.\n     */\n    public void setIdAuthor(Long idAuthor) {\n\n        this.idAuthor = idAuthor;\n    }\n\n}\n
    package com.ccsw.tutorialgame.game.model;\n\n\nimport com.ccsw.tutorialgame.author.model.AuthorDto;\nimport com.ccsw.tutorialgame.category.model.CategoryDto;\n\n/**\n * @author ccsw\n *\n */\npublic class GameDto {\n\n    private Long id;\n\n    private String title;\n\n    private String age;\n\n    private Long idCategory;\n\n    private Long idAuthor;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return title\n     */\n    public String getTitle() {\n\n        return this.title;\n    }\n\n    /**\n     * @param title new value of {@link #getTitle}.\n     */\n    public void setTitle(String title) {\n\n        this.title = title;\n    }\n\n    /**\n     * @return age\n     */\n    public String getAge() {\n\n        return this.age;\n    }\n\n    /**\n     * @param age new value of {@link #getAge}.\n     */\n    public void setAge(String age) {\n\n        this.age = age;\n    }\n\n    /**\n     * @return idCategory\n     */\n    public Long getIdCategory() {\n\n        return this.idCategory;\n    }\n\n    /**\n     * @param idCategory new value of {@link #getIdCategory}.\n     */\n    public void setIdCategory(Long idCategory) {\n\n        this.idCategory = idCategory;\n    }\n\n    /**\n     * @return idAuthor\n     */\n    public Long getIdAuthor() {\n\n        return this.idAuthor;\n    }\n\n    /**\n     * @param idAuthor new value of {@link #getIdAuthor}.\n     */\n    public void setIdAuthor(Long idAuthor) {\n\n        this.idAuthor = idAuthor;\n    }\n\n}\n
    "},{"location":"appendix/springcloud/filtered/#repository-service-controller","title":"Repository, Service, Controller","text":"

    Posteriormente, emplazamos el resto de clases dentro del package com.ccsw.tutorialgame.game.

    GameRepository.javaGameService.javaGameSpecification.javaGameServiceImpl.javaGameController.java
    package com.ccsw.tutorialgame.game;\n\nimport com.ccsw.tutorialgame.game.model.Game;\nimport org.springframework.data.jpa.repository.JpaSpecificationExecutor;\nimport org.springframework.data.repository.CrudRepository;\n\n/**\n * @author ccsw\n *\n */\npublic interface GameRepository extends CrudRepository<Game, Long>, JpaSpecificationExecutor<Game> {\n\n}\n
    package com.ccsw.tutorialgame.game;\n\nimport com.ccsw.tutorialgame.game.model.Game;\nimport com.ccsw.tutorialgame.game.model.GameDto;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\npublic interface GameService {\n\n    /**\n     * Recupera los juegos filtrando opcionalmente por t\u00edtulo y/o categor\u00eda\n     *\n     * @param title t\u00edtulo del juego\n     * @param idCategory PK de la categor\u00eda\n     * @return {@link List} de {@link Game}\n     */\n    List<Game> find(String title, Long idCategory);\n\n    /**\n     * Guarda o modifica un juego, dependiendo de si el identificador est\u00e1 o no informado\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    void save(Long id, GameDto dto);\n\n}\n
    package com.ccsw.tutorialgame.game;\n\nimport com.ccsw.tutorialgame.common.criteria.SearchCriteria;\nimport com.ccsw.tutorialgame.game.model.Game;\nimport jakarta.persistence.criteria.*;\nimport org.springframework.data.jpa.domain.Specification;\n\n\npublic class GameSpecification implements Specification<Game> {\n\n    private static final long serialVersionUID = 1L;\n\n    private final SearchCriteria criteria;\n\n    public GameSpecification(SearchCriteria criteria) {\n\n        this.criteria = criteria;\n    }\n\n    @Override\n    public Predicate toPredicate(Root<Game> root, CriteriaQuery<?> query, CriteriaBuilder builder) {\n        if (criteria.getOperation().equalsIgnoreCase(\":\") && criteria.getValue() != null) {\n            Path<String> path = getPath(root);\n            if (path.getJavaType() == String.class) {\n                return builder.like(path, \"%\" + criteria.getValue() + \"%\");\n            } else {\n                return builder.equal(path, criteria.getValue());\n            }\n        }\n        return null;\n    }\n\n    private Path<String> getPath(Root<Game> root) {\n        String key = criteria.getKey();\n        String[] split = key.split(\"[.]\", 0);\n\n        Path<String> expression = root.get(split[0]);\n        for (int i = 1; i < split.length; i++) {\n            expression = expression.get(split[i]);\n        }\n\n        return expression;\n    }\n\n}\n
    package com.ccsw.tutorialgame.game;\n\nimport com.ccsw.tutorialgame.common.criteria.SearchCriteria;\nimport com.ccsw.tutorialgame.game.model.Game;\nimport com.ccsw.tutorialgame.game.model.GameDto;\nimport jakarta.transaction.Transactional;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.jpa.domain.Specification;\nimport org.springframework.stereotype.Service;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Service\n@Transactional\npublic class GameServiceImpl implements GameService {\n\n    @Autowired\n    GameRepository gameRepository;\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public List<Game> find(String title, Long idCategory) {\n\n        GameSpecification titleSpec = new GameSpecification(new SearchCriteria(\"title\", \":\", title));\n        GameSpecification categorySpec = new GameSpecification(new SearchCriteria(\"idCategory\", \":\", idCategory));\n\n        Specification<Game> spec = Specification.where(titleSpec).and(categorySpec);\n        // Desde la versi\u00f3n 3.5.0 de Spring Boot, la nueva manera es\n        Specification<Game> spec = titleSpec.and(categorySpec);\n\n        return this.gameRepository.findAll(spec);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void save(Long id, GameDto dto) {\n\n        Game game;\n\n        if (id == null) {\n            game = new Game();\n        } else {\n            game = this.gameRepository.findById(id).orElse(null);\n        }\n\n        BeanUtils.copyProperties(dto, game, \"id\");\n\n        game.setIdAuthor(dto.getIdAuthor());\n        game.setIdCategory(dto.getIdCategory());\n\n        this.gameRepository.save(game);\n    }\n\n}\n
    package com.ccsw.tutorialgame.game;\n\nimport com.ccsw.tutorialgame.game.model.Game;\nimport com.ccsw.tutorialgame.game.model.GameDto;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.modelmapper.ModelMapper;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * @author ccsw\n *\n */\n@Tag(name = \"Game\", description = \"API of Game\")\n@RequestMapping(value = \"/game\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class GameController {\n\n    @Autowired\n    GameService gameService;\n\n    @Autowired\n    ModelMapper mapper;\n\n    /**\n     * M\u00e9todo para recuperar una lista de {@link Game}\n     *\n     * @param title t\u00edtulo del juego\n     * @param idCategory PK de la categor\u00eda\n     * @return {@link List} de {@link GameDto}\n     */\n    @Operation(summary = \"Find\", description = \"Method that return a filtered list of Games\")\n    @RequestMapping(path = \"\", method = RequestMethod.GET)\n    public List<GameDto> find(@RequestParam(value = \"title\", required = false) String title,\n                              @RequestParam(value = \"idCategory\", required = false) Long idCategory) {\n\n        List<Game> game = this.gameService.find(title, idCategory);\n\n        return game.stream().map(e -> mapper.map(e, GameDto.class)).collect(Collectors.toList());\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Game}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Game\")\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody GameDto dto) {\n\n        gameService.save(id, dto);\n    }\n\n}\n
    "},{"location":"appendix/springcloud/filtered/#sql-y-configuracion","title":"SQL y Configuraci\u00f3n","text":"

    Finalmente, debemos crear el script de inicializaci\u00f3n de base de datos con solo los datos de juegos y modificar ligeramente la configuraci\u00f3n inicial para a\u00f1adir un puerto manualmente para poder tener multiples micro servicios funcionando simult\u00e1neamente.

    data.sqlapplication.properties
    INSERT INTO game(title, age, category_id, author_id) VALUES ('On Mars', '14', 1, 2);\nINSERT INTO game(title, age, category_id, author_id) VALUES ('Aventureros al tren', '8', 3, 1);\nINSERT INTO game(title, age, category_id, author_id) VALUES ('1920: Wall Street', '12', 1, 4);\nINSERT INTO game(title, age, category_id, author_id) VALUES ('Barrage', '14', 1, 3);\nINSERT INTO game(title, age, category_id, author_id) VALUES ('Los viajes de Marco Polo', '12', 1, 3);\nINSERT INTO game(title, age, category_id, author_id) VALUES ('Azul', '8', 3, 5);\n
    server.port=8093\n\n#Database\nspring.datasource.url=jdbc:h2:mem:testdb\nspring.datasource.username=sa\nspring.datasource.password=sa\nspring.datasource.driver-class-name=org.h2.Driver\n\nspring.jpa.database-platform=org.hibernate.dialect.H2Dialect\nspring.jpa.defer-datasource-initialization=true\nspring.jpa.show-sql=true\n\nspring.h2.console.enabled=true\n
    "},{"location":"appendix/springcloud/filtered/#pruebas","title":"Pruebas","text":"

    Ahora si arrancamos la aplicaci\u00f3n server y abrimos el Postman podemos realizar las mismas pruebas del apartado de Listado filtrado pero esta vez apuntado al puerto 8093.

    F\u00edjate que cuando probemos el listado de juegos, devolver\u00e1 identificadores en idAuthor y idCategory, y no objetos como funcionaba hasta ahora en la aplicaci\u00f3n monol\u00edtica. As\u00ed que las pruebas que realices para insertar tambi\u00e9n deben utilizar esas propiedades y NO objetos.

    "},{"location":"appendix/springcloud/filtered/#siguientes-pasos","title":"Siguientes pasos","text":"

    En este punto ya tenemos un micro servicio de categor\u00edas en el puerto 8091, un micro servicio de autores en el puerto 8092 y un \u00faltimo micro servicio de juegos en el puerto 8093.

    Si ahora fueramos a conectarlo con el frontend tendr\u00edamos dos problemas:

    • Por un lado, el frontend debe recordar la IP y el puerto en el que se encuentra cada servicio. Adem\u00e1s, este podr\u00eda cambiar si lo desplegamos en nube o lo movemos de servidor, y el frontend deber\u00eda ser capaz de refrescarse para actualizar la informaci\u00f3n.
    • Por otro lado, como hemos comentado, se ha cambiado el contrato del endpoint de juegos. Ahora ya no devuelve la informaci\u00f3n de author y category sino que devuelve su ID. Esto obliga al frontend a tener que hacer dos llamadas extra para completar la informaci\u00f3n. Estar\u00edamos llevando l\u00f3gica de negocio al frontend y esto no nos convence.

    Para poder solverntar ambos problemas, necesitamos conectar todos nuestros micro servicios con una infraestructura que nos ayudar\u00e1 a gestionar todo el ecosistema de micro servicios. Vamos all\u00e1 con el \u00faltimo punto.

    "},{"location":"appendix/springcloud/infra/","title":"Infraestructura - Spring Cloud","text":"

    Creados los tres micro servicios que compondr\u00e1n nuestro aplicativo, ya podemos empezar con la creaci\u00f3n de las piezas de infraestructura que ser\u00e1n las encargadas de realizar la orquestaci\u00f3n.

    "},{"location":"appendix/springcloud/infra/#service-discovery-eureka","title":"Service Discovery - Eureka","text":"

    Para esta pieza hay muchas aplicaciones de mercado, incluso los propios proveedores de cloud tiene la suya propia, pero en este caso, vamos a utilizar la que ofrece Spring Cloud, as\u00ed que vamos a crear un proyecto de una forma similar a la que estamos acostumbrados.

    "},{"location":"appendix/springcloud/infra/#crear-el-servicio","title":"Crear el servicio","text":"

    Volviendo una vez m\u00e1s a Spring Initializr seleccionaremos los siguientes datos:

    • Tipo de proyecto: Maven
    • Lenguage: Java
    • Versi\u00f3n Spring boot: 3.0.4 (o alguna similar)
    • Group: com.ccsw
    • ArtifactId: tutorial-eureka
    • Versi\u00f3n Java: 19
    • Dependencias: Eureka Server

    Es importante que a\u00f1adamos la dependencia de Eureka Server para que sea capaz de ejecutar el proyecto como si fuera un servidor Eureka.

    "},{"location":"appendix/springcloud/infra/#configurar-el-servicio","title":"Configurar el servicio","text":"

    Importamos el proyecto dentro del IDE y ya solo nos queda activar el servidor y configurarlo.

    En primer lugar, a\u00f1adimos la anotaci\u00f3n que habilita el servidor de Eureka.

    TutorialEurekaApplication.java
    package com.ccsw.tutorialeureka;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;\n\n@SpringBootApplication\n@EnableEurekaServer\npublic class TutorialEurekaApplication {\n\n    public static void main(String[] args) {\n        SpringApplication.run(TutorialEurekaApplication.class, args);\n    }\n\n}\n

    Ahora debemos a\u00f1adir las configuraciones necesarias. En primer lugar para facilitar la visualizaci\u00f3n de las propiedades vamos a renombrar nuestro fichero application.properties a application.yml. Hecho esto, a\u00f1adimos la configuraci\u00f3n de puerto que ya conocemos y a\u00f1adimos directivas sobre que Eureka no se registre a s\u00ed mismo dentro del cat\u00e1logo de servicios.

    application.yml
    server:\n  port: 8761\neureka:\n  client:\n    registerWithEureka: false\n    fetchRegistry: false\n
    "},{"location":"appendix/springcloud/infra/#probar-el-servicio","title":"Probar el servicio","text":"

    Hechas estas sencillas configuraciones y arrancando el proyecto, nos dirigimos a la http://localhost/8761 donde podemos ver la interfaz de Eureka y si miramos con detenimiento, vemos que el cat\u00e1logo de servicios aparece vac\u00edo, ya que a\u00fan no se ha registrado ninguno de ellos.

    "},{"location":"appendix/springcloud/infra/#micro-servicios","title":"Micro servicios","text":"

    Ahora que ya tenemos disponible Eureka, ya podemos proceder a registrar nuestros micro servicios dentro del cat\u00e1logo. Para ello vamos a realizar las mismas modificaciones sobre los tres micro servicios. Recuerda que hay que realizarlo sobre los tres para que se registren todos.

    "},{"location":"appendix/springcloud/infra/#configurar-micro-servicios","title":"Configurar micro servicios","text":"

    Para este fin debemos a\u00f1adir una nueva dependencia dentro del pom.xml y modificar la configuraci\u00f3n del proyecto.

    pom.xmlapplication.properties
    <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\nxsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>3.0.4</version>\n        <relativePath/> <!-- lookup parent from repository -->\n    </parent>\n    <groupId>com.ccsw</groupId>\n    <artifactId>tutorial-XXX</artifactId> <!-- Cada proyecto tiene su configaci\u00f3n propia, NO modificar -->\n    <version>0.0.1-SNAPSHOT</version>\n    <name>tutorial-XXX</name> <!-- Cada proyecto tiene su configaci\u00f3n propia, NO modificar -->\n    <description>Demo project for Spring Boot</description>\n    <properties>\n        <java.version>19</java.version>\n        <spring-cloud.version>2022.0.1</spring-cloud.version>\n    </properties>\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-jpa</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springdoc</groupId>\n            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>\n            <version>2.0.3</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.hibernate</groupId>\n            <artifactId>hibernate-validator</artifactId>\n            <version>8.0.0.Final</version>\n        </dependency>\n\n        <dependency>\n            <groupId>net.sf.dozer</groupId>\n            <artifactId>dozer</artifactId>\n            <version>5.5.1</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.cloud</groupId>\n            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.h2database</groupId>\n            <artifactId>h2</artifactId>\n            <scope>runtime</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n    <dependencyManagement>\n        <dependencies>\n            <dependency>\n                <groupId>org.springframework.cloud</groupId>\n                <artifactId>spring-cloud-dependencies</artifactId>\n                <version>${spring-cloud.version}</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n        </dependencies>\n    </dependencyManagement>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n
    spring.application.name=spring-cloud-eureka-client-XXX\nserver.port=809X\n\n#Database\nspring.datasource.url=jdbc:h2:mem:testdb\nspring.datasource.username=sa\nspring.datasource.password=sa\nspring.datasource.driver-class-name=org.h2.Driver\n\nspring.jpa.database-platform=org.hibernate.dialect.H2Dialect\nspring.jpa.defer-datasource-initialization=true\nspring.jpa.show-sql=true\n\nspring.h2.console.enabled=true\n\n#Eureka\neureka.client.serviceUrl.defaultZone=${EUREKA_URI:http://localhost:8761/eureka}\neureka.instance.preferIpAddress=true\n

    Como podemos observar, lo que hemos hecho, es a\u00f1adir la dependencia de Eureka Client y le hemos comunicado a cada micro servicio donde tenemos arrancado Eureka. De este modo al arrancar cada micro servicio, este se registrar\u00e1 autom\u00e1ticamente dentro de Eureka.

    Para poder diferenciar cada micro servicio, estos tienen su configuraci\u00f3n de nombre y puerto (mantenemos el puerto que hab\u00edamos configurado en pasos previos):

    • Categor\u00edas: spring.application.name=spring-cloud-eureka-client-category
    • Autores: spring.application.name=spring-cloud-eureka-client-author
    • Juegos: spring.application.name=spring-cloud-eureka-client-game

    Nombres en vez de rutas

    Estos nombres ser\u00e1n por los que vamos a identificar cada micro servicio dentro de Eureka que ser\u00e1 quien conozca las rutas de los mismos, asi cuando queramos realizar redirecciones a estos no necesitaremos conocerlas rutas ni los puertos de los mismos, con proporcionar los nombres tendremos la informaci\u00f3n completa de como llegar a ellos.

    "},{"location":"appendix/springcloud/infra/#probar-micro-servicios","title":"Probar micro servicios","text":"

    Hechas estas configuraciones y arrancados los micro servicios, volvemos a dirigirnos a Eureka en http://localhost/8761 donde podemos ver que estos aparecen en el listado de servicios registrados.

    "},{"location":"appendix/springcloud/infra/#gateway","title":"Gateway","text":"

    Para esta pieza, de nuevo, hay muchas implementaciones y aplicaciones de mercado, pero nosotros vamos a utilizar la de Spring Cloud, as\u00ed que vamos a crear un nuevo proyecto de una forma similar a la de Eureka.

    "},{"location":"appendix/springcloud/infra/#crear-el-servicio_1","title":"Crear el servicio","text":"

    Volviendo una vez m\u00e1s a Spring Initializr seleccionaremos los siguientes datos:

    • Tipo de proyecto: Maven
    • Lenguage: Java
    • Versi\u00f3n Spring boot: 3.0.4 (o alguna similar)
    • Group: com.ccsw
    • ArtifactId: tutorial-gateway
    • Versi\u00f3n Java: 19
    • Dependencias: Gateway, Eureka Client

    Ojo con las dependencias de Gateway y de Eureka Client que debemos a\u00f1adir.

    "},{"location":"appendix/springcloud/infra/#configurar-el-servicio_1","title":"Configurar el servicio","text":"

    De nuevo lo importamos en nuestro IDE y pasamos a a\u00f1adir las configuraciones pertinentes.

    Al igual que en el caso de Eureka vamos a renombrar nuestro fichero application.properties a application.yml.

    application.yml
    server:\n  port: 8080\neureka:\n  client:\n    serviceUrl:\n      defaultZone: http://localhost:8761/eureka\nspring:\n  application:\n    name: spring-cloud-eureka-client-gateway\n  cloud:\n    gateway:\n      default-filters:\n        - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin\n      globalcors:\n        corsConfigurations:\n          '[/**]':\n             allowedOrigins: \"*\"\n             allowedMethods: \"*\"\n             allowedHeaders: \"*\"\n      routes:\n        - id: category\n          uri: lb://SPRING-CLOUD-EUREKA-CLIENT-CATEGORY\n          predicates:\n            - Path=/category/**\n        - id: author\n          uri: lb://SPRING-CLOUD-EUREKA-CLIENT-AUTHOR\n          predicates:\n            - Path=/author/**\n        - id: game\n          uri: lb://SPRING-CLOUD-EUREKA-CLIENT-GAME\n          predicates:\n            - Path=/game/**\n

    Lo que hemos hecho aqu\u00ed es configurar el puerto como 8080 ya que el Gateway va a ser nuestro punto de acceso y el encargado de redirigir cada petici\u00f3n al micro servicio correcto.

    Posteriormente hemos configurado el cliente de Eureka para que el Gateway establezca comunicaci\u00f3n con Eureka que hemos configurado previamente para, en primer lugar, registrarse como un cliente y seguidamente obtener informaci\u00f3n del cat\u00e1logo de servicios existentes.

    El paso siguiente es darle un nombre a la aplicaci\u00f3n para que se registre en Eureka y a\u00f1adir configuraci\u00f3n de CORS para que cuando realicemos las llamadas desde navegador pueda realizar la redirecci\u00f3n correctamente.

    Finalmente a\u00f1adimos las directrices de redirecci\u00f3n al Gateway indic\u00e1ndole los nombres de los micro servicios con los que estos se han registrado en Eureka junto a los predicados que incluyen las rutas parciales que queremos que sean redirigidas a cada micro servicio.

    Con esto nos queda la siguiente configuraci\u00f3n:

    • Las rutas que incluyan en su path category redirigir\u00e1n al micro servicio de Categorias
    • Las rutas que incluyan en su path author redirigir\u00e1n al micro servicio de Autores
    • Las rutas que incluyan en su path game redirigir\u00e1n al micro servicio de Juegos
    "},{"location":"appendix/springcloud/infra/#probar-el-servicio_1","title":"Probar el servicio","text":"

    Hechas esto y arrancado el proyecto, volvemos a dirigirnos a Eureka en http://localhost/8761 donde podemos ver que el Gateway se ha registrado correctamente junto al resto de clientes.

    "},{"location":"appendix/springcloud/infra/#feign-client","title":"Feign Client","text":"

    El \u00faltimo paso es la implementaci\u00f3n de la comunicaci\u00f3n entre los micro servicios, en este caso necesitamos que nuestro micro servicio de Game obtenga datos de Category y Author para poder servir informaci\u00f3n completa de los Game ya que en su modelo solo posee los identificadores. Si record\u00e1is, est\u00e1bamos respondiendo solamente con los id.

    "},{"location":"appendix/springcloud/infra/#configurar-el-servicio_2","title":"Configurar el servicio","text":"

    Para la comunicaci\u00f3n entre los distintos servicios, Spring Cloud nos prove de Feign Clients que ofrecen una interfaz muy sencilla de comunicaci\u00f3n y que utiliza a la perfecci\u00f3n la infraestructura que ya hemos construido.

    En primer lugar debemos a\u00f1adir la dependencia necesaria dentro de nuestro pom.xml del micro servicio de Game.

    pom.xml
    ...\n        <dependency>\n            <groupId>org.springframework.cloud</groupId>\n            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.cloud</groupId>\n            <artifactId>spring-cloud-starter-openfeign</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.h2database</groupId>\n            <artifactId>h2</artifactId>\n            <scope>runtime</scope>\n        </dependency>\n...\n

    El siguiente paso es habilitar el uso de los Feign Clients mediante la anotaci\u00f3n de SpringCloud.

    TutorialGameApplication.java
    package com.ccsw.tutorialgame;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.cloud.openfeign.EnableFeignClients;\n\n@SpringBootApplication\n@EnableFeignClients\npublic class TutorialGameApplication {\n\n    public static void main(String[] args) {\n        SpringApplication.run(TutorialGameApplication.class, args);\n    }\n\n}\n
    "},{"location":"appendix/springcloud/infra/#configurar-los-clientes","title":"Configurar los clientes","text":"

    Realizadas las configuraciones ya podemos realizar los cambios necesarios en nuestro c\u00f3digo para implementar la comunicaci\u00f3n. En primer lugar vamos a crear los clientes de Categor\u00edas y Autores.

    CategoryClient.javaAuthorClient.java
    package com.ccsw.tutorialgame.category;\n\nimport com.ccsw.tutorialgame.category.model.CategoryDto;\nimport org.springframework.cloud.openfeign.FeignClient;\nimport org.springframework.web.bind.annotation.GetMapping;\n\nimport java.util.List;\n\n@FeignClient(value = \"SPRING-CLOUD-EUREKA-CLIENT-CATEGORY\", url = \"http://localhost:8080\")\npublic interface CategoryClient {\n\n    @GetMapping(value = \"/category\")\n    List<CategoryDto> findAll();\n}\n
    package com.ccsw.tutorialgame.author;\n\nimport com.ccsw.tutorialgame.author.model.AuthorDto;\nimport org.springframework.cloud.openfeign.FeignClient;\nimport org.springframework.web.bind.annotation.GetMapping;\n\nimport java.util.List;\n\n@FeignClient(value = \"SPRING-CLOUD-EUREKA-CLIENT-AUTHOR\", url = \"http://localhost:8080\")\npublic interface AuthorClient {\n\n    @GetMapping(value = \"/author\")\n    List<AuthorDto> findAll();\n}\n

    Lo que hacemos aqu\u00ed es crear una simple interfaz donde a\u00f1adimos la configuraci\u00f3n del Feign Client con la url del Gateway a trav\u00e9s del cual vamos a realizar todas las comunicaciones y creamos un m\u00e9todo abstracto con la anotaci\u00f3n pertinente para hacer referencia al endpoint de obtenci\u00f3n del listado.

    "},{"location":"appendix/springcloud/infra/#invocar-los-clientes","title":"Invocar los clientes","text":"

    Con esto ya podemos inyectar estas interfaces dentro de nuestro controlador para obtener todos los datos necesarios que completaran la informaci\u00f3n de la Category y Author de cada Game.

    Adem\u00e1s, vamos a cambiar el Dto de respuesta, para que en vez de devolver ids, devuelva los objetos correspondientes, que son los que est\u00e1 esperando nuestro frontend. Para ello, primero crearemos los Dtos que necesitamos. Los crearemos en:

    • com.ccsw.tutorialgame.category.model
    • com.ccsw.tutorialgame.author.model
    CategoryDto.javaAuthorDto.java
    package com.ccsw.tutorialgame.category.model;\n\n/**\n * @author ccsw\n *\n */\npublic class CategoryDto {\n\n    private Long id;\n\n    private String name;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return name\n     */\n    public String getName() {\n\n        return this.name;\n    }\n\n    /**\n     * @param name new value of {@link #getName}.\n     */\n    public void setName(String name) {\n\n        this.name = name;\n    }\n\n}\n
    package com.ccsw.tutorialgame.author.model;\n\n/**\n * @author ccsw\n *\n */\npublic class AuthorDto {\n\n    private Long id;\n\n    private String name;\n\n    private String nationality;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return name\n     */\n    public String getName() {\n\n        return this.name;\n    }\n\n    /**\n     * @param name new value of {@link #getName}.\n     */\n    public void setName(String name) {\n\n        this.name = name;\n    }\n\n    /**\n     * @return nationality\n     */\n    public String getNationality() {\n\n        return this.nationality;\n    }\n\n    /**\n     * @param nationality new value of {@link #getNationality}.\n     */\n    public void setNationality(String nationality) {\n\n        this.nationality = nationality;\n    }\n\n}\n

    Adem\u00e1s, modificaremos nuestro GameDto para hacer uso de esos objetos.

    GameDto.java
    package com.ccsw.tutorialgame.game.model;\n\n\nimport com.ccsw.tutorialgame.author.model.AuthorDto;\nimport com.ccsw.tutorialgame.category.model.CategoryDto;\n\n/**\n * @author ccsw\n *\n */\npublic class GameDto {\n\n    private Long id;\n\n    private String title;\n\n    private String age;\n\n    private CategoryDto category;\n\n    private AuthorDto author;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return title\n     */\n    public String getTitle() {\n\n        return this.title;\n    }\n\n    /**\n     * @param title new value of {@link #getTitle}.\n     */\n    public void setTitle(String title) {\n\n        this.title = title;\n    }\n\n    /**\n     * @return age\n     */\n    public String getAge() {\n\n        return this.age;\n    }\n\n    /**\n     * @param age new value of {@link #getAge}.\n     */\n    public void setAge(String age) {\n\n        this.age = age;\n    }\n\n    /**\n     * @return category\n     */\n    public CategoryDto getCategory() {\n\n        return this.category;\n    }\n\n    /**\n     * @param category new value of {@link #getCategory}.\n     */\n    public void setCategory(CategoryDto category) {\n\n        this.category = category;\n    }\n\n    /**\n     * @return author\n     */\n    public AuthorDto getAuthor() {\n\n        return this.author;\n    }\n\n    /**\n     * @param author new value of {@link #getAuthor}.\n     */\n    public void setAuthor(AuthorDto author) {\n\n        this.author = author;\n    }\n\n}\n

    Y por \u00faltimo implementaremos el c\u00f3digo necesario para transformar los ids en objetos dto. Aqu\u00ed lo que haremos ser\u00e1 recuperar todos los autores y categor\u00edas, haciendo uso de los Feign Client, y cuando ejecutemos el mapeo de los juegos, ir sustituyendo sus valores por los dtos correspondientes.

    GameController.java
    package com.ccsw.tutorialgame.game;\n\nimport com.ccsw.tutorialgame.author.AuthorClient;\nimport com.ccsw.tutorialgame.author.model.AuthorDto;\nimport com.ccsw.tutorialgame.category.CategoryClient;\nimport com.ccsw.tutorialgame.category.model.CategoryDto;\nimport com.ccsw.tutorialgame.game.model.Game;\nimport com.ccsw.tutorialgame.game.model.GameDto;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * @author ccsw\n *\n */\n@Tag(name = \"Game\", description = \"API of Game\")\n@RequestMapping(value = \"/game\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class GameController {\n\n    @Autowired\n    GameService gameService;\n\n    @Autowired\n    CategoryClient categoryClient;\n\n    @Autowired\n    AuthorClient authorClient;\n\n    /**\n     * M\u00e9todo para recuperar una lista de {@link Game}\n     *\n     * @param title t\u00edtulo del juego\n     * @param idCategory PK de la categor\u00eda\n     * @return {@link List} de {@link GameDto}\n     */\n    @Operation(summary = \"Find\", description = \"Method that return a filtered list of Games\")\n    @RequestMapping(path = \"\", method = RequestMethod.GET)\n    public List<GameDto> find(@RequestParam(value = \"title\", required = false) String title,\n                              @RequestParam(value = \"idCategory\", required = false) Long idCategory) {\n\n        List<CategoryDto> categories = categoryClient.findAll();\n        List<AuthorDto> authors = authorClient.findAll();\n\n        return gameService.find(title, idCategory).stream().map(game -> {\n            GameDto gameDto = new GameDto();\n\n            gameDto.setId(game.getId());\n            gameDto.setTitle(game.getTitle());\n            gameDto.setAge(game.getAge());\n            gameDto.setCategory(categories.stream().filter(category -> category.getId().equals(game.getIdCategory())).findFirst().orElse(null));\n            gameDto.setAuthor(authors.stream().filter(author -> author.getId().equals(game.getIdAuthor())).findFirst().orElse(null));\n\n            return gameDto;\n        }).collect(Collectors.toList());\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Game}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Game\")\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody GameDto dto) {\n\n        gameService.save(id, dto);\n    }\n\n}\n

    Con todo esto, ya tenemos construido nuestro aplicativo de micro servicios con la arquitectura Spring Cloud. Podemos proceder a realizar las mismas pruebas tanto manuales como a trav\u00e9s de los frontales.

    Escalado

    Una de las principales ventajas de las arquitecturas de micro servicios, es la posibilidad de escalar partes de los aplicativos sin tener que escalar el sistema completo. Para confirmar que esto es asi, podemos levantar multiples instancias de cada servicio en puertos diferentes y veremos que esto se refleja en Eureka y el Gateway balancear\u00e1 autom\u00e1ticamente entre las distintas instancias.

    "},{"location":"appendix/springcloud/intro/","title":"Introducci\u00f3n Micro Servicios - Spring Cloud","text":""},{"location":"appendix/springcloud/intro/#que-son-los-micro-servicios","title":"Que son los micro servicios?","text":"

    Pues como su nombre indica, son servicios peque\u00f1itos

    Aunque si nos vamos a una definici\u00f3n m\u00e1s t\u00e9cnica (seg\u00fan ChatGPT):

    Los micro servicios son una arquitectura de software en la que una aplicaci\u00f3n est\u00e1 compuesta por peque\u00f1os servicios independientes que se comunican entre s\u00ed a trav\u00e9s de interfaces bien definidas. Cada servicio se enfoca en realizar una tarea espec\u00edfica dentro de la aplicaci\u00f3n y se ejecuta de manera aut\u00f3noma.

    Cada micro servicio es responsable de un dominio del negocio y puede ser desarrollado, probado, implementado y escalado de manera independiente. Esto permite una mayor flexibilidad y agilidad en el desarrollo y la implementaci\u00f3n de aplicaciones, ya que los cambios en un servicio no afectan a otros servicios.

    Adem\u00e1s, los micro servicios son escalables y resistentes a fallos, ya que si un servicio falla, los dem\u00e1s servicios pueden seguir funcionando. Tambi\u00e9n permiten la utilizaci\u00f3n de diferentes tecnolog\u00edas para cada servicio, lo que ayuda a optimizar el rendimiento y la eficiencia en la aplicaci\u00f3n en general.

    "},{"location":"appendix/springcloud/intro/#spring-cloud","title":"Spring Cloud","text":"

    Existente multiples soluciones para implementar micro servicios, en nuestro caso vamos a utilizar la soluci\u00f3n que nos ofrece Spring Framework y que est\u00e1 incluido dentro del m\u00f3dulo Spring Cloud.

    Esta soluci\u00f3n nace hace ya varios a\u00f1os como parte de la infraestructura de Netflix para dar soluci\u00f3n a sus propias necesidades. Con el tiempo este c\u00f3digo opensource ha sido adquirido por Spring Framework y se ha incluido dentro de su ecosistema, evolucionandolo con nuevas funcionalidades. Todo ello ha sido publicado bajo el m\u00f3dulo de Spring Cloud.

    "},{"location":"appendix/springcloud/intro/#contexto-de-la-aplicacion","title":"Contexto de la aplicaci\u00f3n","text":"

    Llegados a este punto, \u00bfqu\u00e9 es lo que vamos a hacer en los siguientes puntos?. Pues vamos a coger nuestra aplicaci\u00f3n monol\u00edtica que ya tenemos implementada durante todo el tutorial, y vamos a proceder a trocearla e implementarla con una metodolog\u00eda de micro servicios.

    Pero, adem\u00e1s de trocear la aplicaci\u00f3n en peque\u00f1os servicios, nos va a hacer falta una serie de servicios / utilidades para conectar todo el ecosistema. Nos har\u00e1 falta una infraestructura.

    "},{"location":"appendix/springcloud/intro/#infraestructura","title":"Infraestructura","text":"

    A diferencia de una aplicaci\u00f3n monol\u00edtica, en un enfoque de micro servicios, ya no basta \u00fanicamente con la aplicaci\u00f3n desplegada en su servidor, sino que ser\u00e1n necesarios varios actores que se responsabilizar\u00e1n de darle consistencia al sistema, permitir la comunicaci\u00f3n entre ellos, y ayudar\u00e1n a solventar ciertos problemas que nos surgir\u00e1n al trocear nuestras aplicaciones.

    Las principales piezas que vamos a utilizar para la implementaci\u00f3n de nuestra infraestructura, ser\u00e1n:

    • Service Discovery / Eureka Server: Como vamos a tener varios servicios distribuidos por nuestra red, necesitaremos conocer donde est\u00e1 funcionando cada uno de ellos, su IP, su puerto e incluso sus m\u00e9tricas de acceso (localizaci\u00f3n, zona, estado de carga, etc.). Vamos a necesitar un Service Discovery que no es m\u00e1s que un cat\u00e1logo de todos los servicios que componen el ecosistema al cual cada servicio debe informar de forma proactiva, de su localizaci\u00f3n y disponibilidad.
    • Client-side Service Discovery / Eureka Client: Como hemos mencionado en el punto anterior, todos los servicios del ecosistema (incluidos nuestros micro servicios) deben conectarse con el Service Discovery e informar peri\u00f3dicamente a este cat\u00e1logo de su estado y sus m\u00e9tricas para que en caso de perdida de servicio, el resto de elementos lo sepan y puedan tomar decisiones al respecto. Tambi\u00e9n nos servir\u00e1 para que cada elemento pueda guardar en local una cach\u00e9 del cat\u00e1logo publicado, que se ir\u00e1 refrescando cada vez que lance un health check.
    • Edge Server / Gateway / Proxy: Se trata de un servicio que har\u00e1 de intermediario entre el mundo exterior y el mundo de microservicios. Adem\u00e1s permitir\u00e1 hacer redirecci\u00f3n y balanceo entre todos los elementos registrados en el Service Discovery. Es altamente configurable (rutas, redirecciones, carga, etc.) y es una pieza fundamental para unificar todas las llamadas en un \u00fanico punto del ecosistema.
    • Feign Client: Esta utilidad que provee directamente Spring Cloud nos permite comunicarnos entre los diferentes micro servicios de Spring, de una forma muy sencilla y sin tener que estar gestionando llamadas API Rest.
    "},{"location":"appendix/springcloud/intro/#diagrama-de-la-arquitectura","title":"Diagrama de la arquitectura","text":"

    Con las piezas identificadas anteriormente y con el Contexto de la aplicaci\u00f3n en mente, lo que vamos a hacer en los siguientes puntos es trocear el sistema y generar la siguiente arquitectura:

    Ya deber\u00edamos tener claros los conceptos y los actores que compondr\u00e1n nuestro sistema, as\u00ed que, all\u00e1 vamos!!!

    "},{"location":"appendix/springcloud/paginated/","title":"Listado paginado - Spring Boot","text":"

    Al igual que en el caso anterior vamos a crear un nuevo proyecto que contendr\u00e1 un nuevo micro servicio.

    Para la creaci\u00f3n de proyecto nos remitimos a la gu\u00eda de instalaci\u00f3n donde se detalla el proceso de creaci\u00f3n de nuevo proyecto Entorno de desarrollo

    Todos los pasos son exactamente iguales, lo \u00fanico que va a variar, es el nombre de nuestro proyecto, que en este caso se va a llamar tutorial-author. El campo que debemos modificar es artifact en Spring Initilizr, el resto de campos se cambiaran autom\u00e1ticamente.

    "},{"location":"appendix/springcloud/paginated/#codigo","title":"C\u00f3digo","text":"

    Dado de vamos a implementar el micro servicio Spring Boot de Autores, vamos a respetar la misma estructura del Listado paginado de la version monol\u00edtica.

    "},{"location":"appendix/springcloud/paginated/#paginacion","title":"Paginaci\u00f3n","text":"

    En primer lugar, vamos a a\u00f1adir la clase que necesitamos para realizar la paginaci\u00f3n y vimos en la version monol\u00edtica del tutorial en el package com.ccsw.tutorialauthor.common.pagination. Ojo al package que lo hemos renombrado con respecto al listado monol\u00edtico.

    PageableRequest.java
    package com.ccsw.tutorialauthor.common.pagination;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport org.springframework.data.domain.*;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\npublic class PageableRequest implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private int pageNumber;\n\n    private int pageSize;\n\n    private List<SortRequest> sort;\n\n    public PageableRequest() {\n\n        sort = new ArrayList<>();\n    }\n\n    public PageableRequest(int pageNumber, int pageSize) {\n\n        this();\n        this.pageNumber = pageNumber;\n        this.pageSize = pageSize;\n    }\n\n    public PageableRequest(int pageNumber, int pageSize, List<SortRequest> sort) {\n\n        this();\n        this.pageNumber = pageNumber;\n        this.pageSize = pageSize;\n        this.sort = sort;\n    }\n\n    public int getPageNumber() {\n        return pageNumber;\n    }\n\n    public void setPageNumber(int pageNumber) {\n        this.pageNumber = pageNumber;\n    }\n\n    public int getPageSize() {\n        return pageSize;\n    }\n\n    public void setPageSize(int pageSize) {\n        this.pageSize = pageSize;\n    }\n\n    public List<SortRequest> getSort() {\n        return sort;\n    }\n\n    public void setSort(List<SortRequest> sort) {\n        this.sort = sort;\n    }\n\n    @JsonIgnore\n    public Pageable getPageable() {\n\n        return PageRequest.of(this.pageNumber, this.pageSize, Sort.by(sort.stream().map(e -> new Sort.Order(e.getDirection(), e.getProperty())).collect(Collectors.toList())));\n    }\n\n    public static class SortRequest implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        private String property;\n\n        private Sort.Direction direction;\n\n        protected String getProperty() {\n            return property;\n        }\n\n        protected void setProperty(String property) {\n            this.property = property;\n        }\n\n        protected Sort.Direction getDirection() {\n            return direction;\n        }\n\n        protected void setDirection(Sort.Direction direction) {\n            this.direction = direction;\n        }\n    }\n\n}\n
    "},{"location":"appendix/springcloud/paginated/#entity-y-dto","title":"Entity y Dto","text":"

    Seguimos con la entidad y los DTOs dentro del package com.ccsw.tutorialauthor.author.model.

    Author.javaAuthorDto.javaAuthorSearchDto.java
    package com.ccsw.tutorialauthor.author.model;\n\nimport jakarta.persistence.*;\n\n/**\n * @author ccsw\n *\n */\n@Entity\n@Table(name = \"author\")\npublic class Author {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"id\", nullable = false)\n    private Long id;\n\n    @Column(name = \"name\", nullable = false)\n    private String name;\n\n    @Column(name = \"nationality\")\n    private String nationality;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return name\n     */\n    public String getName() {\n\n        return this.name;\n    }\n\n    /**\n     * @param name new value of {@link #getName}.\n     */\n    public void setName(String name) {\n\n        this.name = name;\n    }\n\n    /**\n     * @return nationality\n     */\n    public String getNationality() {\n\n        return this.nationality;\n    }\n\n    /**\n     * @param nationality new value of {@link #getNationality}.\n     */\n    public void setNationality(String nationality) {\n\n        this.nationality = nationality;\n    }\n\n}\n
    package com.ccsw.tutorialauthor.author.model;\n\n/**\n * @author ccsw\n *\n */\npublic class AuthorDto {\n\n    private Long id;\n\n    private String name;\n\n    private String nationality;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return name\n     */\n    public String getName() {\n\n        return this.name;\n    }\n\n    /**\n     * @param name new value of {@link #getName}.\n     */\n    public void setName(String name) {\n\n        this.name = name;\n    }\n\n    /**\n     * @return nationality\n     */\n    public String getNationality() {\n\n        return this.nationality;\n    }\n\n    /**\n     * @param nationality new value of {@link #getNationality}.\n     */\n    public void setNationality(String nationality) {\n\n        this.nationality = nationality;\n    }\n\n}\n
    package com.ccsw.tutorialauthor.author.model;\n\nimport com.ccsw.tutorialauthor.common.pagination.PageableRequest;\n\n/**\n * @author ccsw\n *\n */\npublic class AuthorSearchDto {\n\n    private PageableRequest pageable;\n\n    public PageableRequest getPageable() {\n        return pageable;\n    }\n\n    public void setPageable(PageableRequest pageable) {\n        this.pageable = pageable;\n    }\n}\n
    "},{"location":"appendix/springcloud/paginated/#repository-service-y-controller","title":"Repository, Service y Controller","text":"

    Posteriormente, emplazamos el resto de clases dentro del package com.ccsw.tutorialauthor.author.

    AuthorRepository.javaAuthorService.javaAuthorServiceImpl.javaAuthorController.java
    package com.ccsw.tutorialauthor.author;\n\nimport com.ccsw.tutorialauthor.author.model.Author;\nimport org.springframework.data.domain.Page;\nimport org.springframework.data.domain.Pageable;\nimport org.springframework.data.repository.CrudRepository;\n\n/**\n * @author ccsw\n *\n */\npublic interface AuthorRepository extends CrudRepository<Author, Long> {\n\n    /**\n     * M\u00e9todo para recuperar un listado paginado de {@link Author}\n     *\n     * @param pageable pageable\n     * @return {@link Page} de {@link Author}\n     */\n    Page<Author> findAll(Pageable pageable);\n\n}\n
    package com.ccsw.tutorialauthor.author;\n\nimport com.ccsw.tutorialauthor.author.model.Author;\nimport com.ccsw.tutorialauthor.author.model.AuthorDto;\nimport com.ccsw.tutorialauthor.author.model.AuthorSearchDto;\nimport org.springframework.data.domain.Page;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\npublic interface AuthorService {\n\n    /**\n     * Recupera un {@link Author} a trav\u00e9s de su ID\n     *\n     * @param id PK de la entidad\n     * @return {@link Author}\n     */\n    Author get(Long id);\n\n    /**\n     * M\u00e9todo para recuperar un listado paginado de {@link Author}\n     *\n     * @param dto dto de b\u00fasqueda\n     * @return {@link Page} de {@link Author}\n     */\n    Page<Author> findPage(AuthorSearchDto dto);\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    void save(Long id, AuthorDto dto);\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     */\n    void delete(Long id) throws Exception;\n\n    /**\n     * Recupera un listado de autores {@link Author}\n     *\n     * @return {@link List} de {@link Author}\n     */\n    List<Author> findAll();\n\n}\n
    package com.ccsw.tutorialauthor.author;\n\nimport com.ccsw.tutorialauthor.author.model.Author;\nimport com.ccsw.tutorialauthor.author.model.AuthorDto;\nimport com.ccsw.tutorialauthor.author.model.AuthorSearchDto;\nimport jakarta.transaction.Transactional;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.domain.Page;\nimport org.springframework.stereotype.Service;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Service\n@Transactional\npublic class AuthorServiceImpl implements AuthorService {\n\n    @Autowired\n    AuthorRepository authorRepository;\n\n    /**\n     * {@inheritDoc}\n     * @return\n     */\n    @Override\n    public Author get(Long id) {\n\n        return this.authorRepository.findById(id).orElse(null);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public Page<Author> findPage(AuthorSearchDto dto) {\n\n        return this.authorRepository.findAll(dto.getPageable().getPageable());\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void save(Long id, AuthorDto data) {\n\n        Author author;\n\n        if (id == null) {\n            author = new Author();\n        } else {\n            author = this.get(id);\n        }\n\n        BeanUtils.copyProperties(data, author, \"id\");\n\n        this.authorRepository.save(author);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void delete(Long id) throws Exception {\n\n        if(this.get(id) == null){\n            throw new Exception(\"Not exists\");\n        }\n\n        this.authorRepository.deleteById(id);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public List<Author> findAll() {\n\n        return (List<Author>) this.authorRepository.findAll();\n    }\n\n}\n
    package com.ccsw.tutorialauthor.author;\n\nimport com.ccsw.tutorialauthor.author.model.Author;\nimport com.ccsw.tutorialauthor.author.model.AuthorDto;\nimport com.ccsw.tutorialauthor.author.model.AuthorSearchDto;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.modelmapper.ModelMapper;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.domain.Page;\nimport org.springframework.data.domain.PageImpl;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * @author ccsw\n *\n */\n@Tag(name = \"Author\", description = \"API of Author\")\n@RequestMapping(value = \"/author\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class AuthorController {\n\n    @Autowired\n    AuthorService authorService;\n\n    @Autowired\n    ModelMapper mapper;\n\n    /**\n     * M\u00e9todo para recuperar un listado paginado de {@link Author}\n     *\n     * @param dto dto de b\u00fasqueda\n     * @return {@link Page} de {@link AuthorDto}\n     */\n    @Operation(summary = \"Find Page\", description = \"Method that return a page of Authors\")\n    @RequestMapping(path = \"\", method = RequestMethod.POST)\n    public Page<AuthorDto> findPage(@RequestBody AuthorSearchDto dto) {\n\n        Page<Author> page = this.authorService.findPage(dto);\n\n        return new PageImpl<>(page.getContent().stream().map(e -> mapper.map(e, AuthorDto.class)).collect(Collectors.toList()), page.getPageable(), page.getTotalElements());\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Author\")\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody AuthorDto dto) {\n\n        this.authorService.save(id, dto);\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     */\n    @Operation(summary = \"Delete\", description = \"Method that deletes a Author\")\n    @RequestMapping(path = \"/{id}\", method = RequestMethod.DELETE)\n    public void delete(@PathVariable(\"id\") Long id) throws Exception {\n\n        this.authorService.delete(id);\n    }\n\n    /**\n     * Recupera un listado de autores {@link Author}\n     *\n     * @return {@link List} de {@link AuthorDto}\n     */\n    @Operation(summary = \"Find\", description = \"Method that return a list of Authors\")\n    @RequestMapping(path = \"\", method = RequestMethod.GET)\n    public List<AuthorDto> findAll() {\n\n        List<Author> authors = this.authorService.findAll();\n\n        return authors.stream().map(e -> mapper.map(e, AuthorDto.class)).collect(Collectors.toList());\n    }\n\n}\n
    "},{"location":"appendix/springcloud/paginated/#sql-y-configuracion","title":"SQL y Configuraci\u00f3n","text":"

    Finalmente, debemos crear el script de inicializaci\u00f3n de base de datos con solo los datos de author y modificar ligeramente la configuraci\u00f3n inicial para a\u00f1adir un puerto manualmente para poder tener multiples micro servicios funcionando simult\u00e1neamente.

    data.sqlapplication.properties
    INSERT INTO author(name, nationality) VALUES ('Alan R. Moon', 'US');\nINSERT INTO author(name, nationality) VALUES ('Vital Lacerda', 'PT');\nINSERT INTO author(name, nationality) VALUES ('Simone Luciani', 'IT');\nINSERT INTO author(name, nationality) VALUES ('Perepau Llistosella', 'ES');\nINSERT INTO author(name, nationality) VALUES ('Michael Kiesling', 'DE');\nINSERT INTO author(name, nationality) VALUES ('Phil Walker-Harding', 'US');\n
    server.port=8092\n\n#Database\nspring.datasource.url=jdbc:h2:mem:testdb\nspring.datasource.username=sa\nspring.datasource.password=sa\nspring.datasource.driver-class-name=org.h2.Driver\n\nspring.jpa.database-platform=org.hibernate.dialect.H2Dialect\nspring.jpa.defer-datasource-initialization=true\nspring.jpa.show-sql=true\n\nspring.h2.console.enabled=true\n
    "},{"location":"appendix/springcloud/paginated/#pruebas","title":"Pruebas","text":"

    Ahora si arrancamos la aplicaci\u00f3n server y abrimos el Postman podemos realizar las mismas pruebas del apartado de Listado paginado pero esta vez apuntado al puerto 8092.

    "},{"location":"appendix/springcloud/paginated/#siguientes-pasos","title":"Siguientes pasos","text":"

    En este punto ya tenemos un micro servicio de categor\u00edas en el puerto 8091 y un micro servicio de autores en el puerto 8092. Al igual que antes, con estos datos ya podr\u00edamos conectar el frontend a estos servicios, pero vamos a esperar un poquito m\u00e1s a tener toda la infraestructura, para que sea m\u00e1s sencillo.

    Vamos a convertir en micro servicio el \u00faltimo listado.

    "},{"location":"appendix/springcloud/summary/","title":"Resumen Micro Servicios - Spring Cloud","text":""},{"location":"appendix/springcloud/summary/#que-hemos-hecho","title":"\u00bfQu\u00e9 hemos hecho?","text":"

    Llegados a este punto, ya has podido comprobar que implementar una aplicaci\u00f3n orientada a micro servicios es bastante similar a una aplicaci\u00f3n monol\u00edtica, con la salvedad de que tienes que tener en cuenta la distribuci\u00f3n de estos, y por tanto su gesti\u00f3n y coordinaci\u00f3n.

    En definitiva, lo que hemos implementado ha sido:

    • Service Discovery: Que ayudar\u00e1 a tener un cat\u00e1logo de todos las piezas de mi infraestructura, su IP, su puerto y ciertas m\u00e9tricas que ayuden luego en la elecci\u00f3n de servicio.

    • Gateway: Que centraliza las peticiones en un \u00fanico punto y permite hacer de balanceo de carga, seguridad, etc. Ser\u00e1 el punto de entrada a nuestro ecosistema.

    • Micro servicio Category: Contiene las operaciones sobre el \u00e1mbito funcional de categor\u00edas, guarda y recupera informaci\u00f3n de ese \u00e1mbito funcional.

    • Micro servicio Author: Contiene las operaciones sobre el \u00e1mbito funcional de autores, guarda y recupera informaci\u00f3n de ese \u00e1mbito funcional.

    • Micro servicio Game: Contiene las operaciones sobre el \u00e1mbito funcional de autores, guarda y recupera informaci\u00f3n de ese \u00e1mbito funcional. Adem\u00e1s, realiza llamadas entre los otros dos micro servicios para nutrir de m\u00e1s informaci\u00f3n sus endpoints.

    El diagrama de nuestra aplicaci\u00f3n ahora es as\u00ed:

    "},{"location":"appendix/springcloud/summary/#siguientes-pasos","title":"Siguientes pasos","text":"

    Bueno, el siguiente paso m\u00e1s evidente, ser\u00e1 ver que si conectas el frontend sigue funcionando exactamente igual que lo estaba haciendo antes.

    Ahora te propongo hacer el mismo ejercicio con los otros dos m\u00f3dulos Cliente y Pr\u00e9stamo que has tenido que implementar en el punto Ahora hazlo tu!.

    Ten en cuenta que Cliente no depende de nadie, pero Pr\u00e9stamo si que depende de Cliente y de Game. A ver como solucionas los cruces y sobre todo los filtros

    "},{"location":"appendix/springcloud/summary/#mas-formacion-mas-informacion","title":"M\u00e1s formaci\u00f3n, m\u00e1s informaci\u00f3n","text":"

    Pues ya estar\u00eda todo, ahora solo te puedo dar la enhorabuena y pasar algo de informaci\u00f3n extra / cursos / formaciones por si quieres seguir aprendiendo.

    Por un lado tienes el itinerario avanzado de Springboot donde se puede m\u00e1s detalle de micro servicios.

    Por otro lado tambi\u00e9n tienes los itinerarios de Cloud ya que no todo va a ser micro servicios con Spring Cloud, tambi\u00e9n existen micro servicios con otras tecnolog\u00edas, aunque el concepto es muy similar.

    "},{"location":"cleancode/angular/","title":"Estructura y Buenas pr\u00e1cticas - Angular","text":"

    Nota

    Antes de empezar y para puntualizar, Angular se considera un framework SPA Single-page application.

    En esta parte vamos a explicar los fundamentos de un proyecto en Angular y las recomendaciones existentes.

    "},{"location":"cleancode/angular/#estructura-y-funcionamiento","title":"Estructura y funcionamiento","text":""},{"location":"cleancode/angular/#ciclo-de-vida-de-angular","title":"Ciclo de vida de Angular","text":"

    El comportamiento de ciclo de vida de un componente Angular pasa por diferentes etapas que podemos ver en el esquema que mostramos a continuaci\u00f3n:

    Es importante tenerlo claro para saber que m\u00e9todos podemos utilizar para realizar operaciones con el componente.

    "},{"location":"cleancode/angular/#carpetas-creadas-por-angular","title":"Carpetas creadas por Angular","text":"

    Al crear una aplicaci\u00f3n Angular, tendremos los siguientes directorios:

    • node_modules: Todos los m\u00f3dulos de librer\u00edas usadas por el proyecto.
    • \\src\\app: Contiene todo el c\u00f3digo asociado al proyecto.
      • \\src\\assets: Normalmente la carpeta usada para los recursos.
      • \\src\\environments: Aqu\u00ed ir\u00e1n los ficheros relacionados con los entornos de desarrollos.

    Otros ficheros importantes de un proyecto de Angular

    Otros archivos que debemos tener en cuenta dentro del proyecto son:

    • angular.json: Configuraci\u00f3n del propio CLI. La madre de todos los configuradores
    • package.json: Dependencias de librer\u00edas y scripts
    "},{"location":"cleancode/angular/#estructura-de-modulos","title":"Estructura de m\u00f3dulos","text":"

    Existe m\u00faltiples consensos al respecto de como estructurar un proyecto en Angular, pero al final, depende de los requisitos del proyecto. Una sugerencia de como hacerlo es la siguiente:

    - src\\app\n    - core              /* Componentes y utilidades comunes */ \n        - header        /* Estructura del header */ \n        - footer        /* Estructura del footer */ \n  - domain1       /* M\u00f3dulo con los componentes del dominio1 */\n      - services        /* Servicios con operaciones del dominio1 */ \n      - models          /* Modelos de datos del dominio1 */ \n      - component1      /* Componente1 del dominio1 */ \n      - componentX      /* ComponenteX del dominio1 */ \n  - domainX       /* As\u00ed para el resto de dominios de la aplicaci\u00f3n */\n

    Recordar, que esto es una sugerencia para una estructura de carpetas y componentes. No existe un estandar.

    ATENCI\u00d3N: Componentes gen\u00e9ricos

    Debemos tener en cuenta que a la hora de programar un componente core, lo ideal es pensar que sea un componente plug & play, es decir que si lo copias y lo llevas a otro proyecto funcione sin la necesidad de adaptarlo.

    "},{"location":"cleancode/angular/#buenas-practicas","title":"Buenas pr\u00e1cticas","text":"

    A continuaci\u00f3n veremos un listado de buenas pr\u00e1cticas de Angular y de c\u00f3digo limpio que deber\u00edamos intentar seguir en nuestro desarrollo.

    "},{"location":"cleancode/angular/#estructura-de-archivos","title":"Estructura de archivos","text":"

    Antes de empezar con un proyecto lo ideal, es pararse y pensar en los requerimientos de una buena estructura, en un futuro lo agradecer\u00e1s.

    "},{"location":"cleancode/angular/#nombres-claros","title":"Nombres claros","text":"

    Utilizar la S de los principios S.O.L.I.D para los nombres de variables, m\u00e9todos y dem\u00e1s c\u00f3digo.

    El efecto que produce este principio son clases con nombres muy descriptivos y por tanto largos.

    Tambi\u00e9n se recomienta utilizar kebab-case para los nombres de ficheros. Ej. hero-button.component.ts

    "},{"location":"cleancode/angular/#organiza-tu-codigo","title":"Organiza tu c\u00f3digo","text":"

    Intenta organizar tu c\u00f3digo fuente:

    • Lo m\u00e1s importante debe ir arriba.
    • Primero propiedades, despu\u00e9s m\u00e9todos.
    • Un Item para un archivo: cada archivo deber\u00eda contener solamente un componente, al igual que los servicios.
    • Solo una responsabilidad: Cada clase o modulo deber\u00eda tener solamente una responsabilidad.
    • El nombre correcto: las propiedades y m\u00e9todos deber\u00edan usar el sistema de camel case (ej: getUserByName), al contrario, las clases (componentes, servicios, etc) deben usar upper camel case (ej: UserComponent).
    • Los componentes y servicios deben tener su respectivo sufijo: UserComponent, UserService.
    • Imports: los archivos externos van primero.
    "},{"location":"cleancode/angular/#usar-linters-prettier-eslint","title":"Usar linters Prettier & ESLint","text":"

    Un linter es una herramienta que nos ayuda a seguir las buenas pr\u00e1cticas o gu\u00edas de estilo de nuestro c\u00f3digo fuente. En este caso, para JavaScript, proveeremos de unos muy famosos. Una de las m\u00e1s famosas es la combinaci\u00f3n de Angular app to ESLint with Prettier, AirBnB Styleguide Recordar que a\u00f1adir este tipo de configuraci\u00f3n es opcional, pero necesaria para tener un buen c\u00f3digo de calidad.

    "},{"location":"cleancode/angular/#git-hooks","title":"Git Hooks","text":"

    Los Git Hooks son scripts de shell que se ejecutan autom\u00e1ticamente antes o despu\u00e9s de que Git ejecute un comando importante como Commit o Push. Para hacer uso de el es tan sencillo como:

    npm install husky --save-dev

    Y a\u00f1adir en el fichero lo siguiente:

    // package.json\n{\n  \"husky\": {\n    \"hooks\": {\n      \"pre-commit\": \"npm test\",\n      \"pre-push\": \"npm test\",\n      \"...\": \"...\"\n    }\n  }\n}\n

    Usar husky para el preformateo de c\u00f3digo antes de subirlo

    Es una buena pr\u00e1ctica que todo el equipo use el mismo est\u00e1ndar de formateo de codigo, con husky se puede solucionar.

    "},{"location":"cleancode/angular/#utilizar-banana-in-the-box","title":"Utilizar Banana in the Box","text":"

    Como el nombre sugiere banana in the box se debe a la forma que tiene lo siguiente: [{}] Esto es una forma muy sencilla de trabajar los cambios en la forma de Two ways binding. Es decir, el padre informa de un valor u objeto y el hijo lo manipula y actualiza el estado/valor al padre inmediatamente. La forma de implementarlo es sencillo

    Padre: HTML:

    <my-input [(text)]=\"text\"></my-input>

    Hijo

    @Input() value: string;\n@Output() valueChange = new EventEmitter<string>();\nupdateValue(value){\n    this.value = value;\n    this.valueChange.emit(value);\n}\n

    Prefijo Change

    Destacar que el prefijo 'Change' es necesario incluirlo en el Hijo para que funcione

    "},{"location":"cleancode/angular/#correcto-uso-de-los-servicios","title":"Correcto uso de los servicios","text":"

    Una buena practica es aconsejable no declarar los servicios en el provides, sino usar un decorador que forma parte de las ultimas versiones de Angular

    @Injectable({\n  providedIn: 'root',\n})\nexport class HeroService {\n  constructor() { }\n}\n
    "},{"location":"cleancode/angular/#lazy-load","title":"Lazy Load","text":"

    Lazy Load es un patr\u00f3n de dise\u00f1o que consiste en retrasar la carga o inicializaci\u00f3n

    desde el app-routing.module.ts o desde app.routes.ts si estamos en Angular 17+

    A\u00f1adiremos un codigo parecido a este

      // Para cargar modulos\n  {\n    path: 'customers',\n    loadChildren: () => import('./customers/customers.module').then(m => m.CustomersModule)\n  },\n\n  // Para cargar standalone components\n  {\n    path: 'customers',\n    loadComponent: () => import('./customers/customers.component').then(m => m.CustomersComponent)\n  },\n

    Con esto veremos que el m\u00f3dulo o componente se cargar\u00e1 seg\u00fan se necesite.

    "},{"location":"cleancode/nodejs/","title":"Estructura y Buenas pr\u00e1cticas - Nodejs","text":""},{"location":"cleancode/nodejs/#estructura-y-funcionamiento","title":"Estructura y funcionamiento","text":"

    En los proyectos Nodejs no existe nada estandarizado y oficial que hable sobre estructura de proyectos y nomenclatura de Nodejs. Tan solo existen algunas sugerencias y buenas pr\u00e1cticas a la hora de desarrollar que te recomiendo que utilices en la medida de lo posible.

    Tip

    Piensa que el c\u00f3digo fuente que escribes hoy, es como un libro que se leer\u00e1 durante a\u00f1os. Alguien tendr\u00e1 que coger tu c\u00f3digo y leerlo en unos meses o a\u00f1os para hacer alguna modificaci\u00f3n y, como buenos desarrolladores que somos, tenemos la obligaci\u00f3n de facilitarle en todo lo posible la comprensi\u00f3n de ese c\u00f3digo fuente. Quiz\u00e1 esa persona futura podr\u00edas ser tu en unos meses y quedar\u00eda muy mal que no entendieras ni tu propio c\u00f3digo

    "},{"location":"cleancode/nodejs/#estructura-en-capas","title":"Estructura en capas","text":"

    Todos los proyectos para crear una Rest API con node y express est\u00e1n divididos en capas. Como m\u00ednimo estar\u00e1 la capa de rutas, controlador y modelo. En nuestro caso vamos a a\u00f1adir una capa mas de servicios para quitarle trabajo al controlador y desacoplarlo de la capa de datos. As\u00ed si en el futuro queremos cambiar nuestra base de datos no romperemos tanto \ud83d\ude0a

    Rutas

    En nuestro proyecto una ruta ser\u00e1 una secci\u00f3n de c\u00f3digo express que asociar\u00e1 un verbo http, una ruta o patr\u00f3n de url y una funci\u00f3n perteneciente al controlador para manejar esa petici\u00f3n.

    Controladores

    En nuestros controladores tendremos los m\u00e9todos que obtendr\u00e1n las solicitudes de las rutas, se comunicar\u00e1n con la capa de servicio y convertir\u00e1n estas solicitudes en respuestas http.

    Servicio

    Nuestra capa de servicio incluir\u00e1 toda la l\u00f3gica de negocio de nuestra aplicaci\u00f3n. Para realizar sus operaciones puede realizar llamadas tanto a otras clases dentro de esta capa, como a clases de la capa inferior.

    Modelo

    Como su nombre indica esta capa representa los modelos de datos de nuestra aplicaci\u00f3n. En nuestro caso, al usar un ODM, solo tendremos modelos de datos definidos seg\u00fan sus requisitos.

    "},{"location":"cleancode/nodejs/#buenas-practicas","title":"Buenas pr\u00e1cticas","text":""},{"location":"cleancode/nodejs/#accesos-entre-capas","title":"Accesos entre capas","text":"

    En base a la divisi\u00f3n por capas que hemos comentado arriba, y el resto de entidades implicadas, hay una serie de reglas important\u00edsimas que debes seguir muy de cerca:

    • Un Controlador

      • NO debe contener l\u00f3gica en su clase. Solo est\u00e1 permitido que ejecute l\u00f3gica a trav\u00e9s de una llamada al objeto de la capa L\u00f3gica.
      • NO puede ejecutar directamente operaciones de la capa Acceso a Datos, siempre debe pasar por la capa de servicios.
      • Debemos seguir una coherencia entre todas las URL de las operaciones. Por ejemplo, si elegimos save para guardar, usemos esa palabra en todas las operaciones que sean de ese tipo. Evitad utilizar diferentes palabras save, guardar, persistir, actualizar para la misma acci\u00f3n.
    • Un Servicio

      • NO puede llamar a objetos de la capa Controlador.
      • NO debe llamar a Acceso a Datos que NO sean de su \u00e1mbito / competencia.
      • Si es necesario puede llamar a otros Servicios para recuperar cierta informaci\u00f3n que no sea de su \u00e1mbito / competencia.
      • Es un buen lugar para implementar la l\u00f3gica de negocio.
    "},{"location":"cleancode/nodejs/#usar-linters-prettier-eslint-se-recomienda-encarecidamente","title":"Usar linters Prettier & ESLint (Se recomienda encarecidamente)","text":"

    Un linter es una herramienta que nos ayuda a seguir las buenas pr\u00e1cticas o gu\u00edas de estilo de nuestro c\u00f3digo fuente. En este caso, para JavaScript, proveeremos de unos muy famosos. Una de las m\u00e1s famosas es la combinaci\u00f3n de Angular app to ESLint with Prettier, AirBnB Styleguide Recordar que a\u00f1adir este tipo de configuraci\u00f3n es opcional, pero necesaria para tener un buen c\u00f3digo de calidad.

    "},{"location":"cleancode/react/","title":"Estructura y Buenas pr\u00e1cticas - React","text":"

    Nota

    Antes de empezar y para puntualizar, React se considera un framework SPA Single-page application.

    Aqu\u00ed tenemos que puntualizar que React por s\u00ed mismo es una librer\u00eda y no un framework, puesto que se ocupa de las interfaces de usuario. Sin embargo, diversos a\u00f1adidos pueden convertir a React en un producto equiparable en caracter\u00edsticas a un framework.

    En esta parte vamos a explicar los fundamentos de un proyecto en React y las recomendaciones existentes.

    "},{"location":"cleancode/react/#estructura-y-funcionamiento","title":"Estructura y funcionamiento","text":""},{"location":"cleancode/react/#como-funciona-react","title":"Como funciona React","text":"

    React es una herramienta para crear interfaces de usuario de una manera \u00e1gil y vers\u00e1til, en lugar de manipular el DOM del navegador directamente, React crea un DOM virtual en la memoria, d\u00f3nde realiza toda la manipulaci\u00f3n necesaria antes de realizar los cambios en el DOM del navegador. Estas interfaces de usuario denominadas componentes pueden definirse como clases o funciones independiente y reutilizables con unos par\u00e1metros de entrada que devuelven elementos de react. En ese tutorial solo utilizaremos componentes de tipo funci\u00f3n.

    Por si no te suena, un componente web es una forma de crear un bloque de c\u00f3digo encapsulado y de responsabilidad \u00fanica que puede reutilizarse en cualquier pagina mediante nuevas etiquetas html.

    Nota

    Desde la versi\u00f3n 16.8 se introdujo en React el concepto de hooks. Esto permiti\u00f3 usar el estado y otras caracter\u00edsticas de React sin necesidad de escribir una clase.

    "},{"location":"cleancode/react/#ciclo-de-vida-de-un-componente-en-react","title":"Ciclo de vida de un componente en React","text":"

    El comportamiento de ciclo de vida de un componente React pasa por diferentes etapas que podemos ver en el esquema que mostramos a continuaci\u00f3n:

    Es importante tenerlo claro para saber que m\u00e9todos podemos utilizar para realizar operaciones con el componente.

    "},{"location":"cleancode/react/#carpetas-creadas-por-react","title":"Carpetas creadas por React","text":"

    Al crear una aplicaci\u00f3n React, tendremos los siguientes directorios:

    • node_modules: Todos los m\u00f3dulos de librer\u00edas usadas por el proyecto.
    • \\src\\app: Contiene todo el c\u00f3digo asociado al proyecto.
      • \\src\\assets: Normalmente la carpeta usada para los recursos.

    Otros ficheros importantes de un proyecto de React

    Otros archivos que debemos tener en cuenta dentro del proyecto son:

    • package.json: Dependencias de librer\u00edas y scripts
    "},{"location":"cleancode/react/#estructura-de-nuestro-proyecto","title":"Estructura de nuestro proyecto","text":"

    Existe m\u00faltiples consensos al respecto de c\u00f3mo estructurar un proyecto en React, pero al final, depende de los requisitos del proyecto. Una sugerencia de c\u00f3mo hacerlo es la siguiente:

    - src\\\n    - components         /* Componentes comunes */ \n  - context            /* Carpeta para almacenar el contexto de la aplicaci\u00f3n */ \n  - pages              /* Carpeta para componentes asociados a rutas del navegador */\n      - components     /* Componentes propios de cada p\u00e1gina */ \n  - redux              /* Para todo aquello relacionado con el estado de nuestra aplicaci\u00f3n */\n  - types              /* Carpeta para los tipos de datos de typescript */\n

    Recordad, que \u00e9sto es una sugerencia para una estructura de carpetas y componentes. No existe un est\u00e1ndar.

    "},{"location":"cleancode/react/#buenas-practicas","title":"Buenas pr\u00e1cticas","text":"

    A continuaci\u00f3n, veremos un listado de buenas pr\u00e1cticas de React y de c\u00f3digo limpio que deber\u00edamos intentar seguir en nuestro desarrollo.

    "},{"location":"cleancode/react/#estructura-de-archivos","title":"Estructura de archivos","text":"

    Antes de empezar con un proyecto lo ideal, es pararse y pensar en los requerimientos de una buena estructura, en un futuro lo agradecer\u00e1s.

    "},{"location":"cleancode/react/#nombres-claros","title":"Nombres claros","text":"

    Utilizar la S de los principios S.O.L.I.D para los nombres de variables, m\u00e9todos y dem\u00e1s c\u00f3digo.

    El efecto que produce este principio son clases con nombres muy descriptivos y por tanto largos.

    "},{"location":"cleancode/react/#organiza-tu-codigo","title":"Organiza tu c\u00f3digo","text":"

    Intenta organizar tu c\u00f3digo fuente:

    • Lo m\u00e1s importante debe ir arriba.
    • Primero propiedades, despu\u00e9s m\u00e9todos.
    • Un Item para un archivo: cada archivo deber\u00eda contener solamente un componente, al igual que los servicios.
    • Solo una responsabilidad: Cada clase o modulo deber\u00eda tener solamente una responsabilidad.
    • El nombre correcto: las propiedades y m\u00e9todos deber\u00edan usar el sistema de camel case (ej: getUserByName), al contrario, las clases (componentes, servicios, etc) deben usar upper camel case (ej: UserComponent).
    • Los componentes y servicios deben tener su respectivo sufijo: UserComponent, UserService.
    • Imports: los archivos externos van primero.
    "},{"location":"cleancode/react/#usar-linters-prettier-eslint","title":"Usar linters Prettier & ESLint","text":"

    Un linter es una herramienta que nos ayuda a seguir las buenas pr\u00e1cticas o gu\u00edas de estilo de nuestro c\u00f3digo fuente. En este caso, para JavaScript, proveeremos de unos muy famosos. Recordar que a\u00f1adir este tipo de configuraci\u00f3n es opcional, pero necesaria para tener un buen c\u00f3digo de calidad.

    "},{"location":"cleancode/react/#usa-el-estado-correctamente","title":"Usa el estado correctamente","text":"

    La primera regla del hook useState es usarlo solo localmente. El estado global de nuestra aplicaci\u00f3n debe de entrar a nuestro componente a trav\u00e9s de las props as\u00ed como las mutaciones de este solo deben realizarse mediante alguna herramienta de gesti\u00f3n de estados como redux. Por otro lado, es preferible no abusar de los hooks y solo usarlos cuando sea realmente necesario ya que pueden reducir el rendimiento de nuestra aplicaci\u00f3n.

    "},{"location":"cleancode/react/#reutiliza-codigo-y-componentes","title":"Reutiliza c\u00f3digo y componentes","text":"

    Siempre que sea posible deberemos de reutilizar c\u00f3digo mediante funciones compartidas o bien si este c\u00f3digo implica almacenamiento de estado u otras caracter\u00edsticas similares mediante custom Hooks.

    "},{"location":"cleancode/react/#usa-ts-en-lugar-de-js","title":"Usa TS en lugar de JS","text":"

    Ya hemos creado nuestro proyecto incluyendo typescript pero esto no viene por defecto en un proyecto React como si pasa con Angular. Nuestra recomendaci\u00f3n es que siempre que puedas a\u00f1adas typescript a tus proyectos React, no solo se gana calidad en el c\u00f3digo, sino que eliminamos la probabilidad de usar un componente incorrectamente y ganamos tiempo de desarrollo.

    "},{"location":"cleancode/springboot/","title":"Estructura y Buenas pr\u00e1cticas - Spring Boot","text":""},{"location":"cleancode/springboot/#estructura-y-funcionamiento","title":"Estructura y funcionamiento","text":"

    En Springboot no existe nada estandarizado y oficial que hable sobre estructura de proyectos y nomenclatura. Tan solo existen algunas sugerencias y buenas pr\u00e1cticas a la hora de desarrollar que te recomiendo que utilices en la medida de lo posible.

    Tip

    Piensa que el c\u00f3digo fuente que escribes hoy, es como un libro que se leer\u00e1 durante a\u00f1os. Alguien tendr\u00e1 que coger tu c\u00f3digo y leerlo en unos meses o a\u00f1os para hacer alguna modificaci\u00f3n y, como buenos desarrolladores que somos, tenemos la obligaci\u00f3n de facilitarle en todo lo posible la comprensi\u00f3n de ese c\u00f3digo fuente. Quiz\u00e1 esa persona futura podr\u00edas ser tu en unos meses y quedar\u00eda muy mal que no entendieras ni tu propio c\u00f3digo

    "},{"location":"cleancode/springboot/#estructura-en-capas","title":"Estructura en capas","text":"

    Todos los proyectos web que construimos basados en Springboot se caracterizan por estar divididos en tres capas (a menos que utilicemos DDD para desarrollar que entonces existen infinitas capas ).

    • Controlador. Es la capa m\u00e1s alta, la que tiene acceso directo con el cliente. En esta capa es donde se exponen las operaciones que queremos publicar y que el cliente puede consumir. Para realizar sus operaciones lo m\u00e1s normal es que realice llamadas a las clases de la capa inmediatamente inferior.
    • L\u00f3gica. Tambi\u00e9n llamada capa de Servicios. Es la capa intermedia que da soporte a las operaciones que est\u00e1n expuestas y ejecutan toda la l\u00f3gica de negocio de la aplicaci\u00f3n. Para realizar sus operaciones puede realizar llamadas tanto a otras clases dentro de esta capa, como a clases de la capa inferior.
    • Acceso a Datos. Como su nombre indica, es la capa que accede a datos. T\u00edpicamente es la capa que ejecuta las consultas contra BBDD, pero esto no tiene por qu\u00e9 ser obligadamente as\u00ed. Tambi\u00e9n entrar\u00edan en esa capa aquellas clases que consumen datos externos, por ejemplo de un servidor externo. Las clases de esta capa deben ser nodos finales, no pueden llamar a ninguna otra clase para ejecutar sus operaciones, ni siquiera de su misma capa.
    "},{"location":"cleancode/springboot/#estructura-de-proyecto","title":"Estructura de proyecto","text":"

    En proyectos medianos o grandes, estructurar los directorios del proyecto en base a la estructura anteriormente descrita ser\u00eda muy complejo, ya que en cada uno de los niveles tendr\u00edamos muchas clases. As\u00ed que lo normal es diferenciar por \u00e1mbito funcional y dentro de cada package realizar la separaci\u00f3n en Controlador, L\u00f3gica y Acceso a datos.

    Tened en cuenta en un mismo \u00e1mbito funcional puede tener varios controladores o varios servicios de l\u00f3gica uno por cada entidad que estemos tratando. Siempre que se pueda, agruparemos entidades que intervengan dentro de una misma funcionalidad.

    En nuestro caso del tutorial, tendremos tres \u00e1mbitos funcionales Categor\u00eda, Autor, y Juego que diferenciaremos cada uno con su propia estructura.

    "},{"location":"cleancode/springboot/#buenas-practicas","title":"Buenas pr\u00e1cticas","text":""},{"location":"cleancode/springboot/#nomenclatura-de-las-clases","title":"Nomenclatura de las clases","text":"

    @TODO: En construcci\u00f3n

    "},{"location":"cleancode/springboot/#accesos-entre-capas","title":"Accesos entre capas","text":"

    En base a la divisi\u00f3n por capas que hemos comentado arriba, y el resto de entidades implicadas, hay una serie de reglas important\u00edsimas que debes seguir muy de cerca:

    • Un Controlador
      • NO debe contener l\u00f3gica en su clase. Solo est\u00e1 permitido que ejecute l\u00f3gica a trav\u00e9s de una llamada al objeto de la capa L\u00f3gica.
      • NO puede ejecutar directamente operaciones de la capa Acceso a Datos, siempre debe pasar por la capa L\u00f3gica.
      • NO debe enviar ni recibir del cliente objetos de tipo Entity.
      • Es un buen lugar para realizar las conversiones de datos entre Entity y Dto.
      • En teor\u00eda cada operaci\u00f3n deber\u00eda tener su propio Dto, aunque los podemos reutilizar entre operaciones similares.
      • Debemos seguir una coherencia entre todas las URL de las operaciones. Por ejemplo si elegimos save para guardar, usemos esa palabra en todas las operaciones que sean de ese tipo. Evitad utilizar diferentes palabras save, guardar, persistir, actualizar para la misma acci\u00f3n.
    • Un Servicio
      • NO puede llamar a objetos de la la capa Controlador.
      • NO puede ejecutar directamente queries contra la BBDD, siempre debe pasar por la capa Acceso a Datos.
      • NO debe llamar a Acceso a Datos que NO sean de su \u00e1mbito / competencia.
      • Si es necesario puede llamar a otros Servicios para recuperar cierta informaci\u00f3n que no sea de su \u00e1mbito / competencia.
      • Debe trabajar en la medida de lo posible con objetos de tipo Entity.
      • Es un buen lugar para implementar la l\u00f3gica de negocio.
    • Un Acceso a Datos
      • NO puede llamar a ninguna otra capa. Ni Controlador, ni Servicios, ni Acceso a Datos.
      • NO debe contener l\u00f3gica en su clase.
      • Esta capa solo debe resolver el dato que se le ha solicitado y devolverlo a la capa de Servicios.
    "},{"location":"cleancode/vuejs/","title":"Estructura y Buenas pr\u00e1cticas - Vue.js","text":"

    Nota

    Antes de empezar y para puntualizar, Vue.js es un framework progresivo para construir interfaces de usuario. A diferencia de otros frameworks monol\u00edticos, Vue.js est\u00e1 dise\u00f1ado desde cero para ser utilizado incrementalmente. La librer\u00eda central est\u00e1 enfocada solo en la capa de visualizaci\u00f3n, y es f\u00e1cil de utilizar e integrar con otras librer\u00edas o proyectos existentes. Por otro lado, Vue.js tambi\u00e9n es perfectamente capaz de impulsar sofisticadas Single-Page Applications cuando se utiliza en combinaci\u00f3n con herramientas modernas y librer\u00edas de apoyo.

    En esta parte vamos a explicar los fundamentos de un proyecto en Vue.js y las recomendaciones existentes.

    "},{"location":"cleancode/vuejs/#estructura-y-funcionamiento","title":"Estructura y funcionamiento","text":""},{"location":"cleancode/vuejs/#ciclos-de-vida-de-un-componente","title":"Ciclos de vida de un componente","text":"

    Vue.js cuenta con un conjunto de ciclos de vida que permiten a los desarrolladores controlar y personalizar el comportamiento de sus componentes en diferentes momentos. Estos ciclos de vida se pueden agrupar en tres fases principales: creaci\u00f3n, actualizaci\u00f3n y eliminaci\u00f3n.

    A continuaci\u00f3n, te explicar\u00e9 cada uno de los ciclos de vida disponibles en Vue.js junto con la Options API:

    1. beforeCreate: Este ciclo de vida se ejecuta inmediatamente despu\u00e9s de que se haya creado una instancia de componente, pero antes de que se haya creado su DOM. En este punto, a\u00fan no es posible acceder a las propiedades del componente y a\u00fan no se han establecido las observaciones reactivas.

    2. created: Este ciclo de vida se ejecuta despu\u00e9s de que se haya creado una instancia de componente y se hayan establecido las observaciones reactivas. En este punto, el componente ya puede acceder a sus propiedades y m\u00e9todos.

    3. beforeMount: Este ciclo de vida se ejecuta justo antes de que el componente se monte en el DOM. En este punto, el componente ya est\u00e1 preparado para ser renderizado, pero a\u00fan no se ha agregado al \u00e1rbol de elementos del DOM.

    4. mounted: Este ciclo de vida se ejecuta despu\u00e9s de que el componente se ha montado en el DOM. En este punto, el componente ya est\u00e1 en el \u00e1rbol de elementos del DOM y se puede acceder a sus elementos hijos y a los elementos del DOM que lo rodean.

    5. beforeUpdate: Este ciclo de vida se ejecuta justo antes de que el componente se actualice en respuesta a un cambio en sus propiedades o estado. En este punto, el componente a\u00fan no se ha actualizado en el DOM.

    6. updated: Este ciclo de vida se ejecuta despu\u00e9s de que el componente se haya actualizado en el DOM en respuesta a un cambio en sus propiedades o estado. En este punto, el componente ya se ha actualizado en el DOM y se puede acceder a sus elementos hijos y a los elementos del DOM que lo rodean.

    7. beforeUnmount: Este ciclo de vida se ejecuta justo antes de que el componente se elimine del DOM. En este punto, el componente a\u00fan est\u00e1 en el \u00e1rbol de elementos del DOM.

    8. unmounted: Este ciclo de vida se ejecuta despu\u00e9s de que el componente se haya eliminado del DOM. En este punto, el componente ya no est\u00e1 en el \u00e1rbol de elementos del DOM y no se puede acceder a sus elementos hijos.

    9. errorCaptured: Este ciclo de vida se ejecuta cuando se produce un error en cualquier descendiente del componente y se captura en el componente actual. Esto permite que el componente maneje el error de forma personalizada en lugar de propagarse hacia arriba en la cadena de componentes.

    10. activated: Este ciclo de vida se ejecuta cuando un componente que se encuentra en un \u00e1rbol de componentes inactivo (por ejemplo, un componente en una pesta\u00f1a inactiva) se activa.

    11. deactivated: Este ciclo de vida se ejecuta cuando un componente que se encuentra en un \u00e1rbol de componentes activo (por ejemplo, un componente en una pesta\u00f1a activa) se desactiva y se vuelve inactivo.

    12. renderTracked: Este ciclo de vida se ejecuta cuando se observa una dependencia en el proceso de renderizado del componente. Esto se utiliza principalmente para fines de depuraci\u00f3n.

    13. renderTriggered: Este ciclo de vida se ejecuta cuando se desencadena un nuevo renderizado del componente. Esto se utiliza principalmente para fines de depuraci\u00f3n.

    14. serverPrefetch: Este ciclo de vida se utiliza en el contexto de renderizado del lado del servidor (SSR). Se ejecuta cuando el componente se preprocesa en el servidor antes de enviarse al cliente. En este punto, el componente a\u00fan no se ha montado en el DOM y no se pueden realizar operaciones que dependan del DOM. Esto se utiliza principalmente para cargar datos de forma as\u00edncrona antes de que se renderice el componente en el servidor.

    Os dejo un peque\u00f1o esquema de los ciclos de vida mas importantes y en que momento se ejecutan:

    Es importante tenerlo claro para saber que m\u00e9todos podemos utilizar para realizar operaciones con el componente.

    "},{"location":"cleancode/vuejs/#carpetas-creadas-por-vuejs","title":"Carpetas creadas por Vue.js","text":"
    • node_modules: Todos los m\u00f3dulos de librer\u00edas usadas por el proyecto.
    • public: Contiene iconos y archivos accesibles por todos los usuarios.
    • .quasar: Contiene configuraci\u00f3n propia de Quasar.
    • \\src: Contiene todo el c\u00f3digo asociado al proyecto.
      • \\src\\assets: Normalmente la carpeta usada para los recursos.
      • \\src\\components: Aqu\u00ed ir\u00e1n los diferentes componentes que iremos creando para la aplicaci\u00f3n.
      • \\src\\router: Es la carpeta donde el scafolding nos mete el router con sus diferentes rutas.
      • \\src\\layouts: Aqu\u00ed iran las diferentes vistas de la aplicaci\u00f3n.

    Otros ficheros importantes de un proyecto de Vue.js

    Otros archivos que debemos tener en cuenta dentro del proyecto son:

    • quasar.d.ts: Configurador de la conexi\u00f3n entre la librer\u00eda y Vue
    • package.json: Dependencias de librer\u00edas y scripts
    • quasar.config.js: Configurador del CLI de Quasar
    • \\src\\App.vue: Punto de entrada a nuestra aplicaci\u00f3n
    "},{"location":"cleancode/vuejs/#buenas-practicas","title":"Buenas pr\u00e1cticas","text":"

    A continuaci\u00f3n veremos un listado de buenas pr\u00e1cticas de Vue.js y de c\u00f3digo limpio que deber\u00edamos intentar seguir en nuestro desarrollo.

    "},{"location":"cleancode/vuejs/#estructura-de-archivos","title":"Estructura de archivos","text":"

    Antes de empezar con un proyecto lo ideal, es pararse y pensar en los requerimientos de una buena estructura, en un futuro lo agradecer\u00e1s.

    "},{"location":"cleancode/vuejs/#nombres-claros","title":"Nombres claros","text":"

    Determinar una manera de nombrar a los componentes (UpperCamelCase, lowerCamelCase, kebab-case, snake_case, ...) y continuarla para todos los archivos, nombres descriptivos de los componentes y en una ruta acorde (si es un componente que forma parte de una pantalla, se ubicar\u00e1 dentro de la carpeta de esa pantalla pero si se usa en m\u00e1s de una pantalla, se ubicar\u00e1 en una carpeta externa a cualquier pantalla llamada common), componentes de m\u00e1ximo 350 l\u00edneas y componentes con finalidad \u00fanica (recibe los datos necesarios para realizar las tareas b\u00e1sicas de ese componente).

    "},{"location":"cleancode/vuejs/#organiza-tu-codigo","title":"Organiza tu c\u00f3digo","text":"

    El c\u00f3digo debe estar ordenado dentro de los componente siguiendo un orden de importancia similar a este:

    1. Importaciones de las diferentes librer\u00edas o componentes usados.
    2. Importaciones de funciones de otros archivos (como utils).
    3. Variables o constantes usadas para almacenar la informaci\u00f3n necesaria en este componente.
    4. Funciones necesarias para el resto del c\u00f3digo.
    5. Variables computadas, watchers, etc.
    6. C\u00f3digo HTML del componente.
    "},{"location":"cleancode/vuejs/#consejos-varios","title":"Consejos varios","text":"

    Modificaciones entre componentes

    A la hora de crear un componente b\u00e1sico (como un input propio) que necesite modificar su propio valor (algo que un componente hijo no debe hacer, ya que la variable estar\u00e1 en el padre), saber diferenciar entre v-model y modelValue (esta \u00faltima s\u00ed que permite modificar el valor en el padre mediante el evento update:modelValue sin tener que hacer nada m\u00e1s en el padre que pasarle el valor).

    Utiliza formateo y correcci\u00f3n de c\u00f3digo

    Si has seguido nuestro tutorial se habr\u00e1 instalado ESLint y Prettier. Si no, deber\u00edas instalarlo para generar c\u00f3digo de buena calidad. Adem\u00e1s de instalar alguna extensi\u00f3n en Visual Studio Code que te ayude a gestionar esas herramientas.

    Nomenclatura de funciones y variables

    El nombre de las funciones, al igual que los path de una API, deber\u00edan ser autoexplicativos y no tener que seguir la traza del c\u00f3digo para saber qu\u00e9 hace. Con un buen nombre para cada funci\u00f3n o variables de estado, evitas tener que a\u00f1adir comentarios para explicar qu\u00e9 hace o qu\u00e9 almacena cada una de ellas.

    "},{"location":"develop/basic/angular/","title":"Listado simple - Angular","text":"

    Ahora que ya tenemos listo el proyecto frontend de Angular (en el puerto 4200), ya podemos empezar a codificar la soluci\u00f3n.

    "},{"location":"develop/basic/angular/#primeros-pasos","title":"Primeros pasos","text":"

    Antes de empezar

    Quiero hacer hincapi\u00e9 que Angular tiene una documentaci\u00f3n muy extensa y completa, as\u00ed que te recomiendo que hagas uso de ella cuando tengas cualquier duda. Tanto en la propia web de documentaci\u00f3n de Angular como en la web de componentes Angular Material puedes buscar casi cualquier ejemplo que necesites.

    Si abrimos el proyecto con el IDE que tengamos (Visual Studio Code en el caso del tutorial) podemos ver que en la carpeta src/app existen unos ficheros ya creados por defecto. Estos ficheros son:

    • app.component.ts \u2192 contiene el c\u00f3digo inicial del proyecto escrito en TypeScript.
    • app.component.html \u2192 contiene la plantilla inicial del proyecto escrita en HTML.
    • app.component.scss \u2192 contiene los estilos CSS privados de la plantilla inicial.

    Vamos a modificar este c\u00f3digo inicial para ver como funciona. Abrimos el fichero app.component.ts y modificamos la l\u00ednea donde se asigna un valor a la variable title.

    app.component.ts
    ...\ntitle = 'Tutorial de Angular';\n...\n

    Ahora abrimos el fichero app.component.html, borramos todo el c\u00f3digo de la plantilla y a\u00f1adimos el siguiente c\u00f3digo:

    app.component.html
    <h1>{{title}}</h1>\n

    Las llaves dobles permiten hacen un binding entre el c\u00f3digo del componente y la plantilla. Es decir, en este caso ir\u00e1 al c\u00f3digo TypeScript y buscar\u00e1 el valor de la variable title.

    Consejo

    El binding tambi\u00e9n nos sirve para ejecutar los m\u00e9todos de TypeScript desde el c\u00f3digo HTML. Adem\u00e1s si el valor que contiene la variable se modificara durante la ejecuci\u00f3n de alg\u00fan m\u00e9todo, autom\u00e1ticamente el c\u00f3digo HTML refrescar\u00eda el nuevo valor de la variable title

    Si abrimos el navegador y accedemos a http://localhost:4200/ podremos ver el resultado del c\u00f3digo.

    "},{"location":"develop/basic/angular/#layout-general","title":"Layout general","text":""},{"location":"develop/basic/angular/#crear-componente","title":"Crear componente","text":"

    Lo primero que vamos a hacer es escoger un tema y una paleta de componentes para trabajar. Lo m\u00e1s c\u00f3modo es trabajar con Material que ya viene perfectamente integrado en Angular. Ejecutamos el comando y elegimos la paleta de colores que m\u00e1s nos guste o bien creamos una custom:

    ng add @angular/material\n

    Recuerda

    Al a\u00f1adir una nueva librer\u00eda tenemos que parar el servidor y volver a arrancarlo para que compile y precargue las nuevas dependencias.

    Una vez a\u00f1adida la dependencia, lo que queremos es crear una primera estructura inicial a la p\u00e1gina. Si te acuerdas cual era la estructura (y si no te acuerdas, vuelve a la secci\u00f3n Contexto de la aplicaci\u00f3n y lo revisas), ten\u00edamos una cabecera superior con un logo y t\u00edtulo y unas opciones de men\u00fa.

    Pues vamos a ello, crearemos esa estructura com\u00fan para toda la aplicaci\u00f3n. Este componente al ser algo core para toda la aplicaci\u00f3n deber\u00edamos crearlo dentro del m\u00f3dulo core como ya vimos anteriormente.

    Pero antes de todo, vamos a crear los m\u00f3dulos generales de la aplicaci\u00f3n, as\u00ed que ejecutamos en consola el comando que nos permite crear un m\u00f3dulo nuevo:

    ng generate module core\n

    Y a\u00f1adimos esos m\u00f3dulos al m\u00f3dulo padre de la aplicaci\u00f3n:

    app.module.ts
    import { BrowserModule } from '@angular/platform-browser';\nimport { NgModule } from '@angular/core';\n\nimport { AppRoutingModule } from './app-routing.module';\nimport { AppComponent } from './app.component';\nimport { BrowserAnimationsModule } from '@angular/platform-browser/animations';\nimport { CoreModule } from './core/core.module';\n\n@NgModule({\n  declarations: [\n    AppComponent\n  ],\n  imports: [\n    BrowserModule,\n    AppRoutingModule,\n    CoreModule,\n    BrowserAnimationsModule,\n  ],\n  providers: [],\n  bootstrap: [AppComponent]\n})\nexport class AppModule { }\n

    Y despu\u00e9s crearemos el componente header, dentro del m\u00f3dulo core. Para eso ejecutaremos el comando:

    ng generate component core/header\n
    "},{"location":"develop/basic/angular/#codigo-de-la-pantalla","title":"C\u00f3digo de la pantalla","text":"

    Esto nos crear\u00e1 una carpeta con los ficheros del componente, donde tendremos que copiar el siguiente contenido:

    header.component.htmlheader.component.scss
    <mat-toolbar>\n    <mat-toolbar-row>\n        <div class=\"header_container\">\n            <div class=\"header_title\">              \n                <mat-icon>storefront</mat-icon> Ludoteca Tan\n            </div>\n\n            <div class=\"header_separator\"> | </div>\n\n            <div class=\"header_menu\">\n                <div class=\"header_button\">\n                    <a routerLink=\"/games\" routerLinkActive=\"active\">Cat\u00e1logo</a>\n                </div>\n                <div class=\"header_button\">\n                    <a routerLink=\"/categories\" routerLinkActive=\"active\">Categor\u00edas</a>\n                </div>\n                <div class=\"header_button\">\n                    <a routerLink=\"/authors\" routerLinkActive=\"active\">Autores</a>\n                </div>\n            </div>\n\n            <div class=\"header_login\">\n                <mat-icon>account_circle</mat-icon> Sign in\n            </div>\n        </div>\n    </mat-toolbar-row>\n</mat-toolbar>\n
    .mat-toolbar {\n  background-color: blue;\n  color: white;\n}\n\n.header_container {\n    display: flex;\n    width: 100%;\n    .header_title {\n        .mat-icon {\n            vertical-align: sub;\n        }\n    }\n\n    .header_separator {\n        margin-left: 30px;\n        margin-right: 30px;\n    }\n\n    .header_menu {\n        flex-grow: 4;\n        display: flex;\n        flex-direction: row;\n\n        .header_button {\n            margin-left: 1em;\n            margin-right: 1em;\n            font-size: 16px;\n\n            a {\n              font-weight: lighter;\n              text-decoration: none;\n              cursor: pointer;\n              color: white;\n            }\n\n            a:hover {\n              color: grey;\n            }\n\n            a.active {\n              font-weight: normal;\n              text-decoration: underline;\n              color: lightyellow;\n            }\n\n        }\n    }\n\n    .header_login {\n      font-size: 16px;\n      cursor: pointer;\n      .mat-icon {\n          vertical-align: sub;\n      }\n  }\n}\n

    Al utilizar etiquetas de material como mat-toolbar o mat-icon y routerLink necesitaremos importar las dependencias. Esto lo podemos hacer directamente en el m\u00f3dulo del que depende, es decir en el fichero core.module.ts

    core.module.ts
    import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatToolbarModule } from '@angular/material/toolbar';\nimport { HeaderComponent } from './header/header.component';\nimport { RouterModule } from '@angular/router';\n\n\n@NgModule({\n  declarations: [HeaderComponent],\n  imports: [\n    CommonModule,\n    RouterModule,\n    MatIconModule, \n    MatToolbarModule,\n  ],\n  exports: [\n    HeaderComponent\n  ]\n})\nexport class CoreModule { }\n

    Adem\u00e1s de a\u00f1adir las dependencias, diremos que este m\u00f3dulo va a exportar el componente HeaderComponent para poder utilizarlo desde otras p\u00e1ginas.

    Ya por \u00faltimo solo nos queda modificar la p\u00e1gina general de la aplicaci\u00f3n app.component.html para a\u00f1adirle el componente HeaderComponent.

    app.component.html
    <div>\n  <app-header></app-header>\n  <div>\n    <router-outlet></router-outlet>\n  </div>\n</div>\n

    Vamos al navegador y refrescamos la p\u00e1gina, deber\u00eda aparecer una barra superior (Header) con las opciones de men\u00fa. Algo similar a esto:

    Recuerda

    Cuando se a\u00f1aden componentes a los ficheros html, siempre se deben utilizar los selectores definidos para el componente. En el caso anterior hemos a\u00f1adido app-header que es el mismo nombre selector que tiene el componente en el fichero header.component.ts. Adem\u00e1s, recuerda que para poder utilizar componentes de otros m\u00f3dulos, los debes exportar ya que de lo contrario tan solo podr\u00e1n utilizarse dentro del m\u00f3dulo donde se declaran.

    "},{"location":"develop/basic/angular/#creando-un-listado-basico","title":"Creando un listado b\u00e1sico","text":""},{"location":"develop/basic/angular/#crear-componente_1","title":"Crear componente","text":"

    Ya tenemos la estructura principal, ahora vamos a crear nuestra primera pantalla. Vamos a empezar por la de Categor\u00edas que es la m\u00e1s sencilla, ya que se trata de un listado, que muestra datos sin filtrar ni paginar.

    Como categor\u00edas es un dominio funcional de la aplicaci\u00f3n, vamos a crear un m\u00f3dulo que contenga toda la funcionalidad de ese dominio. Ejecutamos en consola:

    ng generate module category\n

    Y por tanto, al igual que hicimos anteriormente, hay que a\u00f1adir el m\u00f3dulo al fichero app.module.ts

    app.module.ts
    import { BrowserModule } from '@angular/platform-browser';\nimport { NgModule } from '@angular/core';\n\nimport { AppRoutingModule } from './app-routing.module';\nimport { AppComponent } from './app.component';\nimport { BrowserAnimationsModule } from '@angular/platform-browser/animations';\nimport { CoreModule } from './core/core.module';\nimport { CategoryModule } from './category/category.module';\n\n\n@NgModule({\n  declarations: [\n    AppComponent\n  ],\n  imports: [\n    BrowserModule,\n    AppRoutingModule,\n    CoreModule,\n    CategoryModule,\n    BrowserAnimationsModule,\n  ],\n  providers: [],\n  bootstrap: [AppComponent]\n})\nexport class AppModule { }\n

    Ahora todas las pantallas, componentes y servicios que creemos, referidos a este dominio funcional, deber\u00e1n ir dentro del modulo cagegory.

    Vamos a crear un primer componente que ser\u00e1 un listado de categor\u00edas. Para ello vamos a ejecutar el siguiente comando:

    ng generate component category/category-list\n

    Para terminar de configurar la aplicaci\u00f3n, vamos a a\u00f1adir la ruta del componente dentro del componente routing de Angular, para poder acceder a \u00e9l, para ello modificamos el fichero app-routing.module.ts

    app-routing.module.ts
    import { NgModule } from '@angular/core';\nimport { Routes, RouterModule } from '@angular/router';\nimport { CategoryListComponent } from './category/category-list/category-list.component';\n\nconst routes: Routes = [\n  { path: 'categories', component: CategoryListComponent },\n];\n\n@NgModule({\n  imports: [RouterModule.forRoot(routes)],\n  exports: [RouterModule]\n})\nexport class AppRoutingModule { }\n

    Si abrimos el navegador y accedemos a http://localhost:4200/ podremos navegar mediante el men\u00fa Categor\u00edas el cual abrir\u00e1 el componente que acabamos de crear.

    "},{"location":"develop/basic/angular/#codigo-de-la-pantalla_1","title":"C\u00f3digo de la pantalla","text":"

    Ahora vamos a construir la pantalla. Para manejar la informaci\u00f3n del listado, necesitamos almacenar los datos en un objeto de tipo model. Para ello crearemos un fichero en category\\model\\category.ts donde implementaremos la clase necesaria. Esta clase ser\u00e1 la que utilizaremos en el c\u00f3digo html y ts de nuestro componente.

    category.ts
    export class Category {\n    id: number;\n    name: string;\n}\n

    Tambi\u00e9n, escribiremos el c\u00f3digo de la pantalla de listado.

    category-list.component.htmlcategory-list.component.scsscategory-list.component.ts
    <div class=\"container\">\n    <h1>Listado de Categor\u00edas</h1>\n\n    <mat-table [dataSource]=\"dataSource\">\n        <ng-container matColumnDef=\"id\">\n            <mat-header-cell *matHeaderCellDef> Identificador </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.id}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"name\">\n            <mat-header-cell *matHeaderCellDef> Nombre categor\u00eda  </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.name}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"action\">\n            <mat-header-cell *matHeaderCellDef></mat-header-cell>\n            <mat-cell *matCellDef=\"let element\">\n                <button mat-icon-button color=\"primary\"><mat-icon>edit</mat-icon></button>\n                <button mat-icon-button color=\"accent\"><mat-icon>clear</mat-icon></button>\n            </mat-cell>\n        </ng-container>\n\n        <mat-header-row *matHeaderRowDef=\"displayedColumns; sticky: true\"></mat-header-row>\n        <mat-row *matRowDef=\"let row; columns: displayedColumns;\"></mat-row>\n    </mat-table>\n\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\">Nueva categor\u00eda</button>\n    </div>   \n</div>\n
    .container {\n  margin: 20px;\n\n  mat-table {\n    margin-top: 10px;\n    margin-bottom: 20px;\n\n    .mat-header-row {\n      background-color:#f5f5f5;\n\n      .mat-header-cell {\n        text-transform: uppercase;\n        font-weight: bold;\n        color: #838383;\n      }      \n    }\n\n    .mat-column-id {\n      flex: 0 0 20%;\n      justify-content: center;\n    }\n\n    .mat-column-action {\n      flex: 0 0 10%;\n      justify-content: center;\n    }\n  }\n\n  .buttons {\n    text-align: right;\n  }\n}\n
    import { Component, OnInit } from '@angular/core';\nimport { MatTableDataSource } from '@angular/material/table';\nimport { Category } from '../model/category';\n\n@Component({\n  selector: 'app-category-list',\n  templateUrl: './category-list.component.html',\n  styleUrls: ['./category-list.component.scss']\n})\nexport class CategoryListComponent implements OnInit {\n\n  dataSource = new MatTableDataSource<Category>();\n  displayedColumns: string[] = ['id', 'name', 'action'];\n\n  constructor() { }\n\n  ngOnInit(): void {\n  }\n\n}\n

    El c\u00f3digo HTML es f\u00e1cil de seguir pero por si acaso:

    • L\u00ednea 4: Creamos la tabla con la variable dataSource definida en el fichero .ts
    • L\u00ednea 5: Definici\u00f3n de la primera columna, su cabecera y el dato que va a contener
    • L\u00ednea 10: Definici\u00f3n de la segunda columna, su cabecera y el dato que va a contener
    • L\u00ednea 15: Definici\u00f3n de la tercera columna, su cabecera vac\u00eda y los dos botones de acci\u00f3n
    • L\u00ednea 23 y 24: Construcci\u00f3n de la cabecera y las filas

    Y ya por \u00faltimo, a\u00f1adimos los componentes que se han utilizado de Angular Material a las dependencias del m\u00f3dulo donde est\u00e1 definido el componente en este caso category\\category.module.ts:

    category.module.ts
    import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { MatTableModule } from '@angular/material/table';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatButtonModule } from '@angular/material/button';\nimport { CategoryListComponent } from './category-list/category-list.component';\n\n@NgModule({\n  declarations: [CategoryListComponent],\n  imports: [\n    CommonModule,\n    MatTableModule,\n    MatIconModule, \n    MatButtonModule\n  ],\n})\nexport class CategoryModule { }\n

    Si abrimos el navegador y accedemos a http://localhost:4200/ y pulsamos en el men\u00fa de Categor\u00edas obtendremos una pantalla con un listado vac\u00edo (solo con cabeceras) y un bot\u00f3n de crear Nueva Categor\u00eda que aun no hace nada.

    "},{"location":"develop/basic/angular/#anadiendo-datos","title":"A\u00f1adiendo datos","text":"

    En este punto y para ver como responde el listado, vamos a a\u00f1adir datos. Si tuvieramos el backend implementado podr\u00edamos consultar los datos directamente de una operaci\u00f3n de negocio de backend, pero ahora mismo no lo tenemos implementado as\u00ed que para no bloquear el desarrollo vamos a mockear los datos.

    "},{"location":"develop/basic/angular/#creando-un-servicio","title":"Creando un servicio","text":"

    En angular, cualquier acceso a datos debe pasar por un service, as\u00ed que vamos a crearnos uno para todas las operaciones de categor\u00edas. Vamos a la consola y ejecutamos:

    ng generate service category/category\n

    Esto nos crear\u00e1 un servicio, que adem\u00e1s podemos utilizarlo inyect\u00e1ndolo en cualquier componente que lo necesite.

    "},{"location":"develop/basic/angular/#implementando-un-servicio","title":"Implementando un servicio","text":"

    Vamos a implementar una operaci\u00f3n de negocio que recupere el listado de categor\u00edas y lo vamos a hacer de forma reactiva (as\u00edncrona) para simular una petici\u00f3n a backend. Modificamos los siguientes ficheros:

    category.service.tscategory-list.component.ts
    import { Injectable } from '@angular/core';\nimport { Observable } from 'rxjs';\nimport { Category } from './model/category';\n\n@Injectable({\n  providedIn: 'root'\n})\nexport class CategoryService {\n\n  constructor() { }\n\n  getCategories(): Observable<Category[]> {\n    return new Observable();\n  }\n}\n
    import { Component, OnInit } from '@angular/core';\nimport { MatTableDataSource } from '@angular/material/table';\nimport { Category } from '../model/category';\nimport { CategoryService } from '../category.service';\n\n@Component({\n  selector: 'app-category-list',\n  templateUrl: './category-list.component.html',\n  styleUrls: ['./category-list.component.scss']\n})\nexport class CategoryListComponent implements OnInit {\n\n  dataSource = new MatTableDataSource<Category>();\n  displayedColumns: string[] = ['id', 'name', 'action'];\n\n  constructor(\n    private categoryService: CategoryService,\n  ) { }\n\n  ngOnInit(): void {\n    this.categoryService.getCategories().subscribe(\n      categories => this.dataSource.data = categories\n    );\n  }\n}\n
    "},{"location":"develop/basic/angular/#mockeando-datos","title":"Mockeando datos","text":"

    Como hemos comentado anteriormente, el backend todav\u00eda no est\u00e1 implementado as\u00ed que vamos a mockear datos. Nos crearemos un fichero mock-categories.ts dentro de model, con datos ficticios y modificaremos el servicio para que devuelva esos datos. De esta forma, cuando tengamos implementada la operaci\u00f3n de negocio en backend, tan solo tenemos que sustuir el c\u00f3digo que devuelve datos est\u00e1ticos por una llamada http.

    mock-categories.tscategory.service.ts
    import { Category } from \"./category\";\n\nexport const CATEGORY_DATA: Category[] = [\n    { id: 1, name: 'Dados' },\n    { id: 2, name: 'Fichas' },\n    { id: 3, name: 'Cartas' },\n    { id: 4, name: 'Rol' },\n    { id: 5, name: 'Tableros' },\n    { id: 6, name: 'Tem\u00e1ticos' },\n    { id: 7, name: 'Europeos' },\n    { id: 8, name: 'Guerra' },\n    { id: 9, name: 'Abstractos' },\n]    \n
    import { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Category } from './model/category';\nimport { CATEGORY_DATA } from './model/mock-categories';\n\n@Injectable({\n  providedIn: 'root'\n})\nexport class CategoryService {\n\n  constructor() { }\n\n  getCategories(): Observable<Category[]> {\n    return of(CATEGORY_DATA);\n  }\n}\n

    Si ahora refrescamos la p\u00e1gina web, veremos que el listado ya tiene datos con los que vamos a interactuar.

    "},{"location":"develop/basic/angular/#simulando-las-otras-peticiones","title":"Simulando las otras peticiones","text":"

    Para terminar, vamos a simular las otras dos peticiones, la de editar y la de borrar para cuando tengamos que utilizarlas. El servicio debe quedar m\u00e1s o menos as\u00ed:

    category.service.ts
    import { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Category } from './model/category';\nimport { CATEGORY_DATA } from './model/mock-categories';\n\n@Injectable({\n  providedIn: 'root'\n})\nexport class CategoryService {\n\n  constructor() { }\n\n  getCategories(): Observable<Category[]> {\n    return of(CATEGORY_DATA);\n  }\n\n  saveCategory(category: Category): Observable<Category> {\n    return of(null);\n  }\n\n  deleteCategory(idCategory : number): Observable<any> {\n    return of(null);\n  }  \n}\n
    "},{"location":"develop/basic/angular/#anadiendo-acciones-al-listado","title":"A\u00f1adiendo acciones al listado","text":""},{"location":"develop/basic/angular/#crear-componente_2","title":"Crear componente","text":"

    Ahora nos queda a\u00f1adir las acciones al listado: crear, editar y eliminar. Empezaremos primero por las acciones de crear y editar, que ambas deber\u00edan abrir una ventana modal con un formulario para poder modificar datos de la entidad Categor\u00eda. Como siempre, para crear un componente usamos el asistente de Angular, esta vez al tratarse de una pantalla que solo vamos a utilizar dentro del dominio de categor\u00edas, tiene sentido que lo creemos dentro de ese m\u00f3dulo:

    ng generate component category/category-edit\n

    Ahora vamos a hacer que se abra al pulsar el bot\u00f3n Nueva categor\u00eda. Para eso, vamos al fichero category-list.component.ts y a\u00f1adimos un nuevo m\u00e9todo:

    category-list.component.ts
    ...\nimport { MatDialog } from '@angular/material/dialog';\nimport { CategoryEditComponent } from '../category-edit/category-edit.component';\n...\n  constructor(\n    private categoryService: CategoryService,\n    public dialog: MatDialog,\n  ) { }\n...\n  createCategory() {    \n    const dialogRef = this.dialog.open(CategoryEditComponent, {\n      data: {}\n    });\n\n    dialogRef.afterClosed().subscribe(result => {\n      this.ngOnInit();\n    });    \n  }  \n...\n

    Para poder abrir un componente dentro de un dialogo necesitamos obtener en el constructor un MatDialog. De ah\u00ed que hayamos tenido que a\u00f1adirlo como import y en el constructor.

    Dentro del m\u00e9todo createCategory lo que hacemos es crear un dialogo con el componente CategoryEditComponent en su interior, pasarle unos datos de creaci\u00f3n, donde podemos poner estilos del dialog y un objeto data donde pondremos los datos que queremos pasar entre los componentes. Por \u00faltimo, nos suscribimos al evento afterClosed para ejecutar las acciones que creamos oportunas, en nuestro caso volveremos a cargar el listado inicial.

    Como hemos utilizado un MatDialog en el componente, necesitamos a\u00f1adirlo tambi\u00e9n al m\u00f3dulo, as\u00ed que abrimos el fichero category.module.ts y a\u00f1adimos:

    category.module.ts
    ...\nimport { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';\n\n@NgModule({\n  declarations: [CategoryListComponent, CategoryEditComponent],\n  imports: [\n    ...\n    MatDialogModule\n  ],\n  providers: [\n    {\n      provide: MAT_DIALOG_DATA,\n      useValue: {},\n    },\n  ]\n})\nexport class CategoryModule { }\n

    Y ya por \u00faltimo enlazamos el click en el bot\u00f3n con el m\u00e9todo que acabamos de crear para abrir el dialogo. Modificamos el fichero category-list.component.html y a\u00f1adimos el evento click:

    category-list.component.html
    ...\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\" (click)=\"createCategory()\">Nueva categor\u00eda</button> \n    </div>   \n</div>\n

    Si refrescamos el navegador y pulsamos el bot\u00f3n Nueva categor\u00eda veremos como se abre una ventana modal de tipo Dialog con el componente nuevo que hemos creado, aunque solo se leer\u00e1 category-edit works! que es el contenido por defecto del componente.

    "},{"location":"develop/basic/angular/#codigo-del-dialogo","title":"C\u00f3digo del dialogo","text":"

    Ahora vamos a darle forma al formulario de editar y crear. Para ello vamos al html, ts y css del componente y pegamos el siguiente contenido:

    category-edit.component.htmlcategory-edit.component.scsscategory-edit.component.ts
    <div class=\"container\">\n    <h1>Crear categor\u00eda</h1>\n\n    <form>\n        <mat-form-field>\n            <mat-label>Identificador</mat-label>\n            <input type=\"text\" matInput placeholder=\"Identificador\" [(ngModel)]=\"category.id\" name=\"id\" disabled>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Nombre</mat-label>\n            <input type=\"text\" matInput placeholder=\"Nombre de categor\u00eda\" [(ngModel)]=\"category.name\" name=\"name\" required>\n            <mat-error>El nombre no puede estar vac\u00edo</mat-error>\n        </mat-form-field>\n    </form>\n\n    <div class=\"buttons\">\n        <button mat-stroked-button (click)=\"onClose()\">Cerrar</button>\n        <button mat-flat-button color=\"primary\" (click)=\"onSave()\">Guardar</button>\n    </div>\n</div>\n
    .container {\n    min-width: 350px;\n    max-width: 500px;\n    padding: 20px;\n\n    form {\n        display: flex;\n        flex-direction: column;\n        margin-bottom:20px;\n    }\n\n    .buttons {\n      text-align: right;\n\n      button {\n          margin-left: 10px;\n      }\n    }\n}\n
    import { Component, OnInit } from '@angular/core';\nimport { MatDialogRef } from '@angular/material/dialog';\nimport { CategoryService } from '../category.service';\nimport { Category } from '../model/category';\n\n@Component({\n  selector: 'app-category-edit',\n  templateUrl: './category-edit.component.html',\n  styleUrls: ['./category-edit.component.scss']\n})\nexport class CategoryEditComponent implements OnInit {\n\n  category : Category;\n\n  constructor(\n    public dialogRef: MatDialogRef<CategoryEditComponent>,\n    private categoryService: CategoryService\n  ) { }\n\n  ngOnInit(): void {\n    this.category = new Category();\n  }\n\n  onSave() {\n    this.categoryService.saveCategory(this.category).subscribe(result => {\n      this.dialogRef.close();\n    });    \n  }  \n\n  onClose() {\n    this.dialogRef.close();\n  }\n\n}\n

    Si te fijas en el c\u00f3digo TypeScript, hemos a\u00f1adido en el m\u00e9todo onSave una llamada al servicio de CategoryService que aunque no realice ninguna operaci\u00f3n de momento, por lo menos lo dejamos preparado para conectar con el servidor.

    Adem\u00e1s, como siempre, al utilizar componentes matInput, matForm, matError hay que a\u00f1adirlos como dependencias en el m\u00f3dulo category.module.ts:

    category.module.ts
    ...\nimport { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\n\n@NgModule({\n  declarations: [CategoryListComponent, CategoryEditComponent],\n  imports: [\n    ...\n    MatDialogModule,\n    MatFormFieldModule,\n    MatInputModule,\n    FormsModule,\n    ReactiveFormsModule,\n  ],\n  providers: [\n    {\n      provide: MAT_DIALOG_DATA,\n      useValue: {},\n    },\n  ]\n})\nexport class CategoryModule { }\n

    Ahora podemos navegar y abrir el cuadro de dialogo mediante el bot\u00f3n Nueva categor\u00eda para ver como queda nuestro formulario.

    "},{"location":"develop/basic/angular/#utilizar-el-dialogo-para-editar","title":"Utilizar el dialogo para editar","text":"

    El mismo componente que hemos utilizado para crear una nueva categor\u00eda, nos sirve tambi\u00e9n para editar una categor\u00eda existente. Tan solo tenemos que utilizar la funcionalidad que Angular nos proporciona y pasarle los datos a editar en la llamada de apertura del Dialog. Vamos a implementar funcionalidad sobre el icono editar, tendremos que modificar unos cuantos ficheros:

    category-list.component.htmlcategory-list.component.ts
    <div class=\"container\">\n    <h1>Listado de Categor\u00edas</h1>\n\n    <mat-table [dataSource]=\"dataSource\"> \n        <ng-container matColumnDef=\"id\">\n            <mat-header-cell *matHeaderCellDef> Identificador </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.id}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"name\">\n            <mat-header-cell *matHeaderCellDef> Nombre categor\u00eda  </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.name}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"action\">\n            <mat-header-cell *matHeaderCellDef></mat-header-cell>\n            <mat-cell *matCellDef=\"let element\">\n                <button mat-icon-button color=\"primary\" (click)=\"editCategory(element)\">\n                    <mat-icon>edit</mat-icon>\n                </button>\n                <button mat-icon-button color=\"accent\"><mat-icon>clear</mat-icon></button>\n            </mat-cell>\n        </ng-container>\n\n        <mat-header-row *matHeaderRowDef=\"displayedColumns; sticky: true\"></mat-header-row>\n        <mat-row *matRowDef=\"let row; columns: displayedColumns;\"></mat-row>\n    </mat-table>\n\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\" (click)=\"createCategory()\">Nueva categor\u00eda</button> \n    </div>   \n</div>\n
    export class CategoryListComponent implements OnInit {\n\n  dataSource = new MatTableDataSource<Category>();\n  displayedColumns: string[] = ['id', 'name', 'action'];\n\n  constructor(\n    private categoryService: CategoryService,\n    public dialog: MatDialog,\n  ) { }\n\n  ngOnInit(): void {\n    this.categoryService.getCategories().subscribe(\n      categories => this.dataSource.data = categories\n    );\n  }\n\n  createCategory() {    \n    const dialogRef = this.dialog.open(CategoryEditComponent, {\n      data: {}\n    });\n\n    dialogRef.afterClosed().subscribe(result => {\n      this.ngOnInit();\n    });    \n  }  \n\n  editCategory(category: Category) {\n    const dialogRef = this.dialog.open(CategoryEditComponent, {\n      data: { category: category }\n    });\n\n    dialogRef.afterClosed().subscribe(result => {\n      this.ngOnInit();\n    });\n  }\n}\n

    Y los Dialog:

    category-edit.component.htmlcategory-edit.component.ts
    <div class=\"container\">\n    <h1 *ngIf=\"category.id == null\">Crear categor\u00eda</h1>\n    <h1 *ngIf=\"category.id != null\">Modificar categor\u00eda</h1>\n\n    <form>\n        <mat-form-field>\n...\n
    import { Component, OnInit, Inject } from '@angular/core';\nimport { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';\nimport { CategoryService } from '../category.service';\nimport { Category } from '../model/category';\n\n@Component({\n  selector: 'app-category-edit',\n  templateUrl: './category-edit.component.html',\n  styleUrls: ['./category-edit.component.scss']\n})\nexport class CategoryEditComponent implements OnInit {\n\n  category : Category;\n\n  constructor(\n    public dialogRef: MatDialogRef<CategoryEditComponent>,\n    @Inject(MAT_DIALOG_DATA) public data: any,\n    private categoryService: CategoryService\n  ) { }\n\n  ngOnInit(): void {\n    if (this.data.category != null) {\n      this.category = this.data.category;\n    }\n    else {\n      this.category = new Category();\n    }\n  }\n\n  onSave() {\n    this.categoryService.saveCategory(this.category).subscribe(result => {\n      this.dialogRef.close();\n    });    \n  }  \n\n  onClose() {\n    this.dialogRef.close();\n  }\n\n}\n

    Navegando ahora por la p\u00e1gina y pulsando en el icono de editar, se deber\u00eda abrir una ventana con los datos que hemos seleccionado, similar a esta imagen:

    Si te fijas, al modificar los datos dentro de la ventana de di\u00e1logo se modifica tambi\u00e9n en el listado. Esto es porque estamos pasando el mismo objeto desde el listado a la ventana dialogo y al ser el listado y el formulario reactivos los dos, cualquier cambio sobre los datos se refresca directamente en la pantalla.

    Hay veces en la que este comportamiento nos interesa, pero en este caso no queremos que se modifique el listado. Para solucionarlo debemos hacer una copia del objeto, para que ambos modelos (formulario y listado) utilicen objetos diferentes. Es tan sencillo como modificar category-edit.component.ts y a\u00f1adirle una copia del dato

    category-edit.component.ts
        ...\n    ngOnInit(): void {\n      if (this.data.category != null) {\n        this.category = Object.assign({}, this.data.category);\n      }\n      else {\n        this.category = new Category();\n      }\n    }\n    ...\n

    Cuidado

    Hay que tener mucho cuidado con el binding de los objetos. Hay veces que al modificar un objeto NO queremos que se modifique en todas sus instancias y tenemos que poner especial cuidado en esos aspectos.

    "},{"location":"develop/basic/angular/#accion-de-borrado","title":"Acci\u00f3n de borrado","text":"

    Por norma general, toda acci\u00f3n de borrado de un dato de pantalla requiere una confirmaci\u00f3n previa por parte del usuario. Es decir, para evitar que el dato se borre accidentalmente el usuario tendr\u00e1 que confirmar su acci\u00f3n. Por tanto vamos a crear un componente que nos permita pedir una confirmaci\u00f3n al usuario.

    Como esta pantalla de confirmaci\u00f3n va a ser algo com\u00fan a muchas acciones de borrado de nuestra aplicaci\u00f3n, vamos a crearla dentro del m\u00f3dulo core. Como siempre, ejecutamos el comando en consola:

    ng generate component core/dialog-confirmation\n

    E implementamos el c\u00f3digo que queremos que tenga el componente. Al ser un componente gen\u00e9rico vamos a aprovechar y leeremos las variables que le pasemos en data.

    dialog-confirmation.component.htmldialog-confirmation.component.scssdialog-confirmation.component.ts
    <div class=\"container\">\n    <h1>{{title}}</h1>\n    <div [innerHTML]=\"description\" class=\"description\"></div>\n\n    <div class=\"buttons\">\n        <button mat-stroked-button (click)=\"onNo()\">No</button>\n        <button mat-flat-button color=\"primary\" (click)=\"onYes()\">S\u00ed</button>\n    </div>\n</div>    \n
    .container {\n    min-width: 350px;\n    max-width: 500px;\n    padding: 20px;\n\n    .description {\n      margin-bottom: 20px;\n    }\n\n    .buttons {\n      text-align: right;\n\n      button {\n          margin-left: 10px;\n      }\n    }\n}    \n
    import { Component, OnInit, Inject } from '@angular/core';\nimport { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';\n\n@Component({\n  selector: 'app-dialog-confirmation',\n  templateUrl: './dialog-confirmation.component.html',\n  styleUrls: ['./dialog-confirmation.component.scss']\n})\nexport class DialogConfirmationComponent implements OnInit {\n\n  title : string;\n  description : string;\n\n  constructor(\n    public dialogRef: MatDialogRef<DialogConfirmationComponent>,\n    @Inject(MAT_DIALOG_DATA) public data: any\n  ) { }\n\n  ngOnInit(): void {\n    this.title = this.data.title;\n    this.description = this.data.description;\n  }\n\n  onYes() {\n    this.dialogRef.close(true);\n  }\n\n  onNo() {\n    this.dialogRef.close(false);\n  }\n}\n

    Recuerda

    Recuerda que los componentes utilizados en el di\u00e1logo de confirmaci\u00f3n se deben a\u00f1adir al m\u00f3dulo padre al que pertenecen, en este caso a core.module.ts

    imports: [\n  CommonModule,\n  RouterModule,\n  MatIconModule, \n  MatToolbarModule,\n  MatDialogModule,\n  MatButtonModule,\n],\nproviders: [\n  {\n    provide: MAT_DIALOG_DATA,\n    useValue: {},\n  },\n],\n

    Ya por \u00faltimo, una vez tenemos el componente gen\u00e9rico de dialogo, vamos a utilizarlo en nuestro listado al pulsar el bot\u00f3n eliminar:

    category-list.component.htmlcategory-list.component.ts
        ...\n    <ng-container matColumnDef=\"action\">\n        <mat-header-cell *matHeaderCellDef></mat-header-cell>\n        <mat-cell *matCellDef=\"let element\">\n            <button mat-icon-button color=\"primary\" (click)=\"editCategory(element)\">\n                <mat-icon>edit</mat-icon>\n            </button>\n            <button mat-icon-button color=\"accent\" (click)=\"deleteCategory(element)\">\n                <mat-icon>clear</mat-icon>\n            </button>\n        </mat-cell>\n    </ng-container>\n    ...\n
      ...\n  deleteCategory(category: Category) {    \n    const dialogRef = this.dialog.open(DialogConfirmationComponent, {\n      data: { title: \"Eliminar categor\u00eda\", description: \"Atenci\u00f3n si borra la categor\u00eda se perder\u00e1n sus datos.<br> \u00bfDesea eliminar la categor\u00eda?\" }\n    });\n\n    dialogRef.afterClosed().subscribe(result => {\n      if (result) {\n        this.categoryService.deleteCategory(category.id).subscribe(result => {\n          this.ngOnInit();\n        }); \n      }\n    });\n  }  \n  ...    \n

    Aqu\u00ed tambi\u00e9n hemos realizado la llamada a categoryService, aunque no se realice ninguna acci\u00f3n, pero as\u00ed lo dejamos listo para enlazarlo.

    Llegados a este punto, ya solo nos queda enlazar las acciones de la pantalla con las operaciones de negocio del backend.

    "},{"location":"develop/basic/angular/#conectar-con-backend","title":"Conectar con Backend","text":"

    Antes de seguir

    Antes de seguir con este punto, debes implementar el c\u00f3digo de backend en la tecnolog\u00eda que quieras (Springboot o Nodejs). Si has empezado este cap\u00edtulo implementando el frontend, por favor accede a la secci\u00f3n correspondiente de backend para poder continuar con el tutorial. Una vez tengas implementadas todas las operaciones para este listado, puedes volver a este punto y continuar con Angular.

    El siguiente paso, como es obvio ser\u00e1 hacer que Angular llame directamente al servidor backend para leer y escribir datos y eliminar los datos mockeados en Angular.

    Manos a la obra!

    "},{"location":"develop/basic/angular/#llamada-del-listado","title":"Llamada del listado","text":"

    La idea es que el m\u00e9todo getCategories() de category.service.ts en lugar de devolver datos est\u00e1ticos, realice una llamada al servidor a la ruta http://localhost:8080/category.

    Abrimos el fichero y susituimos la l\u00ednea que antes devolv\u00eda los datos est\u00e1ticos por esto:

    category.service.ts
    import { HttpClient } from '@angular/common/http';\nimport { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Category } from './model/category';\n\n@Injectable({\nprovidedIn: 'root'\n})\nexport class CategoryService { \n\n    constructor(\n        private http: HttpClient\n    ) { }\n\n    getCategories(): Observable<Category[]> {\n        return this.http.get<Category[]>('http://localhost:8080/category');\n    }\n\n    saveCategory(category: Category): Observable<Category> {\n        return of(null);\n    }\n\n    deleteCategory(idCategory : number): Observable<any> {\n        return of(null);\n    }  \n}\n

    Como hemos a\u00f1adido un componente nuevo HttpClient tenemos que a\u00f1adir la dependencia al m\u00f3dulo padre.

    category.module.ts
    import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { MatTableModule } from '@angular/material/table';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatButtonModule } from '@angular/material/button';\nimport { CategoryListComponent } from './category-list/category-list.component';\nimport { CategoryEditComponent } from './category-edit/category-edit.component';\nimport { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\nimport { HttpClientModule } from '@angular/common/http';\n\n@NgModule({\n  declarations: [CategoryListComponent, CategoryEditComponent],\n  imports: [\n    CommonModule,\n    MatTableModule,\n    MatIconModule, \n    MatButtonModule,\n    MatDialogModule,\n    MatFormFieldModule,\n    MatInputModule,\n    FormsModule,\n    ReactiveFormsModule,\n    HttpClientModule,\n  ],\n  providers: [\n    {\n      provide: MAT_DIALOG_DATA,\n      useValue: {},\n    },\n  ]\n})\nexport class CategoryModule { }\n

    Si ahora refrescas el navegador (recuerda tener arrancado tambi\u00e9n el servidor) y accedes a la pantalla de Categor\u00edas deber\u00eda aparecer el listado con los datos que vienen del servidor.

    "},{"location":"develop/basic/angular/#llamada-de-guardado-edicion","title":"Llamada de guardado / edici\u00f3n","text":"

    Para la llamada de guardado har\u00edamos lo mismo, pero invocando la operaci\u00f3n de negocio put.

    category.service.ts
    import { HttpClient } from '@angular/common/http';\nimport { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Category } from './model/category';\n\n@Injectable({\nprovidedIn: 'root'\n})\nexport class CategoryService { \n\n    constructor(\n        private http: HttpClient\n    ) { }\n\n    getCategories(): Observable<Category[]> {\n        return this.http.get<Category[]>('http://localhost:8080/category');\n    }\n\n    saveCategory(category: Category): Observable<Category> {\n\n        let url = 'http://localhost:8080/category';\n        if (category.id != null) url += '/'+category.id;\n\n        return this.http.put<Category>(url, category);\n    }\n\n    deleteCategory(idCategory : number): Observable<any> {\n        return of(null);\n    }  \n\n} \n

    Ahora podemos probar a modificar o a\u00f1adir una nueva categor\u00eda desde la pantalla y deber\u00eda aparecer los nuevos datos en el listado.

    "},{"location":"develop/basic/angular/#llamada-de-borrado","title":"Llamada de borrado","text":"

    Y ya por \u00faltimo, la llamada de borrado, deber\u00edamos cambiarla e invocar a la operaci\u00f3n de negocio delete.

    category.service.ts
    import { HttpClient } from '@angular/common/http';\nimport { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Category } from './model/category';\n\n@Injectable({\nprovidedIn: 'root'\n})\nexport class CategoryService { \n\n    constructor(\n        private http: HttpClient\n    ) { }\n\n    getCategories(): Observable<Category[]> {\n        return this.http.get<Category[]>('http://localhost:8080/category');\n    }\n\n    saveCategory(category: Category): Observable<Category> {\n\n        let url = 'http://localhost:8080/category';\n        if (category.id != null) url += '/'+category.id;\n\n        return this.http.put<Category>(url, category);\n    }\n\n    deleteCategory(idCategory : number): Observable<any> {\n        return this.http.delete('http://localhost:8080/category/'+idCategory);\n    }  \n\n} \n

    Ahora podemos probar a modificar o a\u00f1adir una nueva categor\u00eda desde la pantalla y deber\u00eda aparecer los nuevos datos en el listado.

    Como ves, es bastante sencillo conectar server y client.

    "},{"location":"develop/basic/angular/#depuracion","title":"Depuraci\u00f3n","text":"

    Una parte muy importante del desarrollo es tener la capacidad de depurar nuestro c\u00f3digo, en este apartado vamos a explicar como se realiza debug en Front.

    Esta parte se puede realizar con nuestro navegador favorito, en este caso vamos a utilizar Chrome.

    El primer paso es abrir las herramientas del desarrollador del navegador presionando F12.

    En esta herramienta tenemos varias partes importantes:

    • Elements: Inspector de los elementos del DOM de nuestra aplicaci\u00f3n que nos ayuda identificar el c\u00f3digo generado.
    • Console: Consola donde podemos ver mensajes importantes que nos ayudan a identificar posibles problemas.
    • Source: El navegador de ficheros que componen nuestra aplicaci\u00f3n.
    • Network: El registro de peticiones que realiza nuestra aplicaci\u00f3n.

    Identificados los elementos importantes, vamos a depurar la operaci\u00f3n de crear categor\u00eda.

    Para ello nos dirigimos a la pesta\u00f1a de Source, en el \u00e1rbol de carpetas nos dirigimos a la ruta donde est\u00e1 localizado el c\u00f3digo de nuestra aplicaci\u00f3n webpack://src/app.

    Dentro de esta carpeta est\u00e9 localizado todo el c\u00f3digo fuente de la aplicaci\u00f3n, en nuestro caso vamos a localizar componente category-edit.component que crea una nueva categor\u00eda.

    Dentro del fichero ya podemos a\u00f1adir puntos de ruptura (breakpoint), en nuestro caso queremos comprobar que el nombre introducido se captura bien y se env\u00eda al service correctamente.

    Colocamos el breakpoint en la l\u00ednea de invocaci\u00f3n del service (click sobre el n\u00famero de la l\u00ednea) y desde la interfaz creamos una nueva categor\u00eda.

    Hecho esto, podemos observar que a nivel de interfaz, la aplicaci\u00f3n se detiene y aparece un panel de manejo de los puntos de interrupci\u00f3n:

    En cuanto a la herramienta del desarrollador nos lleva al punto exacto donde hemos a\u00f1adido el breakpoint y se para en este punto ofreci\u00e9ndonos la posibilidad de explorar el contenido de las variables del c\u00f3digo:

    Aqu\u00ed podemos comprobar que efectivamente la variable category tiene el valor que hemos introducido por pantalla y se propaga correctamente hacia el service.

    Para continuar con la ejecuci\u00f3n basta con darle al bot\u00f3n de play del panel de manejo de interrupci\u00f3n o al que aparece dentro de la herramienta de desarrollo (parte superior derecha).

    Por \u00faltimo, vamos a revisar que la petici\u00f3n REST se ha realizado correctamente al backend, para ello nos dirigimos a la pesta\u00f1a Network y comprobamos las peticiones realizadas:

    Aqu\u00ed podemos observar el registro de todas las peticiones y haciendo click sobre una de ellas, obtenemos el detalle de esta.

    • Header: Informaci\u00f3n de las cabeceras enviadas (aqu\u00ed podemos ver que se ha hecho un PUT a la ruta correcta).
    • Payload: El cuerpo de la petici\u00f3n (vemos el cuerpo del mensaje con el nombre enviado).
    • Preview: Respuesta de la petici\u00f3n normalizada (vemos la respuesta con el identificador creado para la nueva categor\u00eda).
    "},{"location":"develop/basic/angular17/","title":"Listado simple - Angular","text":"

    Ahora que ya tenemos listo el proyecto frontend de Angular (en el puerto 4200), ya podemos empezar a codificar la soluci\u00f3n.

    "},{"location":"develop/basic/angular17/#primeros-pasos","title":"Primeros pasos","text":"

    Antes de empezar

    Quiero hacer hincapi\u00e9 que Angular tiene una documentaci\u00f3n muy extensa y completa, as\u00ed que te recomiendo que hagas uso de ella cuando tengas cualquier duda. Tanto en la propia web de documentaci\u00f3n de Angular como en la web de componentes Angular Material puedes buscar casi cualquier ejemplo que necesites.

    Si abrimos el proyecto con el IDE que tengamos (Visual Studio Code en el caso del tutorial) podemos ver que en la carpeta src/app existen unos ficheros ya creados por defecto. Estos ficheros son:

    • app.ts \u2192 contiene el c\u00f3digo inicial del proyecto escrito en TypeScript.
    • app.html \u2192 contiene la plantilla inicial del proyecto escrita en HTML.
    • app.scss \u2192 contiene los estilos CSS privados de la plantilla inicial.

    Vamos a modificar este c\u00f3digo inicial para ver como funciona. Abrimos el fichero app.component.ts y modificamos la l\u00ednea donde se asigna un valor a la variable title.

    app.component.ts
    ...\n   protected readonly title = signal('Tutorial de Angular');\n...\n

    Ahora abrimos el fichero app.component.html, borramos todo el c\u00f3digo de la plantilla y a\u00f1adimos el siguiente c\u00f3digo:

    app.component.html
    <h1>{{ title() }}</h1>\n

    Las llaves dobles permiten hacen un binding entre el c\u00f3digo del componente y la plantilla. Es decir, en este caso ir\u00e1 al c\u00f3digo TypeScript y buscar\u00e1 el valor de la variable title.

    Consejo

    El binding tambi\u00e9n nos sirve para ejecutar los m\u00e9todos de TypeScript desde el c\u00f3digo HTML. Adem\u00e1s, si el valor que contiene la variable se modificar\u00e1 durante la ejecuci\u00f3n de alg\u00fan m\u00e9todo, autom\u00e1ticamente el c\u00f3digo HTML refrescar\u00eda el nuevo valor de la variable title

    Si abrimos el navegador y accedemos a http://localhost:4200/ podremos ver el resultado del c\u00f3digo.

    "},{"location":"develop/basic/angular17/#layout-general","title":"Layout general","text":""},{"location":"develop/basic/angular17/#crear-componente","title":"Crear componente","text":"

    Lo primero que vamos a hacer es escoger un tema y una paleta de componentes para trabajar. Lo m\u00e1s c\u00f3modo es trabajar con Material que ya viene perfectamente integrado en Angular. Ejecutamos el comando y elegimos la paleta de colores que m\u00e1s nos guste o bien creamos una custom:

    ng add @angular/material\n

    Recuerda

    Al a\u00f1adir una nueva librer\u00eda tenemos que parar el servidor y volver a arrancarlo para que compile y recargue las nuevas dependencias.

    Una vez a\u00f1adida la dependencia, lo que queremos es crear una primera estructura inicial a la p\u00e1gina. Si te acuerdas cu\u00e1l era la estructura (y si no te acuerdas, vuelve a la secci\u00f3n Contexto de la aplicaci\u00f3n y lo revisas), ten\u00edamos una cabecera superior con un logo y t\u00edtulo y unas opciones de men\u00fa.

    Para agrupar los componentes comunes de nuestra aplicaci\u00f3n vamos a crear una carpeta llamada \"core\" dentro de la carpeta \"src\", e iremos creando los componentes que necesitemos. Empecemos con el header. Desde la ra\u00edz de nuestro proyecto introducimos el siguiente comando:

    ng generate component core/header\n

    Angular 17+

    A partir de Angular 17, el CLI apuesta por una arquitectura standalone y una estructura inicial m\u00e1s simple al crear un proyecto con ng new.

    Sin embargo, Angular no impone una topolog\u00eda concreta para componentes o servicios. La generaci\u00f3n de archivos como *.component.ts, *.service.ts y sus carpetas asociadas sigue siendo el comportamiento por defecto del CLI y solo cambia si se utilizan flags como --flat, opciones de inline o schematics personalizadas.

    "},{"location":"develop/basic/angular17/#codigo-de-la-pantalla","title":"C\u00f3digo de la pantalla","text":"

    Esto nos crear\u00e1 una carpeta con los ficheros del componente, donde tendremos que copiar el siguiente contenido:

    header.component.htmlheader.component.scss
    <mat-toolbar>\n    <mat-toolbar-row>\n        <div class=\"header_container\">\n            <div class=\"header_title\">              \n                <mat-icon>storefront</mat-icon> Ludoteca Tan\n            </div>\n\n            <div class=\"header_separator\"> | </div>\n\n            <div class=\"header_menu\">\n                <div class=\"header_button\">\n                    <a routerLink=\"/games\" routerLinkActive=\"active\">Cat\u00e1logo</a>\n                </div>\n                <div class=\"header_button\">\n                    <a routerLink=\"/categories\" routerLinkActive=\"active\">Categor\u00edas</a>\n                </div>\n                <div class=\"header_button\">\n                    <a routerLink=\"/authors\" routerLinkActive=\"active\">Autores</a>\n                </div>\n            </div>\n\n            <div class=\"header_login\">\n                <mat-icon>account_circle</mat-icon> Sign in\n            </div>\n        </div>\n    </mat-toolbar-row>\n</mat-toolbar>\n
    .mat-toolbar {\n  background-color: blue;\n  color: white;\n}\n\n.header_container {\n    display: flex;\n    width: 100%;\n    .header_title {\n        .mat-icon {\n            vertical-align: sub;\n        }\n    }\n\n    .header_separator {\n        margin-left: 30px;\n        margin-right: 30px;\n    }\n\n    .header_menu {\n        flex-grow: 4;\n        display: flex;\n        flex-direction: row;\n\n        .header_button {\n            margin-left: 1em;\n            margin-right: 1em;\n            font-size: 16px;\n\n            a {\n              font-weight: lighter;\n              text-decoration: none;\n              cursor: pointer;\n              color: white;\n            }\n\n            a:hover {\n              color: grey;\n            }\n\n            a.active {\n              font-weight: normal;\n              text-decoration: underline;\n              color: lightyellow;\n            }\n\n        }\n    }\n\n    .header_login {\n      font-size: 16px;\n      cursor: pointer;\n      .mat-icon {\n          vertical-align: sub;\n      }\n  }\n}\n

    Al utilizar etiquetas de material como mat-toolbar o mat-icon y routerLink necesitaremos importar las dependencias. Al tratarse de un standalone component lo tendremos que hacer directamente en el atributo \"imports\" de nuestro component en el fichero header.component.ts.

    Angular 17+

    A partir de Angular 17, los componentes se crean por defecto como standalone, para que este no sea el caso debemos indic\u00e1rselo con --standalone false.

    header.component.ts
        import { CommonModule } from '@angular/common';\n    import { Component } from '@angular/core';\n    import { RouterModule } from '@angular/router';\n    import { MatIconModule } from '@angular/material/icon';\n    import { MatToolbarModule } from '@angular/material/toolbar';\n\n    @Component({\n        selector: 'app-header',\n        standalone: true,\n        imports: [\n            CommonModule,\n            RouterModule,\n            MatIconModule, \n            MatToolbarModule,\n        ],\n        templateUrl: './header.component.html',\n        styleUrl: './header.component.scss'\n    })\n    export class HeaderComponent {\n\n    }\n

    Ahora, para poder usar nuestro componente en las p\u00e1ginas del componente AppComponent tendremos tambi\u00e9n que a\u00f1adir en el atributo \"imports\" de app.component.ts nuestro nuevo componente:

    app.component.html
        import { Component, signal } from '@angular/core';\n    import { RouterOutlet } from '@angular/router';\n    import { HeaderComponent } from '../core/header/header.component';\n\n    @Component({\n        selector: 'app-root',\n        standalone: true,\n        imports: [RouterOutlet, HeaderComponent],\n        templateUrl: './app.component.html',\n        styleUrl: './app.component.scss'\n    })\n    export class AppComponent {\n        protected readonly title = signal('Tutorial de Angular');\n    }\n

    Ya por \u00faltimo solo nos queda modificar la p\u00e1gina general de la aplicaci\u00f3n app.component.html para a\u00f1adirle el componente HeaderComponent.

    app.component.html
    <div>\n  <app-header></app-header>\n  <div>\n    <router-outlet></router-outlet>\n  </div>\n</div>\n

    Vamos al navegador y refrescamos la p\u00e1gina, deber\u00eda aparecer una barra superior (Header) con las opciones de men\u00fa. Algo similar a esto:

    Recuerda

    Cuando se a\u00f1aden componentes a los ficheros html, siempre se deben utilizar los selectores definidos para el componente. En el caso anterior hemos a\u00f1adido app-header que es el mismo nombre selector que tiene el componente en el fichero header.component.ts. Adem\u00e1s, recuerda que para poder utilizar componentes, los debes importar en el componente donde vayan a ser utilizados.

    "},{"location":"develop/basic/angular17/#creando-un-listado-basico","title":"Creando un listado b\u00e1sico","text":""},{"location":"develop/basic/angular17/#crear-componente_1","title":"Crear componente","text":"

    Ya tenemos la estructura principal, ahora vamos a crear nuestra primera pantalla. Vamos a empezar por la de Categor\u00edas que es la m\u00e1s sencilla, ya que se trata de un listado, que muestra datos sin filtrar ni paginar.

    Como categor\u00edas es un dominio funcional de la aplicaci\u00f3n, vamos a crear una carpeta que contenga toda la funcionalidad de ese dominio y todas las pantallas, componentes y servicios que creemos referidos a este dominio funcional, deber\u00e1n ir dentro de esta nueva carpeta.

    Vamos a crear un primer componente que ser\u00e1 un listado de categor\u00edas. Para ello vamos a ejecutar el siguiente comando desde src:

    ng generate component category/category-list --type=page\n

    Como en este caso crearemos una p\u00e1gina, hemos a\u00f1adido el par\u00e1metro --type=page al comando. Esto hace que los ficheros generados tengan la extensi\u00f3n .page.ts y .page.html.

    Para terminar de configurar la aplicaci\u00f3n, vamos a a\u00f1adir la ruta del componente dentro del componente routing de Angular, para poder acceder a \u00e9l, para ello modificamos el fichero app.routes.ts

    app.routes.ts
    import { Routes } from '@angular/router';\n\nexport const routes: Routes = [\n    { path: 'categories', loadComponent: () => import('../category/category-list/category-list.page').then(m => m.CategoryListPage)},\n];\n

    Si abrimos el navegador y accedemos a http://localhost:4200/ podremos navegar mediante el men\u00fa Categor\u00edas el cual abrir\u00e1 el componente que acabamos de crear.

    "},{"location":"develop/basic/angular17/#codigo-de-la-pantalla_1","title":"C\u00f3digo de la pantalla","text":"

    Ahora vamos a construir la pantalla. Para manejar la informaci\u00f3n del listado, necesitamos almacenar los datos en un objeto de tipo model. Para ello crearemos un fichero en category\\model\\category.ts donde implementaremos la clase necesaria. Esta clase ser\u00e1 la que utilizaremos en el c\u00f3digo html y ts de nuestro componente.

    category.ts
    export class Category {\n    id: number;\n    name: string;\n}\n

    Tambi\u00e9n, escribiremos el c\u00f3digo de la pantalla de listado.

    category-list.component.htmlcategory-list.component.scsscategory-list.component.ts
    <div class=\"container\">\n    <h1>Listado de Categor\u00edas</h1>\n\n    <mat-table [dataSource]=\"dataSource\">\n        <ng-container matColumnDef=\"id\">\n            <mat-header-cell *matHeaderCellDef> Identificador </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.id}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"name\">\n            <mat-header-cell *matHeaderCellDef> Nombre categor\u00eda  </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.name}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"action\">\n            <mat-header-cell *matHeaderCellDef></mat-header-cell>\n            <mat-cell *matCellDef=\"let element\">\n                <button mat-icon-button color=\"primary\"><mat-icon>edit</mat-icon></button>\n                <button mat-icon-button color=\"accent\"><mat-icon>clear</mat-icon></button>\n            </mat-cell>\n        </ng-container>\n\n        <mat-header-row *matHeaderRowDef=\"displayedColumns; sticky: true\"></mat-header-row>\n        <mat-row *matRowDef=\"let row; columns: displayedColumns;\"></mat-row>\n    </mat-table>\n\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\">Nueva categor\u00eda</button>\n    </div>   \n</div>\n
    .container {\n  margin: 20px;\n\n  mat-table {\n    margin-top: 10px;\n    margin-bottom: 20px;\n\n    .mat-header-row {\n      background-color:#f5f5f5;\n\n      .mat-header-cell {\n        text-transform: uppercase;\n        font-weight: bold;\n        color: #838383;\n      }      \n    }\n\n    .mat-column-id {\n      flex: 0 0 20%;\n      justify-content: center;\n    }\n\n    .mat-column-action {\n      flex: 0 0 10%;\n      justify-content: center;\n    }\n  }\n\n  .buttons {\n    text-align: right;\n  }\n}\n
    import { Component, OnInit } from '@angular/core';\nimport { MatTableDataSource } from '@angular/material/table';\nimport { Category } from '../model/category';\nimport { CommonModule } from '@angular/common';\nimport { MatTableModule } from '@angular/material/table';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatButtonModule } from '@angular/material/button';\n\n@Component({\n    selector: 'app-category-list',\n    standalone: true,\n    imports: [\n        MatButtonModule,\n        MatIconModule,\n        MatTableModule,\n        CommonModule\n    ],\n    templateUrl: './category-list.component.html',\n    styleUrl: './category-list.component.scss'\n})\nexport class CategoryListComponent implements OnInit{\n\n    dataSource = new MatTableDataSource<Category>();\n    displayedColumns: string[] = ['id', 'name', 'action'];\n\n    constructor() { }\n\n    ngOnInit(): void {\n    }\n}\n

    El c\u00f3digo HTML es f\u00e1cil de seguir, pero por si acaso:

    • L\u00ednea 4: Creamos la tabla con la variable dataSource definida en el fichero .ts
    • L\u00ednea 5: Definici\u00f3n de la primera columna, su cabecera y el dato que va a contener
    • L\u00ednea 10: Definici\u00f3n de la segunda columna, su cabecera y el dato que va a contener
    • L\u00ednea 15: Definici\u00f3n de la tercera columna, su cabecera vac\u00eda y los dos botones de acci\u00f3n
    • L\u00ednea 23 y 24: Construcci\u00f3n de la cabecera y las filas

    Si abrimos el navegador y accedemos a http://localhost:4200/ y pulsamos en el men\u00fa de Categor\u00edas obtendremos una pantalla con un listado vac\u00edo (solo con cabeceras) y un bot\u00f3n de crear Nueva Categor\u00eda que a\u00fan no hace nada.

    "},{"location":"develop/basic/angular17/#anadiendo-datos","title":"A\u00f1adiendo datos","text":"

    En este punto y para ver como responde el listado, vamos a a\u00f1adir datos. Si tuvi\u00e9ramos el backend implementado podr\u00edamos consultar los datos directamente de una operaci\u00f3n de negocio de backend, pero ahora mismo no lo tenemos implementado, as\u00ed que para no bloquear el desarrollo vamos a mockear los datos.

    "},{"location":"develop/basic/angular17/#creando-un-servicio","title":"Creando un servicio","text":"

    En angular, cualquier acceso a datos debe pasar por un service, as\u00ed que vamos a crearnos uno para todas las operaciones de categor\u00edas. Vamos a la consola y ejecutamos:

    ng generate service category/category\n

    Esto nos crear\u00e1 un servicio, que adem\u00e1s podemos utilizarlo inyect\u00e1ndolo en cualquier componente que lo necesite.

    "},{"location":"develop/basic/angular17/#implementando-un-servicio","title":"Implementando un servicio","text":"

    Vamos a implementar una operaci\u00f3n de negocio que recupere el listado de categor\u00edas y lo vamos a hacer de forma reactiva (as\u00edncrona) para simular una petici\u00f3n a backend. Modificamos los siguientes ficheros:

    category.service.tscategory-list.component.ts
    import { Injectable } from '@angular/core';\nimport { Observable } from 'rxjs';\nimport { Category } from './model/category';\n\n@Injectable({\n  providedIn: 'root'\n})\nexport class CategoryService {\n\n  constructor() { }\n\n  getCategories(): Observable<Category[]> {\n    return new Observable();\n  }\n}\n
    import { Component, OnInit } from '@angular/core';\nimport { MatTableDataSource } from '@angular/material/table';\nimport { Category } from '../model/category';\nimport { CommonModule } from '@angular/common';\nimport { MatTableModule } from '@angular/material/table';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatButtonModule } from '@angular/material/button';\nimport { CategoryService } from '../category.service';\n\n@Component({\n    selector: 'app-category-list',\n    standalone: true,\n    imports: [\n        MatButtonModule,\n        MatIconModule,\n        MatTableModule,\n        CommonModule\n    ],\n    templateUrl: './category-list.component.html',\n    styleUrl: './category-list.component.scss'\n})\nexport class CategoryListComponent implements OnInit{\n    dataSource = new MatTableDataSource<Category>();\n    displayedColumns: string[] = ['id', 'name', 'action'];\n\n    constructor(\n        private categoryService: CategoryService,\n    ) { }\n\n    ngOnInit(): void {\n        this.categoryService.getCategories().subscribe(\n            categories => this.dataSource.data = categories\n        );\n    }\n}\n
    "},{"location":"develop/basic/angular17/#mockeando-datos","title":"Mockeando datos","text":"

    Como hemos comentado anteriormente, el backend todav\u00eda no est\u00e1 implementado, as\u00ed que vamos a mockear datos. Nos crearemos un fichero mock-categories.ts dentro de model, con datos ficticios y modificaremos el servicio para que devuelva esos datos. De esta forma, cuando tengamos implementada la operaci\u00f3n de negocio en backend, tan solo tenemos que sustituir el c\u00f3digo que devuelve datos est\u00e1ticos por una llamada http.

    mock-categories.tscategory.service.ts
    import { Category } from \"./category\";\n\nexport const CATEGORY_DATA: Category[] = [\n    { id: 1, name: 'Dados' },\n    { id: 2, name: 'Fichas' },\n    { id: 3, name: 'Cartas' },\n    { id: 4, name: 'Rol' },\n    { id: 5, name: 'Tableros' },\n    { id: 6, name: 'Tem\u00e1ticos' },\n    { id: 7, name: 'Europeos' },\n    { id: 8, name: 'Guerra' },\n    { id: 9, name: 'Abstractos' },\n]    \n
    import { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Category } from './model/category';\nimport { CATEGORY_DATA } from './model/mock-categories';\n\n@Injectable({\n  providedIn: 'root'\n})\nexport class CategoryService {\n\n  constructor() { }\n\n  getCategories(): Observable<Category[]> {\n    return of(CATEGORY_DATA);\n  }\n}\n

    Si ahora refrescamos la p\u00e1gina web, veremos que el listado ya tiene datos con los que vamos a interactuar.

    "},{"location":"develop/basic/angular17/#simulando-las-otras-peticiones","title":"Simulando las otras peticiones","text":"

    Para terminar, vamos a simular las otras dos peticiones, la de editar y la de borrar para cuando tengamos que utilizarlas. El servicio debe quedar m\u00e1s o menos as\u00ed:

    category.service.ts
    import { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Category } from './model/category';\nimport { CATEGORY_DATA } from './model/mock-categories';\n\n@Injectable({\n  providedIn: 'root'\n})\nexport class CategoryService {\n\n  constructor() { }\n\n  getCategories(): Observable<Category[]> {\n    return of(CATEGORY_DATA);\n  }\n\n  saveCategory(category: Category): Observable<Category> {\n    return of(null);\n  }\n\n  deleteCategory(idCategory : number): Observable<any> {\n    return of(null);\n  }  \n}\n
    "},{"location":"develop/basic/angular17/#anadiendo-acciones-al-listado","title":"A\u00f1adiendo acciones al listado","text":""},{"location":"develop/basic/angular17/#crear-componente_2","title":"Crear componente","text":"

    Ahora nos queda a\u00f1adir las acciones al listado: crear, editar y eliminar. Empezaremos primero por las acciones de crear y editar, que ambas deber\u00edan abrir una ventana modal con un formulario para poder modificar datos de la entidad Categor\u00eda. Como siempre, para crear un componente usamos el asistente de Angular, esta vez al tratarse de una pantalla que solo vamos a utilizar dentro del dominio de categor\u00edas, tiene sentido que lo creemos dentro de ese m\u00f3dulo:

    ng generate component category/category-edit\n

    Ahora vamos a hacer que se abra al pulsar el bot\u00f3n Nueva categor\u00eda. Para eso, vamos al fichero category-list.component.ts y a\u00f1adimos un nuevo m\u00e9todo:

    category-list.component.ts
    ...\nimport { MatDialog } from '@angular/material/dialog';\nimport { CategoryEditComponent } from '../category-edit/category-edit.component';\n...\n  imports: [\n    MatButtonModule,\n    MatIconModule,\n    MatTableModule,\n    CommonModule,\n],\n...\n  protected readonly categoryService = inject(CategoryService);\n  protected readonly dialog = inject(MatDialog);\n...\n  createCategory() {    \n    const dialogRef = this.dialog.open(CategoryEditComponent, {\n      data: {}\n    });\n\n    dialogRef.afterClosed().subscribe(result => {\n      if(!result) return;\n      this.loadData();\n    });    \n  }  \n...\n

    Para poder abrir un componente dentro de un di\u00e1logo necesitamos inyectar un MatDialog. De ah\u00ed que hayamos tenido que a\u00f1adirlo como import y usarlo con un inject.

    Dentro del m\u00e9todo createCategory lo que hacemos es crear un di\u00e1logo con el componente CategoryEditComponent en su interior, pasarle unos datos de creaci\u00f3n, donde podemos poner estilos del dialog y un objeto data donde pondremos los datos que queremos pasar entre los componentes. Por \u00faltimo, nos suscribimos al evento afterClosed para ejecutar las acciones que creamos oportunas, solo en el caso de que result sea true, en nuestro caso volveremos a cargar el listado inicial.

    Y ya por \u00faltimo enlazamos el click en el bot\u00f3n con el m\u00e9todo que acabamos de crear para abrir el di\u00e1logo. Modificamos el fichero category-list.component.html y a\u00f1adimos el evento click:

    category-list.component.html
    ...\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\" (click)=\"createCategory()\">Nueva categor\u00eda</button> \n    </div>   \n</div>\n

    Si refrescamos el navegador y pulsamos el bot\u00f3n Nueva categor\u00eda, veremos como se abre una ventana modal de tipo Dialog con el componente nuevo que hemos creado, aunque solo se leer\u00e1 category-edit works! que es el contenido por defecto del componente.

    "},{"location":"develop/basic/angular17/#codigo-del-dialogo","title":"C\u00f3digo del di\u00e1logo","text":"

    Ahora vamos a darle forma al formulario de editar y crear. Para ello vamos al html, ts y css del componente y pegamos el siguiente contenido:

    Errores de validaci\u00f3n

    Os recomendamos seguir el siguiente formato para los errores de validaci\u00f3n en formularios (ngModel, mat-error)

    category-edit.component.htmlcategory-edit.component.scsscategory-edit.component.ts
    <div class=\"container\">\n    <h1>Crear categor\u00eda</h1>\n\n    <form>\n        <mat-form-field>\n            <mat-label>Identificador</mat-label>\n            <input type=\"text\" matInput placeholder=\"Identificador\" [(ngModel)]=\"id\" name=\"id\" disabled>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Nombre</mat-label>\n            <input type=\"text\" matInput placeholder=\"Nombre de categor\u00eda\" [(ngModel)]=\"name\" name=\"name\" required>\n            <mat-error>El nombre no puede estar vac\u00edo</mat-error>\n        </mat-form-field>\n    </form>\n\n    <div class=\"buttons\">\n        <button mat-stroked-button (click)=\"onClose()\">Cerrar</button>\n        <button mat-flat-button color=\"primary\" (click)=\"onSave()\">Guardar</button>\n    </div>\n</div>\n
    .container {\n    min-width: 350px;\n    max-width: 500px;\n    padding: 20px;\n\n    form {\n        display: flex;\n        flex-direction: column;\n        margin-bottom:20px;\n    }\n\n    .buttons {\n      text-align: right;\n\n      button {\n          margin-left: 10px;\n      }\n    }\n}\n
    import { Component, OnInit, inject, signal } from '@angular/core';\nimport { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';\nimport { CategoryService } from '../category.service';\nimport { Category } from '../model/category';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatButtonModule } from '@angular/material/button';\n\n@Component({\n    selector: 'app-category-edit',\n    standalone: true,\n    imports: [FormsModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatButtonModule ],\n    templateUrl: './category-edit.component.html',\n    styleUrl: './category-edit.component.scss'\n})\nexport class CategoryEditComponent implements OnInit {\n    protected readonly dialogRef = inject(MatDialogRef<CategoryEditComponent>);\n    protected readonly categoryService = inject(CategoryService);\n\n    protected readonly id = signal<number | null>(null);\n    protected readonly name = signal<string | null>(null);\n\n    ngOnInit(): void {\n        this.loadFormData();\n    }\n\n    loadFormData(): void {\n        this.id.set(null);\n        this.name.set(null);\n    }\n\n    onSave() {\n        const category: Category = { id: this.id(), name: this.name() };\n        this.categoryService.saveCategory(category).subscribe(() => {\n            this.dialogRef.close(true);\n        });\n    }\n\n    onClose() {\n        this.dialogRef.close();\n    }\n}\n

    Si te fijas en el c\u00f3digo TypeScript, hemos a\u00f1adido en el m\u00e9todo onSave una llamada al servicio de CategoryService que aunque no realice ninguna operaci\u00f3n de momento, por lo menos lo dejamos preparado para conectar con el servidor.

    Adem\u00e1s, como siempre, al utilizar componentes matInput, matForm, matError hay que a\u00f1adir los m\u00f3dulos correspondientes como dependencias en el atributo imports.

    Ahora podemos navegar y abrir el cuadro de di\u00e1logo mediante el bot\u00f3n Nueva categor\u00eda para ver como queda nuestro formulario.

    "},{"location":"develop/basic/angular17/#utilizar-el-dialogo-para-editar","title":"Utilizar el di\u00e1logo para editar","text":"

    El mismo componente que hemos utilizado para crear una nueva categor\u00eda, nos sirve tambi\u00e9n para editar una categor\u00eda existente. Tan solo tenemos que utilizar la funcionalidad que Angular nos proporciona y pasarle los datos a editar en la llamada de apertura del Dialog. Vamos a implementar funcionalidad sobre el icono editar, tendremos que modificar unos cuantos ficheros:

    category-list.component.htmlcategory-list.component.ts
    <div class=\"container\">\n    <h1>Listado de Categor\u00edas</h1>\n\n    <mat-table [dataSource]=\"dataSource\"> \n        <ng-container matColumnDef=\"id\">\n            <mat-header-cell *matHeaderCellDef> Identificador </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.id}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"name\">\n            <mat-header-cell *matHeaderCellDef> Nombre categor\u00eda  </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.name}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"action\">\n            <mat-header-cell *matHeaderCellDef></mat-header-cell>\n            <mat-cell *matCellDef=\"let element\">\n                <button mat-icon-button color=\"primary\" (click)=\"editCategory(element)\">\n                    <mat-icon>edit</mat-icon>\n                </button>\n                <button mat-icon-button color=\"accent\"><mat-icon>clear</mat-icon></button>\n            </mat-cell>\n        </ng-container>\n\n        <mat-header-row *matHeaderRowDef=\"displayedColumns; sticky: true\"></mat-header-row>\n        <mat-row *matRowDef=\"let row; columns: displayedColumns;\"></mat-row>\n    </mat-table>\n\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\" (click)=\"createCategory()\">Nueva categor\u00eda</button> \n    </div>   \n</div>\n
    export class CategoryListComponent implements OnInit {\n\n  dataSource = new MatTableDataSource<Category>();\n  displayedColumns: string[] = ['id', 'name', 'action'];\n\n  protected readonly categoryService = inject(CategoryService);\n  protected readonly dialog = inject(MatDialog);\n\n  loadData(): void {\n    this.categoryService.getCategories().subscribe(\n      categories => this.dataSource.data = categories\n    );\n  }\n\n  ngOnInit(): void {\n    this.loadData();\n  }\n\n  createCategory() {    \n    const dialogRef = this.dialog.open(CategoryEditComponent, {\n      data: {}\n    });\n\n    dialogRef.afterClosed().subscribe(result => {\n      if(!result) return;\n      this.loadData();\n    });    \n  }  \n\n  editCategory(category: Category) {\n    const dialogRef = this.dialog.open(CategoryEditComponent, {\n      data: { category }\n    });\n\n    dialogRef.afterClosed().subscribe(result => {\n      if(!result) return;\n      this.loadData();\n    });\n  }\n}\n

    Y los Dialog:

    category-edit.component.htmlcategory-edit.component.ts
    <div class=\"container\">\n    @if (id()) {\n        <h1>Modificar categor\u00eda</h1>\n    } @else {\n        <h1>Crear categor\u00eda</h1>\n    }\n\n    <form>\n        <mat-form-field>\n...\n
    import { Component, OnInit, inject, signal } from '@angular/core';\nimport { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';\nimport { CategoryService } from '../category.service';\nimport { Category } from '../model/category';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatButtonModule } from '@angular/material/button';\n\n@Component({\n    selector: 'app-category-edit',\n    standalone: true,\n    imports: [FormsModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatButtonModule ],\n    templateUrl: './category-edit.component.html',\n    styleUrl: './category-edit.component.scss'\n})\nexport class CategoryEditComponent implements OnInit {\n    protected readonly dialogRef = inject(MatDialogRef<CategoryEditComponent>);\n    protected readonly data = inject(MAT_DIALOG_DATA) as { category: Category };\n    protected readonly categoryService = inject(CategoryService);\n\n    protected readonly id = signal<number | null>(null);\n    protected readonly name = signal<string | null>(null);\n\n    ngOnInit(): void {\n        this.loadFormData(this.data.category ?? null);\n    }\n\n    loadFormData(initialData: Category | null): void {\n        this.id.set(initialData?.id ?? null);\n        this.name.set(initialData?.name ?? null);\n    }\n\n    onSave() {\n        const category: Category = { id: this.id(), name: this.name() };\n        this.categoryService.saveCategory(category).subscribe(() => {\n            this.dialogRef.close(true);\n        });\n    }\n\n    onClose() {\n        this.dialogRef.close();\n    }\n}\n

    Navegando ahora por la p\u00e1gina y pulsando en el icono de editar, se deber\u00eda abrir una ventana con los datos que hemos seleccionado, similar a esta imagen:

    En el tutorial de Angular antiguo se aprovechaba que, al modificar los datos dentro de la ventana de di\u00e1logo, el listado se actualizaba autom\u00e1ticamente. Esto suced\u00eda porque se pasaba la misma referencia del objeto desde el listado al formulario y, al trabajar con objetos mutables, cualquier cambio se reflejaba en ambos.

    Con la llegada de Signals, este enfoque deja de ser recomendable. Los signals est\u00e1n dise\u00f1ados para modelar el estado de forma expl\u00edcita, predecible y controlada, evitando efectos colaterales derivados de compartir y mutar referencias entre distintas partes de la aplicaci\u00f3n.

    En lugar de modificar directamente un objeto que tambi\u00e9n utiliza el listado, trabajamos con estados independientes (por ejemplo, inicializando un signal con una copia del valor original) y aplicamos los cambios solo cuando el usuario los confirma. De esta forma, el listado no se ve afectado durante la edici\u00f3n.

    Esta filosof\u00eda \u2014estado local, sin mutaciones impl\u00edcitas y flujos de datos claros\u2014 hace que la interfaz sea m\u00e1s f\u00e1cil de razonar, mantener y depurar, y encaja con el modelo reactivo que propone Angular moderno.

    Recomendaci\u00f3n

    En Angular moderno se recomienda evitar el binding directo de objetos mutables. Compartir referencias puede provocar cambios no deseados en distintas partes de la aplicaci\u00f3n.

    Con el nuevo enfoque basado en Signals, es preferible modelar el estado de forma expl\u00edcita, trabajando con copias o estados independientes y aplicando los cambios de manera controlada. Esto reduce efectos colaterales y hace el flujo de datos m\u00e1s claro y predecible.

    "},{"location":"develop/basic/angular17/#accion-de-borrado","title":"Acci\u00f3n de borrado","text":"

    Por norma general, toda acci\u00f3n de borrado de un dato de pantalla requiere una confirmaci\u00f3n previa por parte del usuario. Es decir, para evitar que el dato se borre accidentalmente, el usuario tendr\u00e1 que confirmar su acci\u00f3n. Por tanto, vamos a crear un componente que nos permita pedir una confirmaci\u00f3n al usuario.

    Como esta pantalla de confirmaci\u00f3n va a ser algo com\u00fan a muchas acciones de borrado de nuestra aplicaci\u00f3n, vamos a crearla dentro del m\u00f3dulo core. Como siempre, ejecutamos el comando en consola:

    ng generate component core/dialog-confirmation\n

    E implementamos el c\u00f3digo que queremos que tenga el componente. Al ser un componente gen\u00e9rico vamos a aprovechar y leeremos las variables que le pasemos en data.

    dialog-confirmation.component.htmldialog-confirmation.component.scssdialog-confirmation.component.ts
    <div class=\"container\">\n    <h1>{{title}}</h1>\n    <div [innerHTML]=\"description\" class=\"description\"></div>\n\n    <div class=\"buttons\">\n        <button mat-stroked-button (click)=\"onClose()\">No</button>\n        <button mat-flat-button color=\"primary\" (click)=\"onClose(true)\">S\u00ed</button>\n    </div>\n</div>    \n
    .container {\n    min-width: 350px;\n    max-width: 500px;\n    padding: 20px;\n\n    .description {\n      margin-bottom: 20px;\n    }\n\n    .buttons {\n      text-align: right;\n\n      button {\n          margin-left: 10px;\n      }\n    }\n}    \n
    import { Component, inject, signal } from '@angular/core';\nimport { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';\nimport { MatButtonModule } from '@angular/material/button';\n\n@Component({\n    selector: 'app-dialog-confirmation',\n    standalone: true,\n    imports: [MatButtonModule],\n    templateUrl: './dialog-confirmation.component.html',\n    styleUrl: './dialog-confirmation.component.scss',\n})\nexport class DialogConfirmationComponent {\n    protected readonly title = signal<string | null>(null);\n    protected readonly description = signal<string | null>(null);\n\n    protected readonly dialogRef = inject(MatDialogRef<DialogConfirmationComponent>);\n    protected readonly data = inject(MAT_DIALOG_DATA);\n\n    ngOnInit(): void {\n        this.title.set(this.data.title);\n        this.description.set(this.data.description);\n    }\n\n    onClose(value = false) {\n        this.dialogRef.close(value);\n    }\n}\n

    Ya por \u00faltimo, una vez tenemos el componente gen\u00e9rico de di\u00e1logo, vamos a utilizarlo en nuestro listado al pulsar el bot\u00f3n eliminar:

    category-list.component.htmlcategory-list.component.ts
        ...\n    <ng-container matColumnDef=\"action\">\n        <mat-header-cell *matHeaderCellDef></mat-header-cell>\n        <mat-cell *matCellDef=\"let element\">\n            <button mat-icon-button color=\"primary\" (click)=\"editCategory(element)\">\n                <mat-icon>edit</mat-icon>\n            </button>\n            <button mat-icon-button color=\"accent\" (click)=\"deleteCategory(element)\">\n                <mat-icon>clear</mat-icon>\n            </button>\n        </mat-cell>\n    </ng-container>\n    ...\n
      ...\n  import { DialogConfirmationComponent } from '../../core/dialog-confirmation/dialog-confirmation.component';\n  ...\n  deleteCategory(category: Category) {    \n    const dialogRef = this.dialog.open(DialogConfirmationComponent, {\n      data: { title: \"Eliminar categor\u00eda\", description: \"Atenci\u00f3n si borra la categor\u00eda se perder\u00e1n sus datos.<br> \u00bfDesea eliminar la categor\u00eda?\" }\n    });\n\n    dialogRef.afterClosed().subscribe(result => {\n      if (result) {\n        this.categoryService.deleteCategory(category.id).subscribe(result => {\n          this.loadData();\n        }); \n      }\n    });\n  }  \n  ...    \n

    Aqu\u00ed tambi\u00e9n hemos realizado la llamada a categoryService, aunque no se realice ninguna acci\u00f3n, pero as\u00ed lo dejamos listo para enlazarlo.

    Llegados a este punto, ya solo nos queda enlazar las acciones de la pantalla con las operaciones de negocio del backend.

    "},{"location":"develop/basic/angular17/#conectar-con-backend","title":"Conectar con Backend","text":"

    Antes de seguir

    Antes de seguir con este punto, debes implementar el c\u00f3digo de backend en la tecnolog\u00eda que quieras (Springboot o Nodejs). Si has empezado este cap\u00edtulo implementando el frontend, por favor accede a la secci\u00f3n correspondiente de backend para poder continuar con el tutorial. Una vez tengas implementadas todas las operaciones para este listado, puedes volver a este punto y continuar con Angular.

    El siguiente paso, como es obvio, ser\u00e1 hacer que Angular llame directamente al servidor backend para leer y escribir datos y eliminar los datos mockeados en Angular.

    \u00a1Manos a la obra!

    "},{"location":"develop/basic/angular17/#llamada-del-listado","title":"Llamada del listado","text":"

    La idea es que el m\u00e9todo getCategories() de category.service.ts en lugar de devolver datos est\u00e1ticos, realice una llamada al servidor a la ruta http://localhost:8080/category.

    Abrimos el fichero y sustituimos la l\u00ednea que antes devolv\u00eda los datos est\u00e1ticos, por esto:

    category.service.ts
    import { Injectable, inject } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { Observable, of } from 'rxjs';\nimport { Category } from './model/category';\n\n@Injectable({\nprovidedIn: 'root'\n})\nexport class CategoryService { \n\n    protected readonly http = inject(HttpClient);\n\n    private baseUrl = 'http://localhost:8080/category';\n\n    getCategories(): Observable<Category[]> {\n        return this.http.get<Category[]>(this.baseUrl);\n    }\n\n    saveCategory(category: Category): Observable<Category> {\n        return of(null);\n    }\n\n    deleteCategory(idCategory : number): Observable<any> {\n        return of(null);\n    }  \n}\n

    Como hemos a\u00f1adido un componente nuevo HttpClient tenemos que configurar nuestro proyecto para poder realizar llamadas, para eso modificamos el fichero app.config.ts.

    app.config.ts
    import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';\nimport { provideRouter } from '@angular/router';\n\nimport { routes } from './app.routes';\nimport { provideAnimationsAsync } from '@angular/platform-browser/animations/async';\nimport { provideHttpClient, withFetch } from '@angular/common/http';\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideZoneChangeDetection({ eventCoalescing: true }),\n    provideRouter(routes),\n    provideAnimationsAsync(),\n    provideHttpClient(withFetch())\n  ]\n};\n

    Si ahora refrescas el navegador (recuerda tener arrancado tambi\u00e9n el servidor) y accedes a la pantalla de Categor\u00edas deber\u00eda aparecer el listado con los datos que vienen del servidor.

    "},{"location":"develop/basic/angular17/#llamada-de-guardado-edicion","title":"Llamada de guardado / edici\u00f3n","text":"

    Para la llamada de guardado har\u00edamos lo mismo, pero invocando la operaci\u00f3n de negocio put.

    category.service.ts
    import { Injectable, inject } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { Observable, of } from 'rxjs';\nimport { Category } from './model/category';\n\n@Injectable({\nprovidedIn: 'root'\n})\nexport class CategoryService { \n\n    protected readonly http = inject(HttpClient);\n\n    private baseUrl = 'http://localhost:8080/category';\n\n    getCategories(): Observable<Category[]> {\n        return this.http.get<Category[]>(this.baseUrl);\n    }\n\n    saveCategory(category: Category): Observable<Category> {\n        const { id } = category;\n        const url = id ? `${this.baseUrl}/${id}` : this.baseUrl;\n        return this.http.put<Category>(url, category);\n    }\n\n    deleteCategory(idCategory : number): Observable<any> {\n        return of(null);\n    }  \n\n} \n

    Ahora podemos probar a modificar o a\u00f1adir una nueva categor\u00eda desde la pantalla y deber\u00eda aparecer los nuevos datos en el listado.

    "},{"location":"develop/basic/angular17/#llamada-de-borrado","title":"Llamada de borrado","text":"

    Y ya por \u00faltimo, la llamada de borrado, deber\u00edamos cambiarla e invocar a la operaci\u00f3n de negocio delete.

    category.service.ts
    import { Injectable, inject } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { Observable, of } from 'rxjs';\nimport { Category } from './model/category';\n\n@Injectable({\nprovidedIn: 'root'\n})\nexport class CategoryService {\n    protected readonly http = inject(HttpClient);\n\n    private baseUrl = 'http://localhost:8080/category';\n\n    getCategories(): Observable<Category[]> {\n        return this.http.get<Category[]>(this.baseUrl);\n    }\n\n    saveCategory(category: Category): Observable<Category> {\n        const { id } = category;\n        const url = id ? `${this.baseUrl}/${id}` : this.baseUrl;\n        return this.http.put<Category>(url, category);\n    }\n\n    deleteCategory(idCategory : number): Observable<any> {\n        return this.http.delete(`${this.baseUrl}/${idCategory}`);\n    }  \n}\n

    Ahora podemos probar a modificar o a\u00f1adir una nueva categor\u00eda desde la pantalla y deber\u00eda aparecer los nuevos datos en el listado.

    Como ves, es bastante sencillo conectar server y client.

    "},{"location":"develop/basic/angular17/#depuracion","title":"Depuraci\u00f3n","text":"

    Una parte muy importante del desarrollo es tener la capacidad de depurar nuestro c\u00f3digo, en este apartado vamos a explicar como se realiza debug en Front.

    Esta parte se puede realizar con nuestro navegador favorito, en este caso vamos a utilizar Chrome.

    El primer paso es abrir las herramientas del desarrollador del navegador presionando F12.

    En esta herramienta tenemos varias partes importantes:

    • Elements: Inspector de los elementos del DOM de nuestra aplicaci\u00f3n que nos ayuda identificar el c\u00f3digo generado.
    • Console: Consola donde podemos ver mensajes importantes que nos ayudan a identificar posibles problemas.
    • Source: El navegador de ficheros que componen nuestra aplicaci\u00f3n.
    • Network: El registro de peticiones que realiza nuestra aplicaci\u00f3n.

    Identificados los elementos importantes, vamos a depurar la operaci\u00f3n de crear categor\u00eda.

    Para ello nos dirigimos a la pesta\u00f1a de Source, en el \u00e1rbol de carpetas nos dirigimos a la ruta donde est\u00e1 localizado el c\u00f3digo de nuestra aplicaci\u00f3n webpack://src/app.

    Dentro de esta carpeta est\u00e9 localizado todo el c\u00f3digo fuente de la aplicaci\u00f3n, en nuestro caso vamos a localizar componente category-edit.component que crea una nueva categor\u00eda.

    Dentro del fichero ya podemos a\u00f1adir puntos de ruptura (breakpoint), en nuestro caso queremos comprobar que el nombre introducido se captura bien y se env\u00eda al service correctamente.

    Colocamos el breakpoint en la l\u00ednea de invocaci\u00f3n del service (click sobre el n\u00famero de la l\u00ednea) y desde la interfaz creamos una nueva categor\u00eda.

    Hecho esto, podemos observar que a nivel de interfaz, la aplicaci\u00f3n se detiene y aparece un panel de manejo de los puntos de interrupci\u00f3n:

    En cuanto a la herramienta del desarrollador, nos lleva al punto exacto donde hemos a\u00f1adido el breakpoint y se para en este punto ofreci\u00e9ndonos la posibilidad de explorar el contenido de las variables del c\u00f3digo:

    Aqu\u00ed podemos comprobar que efectivamente la variable category tiene el valor que hemos introducido por pantalla y se propaga correctamente hacia el service.

    Para continuar con la ejecuci\u00f3n basta con darle al bot\u00f3n de play del panel de manejo de interrupci\u00f3n o al que aparece dentro de la herramienta de desarrollo (parte superior derecha).

    Por \u00faltimo, vamos a revisar que la petici\u00f3n REST se ha realizado correctamente al backend, para ello nos dirigimos a la pesta\u00f1a Network y comprobamos las peticiones realizadas:

    Aqu\u00ed podemos observar el registro de todas las peticiones y haciendo click sobre una de ellas, obtenemos el detalle de esta.

    • Header: Informaci\u00f3n de las cabeceras enviadas (aqu\u00ed podemos ver que se ha hecho un PUT a la ruta correcta).
    • Payload: El cuerpo de la petici\u00f3n (vemos el cuerpo del mensaje con el nombre enviado).
    • Preview: Respuesta de la petici\u00f3n normalizada (vemos la respuesta con el identificador creado para la nueva categor\u00eda).
    "},{"location":"develop/basic/nodejs/","title":"Listado simple - Nodejs","text":"

    Ahora que ya tenemos listo el proyecto backend de nodejs (en el puerto 8080) ya podemos empezar a codificar la soluci\u00f3n.

    "},{"location":"develop/basic/nodejs/#primeros-pasos","title":"Primeros pasos","text":"

    Antes de empezar

    Quiero hacer hincapi\u00e9 en Node tiene una documentaci\u00f3n muy extensa y completa, as\u00ed que te recomiendo que hagas uso de ella cuando tengas cualquier duda. Tanto en la web de node como de express encontrar\u00e1s informaci\u00f3n detallada del proceso que vamos a seguir.

    "},{"location":"develop/basic/nodejs/#estructurar-el-codigo","title":"Estructurar el c\u00f3digo","text":"

    La estructura de nuestro proyecto ser\u00e1 la siguiente:

    Vamos a aplicar una separaci\u00f3n por capas. En primer lugar, tendremos una capa de rutas para reenviar las solicitudes admitidas y cualquier informaci\u00f3n codificada en las urls de solicitud a la siguiente capa de controladores. La capa de control procesar\u00e1 las peticiones de las rutas y se comunicar\u00e1 con la capa de servicios devolviendo la respuesta de esta mediante respuestas http. En la capa de servicio se ejecutar\u00e1 toda la l\u00f3gica de la petici\u00f3n y se comunicar\u00e1 con los modelos de base de datos

    En nuestro caso una ruta es una secci\u00f3n de c\u00f3digo Express que asocia un verbo HTTP (GET, POST, PUT, DELETE, etc.), una ruta/patr\u00f3n de URL y una funci\u00f3n que se llama para manejar ese patr\u00f3n.

    \u00a1Ahora s\u00ed, vamos a programar!

    "},{"location":"develop/basic/nodejs/#capa-de-routes","title":"Capa de Routes","text":"

    Lo primero de vamos a crear es la carpeta principal de nuestra aplicaci\u00f3n donde estar\u00e1n contenidos los distintos elementos de la misma. Para ello creamos una carpeta llamada src en la ra\u00edz de nuestra aplicaci\u00f3n.

    El primero elemento que vamos a crear va a ser el fichero de rutas para la categor\u00eda. Para ello creamos una carpeta llamada routes en la carpeta src y dentro de esta carpeta crearemos un archivo llamado category.routes.js:

    category.routes.js
    import { Router } from 'express';\nimport { createCategory } from '../controllers/category.controller.js';\n\nconst categoryRouter = Router();\ncategoryRouter.put('/', createCategory);\n\nexport default categoryRouter;\n

    En este archivo estamos creando una ruta de tipo PUT que llamara al m\u00e9todo createCategory de nuestro futuro controlador de categor\u00edas (aunque todav\u00eda no lo hemos creado y por tanto fallar\u00e1).

    Ahora en nuestro archivo index.js vamos a a\u00f1adir lo siguiente justo despu\u00e9s de declarar la constante app:

    index.js
    ...\nimport categoryRouter from './src/routes/category.routes.js';\n...\n\n...\napp.use(cors({\n    origin: '*'\n}));\n\napp.use(express.json());\napp.use('/category', categoryRouter);\n\n...\n

    De este modo estamos asociando la url http://localhost:8080/category a nuestro router. Tambi\u00e9n usaremos express.json() para parsear las peticiones entrantes a formato json.

    "},{"location":"develop/basic/nodejs/#capa-de-controller","title":"Capa de Controller","text":"

    Lo siguiente ser\u00e1 crear el m\u00e9todo createCategory en nuestro controller. Para ello lo primero ser\u00e1 crear una carpeta controllers en la carpeta src de nuestro proyecto y dentro de esta un archivo llamado category.controller.js:

    category.controller.js
    export const createCategory = async (req, res) => {\n    console.log(req.body);\n    res.status(200).json(1);\n}\n

    Hemos creado la funci\u00f3n createCategory que recibir\u00e1 una request y una response. Estos par\u00e1metros vienen de la ruta de express y son la request y response de la petici\u00f3n HTTP. De momento simplemente vamos a hacer un console.log de req.body para ver el body de la petici\u00f3n y vamos a hacer una response 200 para indicar que todo ha ido correctamente.

    Si arrancamos el servidor y hacemos una petici\u00f3n PUT con Postman a http://localhost:8080/category con un body que pongamos formado correctamente podremos ver la salida que hemos programado en nuestro controller y en la consola de node podemos ver el contenido de req.body.

    "},{"location":"develop/basic/nodejs/#capa-de-modelo","title":"Capa de Modelo","text":"

    Ahora para que los datos que pasemos en el body los podamos guardar en BBDD necesitaremos un modelo y un esquema para la entidad Category. Vamos a crear una carpeta llamada schemas en la carpeta src de nuestro proyecto. Un schema no es m\u00e1s que un modelo de BBDD que especifica que campos estar\u00e1n presentes y cu\u00e1les ser\u00e1n sus tipos. Dentro de la carpeta de schemas creamos un archivo con el nombre category.schema.js:

    category.schema.js
    import mongoose from \"mongoose\";\nconst { Schema, model } = mongoose;\nimport normalize from 'normalize-mongoose';\n\nconst categorySchema = new Schema({\n    name: {\n        type: String,\n        require: true\n    }\n});\ncategorySchema.plugin(normalize);\nconst CategoryModel = model('Category', categorySchema);\n\nexport default CategoryModel;\n

    En este archivo estamos definiendo nuestro schema indicando sus propiedades y tipos, en nuestro caso \u00fanicamente name. Adem\u00e1s del tipo tambi\u00e9n indicaremos que el campo es obligatorio con la validation require para indicar que ese campo es obligatorio. Si quieres conocer otras validaciones aqu\u00ed tienes m\u00e1s info. Aparte de definir nuestro schema tambi\u00e9n lo estamos transformado en un modelo para poder trabajar con \u00e9l. En el constructor de model le pasamos el nombre del modelo y el schema que vamos a utilizar.

    "},{"location":"develop/basic/nodejs/#capa-de-servicio","title":"Capa de Servicio","text":"

    Como hemos visto en nuestra estructura la capa controller no puede comunicarse con la capa modelo, debe de haber una capa intermedia, para ello vamos a crear una carpeta services en la carpeta src de nuestro proyecto y dentro un archivo category.service.js:

    category.service.js
    import CategoryModel from '../schemas/category.schema.js';\n\nexport const createCategory = async function(name) {\n    try {\n        const category = new CategoryModel({ name });\n        return await category.save();\n    } catch (e) {\n        throw Error('Error creating category');\n    }\n}\n

    Hemos importado el modelo de categor\u00eda para poder realizar acciones sobre la BBDD y hemos creado una funci\u00f3n que recoger\u00e1 el nombre de la categor\u00eda y crear\u00e1 una nueva categor\u00eda con \u00e9l. Llamamos al m\u00e9todo save para guardar nuestra categor\u00eda y devolvemos el resultado. Ahora en nuestro m\u00e9todo del controller solo tenemos que llamar al servicio pas\u00e1ndole los par\u00e1metros que nos llegan en la petici\u00f3n:

    category.controller.js
    import * as CategoryService from '../services/category.service.js';\n\nexport const createCategory = async (req, res) => {\n    const { name } = req.body;\n    try {\n        const category = await CategoryService.createCategory(name);\n        res.status(200).json({\n            category\n        });\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n

    Si todo ha ido correctamente llamaremos al m\u00e9todo de respuesta con el c\u00f3digo 200 y la categor\u00eda creada. En caso contrario mandaremos un c\u00f3digo de error. Si ahora de nuevo vamos a postman y volvemos a lanzar nuestra petici\u00f3n podemos ver como nos devuelve una nueva categor\u00eda:

    "},{"location":"develop/basic/nodejs/#resto-de-operaciones","title":"Resto de Operaciones","text":""},{"location":"develop/basic/nodejs/#recuperacion-categorias","title":"Recuperaci\u00f3n categor\u00edas","text":"

    Ahora que ya podemos crear categor\u00edas lo siguiente ser\u00e1 crear un endpoint para recuperar las categor\u00edas creadas en nuestra base de datos. Podemos empezar a\u00f1adiendo un nuevo m\u00e9todo en nuestro servicio:

    category.service.js
    export const getCategories = async function () {\n    try {\n        return await CategoryModel.find().sort('name');\n    } catch (e) {\n        throw Error('Error fetching categories');\n    }\n}\n

    Al igual que en el anterior m\u00e9todo haremos uso del modelo, pero esta vez para hacer un find y ordenando los resultados por el campo name. Al m\u00e9todo find se le pueden pasar queries, projections y options. Te dejo por aqu\u00ed m\u00e1s info. En nuestro caso simplemente queremos que nos devuelva todas las categor\u00edas por lo que no le pasaremos nada.

    Creamos tambi\u00e9n un m\u00e9todo en el controlador para recuperar las categor\u00edas y que har\u00e1 uso del servicio:

    category.controller.js
    export const getCategories = async (req, res) => {\n    try {\n        const categories = await CategoryService.getCategories();\n        res.status(200).json(\n            categories\n        );\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n

    Y ahora que ya tenemos el m\u00e9todo creado en el controlador lo siguiente ser\u00e1 relacionar este m\u00e9todo con una ruta. Para ello en nuestro archivo category.routes.js tendremos que a\u00f1adir una nueva l\u00ednea:

    category.routes.js
    import { Router } from 'express';\nimport { createCategory, getCategories } from '../controllers/category.controller.js';\n\nconst categoryRouter = Router();\ncategoryRouter.put('/', createCategory);\ncategoryRouter.get('/', getCategories);\n\nexport default categoryRouter;\n

    De este modo cuando hagamos una petici\u00f3n GET a http://localhost:8080/category nos devolver\u00e1 el listado de categor\u00edas existentes:

    "},{"location":"develop/basic/nodejs/#actualizar-categoria","title":"Actualizar categor\u00eda","text":"

    Ahora vamos a por el m\u00e9todo para actualizar nuestras categor\u00edas. En el servicio creamos el siguiente m\u00e9todo:

    category.service.js
    export const updateCategory = async (id, name) => {\n    try {\n        const category = await CategoryModel.findById(id);\n        if (!category) {\n            throw Error('There is no category with that Id');\n        }    \n        return await CategoryModel.findByIdAndUpdate(id, {name});\n    } catch (e) {\n        throw Error(e);\n    }\n}\n

    A este m\u00e9todo le pasaremos de entrada el id y el nombre. Con ese id realizaremos una b\u00fasqueda para asegurarnos que esa categor\u00eda existe en nuestra base de datos. Si existe la categor\u00eda haremos una petici\u00f3n con findByIdAndUpdate donde el primer par\u00e1metro es el id y el segundo es el resto de los campos de nuestra entidad.

    En el controlador creamos el m\u00e9todo correspondiente:

    category.controller.js
    export const updateCategory = async (req, res) => {\n    const categoryId = req.params.id;\n    const { name } = req.body;\n    try {\n        await CategoryService.updateCategory(categoryId, name);\n        res.status(200).json(1);\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n

    Aqu\u00ed recogeremos el par\u00e1metro id que nos vendr\u00e1 en la url, por ejemplo: http://localhost:8080/category/1. Esto lo hacemos con req.params.id. El id es el nombre de la variable que le daremos en el router como veremos m\u00e1s adelante. Y una vez creado el m\u00e9todo en el controlador tendremos que a\u00f1adir la ruta en nuestro fichero de rutas correspondiente, pero como ya hemos dicho tendremos que indicar que nuestra ruta espera un par\u00e1metro id, lo haremos de la siguiente forma:

    category.routes.js
    import { Router } from 'express';\nimport { createCategory, getCategories, updateCategory } from '../controllers/category.controller.js';\n\nconst categoryRouter = Router();\ncategoryRouter.put('/', createCategory);\ncategoryRouter.get('/', getCategories);\ncategoryRouter.put('/:id', updateCategory);\n\nexport default categoryRouter;\n

    Y volvemos a probar en Postman:

    Y si hacemos de nuevo un GET vemos como la categor\u00eda se ha modificado correctamente:

    "},{"location":"develop/basic/nodejs/#borrado-categoria","title":"Borrado categor\u00eda","text":"

    Ya solo nos faltar\u00eda la operaci\u00f3n de delete para completar nuestro CRUD, en el servicio a\u00f1adimos un nuevo m\u00e9todo:

    category.service.js
    export const deleteCategory = async (id) => {\n    try {\n        const category = await CategoryModel.findById(id);\n        if (!category) {\n            throw Error('There is no category with that Id');\n        }\n        return await CategoryModel.findByIdAndDelete(id);\n    } catch (e) {\n        throw Error('Error deleting category');\n    }\n}\n

    Como vemos es muy parecido al update, recuperamos el id de los par\u00e1metros de la ruta y en este caso llamaremos al m\u00e9todo findByIdAndDelete. En nuestro controlador creamos el m\u00e9todo correspondiente:

    category.controller.js
    export const deleteCategory = async (req, res) => {\n    const categoryId = req.params.id;\n    try {\n        const deletedCategory = await CategoryService.deleteCategory(categoryId);\n        res.status(200).json({\n            category: deletedCategory\n        });\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n

    Y de nuevo a\u00f1adimos la ruta correspondiente al archivo de rutas:

    category.routes.js
    import { Router } from 'express';\nimport { createCategory, getCategories, updateCategory, deleteCategory } from '../controllers/category.controller.js';\n\nconst categoryRouter = Router();\ncategoryRouter.put('/', createCategory);\ncategoryRouter.get('/', getCategories);\ncategoryRouter.put('/:id', updateCategory);\ncategoryRouter.delete('/:id', deleteCategory);\n\nexport default categoryRouter;\n

    Y de nuevo, probamos en postman:

    Hacemos un get para comprobar que se ha borrado de nuestra base de datos:

    "},{"location":"develop/basic/nodejs/#capa-de-middleware-validaciones","title":"Capa de Middleware (Validaciones)","text":"

    Antes de pasar a nuestro siguiente CRUD vamos a ver en que consiste la Capa de Middleware. Un middleware es un c\u00f3digo que se ejecuta antes de que una petici\u00f3n http llegue a nuestro manejador de rutas o antes de que el cliente reciba una respuesta.

    En nuestro caso vamos a crear un middleware para asegurarnos que todos los campos que necesitamos en nuestras entidades vienen en el body de la petici\u00f3n. Vamos a crear una carpeta middlewares en la carpeta src de nuestro proyecto y dentro crearemos el fichero validateFields.js:

    validateFields.js
    import { response } from 'express';\nimport { validationResult } from 'express-validator';\n\nconst validateFields = (req, res = response, next) => {\n    const errors = validationResult(req);\n    if (!errors.isEmpty()) {\n        return res.status(400).json({\n            errors: errors.mapped()\n        });\n    }\n    next();\n}\n\nexport default validateFields;\n

    En este m\u00e9todo nos ayudaremos de la librer\u00eda express-validator para ver los errores que tenemos en nuestras rutas. Para ello llamaremos a la funci\u00f3n validationResult que nos devolver\u00e1 un array de errores que m\u00e1s tarde definiremos. Si el array no va vac\u00edo es porque se ha producido alg\u00fan error en las validaciones y ejecutara la response con un c\u00f3digo de error.

    Ahora definiremos las validaciones en nuestro archivo de rutas, deber\u00eda quedar de la siguiente manera:

    category.routes.js
    import { Router } from 'express';\nimport { check } from 'express-validator';\nimport validateFields from '../middlewares/validateFields.js';\nimport { getCategories, createCategory, deleteCategory, updateCategory } from '../controllers/category.controller.js';\nconst categoryRouter = Router();\n\ncategoryRouter.put('/:id', [\n    check('name').not().isEmpty(),\n    validateFields\n], updateCategory);\n\ncategoryRouter.put('/', [\n    check('name').not().isEmpty(),\n    validateFields\n], createCategory);\n\ncategoryRouter.get('/', getCategories);\ncategoryRouter.delete('/:id', deleteCategory);\n\nexport default categoryRouter;\n

    Aqu\u00ed nos ayudamos de nuevo de express-validator y de su m\u00e9todo check. Para las rutas en las que necesitemos validaciones, a\u00f1adimos un array como segundo par\u00e1metro. En este array vamos a\u00f1adiendo todas las validaciones que necesitemos. En nuestro caso solo queremos que el campo name no sea vac\u00edo, pero existen muchas m\u00e1s validaciones que puedes encontrar en la documentaci\u00f3n de express-validator. Importamos nuestro middleware y lo a\u00f1adimos en la \u00faltima posici\u00f3n de este array.

    De este modo no se realizar\u00e1n las peticiones que no pasen las validaciones:

    Y con esto habremos terminado nuestro primer CRUD.

    "},{"location":"develop/basic/nodejs/#depuracion","title":"Depuraci\u00f3n","text":"

    Una parte muy importante del desarrollo es tener la capacidad de depurar nuestro c\u00f3digo, en este apartado vamos a explicar como se realiza debug en Backend.

    Esta parte se realiza con las herramientas incluidas dentro de nuestro IDE favorito, en este caso vamos a utilizar el Visual Estudio.

    Lo primero que debemos hacer es configurar el modo Debug de nuestro proyecto.

    Para ello nos dirigimos a la opci\u00f3n Run and Debug y creamos el fichero de launch necesario:

    Esto nos crear\u00e1 el fichero necesario y ya podremos arrancar la aplicaci\u00f3n mediante esta herramienta presionando el bot\u00f3n Launch Program (seleccionamos tipo de aplicaci\u00f3n Node y el script de arranque que ser\u00e1 el que hemos utilizado en el desarrollo):

    Arrancada la aplicaci\u00f3n de este modo, vamos a depurar la operaci\u00f3n de crear categor\u00eda.

    Para ello vamos a abrir nuestro fichero donde tenemos la implementaci\u00f3n del servicio de creaci\u00f3n de la capa de la l\u00f3gica de negocio category.service.js.

    Dentro del fichero ya podemos a\u00f1adir puntos de ruptura (breakpoint), en nuestro caso queremos comprobar que el nombre introducido se recibe correctamente.

    Colocamos el breakpoint en la primera l\u00ednea del m\u00e9todo (click sobre el n\u00famero de la l\u00ednea) y desde la interfaz/postman creamos una nueva categor\u00eda.

    Hecho esto, podemos observar que a nivel de interfaz/postman, la petici\u00f3n se queda esperando y el IDE mostrar\u00e1 un panel de manejo de los puntos de interrupci\u00f3n:

    El IDE nos lleva al punto exacto donde hemos a\u00f1adido el breakpoint y se para en este punto ofreci\u00e9ndonos la posibilidad de explorar el contenido de las variables del c\u00f3digo:

    Aqu\u00ed podemos comprobar que efectivamente la variable name tiene el valor que hemos introducido por pantalla/postman.

    Para continuar con la ejecuci\u00f3n basta con darle al bot\u00f3n de play del panel de manejo de los puntos de interrupci\u00f3n.

    "},{"location":"develop/basic/react/","title":"Listado simple - React","text":"

    Ahora que ya tenemos listo el proyecto frontend de React (en el puerto 5173), ya podemos empezar a codificar la soluci\u00f3n.

    "},{"location":"develop/basic/react/#primeros-pasos","title":"Primeros pasos","text":"

    Antes de empezar

    Quiero hacer hincapi\u00e9 que React tiene una documentaci\u00f3n muy extensa y completa, as\u00ed que te recomiendo que hagas uso de ella cuando tengas cualquier duda. Tanto en la propia web de documentaci\u00f3n de React como en la web de componentes Mui puedes buscar casi cualquier ejemplo que necesites.

    Si abrimos el proyecto con el IDE que tengamos (Visual Studio Code en el caso del tutorial) podemos ver que en la carpeta src existen unos ficheros ya creados por defecto. Estos ficheros son:

    • main.tsx \u2192 contiene el componente principal del proyecto.
    • index.css \u2192 contiene los estilos CSS globales de la aplicaci\u00f3n.
    • APP.tsx \u2192 contiene el componente inicial del proyecto
    • APP.css \u2192 contiene los estilos para el componente APP.

    Aunque main.tsx y App.tsx puedan parecer lo mismo main.tsx se suele dejar tal y como esta ya que lo \u00fanico que hace es asociar el div con id \u201croot\u201d del archivo index.html de la ra\u00edz de nuestro proyecto para que sea el nodo principal de React. En el archivo App.tsx es donde realmente empezamos a desarrollar c\u00f3digo.

    Si abrimos main.tsx podemos ver que se esta usando <App /> como una etiqueta html. El nombre con que exportemos nuestros componentes ser\u00e1 el nombre de la etiqueta html utilizado para renderizar los componentes.

    Vamos a modificar este c\u00f3digo inicial para ver c\u00f3mo funciona. Abrimos el fichero App.tsx y vamos a dejarlo de esta manera:

    import { useState } from 'react'\nimport './App.css'\n\nfunction App() {\n  const [count, setCount] = useState(0)\n  const probando = \"probando 123\";\n\n  return (\n    <>\n      <p>{probando}</p>\n      <div className=\"card\">\n        <button onClick={() => setCount((count) => count + 1)}>\n          count is {count}\n        </button>\n      </div>\n    </>\n  )\n}\n\nexport default App\n
    En los componentes React siempre se suele seguir el mismo orden, primero introduciremos los imports necesarios, luego podemos declarar variables y funciones que no se vayan a modificar, despu\u00e9s creamos nuestra funci\u00f3n principal con el nombre del componente y dentro de esta lo primero que se suelen declarar son todas las variables, despu\u00e9s a\u00f1adiremos m\u00e9todos del componente y por \u00faltimo tenemos que llamar a return para devolver lo que queramos renderizar.

    Si ahora abrimos nuestro navegador veremos en pantalla el valor de la variable \"probando\" que hemos introducido mediante una expresi\u00f3n en un tag p de html y un bot\u00f3n que si pulsamos incrementar\u00e1 el valor de la cuenta en uno. Si refrescamos la pantalla el valor de la cuenta volver\u00e1 autom\u00e1ticamente a 0. Es hora de explicar como funciona un componente React y el hook useState.

    "},{"location":"develop/basic/react/#jsx","title":"JSX","text":"

    JSX significa Javascript XML. JSX nos permite escribir elementos HTML en JavaScript y colocarlos en el DOM. Con JSX puedes escribir expresiones dentro de llaves \u201c{}\u201d. Estas expresiones pueden ser variables, propiedades o cualquier expresi\u00f3n Javascript valida. JSX ejecutar\u00e1 esta expresi\u00f3n y devolver\u00e1 el resultado.

    Por ejemplo, si queremos mostrar un elemento de forma condicional lo podemos hacer de la siguiente manera:

            {\n          variableBooleana  && <p>El valor es true</p>\n        }\n

    Tambi\u00e9n podemos usar el operador ternario para condiciones:

            {\n          variableBooleana  ? <p>El valor es true</p> : <p>El valor es false</p>\n        }\n

    Y si lo que queremos es recorrer un array e ir representando los elementos lo podemos hacer de la siguiente manera:

            {\n          arrayNumerico.map(numero => <p>Mi valor es {numero}</p>)\n        }\n

    React solo puede devolver un elemento en su bloque return, es por eso por lo que algunas veces se rodea todo el c\u00f3digo con un elemento llamado Fragment \u201c<></>\u201d. Estos fragment no soportan ni propiedades ni atributos y no tendr\u00e1n visibilidad en el dom.

    Dentro de una expresi\u00f3n podemos ver dos formas de llamar a una funci\u00f3n:

    <Button onClick={callToCancelar}>Cancelar</Button>\n<Button onClick={() => callToCancelar('param1')}>Cancelar</Button>\n

    En la primera se pasa una funci\u00f3n por referencia y Button es el responsable de los par\u00e1metros del evento. En la segunda tras hacer onClick se ejecuta la funci\u00f3n callToCancelar con los par\u00e1metros que nosotros queramos quitando esa responsabilidad a Button. En t\u00e9rminos de rendimiento es mejor la primera manera ya que en la segunda se vuelve a crear la funci\u00f3n en cada renderizado, pero hay veces que es necesario hacerlo as\u00ed para tomar control de los par\u00e1metros.

    "},{"location":"develop/basic/react/#usestate-hook","title":"useState hook","text":"

    Todo componente en React tiene una serie de variables. Algunas de estas son propiedades de entrada como podr\u00edan serlo disabled en un bot\u00f3n y que se trasmiten de componentes padres a hijos.

    Luego tenemos variables y constantes declaradas dentro del componente como por ejemplo la constante probando de nuestro ejemplo. Y finalmente tenemos unas variables especiales dentro de nuestro componente que corresponden al estado de este.

    Si modificamos el estado de un componente este autom\u00e1ticamente se volver\u00e1 a renderizar y producir\u00e1 una nueva representaci\u00f3n en pantalla.

    Como ya hemos comentado previamente los hooks aparecieron en la versi\u00f3n 16.8 de React. Antes de esto si quer\u00edamos acceder al estado de un componente solo pod\u00edamos acceder a este mediante componentes de clase, pero desde esta versi\u00f3n podemos hacer uso de estas funciones especiales para utilizar estas caracter\u00edsticas de React.

    M\u00e1s tarde veremos otras, pero de momento vamos a ver useState.

    const [count, setCount] = useState(0)\n

    En nuestro ejemplo tenemos una variable count que va mostrando su valor en el interior de un bot\u00f3n. Si pulsamos el bot\u00f3n ejecutara la funci\u00f3n setCount que actualiza el valor de nuestro contador. A esta funci\u00f3n se le puede pasar o bien el nuevo valor que tomar\u00e1 esta variable de estado o bien una funci\u00f3n cuyo primer par\u00e1metro es el valor actual de la variable. Siempre que se actualice la variable del estado de producir\u00e1 un nuevo renderizado del componente, eso lo pod\u00e9is comprobar escribiendo un console.log antes del return. En nuestro caso hemos inicializado nuestra variable de estado con el valor 0, pero puede inicializarse con un valor de cualquier tipo javascript. No existe limite en el n\u00famero de variables de estado por componente.

    Debemos tener en cuenta que si modificamos el estado de un componente que renderiza otros componentes, estos tambi\u00e9n se volver\u00e1n a renderizar al cambiar el estado del componente padre. Es por esto por lo que debemos tener cuidado a la hora de modificar estados y renderizar los hijos correctamente.

    Nota

    Para evitar el re-renderizado de los componentes hijos existe una funci\u00f3n especial en React llamada memo que evita este comportamiento si las props de los hijos no se ven modificadas. En este curso no cubriremos esta funcionalidad.

    Nota

    Por convenci\u00f3n todos los hooks empiezan con use. Si en alg\u00fan proyecto tienes que crear un custom hook es importante seguir esta nomenclatura.

    "},{"location":"develop/basic/react/#libreria-de-componentes-y-resto-de-dependencias","title":"Librer\u00eda de componentes y resto de dependencias","text":"

    Antes de continuar con nuestro curso vamos a instalar las dependencias necesarias para empezar a construir la base de nuestra aplicaci\u00f3n. Para ello ejecutamos lo siguiente en la consola en la ra\u00edz de nuestro proyecto:

    npm i @mui/material @mui/icons-material react-router-dom react-redux @reduxjs/toolkit @emotion/react @emotion/styled\n

    Como librer\u00eda de componentes vamos a utilizar Mui, anteriormente conocido como Material ui, es una librer\u00eda muy utilizada en los proyectos de React con una gran documentaci\u00f3n. Tambi\u00e9n necesitaremos las librer\u00edas de emotion necesarias para trabajar con Mui.

    Vamos a utilizar la librer\u00eda react router dom que nos permitir\u00e1 definir y usar rutas de navegaci\u00f3n en nuestra aplicaci\u00f3n.

    Vamos a instalar tambi\u00e9n react redux y redux toolkit para gestionar el estado global de nuestra aplicaci\u00f3n.

    "},{"location":"develop/basic/react/#layout-general","title":"Layout general","text":""},{"location":"develop/basic/react/#crear-componente","title":"Crear componente","text":"

    Lo primero que haremos ser\u00e1 borrar el contenido del archivo App.css y vamos a modificar index.css con el siguiente contenido:

    :root {\n  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;\n}\n\nbody {\n  margin: 0;\n}\n\n@media (prefers-color-scheme: light) {\n  :root {\n    color: #213547;\n    background-color: #ffffff;\n  }\n}\n\n.container {\n  margin: 20px;\n}\n\n.newButton {\n  display: flex;\n  justify-content: flex-end;\n  margin-top: 20px;\n}\n

    Ahora vamos a crear los distintos componentes que compondr\u00e1n nuestra aplicaci\u00f3n. Para ello dentro de la carpeta src vamos a crear una nueva carpeta llamada pages y dentro de esta crearemos tres carpetas relativas a nuestras paginas navegables: \u201cAuthor\u201d, \u201cCategory\u201d y \u201cGame\u201d. Dentro de estas a su vez crearemos un fichero llamado Author.tsx, Category.tsx y Game.tsx respectivamente, cuyo contenido ser\u00e1 una funci\u00f3n que tendr\u00e1 por nombre el mismo nombre que el fichero y que devolver\u00e1 un div cuyo contenido ser\u00e1 tambi\u00e9n el nombre del fichero:

    export const Game = () => {\n  return (\n    <div>Game</div>\n  )\n}\n

    Ahora vamos a crear en la carpeta src otra carpeta cuyo nombre ser\u00e1 \u201ccomponents\u201d y dentro de esta un fichero llamado Layout.tsx cuyo contenido ser\u00e1 el siguiente:

    import { useState } from \"react\";\nimport AppBar from \"@mui/material/AppBar\";\nimport Box from \"@mui/material/Box\";\nimport Toolbar from \"@mui/material/Toolbar\";\nimport IconButton from \"@mui/material/IconButton\";\nimport Typography from \"@mui/material/Typography\";\nimport Menu from \"@mui/material/Menu\";\nimport MenuIcon from \"@mui/icons-material/Menu\";\nimport Container from \"@mui/material/Container\";\nimport Button from \"@mui/material/Button\";\nimport MenuItem from \"@mui/material/MenuItem\";\nimport CasinoIcon from \"@mui/icons-material/Casino\";\nimport { useNavigate, Outlet } from \"react-router-dom\";\n\nconst pages = [\n  { name: \"Catalogo\", link: \"games\" },\n  { name: \"Categor\u00edas\", link: \"categories\" },\n  { name: \"Autores\", link: \"authors\" },\n];\n\nexport const Layout = () => {\n  const navigate = useNavigate();\n\n  const [anchorElNav, setAnchorElNav] = useState<null | HTMLElement>(\n    null\n  );\n\n  const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => {\n    setAnchorElNav(event.currentTarget);\n  };\n\n  const handleCloseNavMenu = (link: string) => {\n    navigate(`/${link}`);\n    setAnchorElNav(null);\n  };\n\n  return (\n    <>\n      <AppBar position=\"static\">\n        <Container maxWidth=\"xl\">\n          <Toolbar disableGutters>\n            <CasinoIcon sx={{ display: { xs: \"none\", md: \"flex\" }, mr: 1 }} />\n            <Typography\n              variant=\"h6\"\n              noWrap\n              component=\"a\"\n              href=\"/\"\n              sx={{\n                mr: 2,\n                display: { xs: \"none\", md: \"flex\" },\n                fontFamily: \"monospace\",\n                fontWeight: 700,\n                letterSpacing: \".3rem\",\n                color: \"inherit\",\n                textDecoration: \"none\",\n              }}\n            >\n              Ludoteca Tan\n            </Typography>\n\n            <Box sx={{ flexGrow: 1, display: { xs: \"flex\", md: \"none\" } }}>\n              <IconButton\n                size=\"large\"\n                aria-label=\"account of current user\"\n                aria-controls=\"menu-appbar\"\n                aria-haspopup=\"true\"\n                onClick={handleOpenNavMenu}\n                color=\"inherit\"\n              >\n                <MenuIcon />\n              </IconButton>\n              <Menu\n                id=\"menu-appbar\"\n                anchorEl={anchorElNav}\n                anchorOrigin={{\n                  vertical: \"bottom\",\n                  horizontal: \"left\",\n                }}\n                keepMounted\n                transformOrigin={{\n                  vertical: \"top\",\n                  horizontal: \"left\",\n                }}\n                open={Boolean(anchorElNav)}\n                onClose={handleCloseNavMenu}\n                sx={{\n                  display: { xs: \"block\", md: \"none\" },\n                }}\n              >\n                {pages.map((page) => (\n                  <MenuItem\n                    key={page.name}\n                    onClick={() => handleCloseNavMenu(page.link)}\n                  >\n                        <Typography textAlign=\"center\">\n                        {page.name}\n                      </Typography>\n                  </MenuItem>\n                ))}\n              </Menu>\n            </Box>\n            <CasinoIcon sx={{ display: { xs: \"flex\", md: \"none\" }, mr: 1 }} />\n            <Typography\n              variant=\"h5\"\n              noWrap\n              component=\"a\"\n              href=\"\"\n              sx={{\n                mr: 2,\n                display: { xs: \"flex\", md: \"none\" },\n                flexGrow: 1,\n                fontFamily: \"monospace\",\n                fontWeight: 700,\n                letterSpacing: \".3rem\",\n                color: \"inherit\",\n                textDecoration: \"none\",\n              }}\n            >\n              Ludoteca Tan\n            </Typography>\n            <Box sx={{ flexGrow: 1, display: { xs: \"none\", md: \"flex\" } }}>\n              {pages.map((page) => (\n                <Button\n                  key={page.name}\n                  onClick={() => handleCloseNavMenu(page.link)}\n                  sx={{ my: 2, color: \"white\", display: \"block\" }}\n                >\n                  {page.name}\n                </Button>\n              ))}\n            </Box>\n          </Toolbar>\n        </Container>\n      </AppBar>\n      <Outlet />\n    </>\n  );\n};\n

    Aunque puede parecer complejo por su tama\u00f1o en realidad no es tanto, casi todo es c\u00f3digo cogido directamente de un ejemplo de layout de navegaci\u00f3n de un componente de MUI.

    Lo m\u00e1s destacable es un nuevo hook (en realidad es un custom hook de react router dom) llamado useNavigate que como su propio nombre indica navegara a la ruta correspondiente seg\u00fan el valor pulsado.

    Las etiquetas sx son para dar estilo a los componentes de MUI. Tambi\u00e9n se puede sobrescribir el estilo mediante hojas css pero es m\u00e1s complejo y requiere una configuraci\u00f3n inicial que no cubriremos en este tutorial.

    Si nos fijamos en la l\u00ednea 90 se introduce una expresi\u00f3n javascript en la cual se recorre el array de pages declarado al inicio del componente y para cada uno de los valores se llama a MenuItem que es otro componente React al que se le pasan las props key, onClick y aunque no lo veamos tambi\u00e9n la prop \u201cchildren\u201d.

    La prop children estar\u00e1 presente cuando pasemos elementos entre los tags de un elemento:

    <MenuItem >     \n<Typography>I\u2019m a child</Typography>\n </MenuItem>\n
    El uso de la prop children no es muy recomendado y se prefiere que se pasen los elementos como una prop m\u00e1s.

    Siempre que rendericemos un array en react es recomendable usar una prop especial llamada \u201ckey\u201d, de hecho, si no la usamos la consola de desarrollo se nos llenar\u00e1 de warnings por no usarla.

    Esta key lo que permite a React es identificar cada elemento de formar m\u00e1s eficiente, as\u00ed si modificamos, a\u00f1adimos o eliminamos un elemento de un array no ser\u00e1 necesario volver a renderizar todo el array, solo se eliminar\u00e1 el elemento necesario.

    En la parte final del archivo tenemos una llamada al elemento Outlet. Este elemento es el que albergara el componente asociado a la ruta seleccionada.

    Por \u00faltimo, el archivo App.tsx se tiene que quedar de esta manera:

    import { BrowserRouter, Routes, Route, Navigate } from \"react-router-dom\";\nimport { Game } from \"./pages/Game/Game\";\nimport { Author } from \"./pages/Author/Author\";\nimport { Category } from \"./pages/Category/Category\";\nimport { Layout } from \"./components/Layout\";\n\nfunction App() {\n  return (\n        <BrowserRouter>\n          <Routes>\n            <Route element={<Layout />}>\n              <Route index path=\"games\" element={<Game />} />\n              <Route path=\"categories\" element={<Category />} />\n              <Route path=\"authors\" element={<Author />} />\n              <Route path=\"*\" element={<Navigate to=\"/games\" />} />\n            </Route>\n          </Routes>\n        </BrowserRouter>\n  );\n}\n\nexport default App;\n
    De esta manera definimos cada una de nuestras rutas y las asociamos a una p\u00e1gina.

    Vamos a arrancar el proyecto de nuevo con npm run dev y navegamos a http://localhost:5173/.

    Ahora podemos ver como autom\u00e1ticamente nos lleva a http://localhost:5173/games debido al \u00faltimo route en el que redirigimos cualquier path que no coincida con los anteriores a /games. Si pulsamos sobre las distintas opciones del men\u00fa podemos ver c\u00f3mo va cambiando el outlet de nuestra aplicaci\u00f3n con los distintos div creados para cada uno de los componentes p\u00e1gina.

    "},{"location":"develop/basic/react/#creando-un-listado-simple","title":"Creando un listado simple","text":""},{"location":"develop/basic/react/#pagina-categorias","title":"P\u00e1gina categor\u00edas","text":"

    Ya tenemos la estructura principal, ahora vamos a crear nuestra primera pantalla. Vamos a empezar por la de categor\u00edas.

    Lo primero que vamos a hacer es crear una carpeta llamada types dentro de src/. Aqu\u00ed crearemos los tipos de typescript. Creamos un nuevo fichero llamado Category.ts cuyo contenido ser\u00e1 el siguiente:

    export interface Category {\n  id: string;\n  name: string;\n}\n

    Ahora vamos a crear un archivo de estilos que ser\u00e1 solo utilizado por el componente Category. Para ello dentro de la carpeta src/pages/Category vamos a crear un archivo llamado Category.module.css. Al llamar al archivo de esta manera React reconoce este archivo como un archivo \u00fanico para un componente y hace que sus reglas css sean m\u00e1s prioritarias, aunque por ejemplo exista una clase con el mismo nombre en el archivo index.css.

    El contenido de nuestro archivo css ser\u00e1 el siguiente:

    .tableActions {\n    margin-right: 20px;\n    display: flex;\n    justify-content: flex-end;\n    align-content: flex-start;\n    gap: 19px;\n}\n

    Y por \u00faltimo el contenido de nuestro fichero src/pages/Category.tsx quedar\u00eda as\u00ed:

    import { useState } from \"react\";\nimport Table from \"@mui/material/Table\";\nimport TableBody from \"@mui/material/TableBody\";\nimport TableCell from \"@mui/material/TableCell\";\nimport TableContainer from \"@mui/material/TableContainer\";\nimport TableHead from \"@mui/material/TableHead\";\nimport TableRow from \"@mui/material/TableRow\";\nimport Paper from \"@mui/material/Paper\";\nimport Button from \"@mui/material/Button\";\nimport EditIcon from \"@mui/icons-material/Edit\";\nimport ClearIcon from \"@mui/icons-material/Clear\";\nimport IconButton from \"@mui/material/IconButton\";\nimport styles from \"./Category.module.css\";\nimport { Category as CategoryModel } from \"../../types/Category\";\n\nexport const Category = () => {\n  const data = [\n    {\n      id: \"1\",\n      name: \"Test 1\",\n    },\n    {\n      id: \"2\",\n      name: \"Test 2\",\n    },\n  ];\n\n  return (\n    <div className=\"container\">\n      <h1>Listado de Categor\u00edas</h1>\n      <TableContainer component={Paper}>\n        <Table sx={{ minWidth: 650 }} aria-label=\"simple table\">\n          <TableHead\n            sx={{\n              \"& th\": {\n                backgroundColor: \"lightgrey\",\n              },\n            }}\n          >\n            <TableRow>\n              <TableCell>Identificador</TableCell>\n              <TableCell>Nombre categor\u00eda</TableCell>\n              <TableCell></TableCell>\n            </TableRow>\n          </TableHead>\n          <TableBody>\n            {data.map((category: CategoryModel) => (\n              <TableRow\n                key={category.id}\n                sx={{ \"&:last-child td, &:last-child th\": { border: 0 } }}\n              >\n                <TableCell component=\"th\" scope=\"row\">\n                  {category.id}\n                </TableCell>\n                <TableCell component=\"th\" scope=\"row\">\n                  {category.name}\n                </TableCell>\n                <TableCell>\n                  <div className={styles.tableActions}>\n                    <IconButton aria-label=\"update\" color=\"primary\">\n                      <EditIcon />\n                    </IconButton>\n                    <IconButton aria-label=\"delete\" color=\"error\">\n                      <ClearIcon />\n                    </IconButton>\n                  </div>\n                </TableCell>\n              </TableRow>\n            ))}\n          </TableBody>\n        </Table>\n      </TableContainer>\n      <div className=\"newButton\">\n        <Button variant=\"contained\">Nueva categor\u00eda</Button>\n      </div>\n    </div>\n  );\n};\n
    De momento vamos a usar un listado mockeado para mostrar nuestras categorias. El c\u00f3digo JSX esta sacado pr\u00e1cticamente en su totalidad del ejemplo de una tabla de Mui y solo hemos modificado el nombre del array que tenemos que recorrer, sus atributos y hemos a\u00f1adido unos botones de acci\u00f3n para editar y borrar autores que de momento no hacen nada.

    Si abrimos un navegador (con el servidor arrancado npm run dev) y vamos a http://localhost:5173/categories podremos ver nuestro listado con los datos mockeados.

    Ahora vamos a crear un componente que se mostrar\u00e1 cuando pulsemos el bot\u00f3n de nueva categor\u00eda. En la carpeta src/pages/category vamos a crear una nueva carpeta llamada components y dentro de esta crearemos un nuevo fichero llamado CreateCategory.tsx que tendr\u00e1 el siguiente contenido:

    import { useState } from \"react\";\nimport Button from \"@mui/material/Button\";\nimport TextField from \"@mui/material/TextField\";\nimport Dialog from \"@mui/material/Dialog\";\nimport DialogActions from \"@mui/material/DialogActions\";\nimport DialogContent from \"@mui/material/DialogContent\";\nimport DialogTitle from \"@mui/material/DialogTitle\";\nimport { Category } from \"../../../types/Category\";\n\ninterface Props {\n  category: Category | null;\n  closeModal: () => void;\n  create: (name: string) => void;\n}\n\nexport default function CreateCategory(props: Props) {\n  const [name, setName] = useState(props?.category?.name || \"\");\n\n  return (\n    <div>\n      <Dialog open={true} onClose={props.closeModal}>\n        <DialogTitle>\n          {props.category ? \"Actualizar Categor\u00eda\" : \"Crear Categor\u00eda\"}\n        </DialogTitle>\n        <DialogContent>\n          {props.category && (\n            <TextField\n              margin=\"dense\"\n              disabled\n              id=\"id\"\n              label=\"Id\"\n              fullWidth\n              value={props.category.id}\n              variant=\"standard\"\n            />\n          )}\n          <TextField\n            margin=\"dense\"\n            id=\"name\"\n            label=\"Nombre\"\n            fullWidth\n            variant=\"standard\"\n            onChange={(event) => setName(event.target.value)}\n            value={name}\n          />\n        </DialogContent>\n        <DialogActions>\n          <Button onClick={props.closeModal}>Cancelar</Button>\n          <Button onClick={() => props.create(name)} disabled={!name}>\n            {props.category ? \"Actualizar\" : \"Crear\"}\n          </Button>\n        </DialogActions>\n      </Dialog>\n    </div>\n  );\n}\n

    Para este componente hemos definido una categor\u00eda como par\u00e1metro de entrada para poder reutilizar el componente en el caso de una edici\u00f3n y poder pasar la categor\u00eda a editar, en nuestro caso inicial al ser un alta esta categor\u00eda tendr\u00e1 el valor null. Tambi\u00e9n hemos definido dos funciones en los par\u00e1metros de entrada para controlar o bien el cerrado del modal o bien la creaci\u00f3n de un autor.

    Esta es la forma directa que tienen de comunicaci\u00f3n los componentes padre/hijo en React, el padre puede pasar datos de lectura o funciones a sus componentes hijos a las que estos llamaran para comunicarse con \u00e9l.

    As\u00ed en nuestro ejemplo el componente CreateCategory llamar\u00e1 a la funci\u00f3n create a la que pasar\u00e1 un nuevo objecto Category y ser\u00e1 el padre (nuestra p\u00e1gina Category) el que decidir\u00e1 qu\u00e9 hacer con esos datos al igual que ocurre con los eventos en los tags de html.

    En el estado de nuestro componente solo vamos a almacenar los datos introducidos en el formulario, en el caso de una edici\u00f3n el valor inicial del nombre de la categor\u00eda ser\u00e1 el que venga de entrada.

    Adem\u00e1s introducido unas expresiones que modificar\u00e1n la visualizaci\u00f3n del componente (titulo, id, texto de los botones, \u2026) dependiendo de si tenemos un autor de entrada o no.

    Ahora tenemos que a\u00f1adir nuestro nuevo componente en nuestra p\u00e1gina Category:

    Importamos el componente:

    import CreateCategory from \"./components/CreateCategory\";\n

    Creamos las funciones que le pasaremos al componente, dej\u00e1ndolas de momento vac\u00edas:

    const createCategory = () => {\n\n  }\n\n  const handleCloseCreate = () => {\n\n  }\n

    Y a\u00f1adimos en el c\u00f3digo JSX lo siguiente tras nuestro button:

          <CreateCategory\n          create={createCategory}\n          category={null}\n          closeModal={handleCloseCreate}\n        />\n

    Si ahora vamos a nuestro navegador, a la p\u00e1gina de categor\u00edas, podremos ver el formulario para crear una categor\u00eda, pero \u00e9sta fijo y no hay manera de cerrarlo. Vamos a cambiar este comportamiento mediante una variable booleana en el estado del componente que decidir\u00e1 cuando se muestra este. Adem\u00e1s, a\u00f1adiremos a nuestro bot\u00f3n el c\u00f3digo necesario para mostrar el componente y a\u00f1adiremos a la funci\u00f3n handleCloseCreate el c\u00f3digo para ocultarlo.

    A\u00f1adimos un nuevo estado:

    const [openCreate, setOpenCreate] = useState(false);\n

    Modificamos la function handleCloseCreate:

    const handleCloseCreate = () => {\n    setOpenCreate(false);\n  };\n

    Y por \u00faltimo modificamos el c\u00f3digo del return de la siguiente manera:

          <div className=\"newButton\">\n        <Button variant=\"contained\" onClick={() => setOpenCreate(true)}>\n          Nueva categor\u00eda\n        </Button>\n      </div>\n      {openCreate && (\n        <CreateCategory\n          create={createCategory}\n          category={null}\n          closeModal={handleCloseCreate}\n        />\n      )}\n

    Si probamos ahora vemos que ya se realiza la funcionalidad de abrir y cerrar nuestro formulario de manera correcta.

    Ahora vamos a a\u00f1adir la funcionalidad para que al pulsar el bot\u00f3n de edici\u00f3n pasemos la categor\u00eda a editar a nuestro formulario. Para esto vamos a necesitar una nueva variable en nuestro estado donde almacenaremos la categor\u00eda a editar:

    const [categoryToUpdate, setCategoryToUpdate] =\n    useState<CategoryModel | null>(null);\n

    Modificamos el c\u00f3digo de nuestro bot\u00f3n:

                        <IconButton\n                      aria-label=\"update\"\n                      color=\"primary\"\n                      onClick={() => {\n                        setCategoryToUpdate(category);\n                        setOpenCreate(true);\n                      }}\n                    >\n                      <EditIcon />\n                    </IconButton>\n

    Y la entrada a nuestro componente:

          {openCreate && (\n        <CreateCategory\n          create={createCategory}\n          category={categoryToUpdate}\n          closeModal={handleCloseCreate}\n        />\n      )}\n

    Si ahora hacemos una prueba en nuestro navegador y pulsamos el bot\u00f3n de editar vemos como nuestro formulario ya se carga correctamente pero hay un problema, si pulsamos el bot\u00f3n de editar, cerramos el formulario y le damos al boton de nueva categor\u00eda vemos que el formulario mantiene los datos anteriores. Vamos a solucionar este problema volviendo a dejar vacia la variable categoryToUpdate cuando se cierre el componente:

    Modificamos la funci\u00f3n handleCloseCreate:

    const handleCloseCreate = () => {\n    setOpenCreate(false);\n    setCategoryToUpdate(null);\n  };\n

    Y vemos que el funcionamiento ya es el correcto.

    Ahora vamos a darle funcionalidad al bot\u00f3n de borrado. Cuando pulsemos sobre este bot\u00f3n se nos debe mostrar un mensaje de alerta para confirmar nuestra decisi\u00f3n. Como este es un mensaje que vamos a reutilizar en el resto de la aplicaci\u00f3n vamos a crear un componente en la carpeta src/components llamado ConfirmDialog.tsx con el siguiente contenido:

    import Button from \"@mui/material/Button\";\nimport DialogContentText from \"@mui/material/DialogContentText\";\nimport Dialog from \"@mui/material/Dialog\";\nimport DialogActions from \"@mui/material/DialogActions\";\nimport DialogContent from \"@mui/material/DialogContent\";\nimport DialogTitle from \"@mui/material/DialogTitle\";\n\ninterface Props {\n  closeModal: () => void;\n  confirm: () => void;\n  title: string;\n  text: string;\n}\n\nexport const ConfirmDialog = (props: Props) => {\n  return (\n    <div>\n      <Dialog open={true} onClose={props.closeModal}>\n        <DialogTitle>{props.title}</DialogTitle>\n        <DialogContent>\n          <DialogContentText>{props.text}</DialogContentText>\n        </DialogContent>\n        <DialogActions>\n          <Button onClick={props.closeModal}>Cancelar</Button>\n          <Button onClick={() => props.confirm()}>Confirmar</Button>\n        </DialogActions>\n      </Dialog>\n    </div>\n  );\n};\n

    Y vamos a a\u00f1adirlo a nuestra p\u00e1gina de categorias, pero al igual que paso con nuestro formulario de altas no queremos que este componente se muestre siempre, sino que estar\u00e1 condicionado al valor de una nueva variable en nuestro estado. En este caso vamos a almacenar el id de la categor\u00eda a borrar.

    Importamos nuestro nuevo componente:

    import { ConfirmDialog } from \"../../components/ConfirmDialog\";\n

    Creamos una nueva variable en el estado:

    const [idToDelete, setIdToDelete] = useState(\"\");\n

    Creamos una nueva funci\u00f3n:

    const deleteCategory = () => {};\n

    Modificamos el bot\u00f3n de borrado:

                          <IconButton\n                        aria-label=\"delete\"\n                        color=\"error\"\n                        onClick={() => {\n                          setIdToDelete(category.id);\n                        }}\n                      >\n                        <ClearIcon />\n                      </IconButton>\n

    Y a\u00f1adimos el c\u00f3digo necesario en nuestro return para incluir el nuevo componente:

          {!!idToDelete && (\n        <ConfirmDialog\n          title=\"Eliminar categor\u00eda\"\n          text=\"Atenci\u00f3n si borra la categor\u00eda se perder\u00e1n sus datos. \u00bfDesea eliminar la categor\u00eda?\"\n          confirm={deleteCategory}\n          closeModal={() => setIdToDelete('')}\n        />\n      )}\n
    "},{"location":"develop/basic/react/#recuperando-datos","title":"Recuperando datos","text":"

    Ya estamos preparados para llamar a nuestro back. Hay muchas maneras de recuperar datos del back en React. Si no queremos usar ninguna librer\u00eda externa podemos hacer uso del m\u00e9todo fetch, pero tendr\u00edamos que repetir mucho c\u00f3digo o bien construir interceptores, para el manejo de errores, construcci\u00f3n de middlewares,\u2026 adem\u00e1s, no es lo mas utilizado. Hoy en d\u00eda se opta por librer\u00edas como Axios o Redux Toolkit query que facilitan el uso de este m\u00e9todo.

    Nosotros vamos a utilizar una herramienta de redux llamada Redux Toolkit Query, pero primero vamos a explicar que es redux.

    "},{"location":"develop/basic/react/#redux","title":"Redux","text":"

    Redux es una librer\u00eda que implementa el patr\u00f3n de dise\u00f1o Flux y que nos permite crear un estado global.

    Nuestros componentes pueden realizar acciones asociadas a un reducer que modificar\u00e1n este estado global llamado generalmente store y a su vez estar\u00e1n subscritos a variables de este estado para estar atentos a posibles cambios.

    Antes se sol\u00edan construir ficheros de actions, de reducers y un fichero de store, pero con redux toolkit se ha simplificado todo. Por un lado, podemos tener slices, que son ficheros que agrupan acciones, reducers y parte del estado y por otro lado podemos tener servicios donde declaramos llamadas a nuestra api y redux guarda las llamadas en nuestro estado global para que sean accesibles desde cualquier parte de nuestra aplicaci\u00f3n.

    Vamos a crear una carpeta llamada redux dentro de la carpeta src y a su vez dentro de src/redux vamos a crear dos carpetas: features donde crearemos nuestros slices y services donde crearemos las llamadas al api.

    Dentro de la carpeta services vamos a crear un fichero llamado ludotecaApi.ts con el siguiente contenido:

    import { createApi, fetchBaseQuery } from \"@reduxjs/toolkit/query/react\";\nimport { Category } from \"../../types/Category\";\n\nexport const ludotecaAPI = createApi({\n  reducerPath: \"ludotecaApi\",\n  baseQuery: fetchBaseQuery({\n    baseUrl: \"http://localhost:8080\",\n  }),\n  tagTypes: [\"Category\"],\n  endpoints: (builder) => ({\n    getCategories: builder.query<Category[], null>({\n      query: () => \"category\",\n      providesTags: [\"Category\"],\n    }),\n    createCategory: builder.mutation({\n      query: (payload) => ({\n        url: \"/category\",\n        method: \"PUT\",\n        body: payload,\n        headers: {\n          \"Content-type\": \"application/json; charset=UTF-8\",\n        },\n      }),\n      invalidatesTags: [\"Category\"],\n    }),\n    deleteCategory: builder.mutation({\n      query: (id: string) => ({\n        url: `/category/${id}`,\n        method: \"DELETE\",\n      }),\n      invalidatesTags: [\"Category\"],\n    }),\n    updateCategory: builder.mutation({\n      query: (payload: Category) => ({\n        url: `category/${payload.id}`,\n        method: \"PUT\",\n        body: payload,\n      }),\n      invalidatesTags: [\"Category\"],\n    }),\n  }),\n});\n\nexport const {\n    useGetCategoriesQuery,\n    useCreateCategoryMutation,\n    useDeleteCategoryMutation,\n    useUpdateCategoryMutation\n} = ludotecaAPI;\n

    Con esto ya habr\u00edamos creado las acciones que llaman al back y almacenan el resultado en nuestro estado. Para configurar nuestra api le tenemos que dar un nombre, una url base, una series de tags y nuestros endpoints que pueden ser de tipo query para realizar consultas o mutation. Tambi\u00e9n exportamos los hooks que nos van a permitir hacer uso de estos endpoints. Si los endpoints los creamos de tipo query, cuando hacemos uso de estos hooks se realizar\u00e1 una consulta al back y recibiremos los datos de la consulta en nuestros par\u00e1metros del hook entre otras cosas. Si los creamos de tipo mutation lo que nos devolver\u00e1 el hook ser\u00e1 la acci\u00f3n que tenemos que llamar para realizar esta llamada.

    Los tags sirven para cachear el resultado, pero cuando llamamos a una mutation y pasamos informaci\u00f3n en invalidateTags, esto va a hacer que se vuelva a lanzar la query afectada por estos tags para actualizar su resultado, por eso hemos a\u00f1adido el providesTags en la query, para que desde nuestras p\u00e1ginas usemos los hooks exportados.

    Ahora vamos a crear dentro de la carpeta src/redux un fichero llamado store.ts con el siguiente contenido:

    import { configureStore } from \"@reduxjs/toolkit\";\nimport { setupListeners } from \"@reduxjs/toolkit/dist/query\";\nimport { ludotecaAPI } from \"./services/ludotecaApi\";\n\nexport const store = configureStore({\n  reducer: {\n    [ludotecaAPI.reducerPath]: ludotecaAPI.reducer,\n  },\n  middleware: (getDefaultMiddleware) =>\n    getDefaultMiddleware().concat([ludotecaAPI.middleware]),\n});\n\nsetupListeners(store.dispatch);\n\n// Infer the `RootState` and `AppDispatch` types from the store itself\nexport type RootState = ReturnType<typeof store.getState>;\n// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}\nexport type AppDispatch = typeof store.dispatch;\n

    Aqu\u00ed b\u00e1sicamente creamos el store con nuestro reducer. Cabe destacar que podemos crear tantos reducers como queramos siempre que les demos distintos nombres.

    Ahora ya podr\u00edamos hacer uso de los hooks que vienen con redux llamados useDispatch para llamar a nuestras actions y useSelect para suscribirnos a los cambios en el estado, pero como estamos usando typescript tendr\u00edamos que tipar todos estos m\u00e9todos y variables que usamos en todos nuestros componentes resultando un c\u00f3digo un tanto sucio y repetitivo. Tambi\u00e9n podemos simplemente ignorar a typescript y deshabilitar las reglas para estos ficheros, pero vamos a hacerlo bien.

    Vamos a crear un fichero llamado hooks.ts dentro de la carpeta de redux y su contenido ser\u00e1 el siguiente:

    import {  useDispatch, useSelector } from 'react-redux'\nimport type { TypedUseSelectorHook } from 'react-redux'\nimport type { RootState, AppDispatch } from './store'\n\n// Use throughout your app instead of plain `useDispatch` and `useSelector`\ntype DispatchFunc = () => AppDispatch\nexport const useAppDispatch: DispatchFunc = useDispatch\nexport const useAppSelector: TypedUseSelectorHook<RootState> = useSelector\n

    Estos ser\u00e1n los m\u00e9todos que usaremos en lugar de useDispatch y useSelector.

    Ahora vamos a modificar nuestro fichero App.tsx a\u00f1adiendo los imports necesarios y rodeando nuestro c\u00f3digo con el tag provider:

    import { Provider } from \"react-redux\";\n\nimport { store } from \"./redux/store\";\n\n    <Provider store={store}>\n      <BrowserRouter>\n        <Routes>\n          <Route element={<Layout />}>\n            <Route index path=\"games\" element={<Game />} />\n            <Route path=\"categories\" element={<Category />} />\n            <Route path=\"authors\" element={<Author />} />\n            <Route path=\"*\" element={<Navigate to=\"/games\" />} />\n          </Route>\n        </Routes>\n      </BrowserRouter>\n    </Provider>\n

    Ahora ya podemos hacer uso de los m\u00e9todos de redux para modificar y leer el estado global de nuestra aplicaci\u00f3n.

    Volvemos a nuestro componente Category y vamos a importar los hooks de nuestra api para hacer uso de ellos:

    import { useAppDispatch } from \"../../redux/hooks\";\nimport {\n  useCreateCategoryMutation,\n  useDeleteCategoryMutation,\n  useGetCategoriesQuery,\n  useUpdateCategoryMutation,\n} from \"../../redux/services/ludotecaApi\";\n

    Eliminamos la variable mockeada data y a\u00f1adimos en su lugar lo siguiente:

    const dispatch = useAppDispatch();\n  const { data, error, isLoading } = useGetCategoriesQuery(null);\n\n  const [\n    deleteCategoryApi,\n    { isLoading: isLoadingDelete, error: errorDelete },\n  ] = useDeleteCategoryMutation();\n  const [createCategoryApi, { isLoading: isLoadingCreate }] =\n    useCreateCategoryMutation();\n\n  const [updateCategoryApi, { isLoading: isLoadingUpdate }] =\n    useUpdateCategoryMutation();\n

    Como ya hemos dicho anteriormente, los hooks de la api de tipo query nos devolver\u00e1n datos mientras que los hooks de tipo mutation nos devuelven acciones que podemos lanzar con el m\u00e9todo dispatch. El resto de los par\u00e1metros nos dan informaci\u00f3n para saber el estado de la llamada, por ejemplo, para saber si esta cargando, si se ha producido un error, etc\u2026

    Tenemos que modificar el c\u00f3digo que recorre data ya que este valor ahora puede estar sin definir:

            <TableBody>\n            {data &&\n              data.map((category: CategoryModel) => (\n

    Y ahora si tenemos datos en la base de datos y vamos a nuestro navegador podemos ver que ya se est\u00e1n representando estos datos en la tabla de categor\u00edas.

    Modificamos el m\u00e9todo createCategory:

    const createCategory = (category: string) => {\n    setOpenCreate(false);\n    if (categoryToUpdate) {\n      updateCategoryApi({ id: categoryToUpdate.id, name: category })\n        .then(() => {\n          setCategoryToUpdate(null);\n        })\n        .catch((err) => console.log(err));\n    } else {\n      createCategoryApi({ name: category })\n        .then(() => {\n          setCategoryToUpdate(null);\n        })\n        .catch((err) => console.log(err));\n    }\n  };\n

    Si tenemos almacenada alguna categor\u00eda para actualizar llamaremos a la acci\u00f3n para actualizar la categor\u00eda que recuperamos del hook y si no tenemos categor\u00eda almacenada llamaremos al m\u00e9todo para crear una categor\u00eda nueva. Estos m\u00e9todos nos devuelven una promesa que cuando resolvemos volvemos a poner el valor de la categor\u00eda a actualizar a null.

    Implementamos el m\u00e9todo para borrar categor\u00edas:

    const deleteCategory = () => {\n    deleteCategoryApi(idToDelete)\n      .then(() => \n      setIdToDelete(''))\n      .catch((err) => console.log(err));\n  };\n

    Ahora si probamos en nuestro navegador ya podremos realizar todas las funciones de la p\u00e1gina: listar, crear, actualizar y borrar, pero aun vamos a darle m\u00e1s funcionalidad.

    Vamos a crear una variable en el estado global de nuestra aplicaci\u00f3n para mostrar alertas de informaci\u00f3n o de error. Para ello creamos un nuevo fichero en la carpeta src/redux/features llamado messageSlice.ts cuyo contenido ser\u00e1 el siguiente:

    import { createSlice } from '@reduxjs/toolkit'\nimport type {PayloadAction} from \"@reduxjs/toolkit\"\n\nexport const messageSlice = createSlice({\n  name: 'message',\n  initialState: {\n    text: '',\n    type: ''\n  },\n  reducers: {\n    deleteMessage: (state) => {\n        state.text = ''\n        state.type = ''\n    },\n    setMessage: (state, action : PayloadAction<{text: string; type: string}>) => {\n        state.text = action.payload.text;\n        state.type = action.payload.type;\n    },\n  },\n})\n\nexport const { deleteMessage, setMessage } = messageSlice.actions;\nexport default messageSlice.reducer;\n

    Como ya hemos dicho anteriormente los slices son un concepto introducido en Redux Toolkit y no es ni m\u00e1s ni menos que un fichero que agrupa reducers, actions y selectors.

    En este fichero declaramos el nombre del selector (message) para despu\u00e9s poder recuperar los datos en un componente, declaramos el estado inicial de nuestro slice, creamos las funciones de los reducers y declaramos dos acciones.

    Los reducers son funciones puras que modifican el estado, en nuestro caso utilizamos un reducer para resetear el estado y otro para setear el texto y el tipo de mensaje. Con Redux Toolkit podemos acceder directamente al estado dentro de nuestros reducers. En los reducers que no usan esta herramienta lo que se hace es devolver un objeto que ser\u00e1 el nuevo estado.

    Las acciones son las que invocan a los reducers. Estas solo dicen que hacer, pero no como hacerlo. Con Redux Toolkit las acciones se generan autom\u00e1ticamente y solo tenemos que hacer un destructuring del objecto actions de nuestro slice para recuperarlas y exportarlas.

    Ahora vamos a modificar el fichero src/redux/store.ts para a\u00f1adir el nuevo reducer:

    import { configureStore } from \"@reduxjs/toolkit\";\nimport { setupListeners } from \"@reduxjs/toolkit/dist/query\";\nimport { ludotecaAPI } from \"./services/ludotecaApi\";\nimport messageReducer from \"./features/messageSlice\";\n\nexport const store = configureStore({\n  reducer: {\n    messageReducer,\n    [ludotecaAPI.reducerPath]: ludotecaAPI.reducer,\n  },\n  middleware: (getDefaultMiddleware) =>\n    getDefaultMiddleware().concat([ludotecaAPI.middleware]),\n});\n\nsetupListeners(store.dispatch);\n\n// Infer the `RootState` and `AppDispatch` types from the store itself\nexport type RootState = ReturnType<typeof store.getState>;\n// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}\nexport type AppDispatch = typeof store.dispatch;\n

    Con esto ya podemos hacer uso de esta funcionalidad. Vamos a modificar el componente Layout para que pueda recibir mensajes y mostrarlos por pantalla.

    import Alert from \"@mui/material/Alert\";\nimport { useAppDispatch, useAppSelector } from \"../redux/hooks\";\nimport { deleteMessage } from \"../redux/features/messageSlice\";\n\n  const dispatch = useAppDispatch();\n  const { text, type } = useAppSelector((state) => state.messageReducer);\n\n  useEffect(() => {\n    setTimeout(() => {\n      dispatch(deleteMessage());\n    }, 3000);\n  }, [text, type]);\n\n      {text && (\n        <Alert severity={type === \"error\" ? \"error\" : \"success\"}>{text}</Alert>\n      )}\n

    Hemos a\u00f1adido c\u00f3digo para que el componente layout este subscrito a las variables text y type de nuestro contexto global. Si tenemos text se mostrar\u00e1 la alerta y adem\u00e1s hemos incluido un nuevo hook useEffect gracias al cual cuando el componente reciba un text llamar\u00e1 a una funci\u00f3n que pasados 3 segundos borrar\u00e1 el mensaje de nuestro estado ocultando as\u00ed el Alert.

    Pero antes de seguir adelante vamos a explicar que hace useEffect exactamente ya que es un hook de React muy utilizado.

    "},{"location":"develop/basic/react/#useeffect","title":"useEffect","text":"

    El ciclo de vida de los componentes en React permit\u00eda en los componentes de tipo clase poder ejecutar c\u00f3digo en diferentes fases de montaje, actualizaci\u00f3n y desmontaje. De esta forma, pod\u00edamos a\u00f1adir cierta funcionalidad en las distintas etapas de nuestro componente.

    Con los hooks tambi\u00e9n podremos acceder a ese ciclo de vida en nuestros componentes funcionales, aunque de una forma m\u00e1s clara y sencilla. Para ello usaremos useEffect, un hook que recibe como par\u00e1metro una funci\u00f3n que se ejecutar\u00e1 cada vez que se modifique el valor de las las dependencias que pasemos como segundo par\u00e1metro.

    Hay otros casos especiales de useEffect, por ejemplo, si hubi\u00e9semos dejado el array de dependencias de useEffect vac\u00edo, solo se llamar\u00eda a la funci\u00f3n la primera vez que se renderiza el componente.

    useEffect(() => {\nconsole.log(\u2018Solo me muestro en el primer render\u2019);\n  }, []);\n

    Y si queremos que solo se llame a la funci\u00f3n cuando se desmonta el componente lo que tenemos que hacer es devolver de useEffect una funci\u00f3n con el c\u00f3digo que queremos que se ejecute una vez que se desmonte:

    useEffect(() => {\nreturn () => {\n        console.log(\u2018Me desmonto!!\u2019)\n}\n  }, []);\n

    Dentro de la carpeta src/types vamos a crear un fichero llamado appTypes.ts que contendr\u00e1 todos aquellos tipos o interfaces auxiliares para construir nuestra aplicaci\u00f3n:

    export interface BackError {\n  msg: string;\n}\n

    Ahora ya podemos incluir en nuestra p\u00e1gina de categor\u00edas el c\u00f3digo para guardar los mensajes de informaci\u00f3n y error en el estado global, importamos lo necesario:

    import { setMessage } from \"../../redux/features/messageSlice\";\nimport { BackError } from \"../../types/appTypes\";\n

    A\u00f1adimos:

    useEffect(() => {\n    if (errorDelete) {\n      if (\"status\" in errorDelete) {\n        dispatch(\n          setMessage({\n            text: (errorDelete?.data as BackError).msg,\n            type: \"error\",\n          })\n        );\n      }\n    }\n  }, [errorDelete, dispatch]);\n\n  useEffect(() => {\n    if (error) {\n      dispatch(setMessage({ text: \"Se ha producido un error\", type: \"error\" }));\n    }\n  }, [error]);\n

    Y modificamos:

    const createCategory = (category: string) => {\n    setOpenCreate(false);\n    if (categoryToUpdate) {\n      updateCategoryApi({ id: categoryToUpdate.id, name: category })\n        .then(() => {\n          dispatch(\n            setMessage({\n              text: \"Categor\u00eda actualizada correctamente\",\n              type: \"ok\",\n            })\n          );\n          setCategoryToUpdate(null);\n        })\n        .catch((err) => console.log(err));\n    } else {\n      createCategoryApi({ name: category })\n        .then(() => {\n          dispatch(\n            setMessage({ text: \"Categor\u00eda creada correctamente\", type: \"ok\" })\n          );\n          setCategoryToUpdate(null);\n        })\n        .catch((err) => console.log(err));\n    }\n  };\n\n  const deleteCategory = () => {\n    deleteCategoryApi(idToDelete)\n      .then(() => {\n        dispatch(\n          setMessage({\n            text: \"Categor\u00eda borrada correctamente\",\n            type: \"ok\",\n          })\n        );\n        setIdToDelete(\"\");\n      })\n      .catch((err) => console.log(err));\n  };\n

    Si ahora probamos nuestra aplicaci\u00f3n al borrar, actualizar o crear una categor\u00eda nos deber\u00eda de mostrar un mensaje de informaci\u00f3n.

    Ya casi estamos terminando con nuestra p\u00e1gina de categor\u00edas, pero vamos a a\u00f1adir tambi\u00e9n un loader para cuando nuestra acciones est\u00e9n en estado de loading. Para esto vamos a hacer uso de otra de las maneras que tiene React de almacenar informaci\u00f3n global, el contexto.

    "},{"location":"develop/basic/react/#context-api","title":"Context API","text":"

    Una de las caracter\u00edsticas que llegaron en las \u00faltimas versiones de React fue el contexto, una forma de pasar datos que pueden considerarse globales a un \u00e1rbol de componentes sin la necesidad de utilizar Redux. El uso de contextos mediante la Context API es una soluci\u00f3n m\u00e1s ligera y sencilla que redux y que no est\u00e1 mal para aplicaciones que no son excesivamente grandes.

    En general cuando queramos usar estados globales que no sean demasiado grandes y no se haga demasiada escritura sobre ellos ser\u00e1 preferible usar Context API en lugar de redux.

    Vamos a crear un contexto para utilizar un loader en nuestra aplicaci\u00f3n.

    Lo primero ser\u00e1 crear una carpeta llamada context dentro de la carpeta src de nuestro proyecto y dentro de esta crearemos un nuevo fichero llamado LoaderProvider.tsx con el siguiente contenido:

    import { createContext, useState } from \"react\";\nimport Backdrop from \"@mui/material/Backdrop\";\nimport CircularProgress from \"@mui/material/CircularProgress\";\n\nexport const LoaderContext = createContext({\n  loading: false,\n  showLoading: (_show: boolean) => {},\n});\n\ntype Props = {\n  children: JSX.Element;\n};\n\nexport const LoaderProvider = ({ children }: Props) => {\n  const showLoading = (show: boolean) => {\n    setState((prev) => ({\n      ...prev,\n      loading: show,\n    }));\n  };\n\n  const [state, setState] = useState({\n    loading: false,\n    showLoading,\n  });\n\n  return (\n    <LoaderContext.Provider value={state}>\n      <Backdrop\n        sx={{ color: \"#fff\", zIndex: (theme) => theme.zIndex.drawer + 1 }}\n        open={state.loading}\n      >\n        <CircularProgress color=\"inherit\" />\n      </Backdrop>\n\n      {children}\n    </LoaderContext.Provider>\n  );\n};\n

    Y ahora modificamos nuestro fichero App.tsx de la siguiente manera:

    import { LoaderProvider } from \"./context/LoaderProvider\";\n    <LoaderProvider>\n      <Provider store={store}>\n        <BrowserRouter>\n          <Routes>\n            <Route element={<Layout />}>\n              <Route index path=\"games\" element={<Game />} />\n              <Route path=\"categories\" element={<Category />} />\n              <Route path=\"authors\" element={<Author />} />\n              <Route path=\"*\" element={<Navigate to=\"/games\" />} />\n            </Route>\n          </Routes>\n        </BrowserRouter>\n      </Provider>\n    </LoaderProvider>\n

    Lo que hemos hecho ha sido envolver toda nuestra aplicaci\u00f3n dentro de nuestro provider de tal modo que esta el children en el fichero LoaderProvider, pero ahora y gracias a la funcionalidad de createContext la variable loading y el m\u00e9todo showLoading estar\u00e1n disponibles en todos los sitios de nuestra aplicaci\u00f3n.

    Ahora para hacer uso de esta funcionalidad nos vamos a nuestra pagina de Categorias e importamos lo siguiente:

    import { useState, useEffect, useContext  } from \"react\";\n\nimport { LoaderContext } from \"../../context/LoaderProvider\";\n

    Declaramos una nueva constante:

    const loader = useContext(LoaderContext);\n

    Podemos hace uso del m\u00e9todo showLoading donde queramos, en nuestro caso vamos a crear otro useEffect que estar\u00e1 pendiente de los cambios en cualquiera de los loadings:

    useEffect(() => {\n    loader.showLoading(\n      isLoadingCreate || isLoading || isLoadingDelete || isLoadingUpdate\n    );\n  }, [isLoadingCreate, isLoading, isLoadingDelete, isLoadingUpdate]);\n

    Probamos la aplicaci\u00f3n y vemos que cuando se carga el listado o realizamos cualquier llamada al back se muestra brevemente nuestro loader.

    "},{"location":"develop/basic/springboot/","title":"Listado simple - Spring Boot","text":"

    Ahora que ya tenemos listo el proyecto backend de Spring Boot (en el puerto 8080) ya podemos empezar a codificar la soluci\u00f3n.

    "},{"location":"develop/basic/springboot/#primeros-pasos","title":"Primeros pasos","text":"

    Antes de empezar

    Quiero hacer hincapi\u00e9 en Spring Boot tiene una documentaci\u00f3n muy extensa y completa, as\u00ed que te recomiendo que hagas uso de ella cuando tengas cualquier duda. Tanto la propia web de Spring como en el portal de tutoriales de Baeldung puedes buscar casi cualquier ejemplo que necesites.

    "},{"location":"develop/basic/springboot/#estructurar-el-codigo","title":"Estructurar el c\u00f3digo","text":"

    Vamos a hacer un breve refresco de la estructura del c\u00f3digo que ya se ha visto en puntos anteriores.

    Las clases deben estar agrupadas por \u00e1mbito funcional, en nuestro caso como vamos a hacer la funcionalidad de Categor\u00edas pues deber\u00eda estar todo dentro de un package del tipo com.ccsw.tutorial.category.

    Adem\u00e1s, deber\u00edamos aplicar la separaci\u00f3n por capas como ya se vi\u00f3 en el esquema:

    La primera capa, la de Controlador, se encargar\u00e1 de procesar las peticiones y transformar datos. Esta capa llamar\u00e1 a la capa de L\u00f3gica de negocio que ejecutar\u00e1 las operaciones, ayud\u00e1ndose de otros objetos de esa misma capa de L\u00f3gica o bien de llamadas a datos a trav\u00e9s de la capa de Acceso a Datos

    Ahora s\u00ed, vamos a programar!.

    "},{"location":"develop/basic/springboot/#capa-de-operaciones-controller","title":"Capa de operaciones: Controller","text":"

    En esta capa es donde se definen las operaciones que pueden ser consumidas por los clientes. Se caracterizan por estar anotadas con las anotaciones @Controller o @RestController y por las anotaciones @RequestMapping que nos permiten definir las rutas de acceso.

    Recomendaci\u00f3n: Breve detalle REST

    Antes de continuar te recomiendo encarecidamente que leas el Anexo: Detalle REST donde se explica brevemente como estructurar los servicios REST que veremos a continuaci\u00f3n.

    "},{"location":"develop/basic/springboot/#controller-de-ejemplo","title":"Controller de ejemplo","text":"

    Vamos a crear una clase CategoryController.java dentro del package com.ccsw.tutorial.category para definir las rutas de las operaciones.

    CategoryController.java
    package com.ccsw.tutorial.category;\n\nimport java.util.List;\n\nimport org.springframework.web.bind.annotation.CrossOrigin;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestMethod;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\n/**\n * @author ccsw\n * \n */\n@Tag(name = \"Category\", description = \"API of Category\")\n@RequestMapping(value = \"/category\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class CategoryController {\n\n    /**\n     * M\u00e9todo para probar el servicio\n     * \n     */\n    @RequestMapping(path = \"\", method = RequestMethod.GET)\n    public String prueba() {\n\n        return \"Probando el Controller\";\n    }\n\n}\n

    Ahora si arrancamos la aplicaci\u00f3n server, abrimos el Postman y creamos una petici\u00f3n GET a la url http://localhost:8080/category nos responder\u00e1 con el mensaje que hemos programado.

    "},{"location":"develop/basic/springboot/#implementar-operaciones","title":"Implementar operaciones","text":"

    Ahora que ya tenemos un controlador y una operaci\u00f3n de negocio ficticia, vamos a borrarla y a\u00f1adir las operaciones reales que consumir\u00e1 nuestra pantalla. Deberemos a\u00f1adir una operaci\u00f3n para listar, una para actualizar, una para guardar y una para borrar. Aunque para hacerlo m\u00e1s c\u00f3modo, utilizaremos la misma operaci\u00f3n para guardar y para actualizar. Adem\u00e1s, no vamos a trabajar directamente con datos simples, sino que usaremos objetos para recibir informaci\u00f3n y para enviar informaci\u00f3n.

    Estos objetos t\u00edpicamente se denominan DTO (Data Transfer Object) y nos sirven justamente para encapsular informaci\u00f3n que queremos transportar. En realidad no son m\u00e1s que clases pojo sencillas con propiedades, getters y setters.

    Para nuestro ejemplo crearemos una clase CategoryDto dentro del package com.ccsw.tutorial.category.model con el siguiente contenido:

    CategoryDto.java
    package com.ccsw.tutorial.category.model;\n\n/**\n * @author ccsw\n * \n */\npublic class CategoryDto {\n\n    private Long id;\n\n    private String name;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return name\n     */\n    public String getName() {\n\n        return this.name;\n    }\n\n    /**\n     * @param name new value of {@link #getName}.\n     */\n    public void setName(String name) {\n\n        this.name = name;\n    }\n\n}\n

    A continuaci\u00f3n utilizaremos esta clase en nuestro Controller para implementar las tres operaciones de negocio.

    CategoryController.java
    package com.ccsw.tutorial.category;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport org.springframework.web.bind.annotation.CrossOrigin;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestMethod;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport com.ccsw.tutorial.category.model.CategoryDto;\n\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\n/**\n * @author ccsw\n * \n */\n@Tag(name = \"Category\", description = \"API of Category\")\n@RequestMapping(value = \"/category\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class CategoryController {\n\n    private long SEQUENCE = 1;\n    private Map<Long, CategoryDto> categories = new HashMap<Long, CategoryDto>();\n\n    /**\n     * M\u00e9todo para recuperar todas las categorias\n     *\n     * @return {@link List} de {@link CategoryDto}\n     */\n    @Operation(summary = \"Find\", description = \"Method that return a list of Categories\")\n    @RequestMapping(path = \"\", method = RequestMethod.GET)\n    public List<CategoryDto> findAll() {\n\n        return new ArrayList<CategoryDto>(this.categories.values());\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar una categoria\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Category\")\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody CategoryDto dto) {\n\n        CategoryDto category;\n\n        if (id == null) {\n            category = new CategoryDto();\n            category.setId(this.SEQUENCE++);\n            this.categories.put(category.getId(), category);\n        } else {\n            category = this.categories.get(id);\n        }\n\n        category.setName(dto.getName());\n    }\n\n    /**\n     * M\u00e9todo para borrar una categoria\n     *\n     * @param id PK de la entidad\n     */\n    @Operation(summary = \"Delete\", description = \"Method that deletes a Category\")\n    @RequestMapping(path = \"/{id}\", method = RequestMethod.DELETE)\n    public void delete(@PathVariable(\"id\") Long id) {\n\n        this.categories.remove(id);\n    }\n}\n

    Como todav\u00eda no tenemos acceso a BD, hemos creado una variable tipo HashMap y una variable Long, que simular\u00e1n una BD y una secuencia. Tambi\u00e9n hemos implementado tres operaciones GET, PUT y DELETE que realizan las acciones necesarias por nuestra pantalla. Ahora podr\u00edamos probarlo desde el Postman con cuatro ejemplo sencillos.

    F\u00edjate que el m\u00e9todo save tiene dos rutas. La ruta normal category/ y la ruta informada category/1. Esto es porque hemos juntado la acci\u00f3n create y update en un mismo m\u00e9todo para facilitar el desarrollo. Es totalmente v\u00e1lido y funcional.

    Atenci\u00f3n

    Los datos que se reciben pueden venir informados como un par\u00e1metro en la URL Get, como una variable en el propio path o dentro del body de la petici\u00f3n. Cada uno de ellos se recupera con una anotaci\u00f3n especial: @RequestParam, @PathVariable y @RequestBody respectivamente.

    Como no tenemos ning\u00fan dato dado de alta, podemos probar en primer lugar a realizar una inserci\u00f3n de datos con el m\u00e9todo PUT.

    PUT /category nos sirve para insertar Categor\u00edas nuevas (si no tienen el id informado) o para actualizar Categor\u00edas (si tienen el id informado). F\u00edjate que los datos que se env\u00edan est\u00e1n en el body como formato JSON (parte izquierda de la imagen). Si no env\u00edas datos, te dar\u00e1 un error.

    GET /category nos devuelve un listado de Categor\u00edas, siempre que hayamos insertado algo antes.

    DELETE /category nos sirve eliminar Categor\u00edas. F\u00edjate que el dato del ID que se env\u00eda est\u00e1 en el path.

    Prueba a jugar borrando categor\u00edas que no existen o modificando categor\u00edas que no existen. Tal y como est\u00e1 programado, el borrado no dar\u00e1 error, pero la modificaci\u00f3n deber\u00eda dar un NullPointerException al no existir el dato a modificar.

    "},{"location":"develop/basic/springboot/#documentacion-openapi","title":"Documentaci\u00f3n (OpenAPI)","text":"

    Si te acuerdas, en el punto de Entorno de desarrollo, a\u00f1adimos el m\u00f3dulo de OpenAPI a nuestro proyecto, y en el desarrollo de nuestro Controller hemos anotado tanto la clase como los m\u00e9todos con sus correspondientes etiquetas @Tag y @Operation.

    Esto nos va a ayudar a generar documentaci\u00f3n autom\u00e1tica de nuestras APIs haciendo que nuestro c\u00f3digo sea m\u00e1s mantenible y nuestra documentaci\u00f3n mucho m\u00e1s fiable.

    Para ver el resultado, con el proyecto arrancado nos dirigimos a la ruta por defecto de OpenAPI: http://localhost:8080/swagger-ui/index.html

    Aqu\u00ed podemos observar el cat\u00e1logo de endpoints generados, ver los tipos de entrada y salida e incluso realizar peticiones a los mismos. Este ser\u00e1 el contrato de nuestros endpoints, que nos ayudar\u00e1 a integrarnos con el equipo frontend (en el caso del tutorial seguramente seremos nosotros mismos).

    "},{"location":"develop/basic/springboot/#aspectos-importantes","title":"Aspectos importantes","text":"

    Los aspectos importantes de la capa Controller son:

    • La clase debe estar anotada con @Controller o @RestController. Mejor usar la \u00faltima anotaci\u00f3n, ya que est\u00e1s diciendo que las operaciones son de tipo Rest y no har\u00e1 falta configurar nada
    • La ruta general al controlador se define con el @RequestMapping global de la clase, aunque tambi\u00e9n se puede obviar esta anotaci\u00f3n y a\u00f1adir a cada una de las operaciones la ruta ra\u00edz.
    • Los m\u00e9todos que queramos exponer como operaciones deben ir anotados tambi\u00e9n con @RequestMapping con la info:
      • path \u2192 Que nos permite definir un path para la operaci\u00f3n, siempre sum\u00e1ndole el path de la clase (si es que tuviera)
      • method \u2192 Que nos permite definir el verbo de http que vamos a atender. Podemos tener el mismo path con diferente method, sin problema. Por lo general utilizaremos:
        • GET \u2192 Generalmente se usa para recuperar informaci\u00f3n
        • POST \u2192 Se utiliza para hacer update y filtrados complejos de informaci\u00f3n
        • PUT \u2192 Se utiliza para hacer save de informaci\u00f3n
        • DELETE \u2192 Se utiliza para hacer borrados de informaci\u00f3n
    "},{"location":"develop/basic/springboot/#capa-de-servicio-service","title":"Capa de Servicio: Service","text":"

    Pero en realidad la cosa no funciona as\u00ed. Hemos implementado parte de la l\u00f3gica de negocio (las operaciones/acciones de guardado, borrado y listado) dentro de lo que ser\u00eda la capa de operaciones o servicios al cliente. Esta capa no debe ejecutar l\u00f3gica de negocio, tan solo debe hacer transformaciones de datos y enrutar peticiones, toda la l\u00f3gica deber\u00eda ir en la capa de servicio.

    "},{"location":"develop/basic/springboot/#implementar-servicios","title":"Implementar servicios","text":"

    Pues vamos a arreglarlo. Vamos a crear un servicio y vamos a mover la l\u00f3gica de negocio al servicio.

    CategoryService.javaCategoryServiceImpl.javaCategoryController.java
    package com.ccsw.tutorial.category;\n\nimport com.ccsw.tutorial.category.model.CategoryDto;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n * \n */\npublic interface CategoryService {\n\n    /**\n     * M\u00e9todo para recuperar todas las categor\u00edas\n     *\n     * @return {@link List} de {@link Category}\n     */\n    List<CategoryDto> findAll();\n\n    /**\n     * M\u00e9todo para crear o actualizar una categor\u00eda\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    void save(Long id, CategoryDto dto);\n\n    /**\n     * M\u00e9todo para borrar una categor\u00eda\n     *\n     * @param id PK de la entidad\n     */\n    void delete(Long id);\n\n}\n
    package com.ccsw.tutorial.category;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport org.springframework.stereotype.Service;\n\nimport com.ccsw.tutorial.category.model.CategoryDto;\n\n/**\n * @author ccsw\n *\n */\n@Service\npublic class CategoryServiceImpl implements CategoryService {\n\n    private long SEQUENCE = 1;\n    private Map<Long, CategoryDto> categories = new HashMap<Long, CategoryDto>();\n\n    /**\n     * {@inheritDoc}\n     */\n    public List<CategoryDto> findAll() {\n\n        return new ArrayList<CategoryDto>(this.categories.values());\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    public void save(Long id, CategoryDto dto) {\n\n        CategoryDto category;\n\n        if (id == null) {\n            category = new CategoryDto();\n            category.setId(this.SEQUENCE++);\n            this.categories.put(category.getId(), category);\n        } else {\n            category = this.categories.get(id);\n        }\n\n        category.setName(dto.getName());\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    public void delete(Long id) {\n\n        this.categories.remove(id);\n    }\n\n}\n
    package com.ccsw.tutorial.category;\n\nimport java.util.List;\n\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.CrossOrigin;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestMethod;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport com.ccsw.tutorial.category.model.CategoryDto;\n\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\n/**\n * @author ccsw\n * \n */\n@Tag(name = \"Category\", description = \"API of Category\")\n@RequestMapping(value = \"/category\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class CategoryController {\n\n    @Autowired\n    private CategoryService categoryService;\n\n    /**\n     * M\u00e9todo para recuperar todas las categorias\n     *\n     * @return {@link List} de {@link CategoryDto}\n     */\n    @Operation(summary = \"Find\", description = \"Method that return a list of Categories\")\n    @RequestMapping(path = \"\", method = RequestMethod.GET)\n    public List<CategoryDto> findAll() {\n\n        return this.categoryService.findAll();\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar una categoria\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Category\")\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody CategoryDto dto) {\n\n        this.categoryService.save(id, dto);\n    }\n\n    /**\n     * M\u00e9todo para borrar una categoria\n     *\n     * @param id PK de la entidad\n     */\n    @Operation(summary = \"Delete\", description = \"Method that deletes a Category\")\n    @RequestMapping(path = \"/{id}\", method = RequestMethod.DELETE)\n    public void delete(@PathVariable(\"id\") Long id) {\n\n        this.categoryService.delete(id);\n    }\n}\n

    Ahora ya tenemos bien estructurado nuestro proyecto. Ya tenemos las dos capas necesarias Controladores y Servicios y cada uno se encarga de llevar a cabo su cometido de forma correcta.

    "},{"location":"develop/basic/springboot/#aspectos-importantes_1","title":"Aspectos importantes","text":"

    Los aspectos importantes de la capa Service son:

    • Toda la l\u00f3gica de negocio, operaciones y dem\u00e1s debe estar implementada en los servicios. Los controladores simplemente invocan servicios y transforman ciertos datos.
    • Es buena pr\u00e1ctica que la capa de servicios se implemente usando el patr\u00f3n fachada, esto quiere decir que necesitamos tener una Interface y al menos una implementaci\u00f3n de esa Interface. Y siempre debemos interactuar con la Interface. Esto nos permitir\u00e1 a futuro poder sustituir la implementaci\u00f3n por otra diferente sin que el resto del c\u00f3digo se vea afectado. Especialmente \u00fatil cuando queremos mockear comportamientos en tests.
    • La capa de servicio puede invocar a otros servicios en sus operaciones, pero nunca debe invocar a un controlador.
    • Para crear un servicio se debe anotar mediante @Service y adem\u00e1s debe implementar la Interface del servicio. Un error muy com\u00fan al arrancar un proyecto y ver que no funcionan las llamadas, es porqu\u00e9 no existe la anotaci\u00f3n @Service o porqu\u00e9 no se ha implementado la Interface.
    • La forma de inyectar y utilizar componentes manejados por Spring Boot es mediante la anotaci\u00f3n @Autowired. NO intentes crear un objeto de CategoryServiceImpl, ni hacer un new, ya que no estar\u00e1 manejado por Springboot y dar\u00e1 fallos de NullPointer. Lo mejor es dejar que Spring Boot lo gestione y utilizar las inyecciones de dependencias.
    "},{"location":"develop/basic/springboot/#capa-de-datos-repository","title":"Capa de Datos: Repository","text":"

    Pero no siempre vamos a acceder a los datos mediante un HasMap en memoria. En algunas ocasiones queremos que nuestro proyecto acceda a un servicio de datos como puede ser una BBDD, un servicio externo, un acceso a disco, etc. Estos accesos se deben hacer desde la capa de acceso a datos, y en concreto para nuestro ejemplo, lo haremos a trav\u00e9s de un Repository para que acceda a una BBDD.

    Para el tutorial no necesitamos configurar una BBDD externa ni complicarnos demasiado. Vamos a utilizar una librer\u00eda muy \u00fatil llamada H2 que nos permite levantar una BBDD en memoria persistiendo los datos en memoria o en disco, de hecho ya la configuramos en el apartado de Entorno de desarrollo.

    "},{"location":"develop/basic/springboot/#implementar-entity","title":"Implementar Entity","text":"

    Lo primero que haremos ser\u00e1 crear nuestra entity con la que vamos a persistir y recuperar informaci\u00f3n. Las entidades igual que los DTOs deber\u00edan estar agrupados dentro del package model de cada funcionalidad, as\u00ed que vamos a crear una nueva clase java.

    Category.java
    package com.ccsw.tutorial.category.model;\n\nimport jakarta.persistence.*;\n\n/**\n * @author ccsw\n * \n */\n@Entity\n@Table(name = \"category\")\npublic class Category {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"id\", nullable = false)\n    private Long id;\n\n    @Column(name = \"name\", nullable = false)\n    private String name;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return name\n     */\n    public String getName() {\n\n        return this.name;\n    }\n\n    /**\n     * @param name new value of {@link #getName}.\n     */\n    public void setName(String name) {\n\n        this.name = name;\n    }\n\n}\n

    Si te fijas, la Entity suele ser muy similar a un DTO, tiene unas propiedades y sus getters y setters. Pero a diferencia de los DTOs, esta clase tiene una serie de anotaciones que permiten a JPA hacer su magia y generar consultas SQL a la BBDD. En este ejemplo vemos 4 anotaciones importantes:

    • @Entity \u2192 Le indica a Springboot que se trata de una clase que implementa una Entidad de BBDD. Sin esta anotaci\u00f3n no es posible hacer queries.
    • @Table \u2192 Le indica a JPA el nombre y el schema de la tabla que representa esta clase. Por claridad se deber\u00eda poner siempre, aunque si el nombre de la tabla es igual al nombre de la clase no es necesaria la anotaci\u00f3n.
    • @Id y @GeneratedValue \u2192 Le indica a JPA que esta propiedad es la que mapea una Primary Key y adem\u00e1s que esta PK se genera con la estrategia que se le indique en la anotaci\u00f3n @GeneratedValue, que puede ser:
      • Generaci\u00f3n de PK por Secuence, la que utiliza Oracle, en este caso habr\u00e1 que indicarle un nombre de secuencia.
      • Generaci\u00f3n de PK por Indentity, la que utiliza MySql o SQLServer, el auto-incremental.
      • Generaci\u00f3n de PK por Table, en algunas BBDD se permite tener una tabla donde se almacenan como registros todas las secuencias.
      • Generaci\u00f3n de PK Auto, elige la mejor estrategia en funci\u00f3n de la BBDD que hemos seleccionado.
    • @Column \u2192 Le indica a JPA que esta propiedad mapea una columna de la tabla y le especifica el nombre de la columna. Al igual que la anotaci\u00f3nd de Table, esta anotaci\u00f3n no es necesaria aunque si es muy recomendable. Por claridad se deber\u00eda poner siempre, aunque si el nombre de la columna es igual al nombre de la propiedad no es necesaria la anotaci\u00f3n.

    Hay muchas otras anotaciones, pero estas son las b\u00e1sicas, ya ir\u00e1s aprendiendo otras.

    Consejo

    Para definir las PK de las tablas, intenta evitar una PK compuesta de m\u00e1s de una columna. La programaci\u00f3n se hace muy compleja y las magias que hace JPA en la oscuridad se complican mucho. Mi recomendaci\u00f3n es que siempre utilices una PK n\u00famerica, en la medida de lo posible, y si es necesario, crees \u00edndices compuestos de b\u00fasqueda o checks compuestos para evitar duplicidades.

    "},{"location":"develop/basic/springboot/#juego-de-datos-de-bbdd","title":"Juego de datos de BBDD","text":"

    Spring Boot autom\u00e1ticamente cuando arranque el proyecto escaner\u00e1 todas las @Entity y crear\u00e1 las estructuras de las tablas en la BBDD en memoria, gracias a las anotaciones que hemos puesto. Adem\u00e1s de esto, lanzar\u00e1 los scripts de construcci\u00f3n de BBDD que tenemos en la carpeta src/main/resources/. As\u00ed que, teniendo clara la estructura de la Entity podemos configurar los ficheros con los juegos de datos que queramos, y para ello vamos a utilizar el fichero data.sql que creamos en su momento.

    Sabemos que la tabla se llamar\u00e1 category y que tendr\u00e1 dos columnas, una columna id, que ser\u00e1 la PK autom\u00e1tica, y una columna name. Podemos escribir el siguiente script para rellenar datos:

    data.sql
    INSERT INTO category(name) VALUES ('Eurogames');\nINSERT INTO category(name) VALUES ('Ameritrash');\nINSERT INTO category(name) VALUES ('Familiar');\n
    "},{"location":"develop/basic/springboot/#implementar-repository","title":"Implementar Repository","text":"

    Ahora que ya tenemos el juego de datos y la entidad implementada, vamos a ver como acceder a BBDD desde Java. Esto lo haremos con un Repository. Existen varias formas de utilizar los repositories, desde el todo autom\u00e1tico y magia de JPA hasta el repositorio manual en el que hay que codificar todo. En el tutorial voy a explicar varias formas de implementarlo para este CRUD y los siguientes CRUDs.

    Como ya se dijo en puntos anteriores, el acceso a datos se debe hacer siempre a trav\u00e9s de un Repository, as\u00ed que vamos a implementar uno. En esta capa, al igual que pasaba con los services, es recomendable utilizar el patr\u00f3n fachada, para poder sustituir implementaciones sin afectar al c\u00f3digo.

    CategoryRepository.java
    package com.ccsw.tutorial.category;\n\nimport com.ccsw.tutorial.category.model.Category;\nimport org.springframework.data.repository.CrudRepository;\n\n/**\n * @author ccsw\n *\n */\npublic interface CategoryRepository extends CrudRepository<Category, Long> {\n\n}\n

    \u00bfQu\u00e9 te parece?, sencillo, \u00bfno?. Spring ya tiene una implementaci\u00f3n por defecto de un CrudRepository, tan solo tenemos que crear una interface que extienda de la interface CrudRepository pas\u00e1ndole como tipos la Entity y el tipo de la Primary Key. Con eso Spring construye el resto y nos provee de los m\u00e9todos t\u00edpicos y necesarios para un CRUD.

    Ahora vamos a utilizarla en \u00e9l Service, pero hay un problema. \u00c9l Repository devuelve un objeto tipo Category y \u00e9l Service y Controller devuelven un objeto tipo CategoryDto. Esto es porque en cada capa se debe con un \u00e1mbito de modelos diferente. Podr\u00edamos hacer que todo el back trabajara con Category que son entidades de persistencia, pero no es lo correcto y nos podr\u00eda llevar a cometer errores, o modificar el objeto y que sin que nosotros lo orden\u00e1semos se persistiera ese cambio en BBDD.

    El \u00e1mbito de trabajo de las capas con el que solemos trabajar y est\u00e1 m\u00e1s extendido es el siguiente:

    • Los datos que vienen y van al cliente, deber\u00edan ser en la mayor\u00eda de los casos datos en formato json
    • Al entrar en un Controller esos datos json se transforman en un DTO. Al salir del Controller hacia el cliente, esos DTOs se transforman en formato json. Estas conversiones son autom\u00e1ticas, las hace Springboot (en realidad las hace la librer\u00eda de jackson codehaus).
    • Cuando un Controller ejecuta una llamada a un Service, generalmente le pasa sus datos en DTO, y el Service se encarga de transformar esto a una Entity. A la inversa, cuando un Service responde a un Controller, \u00e9l responde con una Entity y el Controller ya se encargar\u00e1 de transformarlo a DTO.
    • Por \u00faltimo, para los Repository, siempre se trabaja de entrada y salida con objetos tipo Entity

    Parece un l\u00edo, pero ya ver\u00e1s como es muy sencillo ahora que veremos el ejemplo. Una \u00faltima cosa, para hacer esas transformaciones, las podemos hacer a mano usando getters y setters o bien utilizar el objeto DozerBeanMapper que hemos configurado al principio.

    El c\u00f3digo deber\u00eda quedar as\u00ed:

    CategoryServiceImpl.javaCategoryService.javaCategoryController.java
    package com.ccsw.tutorial.category;\n\nimport com.ccsw.tutorial.category.model.Category;\nimport com.ccsw.tutorial.category.model.CategoryDto;\nimport jakarta.transaction.Transactional;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Service\n@Transactional\npublic class CategoryServiceImpl implements CategoryService {\n\n    @Autowired\n    CategoryRepository categoryRepository;\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public List<Category> findAll() {\n\n          return (List<Category>) this.categoryRepository.findAll();\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void save(Long id, CategoryDto dto) {\n\n          Category category;\n\n          if (id == null) {\n             category = new Category();\n          } else {\n             category = this.categoryRepository.findById(id).orElse(null);\n          }\n\n          category.setName(dto.getName());\n\n          this.categoryRepository.save(category);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void delete(Long id) throws Exception {\n\n          if(this.categoryRepository.findById(id).orElse(null) == null){\n             throw new Exception(\"Not exists\");\n          }\n\n          this.categoryRepository.deleteById(id);\n    }\n\n}\n
    package com.ccsw.tutorial.category;\n\nimport com.ccsw.tutorial.category.model.Category;\nimport com.ccsw.tutorial.category.model.CategoryDto;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n * \n */\npublic interface CategoryService {\n\n    /**\n     * M\u00e9todo para recuperar todas las {@link Category}\n     *\n     * @return {@link List} de {@link Category}\n     */\n    List<Category> findAll();\n\n    /**\n     * M\u00e9todo para crear o actualizar una {@link Category}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    void save(Long id, CategoryDto dto);\n\n    /**\n     * M\u00e9todo para borrar una {@link Category}\n     *\n     * @param id PK de la entidad\n     */\n    void delete(Long id) throws Exception;\n\n}\n
    package com.ccsw.tutorial.category;\n\nimport com.ccsw.tutorial.category.model.Category;\nimport com.ccsw.tutorial.category.model.CategoryDto;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.modelmapper.ModelMapper;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * @author ccsw\n *\n */\n@Tag(name = \"Category\", description = \"API of Category\")\n@RequestMapping(value = \"/category\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class CategoryController {\n\n    @Autowired\n    CategoryService categoryService;\n\n    @Autowired\n    ModelMapper mapper;\n\n    /**\n     * M\u00e9todo para recuperar todas las {@link Category}\n     *\n     * @return {@link List} de {@link CategoryDto}\n     */\n    @Operation(summary = \"Find\", description = \"Method that return a list of Categories\"\n    )\n    @RequestMapping(path = \"\", method = RequestMethod.GET)\n    public List<CategoryDto> findAll() {\n\n        List<Category> categories = this.categoryService.findAll();\n\n        return categories.stream().map(e -> mapper.map(e, CategoryDto.class)).collect(Collectors.toList());\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar una {@link Category}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Category\"\n    )\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody CategoryDto dto) {\n\n        this.categoryService.save(id, dto);\n    }\n\n    /**\n     * M\u00e9todo para borrar una {@link Category}\n     *\n     * @param id PK de la entidad\n     */\n    @Operation(summary = \"Delete\", description = \"Method that deletes a Category\")\n    @RequestMapping(path = \"/{id}\", method = RequestMethod.DELETE)\n    public void delete(@PathVariable(\"id\") Long id) throws Exception {\n\n        this.categoryService.delete(id);\n    }\n\n}\n

    El Service no tiene nada raro, simplemente accede al Repository (siempre anotado como @Autowired) y recupera datos o los guarda. F\u00edjate en el caso especial del save que la \u00fanica diferencia es que en un caso tendr\u00e1 id != null y por tanto internamente har\u00e1 un update, y en otro caso tendr\u00e1 id == null y por tanto internamente har\u00e1 un save.

    En cuanto a la interface, lo \u00fanico que cambiamos fue los objetos de respuesta, que ahora pasan a ser de tipo Category. Los de entrada se mantienen como CategoryDto.

    Por \u00faltimo, en \u00e9l Controller se puede ver como se utiliza el conversor de DozerBeanMapper (siempre anotado como @Autowired), que permite traducir una lista a un tipo concreto, o un objeto \u00fanico a un tipo concreto. La forma de hacer estas conversiones siempre es por nombre de propiedad. Las propiedades del objeto destino se deben llamar igual que las propiedades del objeto origen. En caso contrario se quedar\u00e1n a null.

    Ojo con el mapeo

    Ojo a esta \u00faltima frase, debe quedar meridianamente claro. La forma de mapear de un objeto origen a un objeto destino siempre es a trav\u00e9s del nombre. Los getters del origen deben ser iguales a los getters del destino. Si hay una letra diferente o unas may\u00fasculas o min\u00fasculas diferentes NO realizar\u00e1 el mapeo y se quedar\u00e1 la propiedad a null.

    Para terminar, cuando queramos realizar un mapeo masivo de los diferentes registros, tenemos que itulizar la API Stream de Java, que nos proporciona una forma sencilla de realizar estas operativas, sobre colecciones de elementos, mediante el uso del m\u00e9todo intermedio map y el reductor por defecto para listas. Te recomiendo echarle un ojo a la teor\u00eda de Introducci\u00f3n a API Java Streams.

    BBDD

    Si quires ver el contenido de la base de datos puedes acceder a un IDE web autopublicado por H2 en la ruta http://localhost:8080/h2-console

    "},{"location":"develop/basic/springboot/#aspectos-importantes_2","title":"Aspectos importantes","text":"

    Los aspectos importantes de la capa Repository son:

    • Al igual que los Service, se debe utilizar el patr\u00f3n fachada, por lo que tendremos una Interface y posiblemente una implementaci\u00f3n.
    • A menudo la implementaci\u00f3n la hace internamente Spring Boot, pero hay veces que podemos hacer una implementaci\u00f3n manual. Lo veremos en siguientes puntos.
    • Los Repository trabajan siempre con Entity que no son m\u00e1s que mapeos de una tabla o de una vista que existe en BBDD.
    • Los Repository no contienen l\u00f3gica de negocio, ni transformaciones, simplemente acceder a datos, persisten o devuelven informaci\u00f3n.
    • Los Repository JAM\u00c1S deben llamar a otros Repository ni Service.
    • Intenta que tus clases Entity sean lo m\u00e1s sencillas posible, sobre todo en cuanto a Primary Keys, se simplificar\u00e1 mucho el desarrollo.
    "},{"location":"develop/basic/springboot/#capa-de-testing-tdd","title":"Capa de Testing: TDD","text":"

    Por \u00faltimo y aunque no deber\u00eda ser lo \u00faltimo que se desarrolla sino todo lo contrario, deber\u00eda ser lo primero en desarrollar, tenemos la bater\u00eda de pruebas. Con fines did\u00e1cticos, he querido ense\u00f1arte un ciclo de desarrollo para ir recorriendo las diferentes capas de una aplicaci\u00f3n, pero en realidad, para realizar el desarrollo deber\u00eda aplicar TDD (Test Driven Development). Si quieres aprender las reglas b\u00e1sicas de como aplicar TDD al desarrollo diario, te recomiendo que leas el Anexo. TDD.

    En este caso, y sin que sirva de precedente, ya tenemos implementados los m\u00e9todos de la aplicaci\u00f3n, y ahora vamos a testearlos. Existen muchas formas de testing en funci\u00f3n del componente o la capa que se quiera testear. En realidad, a medida que vayas programando ir\u00e1s aprendiendo todas ellas, de momento realizaremos dos tipos de test simples que prueben las casu\u00edsticas de los m\u00e9todos.

    El enfoque que seguiremos en este tutorial ser\u00e1 realizar las pruebas mediante test unitarios y test de integraci\u00f3n.

    • Test unitarios: Se trata de pruebas estrictamente relativas a la calidad est\u00e1tica del c\u00f3digo de una determinada operaci\u00f3n de la capa de la l\u00f3gica de negocio (Service). Estas pruebas no inicializan el contexto de Spring y deben simular todas las piezas ajenas a la funcionalidad testeada.
    • Test de integraci\u00f3n: Se tratan de pruebas completas de un determinado endpoint que conlleva inicializar el contexto de Spring (base de datos incluida) y realizar una llama REST para comprobar el flujo completo de la API.

    Lo primero ser\u00e1 conocer que queremos probar y para ello nos vamos a hacer una lista:

    Test unitarios:

    • Prueba de listado, debe probar la l\u00f3gica dentro de la operaci\u00f3n de negocio de consulta de Categor\u00eda
    • Prueba de creaci\u00f3n, debe probar la l\u00f3gica dentro de la operaci\u00f3n de negocio de creaci\u00f3n una nueva Categor\u00eda
    • Prueba de modificaci\u00f3n, debe probar la l\u00f3gica dentro de la operaci\u00f3n de negocio de modificaci\u00f3n una Categor\u00eda existente
    • Prueba de borrado, debe probar la l\u00f3gica dentro de la operaci\u00f3n de negocio de borrado de una Categor\u00eda existente

    Test de integraci\u00f3n:

    • Prueba de listado, debe recuperar los elementos de la tabla Categor\u00eda
    • Prueba de creaci\u00f3n, debe crear una nueva Categor\u00eda
    • Prueba de modificaci\u00f3n correcta, debe modificar una Categor\u00eda existente
    • Prueba de modificaci\u00f3n incorrecta, debe dar error al modificar una Categor\u00eda que no existe
    • Prueba de borrado correcta, debe borrar una Categor\u00eda existente
    • Prueba de borrado incorrecta, debe dar error al borrar una Categor\u00eda que no existe

    Se podr\u00edan hacer muchos m\u00e1s tests, pero creo que con esos son suficientes para que entiendas como se comporta esta capa. Si te fijas, hay que probar tanto los resultados correctos como los resultados incorrectos. El usuario no siempre se va a comportar como nosotros pensamos.

    Pues vamos a ello.

    "},{"location":"develop/basic/springboot/#pruebas-de-listado","title":"Pruebas de listado","text":"

    Vamos a empezar haciendo una clase de test dentro de la carpeta src/test/java. No queremos que los test formen parte del c\u00f3digo productivo de la aplicaci\u00f3n, por eso utilizamos esa ruta que queda fuera del scope general de la aplicaci\u00f3n (main).

    Crearemos las clases (en la package category):

    • Test unitarios: com.ccsw.tutorial.category.CategoryTest
    • Test de integraci\u00f3n: com.ccsw.tutorial.category.CategoryIT
    CategoryTest.javaCategoryIT.java
    package com.ccsw.tutorial.category;\n\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\npublic class CategoryTest {\n\n}\n
    package com.ccsw.tutorial.category;\n\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.test.web.client.TestRestTemplate;\nimport org.springframework.boot.test.web.server.LocalServerPort;\nimport org.springframework.test.annotation.DirtiesContext;\n\n@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)\n@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)\npublic class CategoryIT {\n\n    @LocalServerPort\n    private int port;\n\n    @Autowired\n    private TestRestTemplate restTemplate;\n\n}\n

    Estas clases son sencillas y tan solo tienen anotaciones espec\u00edficas de Spring Boot para cada tipo de test:

    • @SpringBootTest. Esta anotaci\u00f3n lo que hace es inicializar el contexto de Spring cada vez que se inician los test de jUnit. Aunque el contexto tarda unos segundos en arrancar, lo bueno que tiene es que solo se inicializa una vez y se lanzan todos los jUnits en bater\u00eda con el mismo contexto.
    • @DirtiesContext. Esta anotaci\u00f3n le indica a Spring que los test van a ser transaccionales, y por tanto cuando termine la ejecuci\u00f3n de cada uno de los test, autom\u00e1ticamente por la anotaci\u00f3n de arriba, Spring har\u00e1 un rearranque parcial del contexto y dejar\u00e1 el estado de la BBDD como estaba inicialmente.
    • @ExtendWith. Esta anotaci\u00f3n le indica a Spring que no debe inicializar el contexto, ya que se trata de test est\u00e1ticos que no lo requieren.

    Para las pruebas de integraci\u00f3n nos faltar\u00e1 configurar la aplicaci\u00f3n de test, al igual que hicimos con la aplicaci\u00f3n 'productiva'. Deberemos abrir el fichero src/test/resources/application.properties y a\u00f1adir la configuraci\u00f3n de la BBDD. Para este tutorial vamos a utilizar la misma BBDD que la aplicaci\u00f3n productiva, pero de normal la aplicaci\u00f3n se conectar\u00e1 a una BBDD, generalmente f\u00edsica, mientras que los test jUnit se conectar\u00e1n a otra BBDD, generalmente en memoria.

    application.properties
    #Database\nspring.datasource.url=jdbc:h2:mem:testdb\nspring.datasource.username=sa\nspring.datasource.password=sa\nspring.datasource.driver-class-name=org.h2.Driver\n\nspring.jpa.database-platform=org.hibernate.dialect.H2Dialect\nspring.jpa.defer-datasource-initialization=true\n

    Con todo esto ya podemos crear nuestro primer test. Iremos a las clases CategoryIT y CategoryTest donde a\u00f1adiremos un m\u00e9todo p\u00fablico. Los test siempre tienen que ser m\u00e9todos p\u00fablicos que devuelvan el tipo void.

    CategoryTest.javaCategoryIT.java
    package com.ccsw.tutorial.category;\n\nimport com.ccsw.tutorial.category.model.Category;\nimport com.ccsw.tutorial.category.model.CategoryDto;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.Mockito.*;\n\n@ExtendWith(MockitoExtension.class)\npublic class CategoryTest {\n\n    @Mock\n    private CategoryRepository categoryRepository;\n\n    @InjectMocks\n    private CategoryServiceImpl categoryService;\n\n    @Test\n    public void findAllShouldReturnAllCategories() {\n\n          List<Category> list = new ArrayList<>();\n          list.add(mock(Category.class));\n\n          when(categoryRepository.findAll()).thenReturn(list);\n\n          List<Category> categories = categoryService.findAll();\n\n          assertNotNull(categories);\n          assertEquals(1, categories.size());\n    }\n}\n
    package com.ccsw.tutorial.category;\n\nimport com.ccsw.tutorial.category.model.CategoryDto;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;    \nimport org.springframework.boot.test.web.client.TestRestTemplate;\nimport org.springframework.boot.test.web.server.LocalServerPort;\nimport org.springframework.core.ParameterizedTypeReference;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.test.annotation.DirtiesContext;\n\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)\n@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)\npublic class CategoryIT {\n\n    public static final String LOCALHOST = \"http://localhost:\";\n    public static final String SERVICE_PATH = \"/category\";\n\n    @LocalServerPort\n    private int port;\n\n    @Autowired\n    private TestRestTemplate restTemplate;\n\n    ParameterizedTypeReference<List<CategoryDto>> responseType = new ParameterizedTypeReference<List<CategoryDto>>(){};\n\n    @Test\n    public void findAllShouldReturnAllCategories() {\n\n          ResponseEntity<List<CategoryDto>> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.GET, null, responseType);\n\n          assertNotNull(response);\n          assertEquals(3, response.getBody().size());\n    }\n}\n

    Es muy importante marcar cada m\u00e9todo de prueba con la anotaci\u00f3n @Test, en caso contrario no se ejecutar\u00e1. Lo que se debe hacer en cada m\u00e9todo que implementemos es probar una y solo una acci\u00f3n.

    En los ejemplos anteriores (CategoryTest), por un lado hemos comprobado el m\u00e9todo findAll() el cual por debajo invoca una llamada al repository de categor\u00eda, la cual hemos simulado con una respuesta ficticia limit\u00e1ndonos \u00fanicamente a la l\u00f3gica contenida en la operaci\u00f3n de negocio.

    Mientras que por otro lado (CategoryIT), hemos probado la llamando al m\u00e9todo GET del endpoint http://localhost:XXXX/category comprobando que realmente nos devuelve 3 resultados, que son los que hay en BBDD inicialmente.

    Muy importante: Nomenclatura de los tests

    La nomenclatura de los m\u00e9todos de test debe sigue una estructura determinada. Hay muchas formas de nombrar a los m\u00e9todos, pero nosotros solemos utilizar la estructura 'should', para indicar lo que va a hacer. En el ejemplo anterior el m\u00e9todo 'findAll' debe devolver 'AllCategories'. De esta forma sabemos cu\u00e1l es la intenci\u00f3n del test, y si por cualquier motivo diera un fallo, sabemos que es lo que NO est\u00e1 funcionando de nuestra aplicaci\u00f3n.

    Para comprobar que el test funciona, podemos darle bot\u00f3n derecho sobre la clase de CategoryIT y pulsar en Run as -> JUnit Test. Si todo funciona correctamente, deber\u00e1 aparecer una pantalla de ejecuci\u00f3n y todos nuestros tests (en este caso solo uno) corriendo correctamente (en verde). El proceso es el mismo para la clase CategoryTest.

    "},{"location":"develop/basic/springboot/#pruebas-de-creacion","title":"Pruebas de creaci\u00f3n","text":"

    Vamos con los siguientes test, los que probar\u00e1n una creaci\u00f3n de una nueva Categor\u00eda. A\u00f1adimos el siguiente m\u00e9todo a la clase de test:

    CategoryTest.javaCategoryIT.java
    public static final String CATEGORY_NAME = \"CAT1\";\n\n@Test\npublic void saveNotExistsCategoryIdShouldInsert() {\n\n      CategoryDto categoryDto = new CategoryDto();\n      categoryDto.setName(CATEGORY_NAME);\n\n      ArgumentCaptor<Category> category = ArgumentCaptor.forClass(Category.class);\n\n      categoryService.save(null, categoryDto);\n\n      verify(categoryRepository).save(category.capture());\n\n      assertEquals(CATEGORY_NAME, category.getValue().getName());\n}\n
    public static final Long NEW_CATEGORY_ID = 4L;\npublic static final String NEW_CATEGORY_NAME = \"CAT4\";\n\n@Test\npublic void saveWithoutIdShouldCreateNewCategory() {\n\n      CategoryDto dto = new CategoryDto();\n      dto.setName(NEW_CATEGORY_NAME);\n\n      restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.PUT, new HttpEntity<>(dto), Void.class);\n\n      ResponseEntity<List<CategoryDto>> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.GET, null, responseType);\n      assertNotNull(response);\n      assertEquals(4, response.getBody().size());\n\n      CategoryDto categorySearch = response.getBody().stream().filter(item -> item.getId().equals(NEW_CATEGORY_ID)).findFirst().orElse(null);\n      assertNotNull(categorySearch);\n      assertEquals(NEW_CATEGORY_NAME, categorySearch.getName());\n}\n

    En el primer caso, estamos construyendo un objeto CategoryDto para darle un nombre a la Categor\u00eda e invocamos a la operaci\u00f3n save pasandole un ID a nulo. Para identificar que el funcionamiento es el esperado, capturamos la categor\u00eda que se proporciona al repository al intentar realizar la acci\u00f3n ficticia de guardado y comprobamos que el valor es el que se proporciona en la invocaci\u00f3n.

    De forma similar en el segundo caso, estamos construyendo un objeto CategoryDto para darle un nombre a la Categor\u00eda e invocamos al m\u00e9todo PUT sin a\u00f1adir en la ruta referencia al identificador. Seguidamente, recuperamos de nuevo la lista de categor\u00edas y en este caso deber\u00eda tener 4 resultados. Hacemos un filtrado buscando la nueva Categor\u00eda que deber\u00eda tener un ID = 4 y deber\u00eda ser la que acabamos de crear.

    Si ejecutamos, veremos que ambos test ahora aparecen en verde.

    "},{"location":"develop/basic/springboot/#pruebas-de-modificacion","title":"Pruebas de modificaci\u00f3n","text":"

    Para este caso de prueba, vamos a realizar varios test, como hemos comentado anteriormente. Tenemos que probar que es lo que pasa cuando intentamos modificar un elemento que existe, pero tambi\u00e9n debemos probar que es lo que pasa cuando intentamos modificar un elemento que no existe.

    Empezamos con el sencillo, un test que pruebe una modificaci\u00f3n existente.

    CategoryTest.javaCategoryIT.java
    public static final Long EXISTS_CATEGORY_ID = 1L;\n\n@Test\npublic void saveExistsCategoryIdShouldUpdate() {\n\n  CategoryDto categoryDto = new CategoryDto();\n  categoryDto.setName(CATEGORY_NAME);\n\n  Category category = mock(Category.class);\n  when(categoryRepository.findById(EXISTS_CATEGORY_ID)).thenReturn(Optional.of(category));\n\n  categoryService.save(EXISTS_CATEGORY_ID, categoryDto);\n\n  verify(categoryRepository).save(category);\n}\n
    public static final Long MODIFY_CATEGORY_ID = 3L;\n\n@Test\npublic void modifyWithExistIdShouldModifyCategory() {\n\n      CategoryDto dto = new CategoryDto();\n      dto.setName(NEW_CATEGORY_NAME);\n\n      restTemplate.exchange(LOCALHOST + port + SERVICE_PATH + \"/\" + MODIFY_CATEGORY_ID, HttpMethod.PUT, new HttpEntity<>(dto), Void.class);\n\n      ResponseEntity<List<CategoryDto>> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.GET, null, responseType);\n      assertNotNull(response);\n      assertEquals(3, response.getBody().size());\n\n      CategoryDto categorySearch = response.getBody().stream().filter(item -> item.getId().equals(MODIFY_CATEGORY_ID)).findFirst().orElse(null);\n      assertNotNull(categorySearch);\n      assertEquals(NEW_CATEGORY_NAME, categorySearch.getName());\n}\n

    En el caso de los test unitarios, comprobamos la l\u00f3gica de la modificaci\u00f3n simulando que el repository nos devuelve una categor\u00eda que modificar y verificado que se invoca el guardado sobre la misma.

    En el caso de los test de integraci\u00f3n, la misma filosof\u00eda que en el test anterior, pero esta vez modificamos la Categor\u00eda de ID = 3. Luego la filtramos y vemos que realmente se ha modificado. Adem\u00e1s comprobamos que el listado de todas los registros sigue siendo 3 y no se ha creado un nuevo registro.

    En el siguiente test, probaremos un resultado err\u00f3neo.

    CategoryIT.java
    @Test\npublic void modifyWithNotExistIdShouldInternalError() {\n\n      CategoryDto dto = new CategoryDto();\n      dto.setName(NEW_CATEGORY_NAME);\n\n      ResponseEntity<?> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH + \"/\" + NEW_CATEGORY_ID, HttpMethod.PUT, new HttpEntity<>(dto), Void.class);\n\n      assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());\n}\n

    Intentamos modificar el ID = 4, que no deber\u00eda existir en BBDD y por tanto lo que se espera en el test es que lance un 500 Internal Server Error al llamar al m\u00e9todo PUT.

    "},{"location":"develop/basic/springboot/#pruebas-de-borrado","title":"Pruebas de borrado","text":"

    Ya por \u00faltimo implementamos las pruebas de borrado.

    CategoryTest.javaCategoryIT.java
    @Test\npublic void deleteExistsCategoryIdShouldDelete() throws Exception {\n\n      Category category = mock(Category.class);\n      when(categoryRepository.findById(EXISTS_CATEGORY_ID)).thenReturn(Optional.of(category));\n\n      categoryService.delete(EXISTS_CATEGORY_ID);\n\n      verify(categoryRepository).deleteById(EXISTS_CATEGORY_ID);\n}\n
    public static final Long DELETE_CATEGORY_ID = 2L;\n\n@Test\npublic void deleteWithExistsIdShouldDeleteCategory() {\n\n      restTemplate.exchange(LOCALHOST + port + SERVICE_PATH + \"/\" + DELETE_CATEGORY_ID, HttpMethod.DELETE, null, Void.class);\n\n      ResponseEntity<List<CategoryDto>> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.GET, null, responseType);\n      assertNotNull(response);\n      assertEquals(2, response.getBody().size());\n}\n\n@Test\npublic void deleteWithNotExistsIdShouldInternalError() {\n\n      ResponseEntity<?> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH + \"/\" + NEW_CATEGORY_ID, HttpMethod.DELETE, null, Void.class);\n\n      assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());\n}\n

    En cuanto al test unitario, se invoca a la operaci\u00f3n delete y se verifica que la operaci\u00f3n requerida del repository es invocado con el atributo correcto.

    En lo relativo a las pruebas de integraci\u00f3n, en el primer test, se invoca el m\u00e9todo DELETE y posteriormente se comprueba que el listado tiene un tama\u00f1o de 2 (uno menos que el original). Mientras que en el segundo test, se comprueba que con ID no v\u00e1lido, devuelve un 500 Internal Server Error.

    Con esto tendr\u00edamos m\u00e1s o menos probados los casos b\u00e1sicos de nuestra aplicaci\u00f3n y tendr\u00edamos una peque\u00f1a red de seguridad que nos ayudar\u00eda por si a futuro necesitamos hacer alg\u00fan cambio o evolutivo.

    "},{"location":"develop/basic/springboot/#que-hemos-aprendido","title":"\u00bfQ\u00fae hemos aprendido?","text":"

    Resumiendo un poco los pasos que hemos seguido:

    • Hay que definir y agrupar por \u00e1mbito funcional, hemos creado el package com.ccsw.tutorial.category para aglutinar todas las clases.
    • Lo primero que debemos empezar a construir siempre son los test, aunque en este cap\u00edtulo del tutorial lo hemos hecho al rev\u00e9s solo con fines did\u00e1cticos. En los siguientes cap\u00edtulos lo haremos de forma correcta, y esto nos ayudar\u00e1 a pensar y dise\u00f1ar que es lo que queremos implementar realmente.
    • La implementaci\u00f3n de la aplicaci\u00f3n se deber\u00eda separar por capas:
      • Controller \u2192 Maneja las peticiones de entrada del cliente y realiza transformaciones. No ejecuta directamente l\u00f3gica de negocio, para eso utiliza llamadas a la siguiente capa.
      • Service \u2192 Ejecuta la l\u00f3gica de negocio en sus m\u00e9todos o llamando a otros objetos de la misma capa. No ejecuta directamente accesos a datos, para eso utiliza la siguiente capa.
      • Repository \u2192 Realiza los accesos a datos de lectura y escritura. NUNCA debe llamar a otros objetos de la misma capa ni de capas anteriores.
    • Hay que tener en cuenta los objetos modelo que se mueven en cada capa. Generalmente son:
      • Json \u2192 Los datos que vienen y van del cliente al Controller.
      • DTO \u2192 Los datos se mueven dentro del Controller y sirven para invocar llamadas. Tambi\u00e9n son los datos que devuelve un Controller.
      • Entity \u2192 Los datos que sirven para persistir y leer datos de una BBDD y que NUNCA deber\u00edan ir m\u00e1s all\u00e1 del Controller.
    "},{"location":"develop/basic/springboot/#depuracion","title":"Depuraci\u00f3n","text":"

    Una parte muy importante del desarrollo es tener la capacidad de depurar nuestro c\u00f3digo, en este apartado vamos a explicar como se realiza debug en Backend.

    Esta parte se realiza con las herramientas incluidas dentro de nuestro IDE favorito, depender\u00e1 del que tengas instalado, se hace de una forma u otra.

    "},{"location":"develop/basic/springboot/#intellij","title":"IntelliJ","text":"

    En caso de que hayas elegido instalar IntelliJ, lo primero que debemos hacer es arrancar la aplicaci\u00f3n en modo Debug:

    o bien

    Arrancada la aplicaci\u00f3n en este modo, vamos a depurar la operaci\u00f3n de crear categor\u00eda.

    Para ello vamos a abrir nuestro fichero donde tenemos la implementaci\u00f3n del servicio de creaci\u00f3n de la capa de la l\u00f3gica de negocio CategoryServiceImpl.

    Dentro del fichero ya podemos a\u00f1adir puntos de ruptura (breakpoint), en nuestro caso queremos comprobar en el m\u00e9todo save que el nombre introducido se recibe correctamente.

    Colocamos el breakpoint en la primera l\u00ednea del m\u00e9todo save (click al lado del n\u00famero de la l\u00ednea):

    y desde la interfaz/postman creamos una nueva categor\u00eda para lanzar la petici\u00f3n y que se detenga la ejecuci\u00f3n en debug.

    Hecho esto, podemos observar que a nivel de interfaz/postman, la petici\u00f3n se queda esperando y el IDE pasa modo Debug. En la parte inferior del IDE podemos ver la pila de llamadas y las variables actuales en memoria:

    El IDE nos lleva al punto exacto donde hemos a\u00f1adido el breakpoint y se para en este punto ofreci\u00e9ndonos la posibilidad de explorar el contenido de las variables del c\u00f3digo:

    Aqu\u00ed podemos comprobar que efectivamente el atributo name de la variable dto tiene el valor que hemos introducido por pantalla/postman.

    Para continuar con la ejecuci\u00f3n basta con darle al bot\u00f3n de play de la barra de herramientas inferior-izquierda, o incluso navegar por las siguientes l\u00edneas de c\u00f3digo.

    "},{"location":"develop/basic/springboot/#eclipse","title":"Eclipse","text":"

    En caso de que hayas elegido instalar Eclipse, lo primero que debemos hacer es arrancar la aplicaci\u00f3n en modo Debug:

    Arrancada la aplicaci\u00f3n en este modo, vamos a depurar la operaci\u00f3n de crear categor\u00eda.

    Para ello vamos a abrir nuestro fichero donde tenemos la implementaci\u00f3n del servicio de creaci\u00f3n de la capa de la l\u00f3gica de negocio CategoryServiceImpl.

    Dentro del fichero ya podemos a\u00f1adir puntos de ruptura (breakpoint), en nuestro caso queremos comprobar que el nombre introducido se recibe correctamente.

    Colocamos el breakpoint en la primera l\u00ednea del m\u00e9todo (click sobre el n\u00famero de la l\u00ednea) y desde la interfaz/postman creamos una nueva categor\u00eda.

    Hecho esto, podemos observar que a nivel de interfaz/postman, la petici\u00f3n se queda esperando y el IDE pasa modo Debug (la primera vez nos preguntar\u00e1 si queremos hacerlo, le decimos que si):

    El IDE nos lleva al punto exacto donde hemos a\u00f1adido el breakpoint y se para en este punto ofreci\u00e9ndonos la posibilidad de explorar el contenido de las variables del c\u00f3digo:

    Aqu\u00ed podemos comprobar que efectivamente el atributo name de la variable dto tiene el valor que hemos introducido por pantalla/postman.

    Para continuar con la ejecuci\u00f3n basta con darle al bot\u00f3n de play de la barra de herramientas superior.

    Nota: para volver al modo Java de Eclipse, presionamos el bot\u00f3n que se sit\u00faa a la izquierda del modo Debug en el que ha entrado el IDE autom\u00e1ticamente.

    "},{"location":"develop/basic/vuejs/","title":"Listado simple - VUE","text":"

    Ahora que ya tenemos listo el proyecto frontend de VUE, ya podemos empezar a codificar la soluci\u00f3n.

    "},{"location":"develop/basic/vuejs/#primeros-pasos","title":"Primeros pasos","text":"

    Antes de empezar

    Quiero hacer hincapi\u00e9 que VUE tiene una documentaci\u00f3n muy extensa y completa, as\u00ed que te recomiendo que hagas uso de ella cuando tengas cualquier duda. En la propia web de documentaci\u00f3n de VUE puedes buscar casi cualquier ejemplo que necesites.

    Vamos a realizar una pantalla lo m\u00e1s parecida a la siguiente captura para empezar:

    Lo primero que vamos a hacer es crear los componentes de las tres pr\u00f3ximas pantallas mediante el siguiente comando:

    npx quasar new page CatalogPage CategoriesPage AuthorsPage\n

    Y ahora vamos a crear las rutas que nos van a hacer llegar hasta ellos:

    import { RouteRecordRaw } from 'vue-router';\nimport MainLayout from 'layouts/MainLayout.vue';\nimport IndexPage from 'pages/IndexPage.vue';\nimport CatalogPage from 'pages/CatalogPage.vue';\nimport CategoriesPage from 'pages/CategoriesPage.vue';\nimport AuthorsPage from 'pages/AuthorsPage.vue';\n\nconst routes: RouteRecordRaw[] = [\n  {\n    path: '/',\n    component: MainLayout,\n    children: [\n      { path: '', component: IndexPage },\n      { path: 'games', component: CatalogPage },\n      { path: 'categories', component: CategoriesPage },\n      { path: 'authors', component: AuthorsPage },\n    ],\n  },\n\n  // Always leave this as last one,\n  // but you can also remove it\n  // {\n  //   path: '/:catchAll(.*)*',\n  //   component: () => import('pages/ErrorNotFound.vue'),\n  // },\n];\n\nexport default routes;\n

    Una vez realizado esto, vamos a ponerle dentro de cada uno de los archivos creados el nombre del archivo donde est\u00e1 el comentario para saber que lleva al lugar correcto:

    <template>\n  <q-page padding> CatalogPage </q-page>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nexport default defineComponent({\n  name: 'CatalogPage',\n});\n</script>\n

    Por \u00faltimo, modificaremos el men\u00fa lateral para que lleve las opciones correctas y nos enlace a dichas pantallas (para esto, iremos al archivo MainLayout.vue):

    const linksList = [\n  {\n    title: 'Cat\u00e1logo',\n    icon: 'list',\n    link: 'games',\n  },\n  {\n    title: 'Categor\u00edas',\n    icon: 'dashboard',\n    link: 'categories',\n  },\n  {\n    title: 'Autores',\n    icon: 'face',\n    link: 'authors',\n  },\n];\n

    En caso de que no funcione correctamente, deber\u00eda solucionarse cambiando en el archivo EssentialLink.vue el prop \u201chref\u201d por el prop \u201cto\u201d:

    <template>\n  <q-item clickable tag=\"a\" :to=\"link\">\n    <q-item-section v-if=\"icon\" avatar>\n      <q-icon :name=\"icon\" />\n    </q-item-section>\n\n    <q-item-section>\n      <q-item-label>{{ title }}</q-item-label>\n    </q-item-section>\n  </q-item>\n</template>\n
    "},{"location":"develop/basic/vuejs/#codigo-de-la-pantalla","title":"C\u00f3digo de la pantalla","text":"

    Para empezar, usaremos un componente de tabla de la librer\u00eda de Quasar. Este componente nos ayudar\u00e1 a mostrar los datos de los juegos en un futuro.

    <template>\n  <q-page padding>\n    <q-table\n      :rows=\"catalogData\"\n      :columns=\"columns\"\n      title=\"Cat\u00e1logo\"\n      row-key=\"id\"\n    />\n  </q-page>\n</template>\n

    As\u00ed es como deber\u00eda quedar nuestro componente de tabla con todas las supuestas variables que m\u00e1s adelante le settearemos:

    <template>\n  <q-page padding>\n    <q-table\n      hide-bottom\n      :rows=\"catalogData\"\n      :columns=\"columns\"\n      v-model:pagination=\"pagination\"\n      title=\"Cat\u00e1logo\"\n      class=\"my-sticky-header-table\"\n      no-data-label=\"No hay resultados\"\n      row-key=\"id\"\n    />\n  </q-page>\n</template>\n

    Y as\u00ed es como vamos a necesitar que est\u00e9, ya que no va a tener paginado. \u00bfPor qu\u00e9?

    • hide-bottom \u2192 hace que no se muestre la zona baja de la tabla que es donde est\u00e1 el paginado.
    • v-model:pagination \u2192 har\u00e1 que vengan los datos que vengan, se muestren todos de la misma manera.
    • class \u2192 esta clase har\u00e1 que, si haciendo scroll pierdes los header, estos te acompa\u00f1en y siempre sepas qu\u00e9 columna es la que est\u00e1s mirando.
    • no-data-label \u2192 un mensaje por si alg\u00fan d\u00eda no hay datos o tiene un fallo el back.

    Todo esto no hace falta aprend\u00e9rselo, est\u00e1 en la documentaci\u00f3n de este componente. Pero vamos a ir usando algunos props como estos para configurar correctamente la tabla.

    "},{"location":"develop/basic/vuejs/#mockeando-datos","title":"Mockeando datos","text":"

    Y esto va a hacer que podamos mostrar datos:

    <script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nconst columns = [\n  { name: 'id', align: 'left', label: 'ID', field: 'id', sortable: true },\n  {\n    name: 'name',\n    align: 'left',\n    label: 'Nombre',\n    field: 'name',\n    sortable: true,\n  },\n  { name: 'options', align: 'left', label: 'Options', field: 'options' },\n];\n\nconst data = [\n  { id: 1, name: 'Dados' },\n  { id: 2, name: 'Fichas' },\n  { id: 3, name: 'Cartas' },\n  { id: 4, name: 'Rol' },\n  { id: 5, name: 'Tableros' },\n  { id: 6, name: 'Tem\u00e1ticos' },\n  { id: 7, name: 'Europeos' },\n  { id: 8, name: 'Guerra' },\n  { id: 9, name: 'Abstractos' },\n];\n\nexport default defineComponent({\n  name: 'CatalogPage',\n\n  setup() {\n    const catalogData = ref(data);\n\n    return {\n      catalogData,\n      columns: columns,\n      pagination: {\n        page: 1,\n        rowsPerPage: 0, // 0 means all rows\n      },\n    };\n  },\n});\n</script>\n
    Lo que estamos haciendo es settear unos datos, los nombres y estilos de las columnas, y los ajustes de la paginaci\u00f3n.

    "},{"location":"develop/basic/vuejs/#anadir-editar-y-eliminar-filas","title":"A\u00f1adir, editar y eliminar filas","text":"

    El c\u00f3digo final para esto, que m\u00e1s adelante explicaremos, es el siguiente:

    <template>\n  <q-page padding>\n    <q-table\n      hide-bottom\n      :rows=\"catalogData\"\n      :columns=\"columns\"\n      v-model:pagination=\"pagination\"\n      title=\"Cat\u00e1logo\"\n      class=\"my-sticky-header-table\"\n      no-data-label=\"No hay resultados\"\n      row-key=\"id\"\n    >\n      <template v-slot:top>\n        <q-btn flat round color=\"primary\" icon=\"add\" @click=\"showAdd = true\" />\n      </template>\n      <template v-slot:body=\"props\">\n        <q-tr :props=\"props\">\n          <q-td key=\"id\" :props=\"props\">{{ props.row.id }}</q-td>\n          <q-td key=\"name\" :props=\"props\">\n            {{ props.row.name }}\n            <q-popup-edit\n              v-model=\"props.row.name\"\n              title=\"Cambiar nombre\"\n              v-slot=\"scope\"\n            >\n              <q-input\n                v-model=\"scope.value\"\n                dense\n                autofocus\n                counter\n                @keyup.enter=\"scope.set\"\n              >\n                <template v-slot:append>\n                  <q-icon name=\"edit\" />\n                </template>\n              </q-input>\n            </q-popup-edit>\n          </q-td>\n          <q-td key=\"options\" :props=\"props\">\n            <q-btn\n              flat\n              round\n              color=\"negative\"\n              icon=\"delete\"\n              @click=\"showDeleteDialog(props.row)\"\n            />\n          </q-td>\n        </q-tr>\n      </template>\n    </q-table>\n    <q-dialog v-model=\"showDelete\" persistent>\n      <q-card>\n        <q-card-section class=\"row items-center\">\n          <q-icon\n            name=\"delete\"\n            size=\"sm\"\n            color=\"negative\"\n            @click=\"showDelete = true\"\n          />\n          <span class=\"q-ml-sm\">\n            \u00bfEst\u00e1s seguro de que quieres borrar este elemento?\n          </span>\n        </q-card-section>\n\n        <q-card-actions align=\"right\">\n          <q-btn flat label=\"Cancelar\" color=\"primary\" v-close-popup />\n          <q-btn\n            flat\n            label=\"Confirmar\"\n            color=\"primary\"\n            v-close-popup\n            @click=\"deleteGame\"\n          />\n        </q-card-actions>\n      </q-card>\n    </q-dialog>\n    <q-dialog v-model=\"showAdd\">\n      <q-card style=\"min-width: 350px\">\n        <q-card-section>\n          <div class=\"text-h6\">Nombre del juego</div>\n        </q-card-section>\n\n        <q-card-section class=\"q-pt-none\">\n          <q-input dense v-model=\"nameToAdd\" autofocus @keyup.enter=\"addGame\" />\n        </q-card-section>\n\n        <q-card-actions align=\"right\" class=\"text-primary\">\n          <q-btn flat label=\"Cancelar\" v-close-popup />\n          <q-btn flat label=\"A\u00f1adir juego\" v-close-popup @click=\"addGame\" />\n        </q-card-actions>\n      </q-card>\n    </q-dialog>\n  </q-page>\n</template>\n\n<script lang=\"ts\">\nimport { ref } from 'vue';\nimport { defineComponent } from 'vue';\n\nconst columns = [\n  { name: 'id', align: 'left', label: 'ID', field: 'id', sortable: true },\n  {\n    name: 'name',\n    align: 'left',\n    label: 'Nombre',\n    field: 'name',\n    sortable: true,\n  },\n  { name: 'options', align: 'left', label: 'Options', field: 'options' },\n];\n\nconst data = [\n  { id: 1, name: 'Dados' },\n  { id: 2, name: 'Fichas' },\n  { id: 3, name: 'Cartas' },\n  { id: 4, name: 'Rol' },\n  { id: 5, name: 'Tableros' },\n  { id: 6, name: 'Tem\u00e1ticos' },\n  { id: 7, name: 'Europeos' },\n  { id: 8, name: 'Guerra' },\n  { id: 9, name: 'Abstractos' },\n];\n\nexport default defineComponent({\n  name: 'CatalogPage',\n\n  setup() {\n    const catalogData = ref(data);\n    const showDelete = ref(false);\n    const showAdd = ref(false);\n    const nameToAdd = ref('');\n    const selectedRow = ref({});\n\n    const deleteGame = () => {\n      catalogData.value.splice(\n        catalogData.value.findIndex((i) => i.id === selectedRow.value.id),\n        1\n      );\n      showDelete.value = false;\n    };\n\n    const showDeleteDialog = (item: any) => {\n      selectedRow.value = item;\n      showDelete.value = true;\n    };\n\n    const addGame = () => {\n      catalogData.value.push({\n        id: Math.max(...catalogData.value.map((o) => o.id)) + 1,\n        name: nameToAdd.value,\n      });\n      nameToAdd.value = '';\n      showAdd.value = false;\n    };\n\n    return {\n      catalogData,\n      columns: columns,\n      pagination: {\n        page: 1,\n        rowsPerPage: 0, // 0 means all rows\n      },\n      showDelete,\n      showAdd,\n      nameToAdd,\n      showDeleteDialog,\n      deleteGame,\n      addGame,\n    };\n  },\n});\n</script>\n
    "},{"location":"develop/basic/vuejs/#anadir-fila","title":"A\u00f1adir fila","text":"

    Para esto hemos necesitado el primer template dentro del componente tabla para mostrar un bot\u00f3n que har\u00e1 que se muestre un dialog para introducir el nombre del juego que es el \u00faltimo q-dialog mostrado en el componente. Tanto al pulsar en el bot\u00f3n como al pulsar Enter se ejecutar\u00e1 la funci\u00f3n para a\u00f1adirlo llamada addGame, que se encarga de a\u00f1adirlo poni\u00e9ndole un id superior a cualquiera de los ya creados, el nombre seleccionado almacenado en la variable nameToAdd y de dejar de mostrar el dialog una vez realizado el proceso.

    "},{"location":"develop/basic/vuejs/#editar-fila","title":"Editar fila","text":"

    Para esto hemos necesitado el segundo template de dentro del componente (a excepci\u00f3n del \u00faltimo q-td). Este hace que cuando sea la columna id simplemente muestre su valor, pero en cambio cuando sea la del nombre, en caso de que se pulse sobre esa casilla se muestre un dialog con un campo de texto con el valor de la casilla pulsada.

    "},{"location":"develop/basic/vuejs/#borrar-fila","title":"Borrar fila","text":"

    Por \u00faltimo, para el borrado hemos necesitado el q-td con la key de options para mostrar un bot\u00f3n para ejecutar la funci\u00f3n showDeleteDialog pas\u00e1ndole el item completo de la fila seleccionada, este hace que se muestre el dialog y se almacene el item seleccionado y por \u00faltimo el dialog se encarga de realizar la pregunta de confirmaci\u00f3n para su posterior borrado. En caso de confirmarlo, la funci\u00f3n deleteGame busca la posici\u00f3n del item seleccionado y lo borra. Una vez hecho eso, limpia el valor de fila seleccionada y deja de mostrar el dialog.

    "},{"location":"develop/basic/vuejs/#conexion-con-backend","title":"Conexi\u00f3n con backend","text":"

    Antes de nada, para poder realizar peticiones vamos a tener que instalar: @vueuse/core.

    "},{"location":"develop/basic/vuejs/#recuperacion-de-datos","title":"Recuperaci\u00f3n de datos","text":"

    Vamos a proceder a modificar lo m\u00ednimo e indispensable para que los datos mostrados no sean los mockeados y vengan del back mediante esta petici\u00f3n:

    const { data } = useFetch('http://localhost:8080/game').get().json();\nwhenever(data, () => (catalogData.value = data.value));\n

    Tambi\u00e9n tendremos que modificar los campos a mostrar, ya que ya no es name, si no title el nombre del juego. Y tambi\u00e9n habr\u00e1 que mostrar la edad, la categor\u00eda y el autor.

    "},{"location":"develop/basic/vuejs/#edicion-de-una-fila","title":"Edici\u00f3n de una fila","text":"

    Solo modificaremos los campos referidos al juego (de momento) para que sea lo m\u00e1s sencillo posible, es decir, solo se modificar\u00e1 el t\u00edtulo y la edad tal y como lo hab\u00edamos hecho antes con el q-popup-edit.

    "},{"location":"develop/basic/vuejs/#creacion-de-una-nueva-fila","title":"Creaci\u00f3n de una nueva fila","text":"

    Ya que no tenemos en el back de Node realizado el back necesario para poder borrar una fila, terminaremos con el a\u00f1adido de una nueva fila.

    Para esto, tendremos que modificar la funci\u00f3n para a\u00f1adir, adem\u00e1s de eliminar la variable nameToAdd y modificar el dialog. As\u00ed deber\u00eda quedar la funci\u00f3n:

    const addGame = async () => {\n  const response = await useFetch('http://localhost:8080/game', {\n    method: 'PUT',\n    redirect: 'manual',\n    headers: {\n      accept: '*/*',\n      origin: window.origin,\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify(gameToAdd.value),\n  })\n    .put()\n    .json();\n\n  getGames();\n  gameToAdd.value = newGame;\n};\n

    Y as\u00ed el dialog:

    <q-dialog v-model=\"showAdd\">\n      <q-card style=\"width: 300px\" class=\"q-px-sm q-pb-md\">\n        <q-card-section>\n          <div class=\"text-h6\">Nuevo juego</div>\n        </q-card-section>\n\n        <q-item-label header>T\u00edtulo</q-item-label>\n        <q-item dense>\n          <q-item-section avatar>\n            <q-icon name=\"sports_esports\" />\n          </q-item-section>\n          <q-item-section>\n            <q-input dense v-model=\"gameToAdd.title\" autofocus />\n          </q-item-section>\n        </q-item>\n\n        <q-item-label header>Edad</q-item-label>\n        <q-item dense>\n          <q-item-section avatar>\n            <q-icon name=\"cake\" />\n          </q-item-section>\n          <q-item-section>\n            <q-slider\n              color=\"teal\"\n              v-model=\"gameToAdd.age\"\n              :min=\"0\"\n              :max=\"100\"\n              :step=\"1\"\n              label\n              label-always\n            />\n          </q-item-section>\n        </q-item>\n\n        <q-item-label header>Categor\u00eda</q-item-label>\n        <q-item dense>\n          <q-item-section avatar>\n            <q-icon name=\"category\" />\n          </q-item-section>\n          <q-item-section>\n            <q-select\n              name=\"category\"\n              v-model=\"gameToAdd.category.id\"\n              :options=\"categories\"\n              filled\n              clearable\n              emit-value\n              map-options\n              option-disable=\"inactive\"\n              option-value=\"id\"\n              option-label=\"name\"\n              color=\"primary\"\n              label=\"Category\"\n            />\n          </q-item-section>\n        </q-item>\n\n        <q-item-label header>Autor</q-item-label>\n        <q-item dense>\n          <q-item-section avatar>\n            <q-icon name=\"face\" />\n          </q-item-section>\n          <q-item-section>\n            <q-select\n              name=\"author\"\n              v-model=\"gameToAdd.author.id\"\n              :options=\"authors\"\n              filled\n              clearable\n              emit-value\n              map-options\n              option-disable=\"inactive\"\n              option-value=\"id\"\n              option-label=\"name\"\n              color=\"primary\"\n              label=\"Author\"\n            />\n          </q-item-section>\n        </q-item>\n\n        <q-card-actions align=\"right\" class=\"text-primary\">\n          <q-btn flat label=\"Cancelar\" v-close-popup />\n          <q-btn flat label=\"A\u00f1adir juego\" v-close-popup @click=\"addGame\" />\n        </q-card-actions>\n      </q-card>\n    </q-dialog>\n
    "},{"location":"develop/basic/vuejs/#ultimo-paso","title":"\u00daltimo paso","text":"

    Este resultado vamos a copiarlo y pegarlo en las pantallas de Categor\u00eda y Autor para que tengamos exactamente el mismo formato cambiando todo donde diga \u201cjuego\u201d o \u201cgame\u201d por su traducci\u00f3n a \u201ccategor\u00eda\u201d o \u201cautor\u201d.

    "},{"location":"develop/basic/vuejs/#ejercicio","title":"Ejercicio","text":"

    Al realizar el cambio descrito anteriormente podremos ver que no todo funciona, ya que el objeto que se env\u00eda para modificar no ser\u00eda correcto adem\u00e1s de que las tablas de Categor\u00eda y Autor s\u00ed que tienen una funci\u00f3n para poder borrar esas filas.

    El ejercicio se va a realizar en la pantalla de Categor\u00eda. Consta en, despu\u00e9s de haber realizado todos los cambios, hacer que a\u00f1ada, edite y borre las filas seg\u00fan sea necesario.

    El c\u00f3digo resultante deber\u00eda ser algo parecido al siguiente c\u00f3digo:

    <template>\n  <q-page padding>\n    <q-table\n      hide-bottom\n      :rows=\"categoriesData\"\n      :columns=\"columns\"\n      v-model:pagination=\"pagination\"\n      title=\"Cat\u00e1logo\"\n      class=\"my-sticky-header-table\"\n      no-data-label=\"No hay resultados\"\n      row-key=\"id\"\n    >\n      <template v-slot:top>\n        <q-btn flat round color=\"primary\" icon=\"add\" @click=\"showAdd = true\" />\n      </template>\n      <template v-slot:body=\"props\">\n        <q-tr :props=\"props\">\n          <q-td key=\"id\" :props=\"props\">{{ props.row.id }}</q-td>\n          <q-td key=\"name\" :props=\"props\">\n            {{ props.row.name }}\n            <q-popup-edit\n              v-model=\"props.row.name\"\n              title=\"Cambiar nombre\"\n              v-slot=\"scope\"\n            >\n              <q-input\n                v-model=\"scope.value\"\n                dense\n                autofocus\n                counter\n                @keyup.enter=\"editRow(props, scope, 'name')\"\n              >\n                <template v-slot:append>\n                  <q-icon name=\"edit\" />\n                </template>\n              </q-input>\n            </q-popup-edit>\n          </q-td>\n          <q-td key=\"options\" :props=\"props\">\n            <q-btn\n              flat\n              round\n              color=\"negative\"\n              icon=\"delete\"\n              @click=\"showDeleteDialog(props.row)\"\n            />\n          </q-td>\n        </q-tr>\n      </template>\n    </q-table>\n    <q-dialog v-model=\"showDelete\" persistent>\n      <q-card>\n        <q-card-section class=\"row items-center\">\n          <q-icon\n            name=\"delete\"\n            size=\"sm\"\n            color=\"negative\"\n            @click=\"showDelete = true\"\n          />\n          <span class=\"q-ml-sm\">\n            \u00bfEst\u00e1s seguro de que quieres borrar este elemento?\n          </span>\n        </q-card-section>\n\n        <q-card-actions align=\"right\">\n          <q-btn flat label=\"Cancelar\" color=\"primary\" v-close-popup />\n          <q-btn\n            flat\n            label=\"Confirmar\"\n            color=\"primary\"\n            v-close-popup\n            @click=\"deleteCategory\"\n          />\n        </q-card-actions>\n      </q-card>\n    </q-dialog>\n    <q-dialog v-model=\"showAdd\">\n      <q-card style=\"width: 300px\" class=\"q-px-sm q-pb-md\">\n        <q-card-section>\n          <div class=\"text-h6\">Nueva categor\u00eda</div>\n        </q-card-section>\n\n        <q-item-label header>Nombre</q-item-label>\n        <q-item dense>\n          <q-item-section avatar>\n            <q-icon name=\"category\" />\n          </q-item-section>\n          <q-item-section>\n            <q-input\n              dense\n              v-model=\"categoryToAdd.name\"\n              autofocus\n              @keyup.enter=\"addCategory\"\n            />\n          </q-item-section>\n        </q-item>\n        <q-card-actions align=\"right\" class=\"text-primary\">\n          <q-btn flat label=\"Cancelar\" v-close-popup />\n          <q-btn\n            flat\n            label=\"A\u00f1adir categor\u00eda\"\n            v-close-popup\n            @click=\"addCategory\"\n          />\n        </q-card-actions>\n      </q-card>\n    </q-dialog>\n  </q-page>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from 'vue';\nimport { useFetch, whenever } from '@vueuse/core';\n\nconst columns = [\n  { name: 'id', align: 'left', label: 'ID', field: 'id', sortable: true },\n  {\n    name: 'name',\n    align: 'left',\n    label: 'Nombre',\n    field: 'name',\n    sortable: true,\n  },\n  { name: 'options', align: 'left', label: 'Options', field: 'options' },\n];\nconst pagination = {\n  page: 1,\n  rowsPerPage: 0,\n};\nconst newCategory = {\n  name: '',\n  id: '',\n};\n\nconst categoriesData = ref([]);\nconst showDelete = ref(false);\nconst showAdd = ref(false);\nconst selectedRow = ref({});\nconst categoryToAdd = ref({ ...newCategory });\n\nconst getCategories = () => {\n  const { data } = useFetch('http://localhost:8080/category').get().json();\n  whenever(data, () => (categoriesData.value = data.value));\n};\ngetCategories();\n\nconst showDeleteDialog = (item: any) => {\n  selectedRow.value = item;\n  showDelete.value = true;\n};\n\nconst addCategory = async () => {\n  await useFetch('http://localhost:8080/category', {\n    method: 'PUT',\n    redirect: 'manual',\n    headers: {\n      accept: '*/*',\n      origin: window.origin,\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify(categoryToAdd.value),\n  })\n    .put()\n    .json();\n\n  getCategories();\n  categoryToAdd.value = newCategory;\n  showAdd.value = false;\n};\n\nconst editRow = (props: any, scope: any, field: any) => {\n  const row = {\n    name: props.row.name,\n  };\n  row[field] = scope.value;\n  scope.set();\n  editCategory(props.row.id, row);\n};\n\nconst editCategory = async (id: string, reqBody: any) => {\n  await useFetch(`http://localhost:8080/category/${id}`, {\n    method: 'PUT',\n    redirect: 'manual',\n    headers: {\n      accept: '*/*',\n      origin: window.origin,\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify(reqBody),\n  })\n    .put()\n    .json();\n\n  getCategories();\n};\n\nconst deleteCategory = async () => {\n  await useFetch(`http://localhost:8080/category/${selectedRow.value.id}`, {\n    method: 'DELETE',\n    redirect: 'manual',\n    headers: {\n      accept: '*/*',\n      origin: window.origin,\n      'Content-Type': 'application/json',\n    },\n  })\n    .delete()\n    .json();\n\n  getCategories();\n};\n</script>\n
    "},{"location":"develop/basic/vuejs/#depuracion","title":"Depuraci\u00f3n","text":"

    Una parte muy importante del desarrollo es tener la capacidad de depurar nuestro c\u00f3digo, en este apartado vamos a explicar como se realiza debug en Front.

    Esta parte se puede realizar con nuestro navegador favorito, en este caso vamos a utilizar Chrome.

    El primer paso es abrir las herramientas del desarrollador del navegador presionando F12.

    En esta herramienta tenemos varias partes importantes:

    • Elements: Inspector de los elementos del DOM de nuestra aplicaci\u00f3n que nos ayuda identificar el c\u00f3digo generado.
    • Console: Consola donde podemos ver mensajes importantes que nos ayudan a identificar posibles problemas.
    • Source: El navegador de ficheros que componen nuestra aplicaci\u00f3n.
    • Network: El registro de peticiones que realiza nuestra aplicaci\u00f3n.

    Identificados los elementos importantes, vamos a depurar la operaci\u00f3n de crear categor\u00eda.

    Para ello nos dirigimos a la pesta\u00f1a de Source, en el \u00e1rbol de carpetas nos dirigimos a la ruta donde est\u00e1 localizado el c\u00f3digo de nuestra aplicaci\u00f3n webpack://src/app.

    Dentro de esta carpeta est\u00e9 localizado todo el c\u00f3digo fuente de la aplicaci\u00f3n, en nuestro caso vamos a localizar componente category-edit.component que crea una nueva categor\u00eda.

    Dentro del fichero ya podemos a\u00f1adir puntos de ruptura (breakpoint), en nuestro caso queremos comprobar que el nombre introducido se captura bien y se env\u00eda al service correctamente.

    Colocamos el breakpoint en la l\u00ednea de invocaci\u00f3n del service (click sobre el n\u00famero de la l\u00ednea) y desde la interfaz creamos una nueva categor\u00eda.

    Hecho esto, podemos observar que a nivel de interfaz, la aplicaci\u00f3n se detiene y aparece un panel de manejo de los puntos de interrupci\u00f3n:

    En cuanto a la herramienta del desarrollador nos lleva al punto exacto donde hemos a\u00f1adido el breakpoint y se para en este punto ofreci\u00e9ndonos la posibilidad de explorar el contenido de las variables del c\u00f3digo:

    Aqu\u00ed podemos comprobar que efectivamente la variable category tiene el valor que hemos introducido por pantalla y se propaga correctamente hacia el service.

    Para continuar con la ejecuci\u00f3n basta con darle al bot\u00f3n de play del panel de manejo de interrupci\u00f3n o al que aparece dentro de la herramienta de desarrollo (parte superior derecha).

    Por \u00faltimo, vamos a revisar que la petici\u00f3n REST se ha realizado correctamente al backend, para ello nos dirigimos a la pesta\u00f1a Network y comprobamos las peticiones realizadas:

    Aqu\u00ed podemos observar el registro de todas las peticiones y haciendo click sobre una de ellas, obtenemos el detalle de esta.

    • Header: Informaci\u00f3n de las cabeceras enviadas (aqu\u00ed podemos ver que se ha hecho un PUT a la ruta correcta).
    • Payload: El cuerpo de la petici\u00f3n (vemos el cuerpo del mensaje con el nombre enviado).
    • Preview: Respuesta de la petici\u00f3n normalizada (vemos la respuesta con el identificador creado para la nueva categor\u00eda).
    "},{"location":"develop/basic/vuejsold/","title":"Listado simple - VUE","text":"

    Ahora que ya tenemos listo el proyecto frontend de VUE, ya podemos empezar a codificar la soluci\u00f3n.

    "},{"location":"develop/basic/vuejsold/#primeros-pasos","title":"Primeros pasos","text":"

    Antes de empezar

    Quiero hacer hincapi\u00e9 que VUE tiene una documentaci\u00f3n muy extensa y completa, as\u00ed que te recomiendo que hagas uso de ella cuando tengas cualquier duda. En la propia web de documentaci\u00f3n de VUE puedes buscar casi cualquier ejemplo que necesites.

    Si abrimos el proyecto con el IDE que tengamos (Visual Studio Code en el caso del tutorial) podemos ver que en la carpeta src/ existen unos ficheros ya creados por defecto. Estos ficheros son:

    • App.vue \u2192 contiene el c\u00f3digo inicial del proyecto.
    • main.ts \u2192 es el punto de entrada a la aplicaci\u00f3n.

    Lo primero que vamos a hacer es instalar SASS para poder trabajar con este preprocesador CSS, para ello tendremos que irnos a la terminal, en la misma carpeta donde tenemos el proyecto y ejecutar el siguiente comando:

    npm install -D sass\n

    Con esto ya lo tendremos instalado y para usarlo es tan f\u00e1cil como poner la etiqueta style de esta manera:

    <style lang=\"scss\"></style>  <--->  con Sass activado\n<style></style>  <--->  sin Sass, css normal\n

    En los estilos tambi\u00e9n veremos la propiedad scoped en VUE, el atributo scoped se utiliza para limitar el \u00e1mbito de los estilos de un componente a los elementos del propio componente y no a los elementos hijos o padres, lo que ayuda a evitar conflictos de estilo entre los diferentes componentes de una aplicaci\u00f3n.

    Esto significa que los estilos definidos en una etiqueta <style scoped> solo se aplicar\u00e1n a los elementos dentro del componente actual, y no se propagar\u00e1n a otros componentes en la jerarqu\u00eda del DOM. De esta manera, se puede evitar que los estilos de un componente afecten a otros componentes en la aplicaci\u00f3n.

    <style scoped></style>  <--->  Estos estilos solo afectar\u00e1n al componente donde se aplican\n<style></style>  <--->  Estos estilos son generales y afectan a toda la aplicaci\u00f3n.\n

    Con estas cositas sobre los estilos en cabeza vamos lo primero a limpiar la aplicaci\u00f3n para poder empezar a trabajar desde cero.

    • Entraremos en la carpeta assets y borraremos todos los archivos excepto base.css.
    • Entraremos en la carpeta components y borraremos todos los archivos dejando solo la carpeta que usaremos m\u00e1s adelante.
    • La carpeta router la dejaremos tal cual esta, sin tocar nada.
    • Entraremos en la carpeta views y borraremos todos los archivos dejando solo la carpeta que usaremos m\u00e1s adelante.

    Con esto tenemos nuestra estructura preparada y quedar\u00eda tal que asi:

    Vamos a a\u00f1adir unas l\u00edneas al tsconfig.json para que el typescript deje de marcarnos lo como error, lo dejaremos asi:

    tsconfig.json
    {\n  \"extends\": \"@vue/tsconfig/tsconfig.web.json\",\n  \"include\": [\"env.d.ts\", \"src/**/*\", \"src/**/*.vue\"],\n  \"compilerOptions\": {\n    \"preserveValueImports\": false,\n    \"importsNotUsedAsValues\": \"remove\",\n    \"verbatimModuleSyntax\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.node.json\"\n    }\n  ]\n}\n

    Para que la aplicaci\u00f3n funcione de nuevo y poder empezar a trabajar faltar\u00eda hacer un par de cositas que os explico:

    • En \u00e9l base.css no hace falta cambiar nada para que funcione, pero tenemos muchas cosas que seguramente no vamos a usar, este archivo lo conservamos solamente para trabajar en variables css todo el tema de los colores de nuestra web o algunas otras cositas como el ancho del menu o del header, etc\u2026 Lo primero vamos a eliminar todas las variables CSS y crearnos las nuestras propias con nuestro color primario y secundario tanto para botones y dem\u00e1s como para texto y tambi\u00e9n para el background principal. Tenemos que dejar nuestro archivo de esta manera:
    base.css
    :root {\n  --primary: #2a6fa8;\n  --secondary: #12abdb;\n  --text-ligth: #2c3e50;\n  --text-dark: #fff;\n  --background-color: #fff;\n}\n\n*,\n*::before,\n*::after {\n  box-sizing: border-box;\n  margin: 0;\n  position: relative;\n  font-weight: normal;\n}\n\nbody {\n  min-height: 100vh;\n  color: var(--text-ligth);\n  background: var(--background-color);\n  line-height: 1.6;\n  font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,\n    Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;\n  font-size: 16px;\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n
    • Despu\u00e9s vamos a abrir el archivo main.ts y cambiaremos el import que hace del CSS por el base que es el que estamos usando, quedar\u00eda de esta manera:
    main.ts
    import { createApp } from 'vue'\nimport App from './App.vue'\nimport router from './router'\n\nimport './assets/base.css'\n\nconst app = createApp(App)\n\napp.use(router)\n\napp.mount('#app')\n
    • Luego abriremos el archivo App.vue y lo dejaremos solo como la entrada a la aplicaci\u00f3n, esto ya son maneras de trabajar de cada uno, pero a m\u00ed me gusta hacerlo asi para tener si hiciera falta diferentes layouts, uno con header y men\u00fa, otro sin header y men\u00fa, otro de la parte de admin, etc\u2026 Lo dejaremos exactamente asi:
    App.vue
    <script setup lang=\"ts\">\nimport { RouterView } from 'vue-router'\n</script>\n\n<template>\n    <RouterView />\n</template>\n
    • Por \u00faltimo crearemos nuestro layout principal al que iremos a\u00f1adiendo luego toda nuestra aplicaci\u00f3n. Lo primero nos pondremos en src y crearemos una nueva carpeta llamada layouts, dentro de esta carpeta crearemos otra que se llamara main-layout (esto lo hacemos por si luego tenemos m\u00e1s de un layout que cada uno tenga su carpeta para tener sus propias cosas) y dentro de la carpeta main-layout crearemos el archivo MainLayout.vue, nos deber\u00eda de quedar asi:

    Una vez tenemos el archivo MainLayout.vue creado lo abriremos y escribiremos el siguiente c\u00f3digo:

    MainLayout.vue
    <script setup lang=\"ts\">\n    const helloWorld = 'Hola Mundo';\n</script>\n\n<template>\n    <h1>{{ helloWorld }}</h1>\n</template>\n

    Vamos a intentar explicar este c\u00f3digo un poco:

    • Dentro de las etiquetas script metemos todo el c\u00f3digo Javascript, en este caso como vamos a trabajar con Typescript le ponemos la etiqueta Lang=\u201dts\u201d para que el compilador sepa que estamos trabajando con Typescript.
    • Ponemos la palabra setup porque estamos trabajando con la composition api, en VUE podemos trabajar con la options api y con la composition api, nosotros vamos a usar la composition api que aunque al principio cuesta un poco m\u00e1s, luego nos va a hacer la vida much\u00edsimo m\u00e1s f\u00e1cil, sobre todo en aplicaciones \"reales\".
    • Dentro de las etiquetas template va el HTML y como estamos usando el m\u00e9todo setup no necesitamos retornar nada para poder acceder a ello desde la plantilla.

    Las llaves dobles permiten hacen un binding entre el c\u00f3digo del componente y la plantilla. Es decir, en este caso ir\u00e1 al c\u00f3digo TypeScript y buscar\u00e1 el valor de la variable helloWorld.

    Consejo

    El binding tambi\u00e9n nos sirve para ejecutar los m\u00e9todos de TypeScript desde el c\u00f3digo HTML. Adem\u00e1s, si el valor que contiene la variable se modificar\u00e1 durante la ejecuci\u00f3n de alg\u00fan m\u00e9todo, autom\u00e1ticamente el c\u00f3digo HTML refrescar\u00eda el nuevo valor de la variable helloWorld.

    Ponemos en marcha la aplicaci\u00f3n con npm run dev.

    Si abrimos el navegador y accedemos a http://localhost:5173/ podremos ver el resultado del c\u00f3digo.

    "},{"location":"develop/basic/vuejsold/#layout-general","title":"Layout general","text":""},{"location":"develop/basic/vuejsold/#crear-componente","title":"Crear componente","text":"

    Lo primero que vamos a hacer es escoger un tema y una paleta de componentes para trabajar. VUE no tiene una librer\u00eda de componentes oficial al igual que, por ejemplo, Angular tiene Material, por lo que podremos elegir entre las diferentes opciones y ver la que m\u00e1s se ajusta a las necesidades del proyecto o crearnos la nuestra propia, si entramos en proyectos ya comenzados, seguramente este paso ya habr\u00e1 sido abordado y ya sabr\u00e1s con qu\u00e9 librer\u00eda de componentes trabajar, para este proyecto vamos a optar por PrimeVue, no tenemos ning\u00fan motivo especial para decidir esa en especial, pero la hemos usado en un curso anterior y optamos por seguir con la misma librer\u00eda.

    Para instalarla bastar\u00e1 con seguir los pasos de su documentaci\u00f3n.

    Vamos a hacerlo y la instalamos en nuestro proyecto:

    • Lo primero ejecutaremos el comando npm o yarn para instalarla, en mi caso lo hare con npm:
    • npm install primevue\n
    • Despu\u00e9s instalaremos PrimeVue con la funci\u00f3n use en el main.ts que es donde tenemos nuestra configuraci\u00f3n, quedando asi nuestro main.ts:

    • Despu\u00e9s a\u00f1adiremos los estilos necesarios a nuestro main.ts:

    • Por \u00faltimo en nuestro base.css cambiaremos la fuente del proyecto por la que trae el tema de PrimeVue, cambiando en el body la l\u00ednea:
    base.css
    ...\n font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,\n Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;\n...\n

    Por:

    base.css
    ...\nfont-family: (--font-family);\n...\n

    Recuerda

    Al a\u00f1adir una nueva librer\u00eda tenemos que parar el servidor y volver a arrancarlo para que compile y pre-cargue las nuevas dependencias.

    Una vez a\u00f1adida la dependencia, lo que queremos es crear una primera estructura inicial a la p\u00e1gina. Si te acuerdas cual era la estructura (y si no te acuerdas, vuelve a la secci\u00f3n Contexto de la aplicaci\u00f3n y lo revisas), ten\u00edamos una cabecera superior con un logo y t\u00edtulo y unas opciones de men\u00fa.

    Antes de empezar a crear y programar vamos a instalar unas extensiones en Visual Studio Code que nos har\u00e1n la vida mucho mas f\u00e1cil, en cada una de ellas podeis ver una descripci\u00f3n de que hacen y para que sirven, tu ya dices si la quieres instalar o no, nosotros vamos a trabajar con ellas y por eso te las recomendamos:

    • Vue Volar extension Pack
    • Vue Discovery
    • IntelliCode
    • npm Intellisense
    • Vue VSCode Snippets

    Para poder seguir trabajando con comodidad vamos a a\u00f1adir una fuente de iconos para todos los iconitos que usemos en la aplicaci\u00f3n, nosotros vamos a usar Material porque es la que estamos acostumbrados, para a\u00f1adirla tenemos una gu\u00eda.

    Lo haremos paso a paso:

    • Lo primero a\u00f1adimos al index.html la fuente a trav\u00e9s de Google fonts, hay muchas otras maneras de hacerlo, como bajarla y servirla desde local, pero para este tutorial vamos a usar esta por ser la m\u00e1s f\u00e1cil, para a\u00f1adirla pegaremos en el index.html esta l\u00ednea:
    <link href=\"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\" rel=\"stylesheet\" />\n

    Quedando de esta manera:

    index.html
    <!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" href=\"/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <link\n      href=\"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\"\n      rel=\"stylesheet\"\n    />\n    <title>Vite App</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/main.ts\"></script>\n  </body>\n</html>\n

    Para que no nos salga el error de comments, a\u00f1adiremos al eslintrc.js estas l\u00edneas:

    eslintrc.js
    ...\nrules: {\n    'vue/comment-directive': 'off'\n}\n...\n

    Despu\u00e9s nos iremos al fichero base.css y a\u00f1adiremos esto al final del archivo:

    base.css
    ...\n.material-symbols-outlined {\n  font-family: \"Material Symbols Outlined\", sans-serif;\n  font-weight: normal;\n  font-style: normal;\n  font-size: 24px;  /* Preferred icon size */\n  display: inline-block;\n  line-height: 1;\n  text-transform: none;\n  letter-spacing: normal;\n  word-wrap: normal;\n  white-space: nowrap;\n  direction: ltr;\n}\n

    Con esto ya tendremos a\u00f1adida la fuente material-symbols y podremos usar todos los iconos disponibles.

    • Despu\u00e9s instalaremos tambi\u00e9n los iconos de PrimeVue para poder usarlos f\u00e1cilmente en los componentes, lo primero pondremos:
    npm install primeicons\n

    Una vez instalados, importaremos los iconos en el main.ts poniendo este import debajo de todos los de css:

    main.ts
    ...\nimport 'primeicons/primeicons.css';\n...\n

    Con esto ya lo tendr\u00edamos todo.

    Pues vamos a ello, con las extensiones ya instaladas y la fuente para los iconos a\u00f1adida crearemos esa estructura com\u00fan para toda la aplicaci\u00f3n.

    Lo primero crearemos el componente header, dentro de la carpeta components al ser un m\u00f3dulo de la aplicaci\u00f3n y no especifico de una vista o p\u00e1gina. Para eso crearemos una nueva carpeta dentro de components que llamaremos header, nos situaremos encima de la carpeta header y crearemos el archivo HeaderComponent.vue, con el archivo vac\u00edo escribiremos, vbase-3-ts-setup y conforme lo escribimos nos aparecer\u00e1 esto:

    Consejo

    Esto nos aparece gracias a las extensiones que hemos instalado, aseg\u00farate de instalarlas para que aparezca o si no las quieres instalar lo puedes crear a mano. Si no te aparece y has instalado las extensiones, cierra vscode y vu\u00e9lvelo a abrir.

    Podemos seleccionar vbase-3-ts-setup, esto es un snippet que lo que har\u00e1 es generarnos todo el c\u00f3digo de un componente vac\u00edo y lo dejara asi:

    HeaderComponent.vue
    <template>\n    <div>\n\n    </div>\n</template>\n\n<script setup lang=\"ts\">\n\n</script>\n\n<style scoped>\n\n</style>\n

    Con esto solo nos faltar\u00eda agregar a la etiqueta style que vamos a trabajar con Sass y la dejar\u00edamos asi:

    HeaderComponent.vue
    ...\n<style lang=\"scss\" scoped>\n\n</style>\n...\n

    Si os dais cuenta hemos a\u00f1adido Lang=\u201dscss\u201d y con esto ya estamos preparados para crear nuestro componente.

    Para continuar cambiaremos el c\u00f3digo del HeaderComponent.vue por este:

    HeaderComponent.vue
    <template>\n  <div class=\"card relative z-2\">\n    <Menubar :model=\"items\">\n      <template #start>\n        <span class=\"material-symbols-outlined\">storefront</span>\n        <span class=\"title\">LUDOTECA TAN</span>\n      </template>\n      <template #end>\n        <Avatar icon=\"pi pi-user\" class=\"mr-2 avatar-image\" size=\"large\" shape=\"circle\" />\n        <span class=\"sign-text\">Sign in</span>\n      </template>\n    </Menubar>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport Menubar from \"primevue/menubar\";\nimport Avatar from \"primevue/avatar\";\n\nconst items = ref([\n  {\n    label: \"Cat\u00e1logo\",\n  },\n  {\n    label: \"Categor\u00edas\",\n  },\n  {\n    label: \"Autores\",\n  },\n]);\n</script>\n\n<style lang=\"scss\" scoped>\n.p-menubar {\n  padding: 0.5rem;\n  background: var(--primary);\n  color: var(--text-dark);\n  border: none;\n  border-radius: 0px;\n}\n\n.title {\n  margin-left: 1rem;\n  font-weight: 600;\n}\n\n.avatar-image {\n  background-color: var(--secondary);\n  color: var(--text-dark);\n  border: 1px solid var(--text-dark);\n  cursor: pointer;\n}\n\n.sign-text {\n  color: var(--text-dark);\n  margin-left: 1rem;\n  cursor: pointer;\n}\n\n:deep(.p-menubar-start) {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: center;\n  margin-right: 1rem;\n}\n\n:deep(.p-menubar-end) {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: center;\n}\n\n:deep(.p-menuitem-text) {\n  color: var(--text-dark) !important;\n}\n\n:deep(.p-menuitem-content:hover) {\n  background: var(--secondary) !important;\n}\n\n.material-symbols-outlined {\n  font-size: 36px;\n}\n</style>\n

    Intentaremos explicarlo un poco:

    En el template estamos a\u00f1adiendo el Menubar de la librer\u00eda de componentes que estamos utilizando, si queremos saber como se a\u00f1ade podemos verlo en este link.

    Veremos que lo primero que hacemos es el import dentro de las etiquetas <script> para poder tener el componente disponible y poder usarlo.

    HeaderComponent.vue
    ...\nimport Menubar from \"primevue/menubar\";\n...\n

    Luego, con el import ya hecho, podemos copiar el HTML que nos dan y ponerlo en nuestro componente:

    HeaderComponent.vue
    ...\n<div class=\"card relative z-2\">\n    <Menubar :model=\"items\">\n        <template #start>\n            <span class=\"material-symbols-outlined\">storefront</span>\n            <span class=\"title\">LUDOTECA TAN</span>\n        </template>\n        <template #end>\n            <Avatar icon=\"pi pi-user\" class=\"mr-2 avatar-image\" size=\"large\" shape=\"circle\" />\n            <span class=\"sign-text\">Sign in</span>\n        </template>\n    </Menubar>\n</div>\n...\n

    Si os dais cuenta es el c\u00f3digo que ellos nos dan retocado para cubrir nuestras necesidades, primero hemos metido un icono de material dentro del template #start que es lo que se situara al principio pegado a la izquierda del Menubar y tras el icono metemos el t\u00edtulo.

    El template #end se situar\u00e1 al final pegado a la derecha y alli estamos metiendo otro componente de la librer\u00eda de componentes, pod\u00e9is ver la info de como usarlo en este link.

    Este simplemente lo pegamos como esta y le a\u00f1adimos detr\u00e1s la frase Sign in.

    En la parte del script metemos todo nuestro Javascript/Typescript:

    HeaderComponent.vue
    ...\n<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport Menubar from \"primevue/menubar\";\nimport Avatar from \"primevue/avatar\";\n\nconst items = ref([\n  {\n    label: \"Cat\u00e1logo\",\n  },\n  {\n    label: \"Categor\u00edas\",\n  },\n  {\n    label: \"Autores\",\n  },\n]);\n</script>\n...\n

    Si os dais cuenta, lo \u00fanico que estamos haciendo son los imports necesarios para que todo funcione y creando una variable \u00edtems que es la que luego estamos usando en el men\u00fa para pintar los diferentes menus. Si os dais cuenta envolvemos el valor de la variable dentro de ref(). En Vue 3, la funci\u00f3n ref() se utiliza para crear una referencia reactiva a un valor. Una referencia reactiva es un objeto que puede ser pasado como prop, utilizado en una plantilla, y observado para detectar cambios en su valor.

    La funci\u00f3n ref() toma un valor como argumento y devuelve un objeto con una propiedad value que contiene el valor proporcionado. Por ejemplo, si queremos crear una referencia a un n\u00famero entero, podemos hacer lo siguiente:

    import { ref } from 'vue'\nconst myNumber = ref(42)\nconsole.log(myNumber.value) // 42\n

    La referencia myNumber es ahora un objeto con una propiedad value que contiene el valor 42. Si cambiamos el valor de la propiedad value, la referencia notificar\u00e1 a cualquier componente que est\u00e9 observando el valor que ha cambiado. Por ejemplo:

    myNumber.value = 21\nconsole.log(myNumber.value) // 21\n

    Cualquier componente que est\u00e9 utilizando myNumber se actualizar\u00e1 autom\u00e1ticamente para reflejar el nuevo valor. La funci\u00f3n ref() es muy \u00fatil en Vue 3 para crear referencias reactivas a valores que pueden cambiar con el tiempo.

    En los styles tenemos poco que explicar, simplemente estamos haciendo que se vea como nosotros queremos, que todos los colores y dem\u00e1s los traemos de las variables que hemos creado antes en el base.css y adem\u00e1s me gustar\u00eda mencionar una cosa:

    HeaderComponent.vue
    ...\n:deep(.p-menubar-start) {\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    justify-content: center;\n    margin-right: 1rem;\n}\n...\n

    Si os dais cuenta algunos estilos llevan el :Deep delante, como seguro ya sabes, puedes utilizar el atributo scoped dentro de la etiqueta <style> para escribir CSS y as\u00ed impedir que tus estilos afecten a posibles sub-componentes. Pero, \u00bfqu\u00e9 ocurre si necesitas que al menos una regla s\u00ed afecte a tu componente hijo?. Para ello puedes usar la pseudo-clase :deep de Vue 3.

    En este ejemplo lo hemos creado asi para que sepas de su existencia y busques un poco de informaci\u00f3n sobre ella y las otras que existen, este CSS lo podr\u00edamos poner en el styles.scss principal y no tendr\u00edamos que poner el :deep que seria lo mas recomendado. Es importante tener en cuenta que la directiva :deep puede tener un impacto en el rendimiento, ya que Vue necesita buscar en todo el \u00e1rbol de elementos para aplicar los estilos. Por lo tanto, se recomienda utilizar esta directiva con moderaci\u00f3n y solo en casos en los que sea necesario seleccionar elementos anidados de forma din\u00e1mica. Tenerlo en cuenta y solo usarla cuando de verdad sea necesario.

    Ya por \u00faltimo nos iremos a nuestro MainLayout.vue y a\u00f1adiremos el header que acabamos de crearnos:

    MainLayout.vue
    <script setup lang=\"ts\">\n    import HeaderComponent from '@/components/header/HeaderComponent.vue';\n\n    const helloWorld = 'Hola Mundo';\n</script>\n\n<template>\n    <HeaderComponent></HeaderComponent>\n    <h1>{{ helloWorld }}</h1>\n</template>\n

    Como antes, lo \u00fanico que hacemos es importar el componente en el script y usarlo en el HTML.

    Lo siguiente iremos a la carpeta router, al archivo index.ts y lo dejaremos asi:

    index.ts
    import { createRouter, createWebHistory } from 'vue-router'\nimport MainLayout from '@/layouts/main-layout/MainLayout.vue'\n\nconst router = createRouter({\n    history: createWebHistory(import.meta.env.BASE_URL),\n    routes: [\n        {\n            path: '/',\n            name: 'home',\n            component: MainLayout\n        }\n    ]\n})\n\nexport default router\n

    Hemos cambiado la ruta principal para que apunte a nuestro layout y nada m\u00e1s entrar en la aplicaci\u00f3n lo carguemos gracias al router de VUE.

    Si guardamos todo y ponemos en marcha el proyecto ya veremos algo como esto:

    "},{"location":"develop/basic/vuejsold/#creando-un-listado-basico","title":"Creando un listado b\u00e1sico","text":""},{"location":"develop/basic/vuejsold/#crear-componente_1","title":"Crear componente","text":"

    Ya tenemos la estructura principal, ahora vamos a crear nuestra primera pantalla. Vamos a empezar por la de Categor\u00edas que es la m\u00e1s sencilla, ya que se trata de un listado, que muestra datos sin filtrar ni paginar.

    Como categor\u00edas es un dominio funcional de la aplicaci\u00f3n, vamos a crear una nueva carpeta dentro de la carpeta views llamada categories, todas las pantallas, componentes y servicios que creemos, referidos a este dominio funcional, deber\u00e1n ir dentro del m\u00f3dulo categories. Dentro de esa carpeta crearemos un fichero que se llamara CategoriesView.vue y dentro nos crearemos el esqueleto de la misma manera que hicimos anteriormente.

    Escribiremos vbase-3-ts-setup, le daremos al enter y nos generara toda la estructura a la que solo faltara agregar a la etiqueta <style> Lang=\u201dscss\u201d para decirle que vamos a trabajar con SASS. Con esto tenemos nuestra vista preparada para empezar a trabajar.

    Lo primero vamos a conectar nuestro componente al router para que cuando hagamos click en el men\u00fa correspondiente podamos llegar hasta \u00e9l y tambi\u00e9n para poder ver lo que vamos trabajando. Para ello lo primero que vamos a hacer en el template de nuestro componente es a\u00f1adir cualquier cosa para saber que estamos donde toca, por ejemplo:

    CategoriesView.vue
    <template>\n  <div>SOY CATEGORIAS</div>\n</template>\n

    Con esto cuando entremos en la ruta de categor\u00edas deber\u00edamos ver SOY CATEGORIAS.

    Lo siguiente crearemos en el layout un sitio para cargar todas nuestras rutas que van a ir dentro de ese layout, para ello iremos al archivo MainLayout.vue y a\u00f1adiremos un <RouterView /> que ser\u00e1 el segundo de nuestra aplicaci\u00f3n, el primero lo tenemos en el App.vue que servir\u00e1 para cargar nuestras rutas principales (diferentes layouts, pagina 404, etc) y el segundo es este que acabamos de crear, podemos tener tantos como queramos en una aplicaci\u00f3n y cada uno tendr\u00e1 su cometido. Este que acabamos de crear ser\u00e1 donde se cargaran todas las rutas que quieran estar dentro del layout principal.

    Para crearlo importaremos \u00e9l RouterView dentro de los <script> desde vue-router:

    MainLayout.vue
    import { RouterView } from 'vue-router';\n

    Lo a\u00f1adiremos dentro de los <template> exactamente donde queramos cargar las rutas y si puede ser con un div padre que haga de contenedor asi podremos darle los estilos sin sufrir demasiado.

    MainLayout.vue
    <div class=\"outlet-container\">\n    <RouterView />\n</div>\n

    Y luego dentro de <style> le daremos estilo al contenedor padre de acuerdo a lo que necesitemos (grid, flex, etc\u2026) en este ejemplo para hacerlo f\u00e1cil lo haremos con flex, con todo esto quedar\u00eda asi:

    MainLayout.vue
    <script setup lang=\"ts\">\nimport { RouterView } from 'vue-router';\nimport HeaderComponent from \"@/components/header/HeaderComponent.vue\";\n</script>\n\n<template>\n  <HeaderComponent></HeaderComponent>\n  <div class=\"outlet-container\">\n    <RouterView />\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n.outlet-container {\n  display: flex;\n  flex-direction: column;\n  flex-grow: 1;\n  width: 100%;\n  min-height: calc(100vh - 65px);\n  padding: 1rem;\n}\n</style>\n

    Ahora vamos a a\u00f1adirlo a nuestras rutas, para ello nos vamos a la carpeta router y dentro tendremos el index.ts con nuestras rutas actuales, vamos a a\u00f1adir la nueva ruta como hija de layout para que siempre se muestre dentro del layout que hemos creado con \u00e9l header:

    index.ts
    import { createRouter, createWebHistory } from 'vue-router'\nimport MainLayout from '@/layouts/main-layout/MainLayout.vue'\n\nconst router = createRouter({\n    history: createWebHistory(import.meta.env.BASE_URL),\n    routes: [\n        {\n            path: '/',\n            name: 'home',\n            component: MainLayout,\n            children: [\n                {\n                    path: '/categories',\n                    name: 'categories',\n                    component: () => import('../views/categories/CategoriesView.vue')\n                }\n            ]\n        }\n    ]\n})\n\nexport default router\n

    Si os dais cuenta lo hemos a\u00f1adido como hijo de layout y adem\u00e1s lo hemos hecho con lazy loading, es decir, este componente solo se cargara cuando el usuario navegue a esa ruta, asi evitamos cargas much\u00edsimo m\u00e1s grandes al inicio de la aplicaci\u00f3n.

    Posteriormente nos iremos al HeaderComponent.vue y a\u00f1adiremos la ruta a los \u00edtems del men\u00fa de esta manera:

    HeaderComponent.vue
    const items = ref([\n    {\n        label: \"Cat\u00e1logo\",\n    },\n    {\n        label: \"Categor\u00edas\",\n        to: { name: 'categories'}\n    },\n    {\n        label: \"Autores\",\n    },\n]);\n

    Si nos fijamos hemos a\u00f1adido la navegaci\u00f3n por el nombre de ruta en el men\u00fa categor\u00edas para que sepa cuando apretemos ese men\u00fa donde nos tiene que llevar.

    Con todo esto si ponemos en marcha nuestra aplicaci\u00f3n, ya podremos navegar haciendo click en el men\u00fa Categor\u00edas a esta nueva ruta que hemos creado y ya ver\u00edamos el SOY CATEGORIAS pero tenemos un problemilla en los menus, cuando apretamos un men\u00fa se pone el fondo gris, lo cual no nos gusta y adem\u00e1s aunque estemos en categor\u00edas si apretamos en otro men\u00fa se pone el otro gris y se quita el categor\u00edas lo cual tampoco es lo deseado ya que queremos que se quede marcado el men\u00fa donde estamos actualmente para informaci\u00f3n del usuario. Para ello nos iremos al base.css y a\u00f1adiremos al final estas l\u00edneas:

    base.css
    ...\n.router-link-active {\n    background: var(--secondary);\n    border-radius: 5px;\n}\n\n.p-menuitem.p-focus > .p-menuitem-content:not(:hover) {\n    background: transparent !important;\n}\n

    En Vue 3, la directiva router-link-active se utiliza para establecer una clase CSS en un enlace de router activo, con esto ya tendremos resuelto el problema y todo estar\u00e1 funcionando como toca y poniendo en marcha la aplicaci\u00f3n y haciendo click en el men\u00fa Categor\u00edas ya deber\u00edamos ver esto:

    "},{"location":"develop/basic/vuejsold/#codigo-de-la-pantalla","title":"C\u00f3digo de la pantalla","text":"

    Ahora vamos a construir la pantalla. Para manejar la informaci\u00f3n del listado, necesitamos tipar los datos para que Typescript no se queje. Para ello crearemos un fichero en categories\\models\\category-interface.ts donde implementaremos la interface necesaria. Esta interface ser\u00e1 la que utilizaremos para tipar el c\u00f3digo de nuestro componente.

    category-interface.ts
    export interface Category {\n    id: number\n    name: string\n}\n

    Tambi\u00e9n, escribiremos el c\u00f3digo de CategoriesView.vue:

    CategoriesView.vue
    <template>\n  <div class=\"card\">\n    <DataTable\n      v-model:editingRows=\"editingRows\"\n      :value=\"categories\"\n      tableStyle=\"min-width: 50rem\"\n      editMode=\"row\"\n      dataKey=\"id\"\n      @row-edit-save=\"onRowEditSave\"\n    >\n      <Column field=\"id\" header=\"IDENTIFICADOR\">\n        <template #editor=\"{ data, field }\">\n          <InputText v-model=\"data[field]\" />\n        </template>\n      </Column>\n      <Column field=\"name\" header=\"NOMBRE CATEGOR\u00cdA\">\n        <template #editor=\"{ data, field }\">\n          <InputText v-model=\"data[field]\" />\n        </template>\n      </Column>\n      <Column\n        :rowEditor=\"true\"\n        style=\"width: 110px\"\n        bodyStyle=\"text-align:center\"\n      ></Column>\n      <Column\n        style=\"width: 30px; padding: 0px 2rem 0px 0px; color: red\"\n        bodyStyle=\"text-align:center\"\n      >\n        <template #body=\"{ data }\">\n          <i class=\"pi pi-times\" @click=\"onRowDelete(data)\"></i>\n        </template>\n      </Column>\n    </DataTable>\n  </div>\n  <div class=\"actions\">\n    <Button label=\"Nueva categor\u00eda\" />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport DataTable, { type DataTableRowEditSaveEvent } from \"primevue/datatable\";\nimport Column from \"primevue/column\";\nimport InputText from \"primevue/inputtext\";\nimport Button from 'primevue/button';\nimport type { CategoryInterface } from \"./model/category.interface\";\nconst categories = ref([]);\nconst editingRows = ref([]);\nconst onRowEditSave = (event: DataTableRowEditSaveEvent) => {\n  console.log(event);\n};\nconst onRowDelete = (data: CategoryInterface) => {\n  console.log(data);\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.actions {\n  display: flex;\n  flex-direction: row;\n  margin-top: 1rem;\n  justify-content: flex-end;\n}\n\n.p-button {\n  background: var(--primary);\n  border: 1px solid var(--primary);\n\n  &:enabled {\n    &:hover {\n      background: var(--secondary);\n      border-color: var(--secondary);\n    }\n  }\n}\n</style>\n

    Intentaremos explicar un poco el c\u00f3digo:

    Lo primero vamos a importar el componente DataTable desde la librer\u00eda de componentes que estamos usando, para ello podemos ver algunos ejemplos de como hacerlo en la documentaci\u00f3n oficial

    Nosotros hemos puesto las importaciones que necesitamos en el <script>:

    CategoriesView.vue
    import DataTable, { type DataTableRowEditSaveEvent } from \"primevue/datatable\";\nimport Column from \"primevue/column\";\n

    Hemos creado nuestra tabla con las exigencias de la aplicaci\u00f3n, hemos puesto dos columnas, la columna identificador donde en \u00e9l header=\u201d\u201d le ponemos que nombre se muestra en la cabecera y le hemos dicho que debe mostrar en ella el dato id poni\u00e9ndolo en \u00e9l field=\u201d\u201d.

    CategoriesView.vue
    <Column field=\"id\" header=\"IDENTIFICADOR\"></Column>\n

    Como a la tabla le hemos dicho que debe ser editable con:

    CategoriesView.vue
    editMode=\"row\"\n

    Le decimos a esta columna que debe hacer cuando entremos en modo de edici\u00f3n, con el template le decimos que mostrara un InputText que es otro componente de la librer\u00eda de componentes que viene a ser un input de toda la vida donde podemos escribir texto para editar el valor, quedando al final asi:

    CategoriesView.vue
    <Column field=\"id\" header=\"IDENTIFICADOR\">\n    <template #editor=\"{ data, field }\">\n        <InputText v-model=\"data[field]\" />\n    </template>\n</Column>\n

    Luego hemos creado dos columnas, una que tiene el l\u00e1piz y activa el modo edici\u00f3n:

    CategoriesView.vue
    <Column\n    :rowEditor=\"true\"\n    style=\"width: 110px\"\n    bodyStyle=\"text-align:center\">\n</Column>\n

    Y otra que tiene la X y lo que har\u00e1 ser\u00e1 borrar la fila:

    CategoriesView.vue
    <Column\n    style=\"width: 30px; padding: 0px 2rem 0px 0px; color: red\"\n    bodyStyle=\"text-align:center\"\n>\n    <template #body=\"{ data }\">\n    <i class=\"pi pi-times\" @click=\"onRowDelete(data)\"></i>\n    </template>\n</Column>\n

    Al final a\u00f1adimos otro contenedor que vale para alojar los botones como en nuestro caso el de crear nueva categor\u00eda, el bot\u00f3n es tambi\u00e9n un componente de la librer\u00eda por lo que tendremos que hacer su import en la etiqueta <script>:

    CategoriesView.vue
    <div class=\"actions\">\n    <Button label=\"Nueva categor\u00eda\" />\n</div>\n

    Si abrimos el navegador y accedemos a http://localhost:5173/ y pulsamos en el men\u00fa de Categor\u00edas obtendremos una pantalla con un listado vac\u00edo (solo con cabeceras) y un bot\u00f3n de crear Nueva Categor\u00eda que a\u00fan no hace nada.

    "},{"location":"develop/basic/vuejsold/#anadiendo-datos","title":"A\u00f1adiendo datos","text":"

    En este punto y para ver como responde el listado, vamos a a\u00f1adir datos. Si tuvi\u00e9ramos el backend implementado podr\u00edamos consultar los datos directamente de una operaci\u00f3n de negocio de backend, pero ahora mismo no lo tenemos implementado as\u00ed que para no bloquear el desarrollo vamos a mockear los datos.

    En Vue para conectar a APIS externas solemos usar una librer\u00eda llamada Axios, lo primero que haremos ser\u00e1 descargarla e instalarla como indica en su documentaci\u00f3n oficial.

    Para instalarla simplemente nos iremos a la terminal dentro de la carpeta donde tenemos el proyecto y pondremos:

    npm install axios\n

    Con esto ya podremos ver que se ha a\u00f1adido a nuestro package.json, luego crearemos una carpeta api dentro de la carpeta src y dentro de la carpeta api crearemos el archivo app-api.ts. Dentro de este archivo vamos a inicializar nuestra config de la API y guardaremos todos los par\u00e1metros iniciales conforme nos vayan haciendo falta, de momento pondremos solo este c\u00f3digo:

    app-api.ts
    import axios from 'axios';\n\nexport const appApi = axios.create({\n    baseURL: 'http://localhost:8080',\n});\n

    Si os dais cuenta lo \u00fanico que hacemos es importar axios que acabamos de instalarlo y definir nuestra url base del api para no tener que escribirla cada vez y para s\u00ed alg\u00fan d\u00eda cambia, tener que cambiarla solo en un sitio y no en todos los servicios que la usen.

    Para mockear los datos con axios usaremos una librer\u00eda que se llama axios-mock-adapter y la pod\u00e9is encontrar en este link.

    Para instalarla lo haremos con npm como siempre, pondremos esta orden en el terminal y enter:

    npm install axios-mock-adapter --save-dev\n

    Si nos vamos al package.json veremos que ya la tenemos en las devDependencies, la diferencia entre estas y las dependencias es que las dependencias las necesitamos en el proyecto y estar\u00e1n en nuestro bundle final que serviremos a la gente, las devDependencies se usan solo mientras programamos y no entraran en el bundle final. Los mocks los usaremos solo en el desarrollo y hasta que podamos conectar con la API real por eso los metemos en las devDependencies.

    "},{"location":"develop/basic/vuejsold/#mockeando-datos","title":"Mockeando datos","text":"

    Como hemos comentado anteriormente, el backend todav\u00eda no est\u00e1 implementado as\u00ed que vamos a mockear datos. Nos crearemos un fichero mock-categories.ts dentro de views/categories/mocks, con datos ficticios y crearemos una llamada a la API que nos devuelva estos datos. De esta forma, cuando tengamos implementada la operaci\u00f3n de negocio en backend, tan solo tenemos que sustituir el c\u00f3digo que devuelve datos est\u00e1ticos por una llamada HTTP.

    Dentro de la carpeta mocks crearemos el archivo mock-categories.ts con el siguiente c\u00f3digo:

    mock-categories.ts
    import type { Category } from \"@/views/categories/models/category-interface\";\n\nexport const CATEGORY_DATA_MOCK: Category[] = [\n    { id: 1, name: 'Dados' },\n    { id: 2, name: 'Fichas' },\n    { id: 3, name: 'Cartas' },\n    { id: 4, name: 'Rol' },\n    { id: 5, name: 'Tableros' },\n    { id: 6, name: 'Tem\u00e1ticos' },\n    { id: 7, name: 'Europeos' },\n    { id: 8, name: 'Guerra' },\n    { id: 9, name: 'Abstractos' },\n]\n

    Despu\u00e9s nos crearemos un composable que usaremos para llamar a la API y poder reutilizarlo en otros componentes si hiciera falta. Dentro de la carpeta categories crearemos otra carpeta llamada composables y dentro crearemos un archivo llamado categories-composable.ts, en ese archivo escribiremos este c\u00f3digo:

    categories-composable.ts
    import appApi from '@/api/app-api'\nimport MockAdapter from 'axios-mock-adapter';\nimport { CATEGORY_DATA_MOCK } from '@/views/categories/mocks/mock-categories'\n\nconst mock = new MockAdapter(appApi);\n\nconst useCategoriesApiComposable = () => {\nmock.onGet(\"/category\").reply(200, CATEGORY_DATA_MOCK);\n\n    const getCategories = async () => {\n        const categories = await appApi.get(\"/category\");\n        return categories.data;\n    };\n\n    return {\n        getCategories\n    }\n}\n\nexport default useCategoriesApiComposable\n

    A\u00f1adiremos \u00e9l composable a nuestro CategoriesView.vue dentro de las etiquetas <script>, lo primero en el import que ya tenemos desde Vue a\u00f1adiremos el m\u00e9todo onMounted dej\u00e1ndolo asi:

    CategoriesView.vue
    import { onMounted, ref } from 'vue'\n

    El m\u00e9todo onMounted es un ciclo de vida que se dispara nada m\u00e1s montarse el componente, despu\u00e9s a\u00f1adiremos al final el import del composable para poder usarlo:

    CategoriesView.vue
    import useCategoriesApiComposable from '@/views/categories/composables/categories-composable'\n

    Nos traeremos el m\u00e9todo del composable con la desestructuraci\u00f3n del objeto:

    CategoriesView.vue
    const { getCategories } = useCategoriesApiComposable()\n

    Crearemos una funci\u00f3n as\u00edncrona para llamar al composable y llamaremos al composable en el onMounted:

    CategoriesView.vue
    async function getInitCategories() {\n    categories.value = await getCategories()\n}\n\nonMounted(() => {\n    getInitCategories()\n})\n

    El CategoriesView.vue quedar\u00eda asi:

    CategoriesView.vue
    <template>\n  <div class=\"card\">\n    <DataTable\n      v-model:editingRows=\"editingRows\"\n      :value=\"categories\"\n      tableStyle=\"min-width: 50rem\"\n      editMode=\"row\"\n      dataKey=\"id\"\n      @row-edit-save=\"onRowEditSave\"\n    >\n      <Column field=\"id\" header=\"IDENTIFICADOR\">\n        <template #editor=\"{ data, field }\">\n          <InputText v-model=\"data[field]\" />\n        </template>\n      </Column>\n      <Column field=\"name\" header=\"NOMBRE CATEGOR\u00cdA\">\n        <template #editor=\"{ data, field }\">\n          <InputText v-model=\"data[field]\" />\n        </template>\n      </Column>\n      <Column :rowEditor=\"true\" style=\"width: 110px\" bodyStyle=\"text-align:center\"></Column>\n      <Column\n        style=\"width: 30px; padding: 0px 2rem 0px 0px; color: red\"\n        bodyStyle=\"text-align:center\"\n      >\n        <template #body=\"{ data }\">\n          <i class=\"pi pi-times\" @click=\"onRowDelete(data)\"></i>\n        </template>\n      </Column>\n    </DataTable>\n  </div>\n  <div class=\"actions\">\n    <Button label=\"Nueva categor\u00eda\" />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { onMounted, ref } from 'vue'\nimport DataTable, { type DataTableRowEditSaveEvent } from 'primevue/datatable'\nimport Column from 'primevue/column'\nimport InputText from 'primevue/inputtext'\nimport Button from 'primevue/button'\nimport type { CategoryInterface } from './model/category.interface'\nimport useCategoriesApiComposable from '@/views/categories/composables/categories-composable'\n\nconst categories = ref([])\nconst editingRows = ref([])\n\nconst { getCategories } = useCategoriesApiComposable()\n\nconst onRowEditSave = (event: DataTableRowEditSaveEvent) => {\n  console.log(event)\n}\n\nconst onRowDelete = (data: CategoryInterface) => {\n  console.log(data)\n}\n\nasync function getInitCategories() {\n  categories.value = await getCategories()\n}\n\nonMounted(() => {\n  getInitCategories()\n})\n</script>\n\n<style lang=\"scss\" scoped>\n.actions {\n  display: flex;\n  flex-direction: row;\n  margin-top: 1rem;\n  justify-content: flex-end;\n}\n\n.p-button {\n  background: var(--primary);\n  border: 1px solid var(--primary);\n\n  &:enabled {\n    &:hover {\n      background: var(--secondary);\n      border-color: var(--secondary);\n    }\n  }\n}\n</style>\n

    Si ahora refrescamos la p\u00e1gina web, veremos que el listado ya tiene datos con los que vamos a interactuar.

    "},{"location":"develop/basic/vuejsold/#simulando-las-otras-peticiones","title":"Simulando las otras peticiones","text":"

    Para terminar, vamos a simular las otras dos peticiones, la de editar y la de borrar para cuando tengamos que utilizarlas. \u00c9l composable debe quedar m\u00e1s o menos as\u00ed:

    categories-composable.ts
    import appApi from '@/api/app-api'\nimport MockAdapter from 'axios-mock-adapter'\nimport type { Category } from '@/views/categories/models/category-interface'\nimport { CATEGORY_DATA_MOCK } from '@/views/categories/mocks/mock-categories'\n\nconst mock = new MockAdapter(appApi)\n\nconst useCategoriesApiComposable = () => {\n  mock.onAny('/category').reply(200, CATEGORY_DATA_MOCK)\n\n  const getCategories = async () => {\n    const categories = await appApi.get('/category')\n    return categories.data\n  }\n\n  const saveCategory = async (category: Category) => {\n    const categoryEdit = await appApi.post('/category', category)\n    return categoryEdit.data\n  }\n\n  const editCategory = async (category: Category) => {\n    const categoryEdit = await appApi.put('/category', category)\n    return categoryEdit.data\n  }\n\n  const deleteCategory = async (categoryId: number) => {\n    const categoryEdit = await appApi.delete(`/category/${categoryId}`)\n    return categoryEdit.data\n  }\n\n  return {\n    getCategories,\n    saveCategory,\n    editCategory,\n    deleteCategory\n  }\n}\n\nexport default useCategoriesApiComposable\n
    "},{"location":"develop/filtered/angular/","title":"Listado filtrado - Angular","text":"

    En este punto ya tenemos dos listados, uno b\u00e1sico y otro paginado. Ahora vamos a implementar un listado un poco diferente, con filtros y con una presentaci\u00f3n un tanto distinta.

    Como ya conocemos como se debe desarrollar, en este ejemplo vamos a ir m\u00e1s r\u00e1pidos y nos vamos a centrar \u00fanicamente en las novedades.

    "},{"location":"develop/filtered/angular/#crear-componentes","title":"Crear componentes","text":"

    Vamos a desarrollar el listado de Juegos. Este listado es un tanto peculiar, porque no tiene una tabla como tal, sino que tiene una tabla con \"tiles\" para cada uno de los juegos. Necesitaremos un componente para el listado y otro componente para el detalle del juego. Tambi\u00e9n necesitaremos otro componente para el dialogo de edici\u00f3n / alta.

    Manos a la obra:

    ng generate module game\n\nng generate component game/game-list\nng generate component game/game-list/game-item\nng generate component game/game-edit\n\nng generate service game/game\n

    Y a\u00f1adimos el nuevo m\u00f3dulo al app.module.ts como hemos hecho con el resto de m\u00f3dulos.

    Game.ts
    import { NgModule } from '@angular/core';\nimport { BrowserModule } from '@angular/platform-browser';\n\nimport { AppRoutingModule } from './app-routing.module';\nimport { AppComponent } from './app.component';\nimport { BrowserAnimationsModule } from '@angular/platform-browser/animations';\nimport { CoreModule } from './core/core.module';\nimport { CategoryModule } from './category/category.module';\nimport { AuthorModule } from './author/author.module';\nimport { GameModule } from './game/game.module';\n\n@NgModule({\n    declarations: [\n        AppComponent\n    ],\n    imports: [\n        BrowserModule,\n        AppRoutingModule,\n        CoreModule,\n        CategoryModule,\n        AuthorModule,\n        GameModule,\n        BrowserAnimationsModule\n    ],\n    providers: [],\n    bootstrap: [AppComponent]\n})\nexport class AppModule { }\n
    "},{"location":"develop/filtered/angular/#crear-el-modelo","title":"Crear el modelo","text":"

    Lo primero que vamos a hacer es crear el modelo en game/model/Game.ts con todas las propiedades necesarias para trabajar con un juego:

    Game.ts
    import { Category } from \"src/app/category/model/Category\";\nimport { Author } from \"src/app/author/model/Author\";\n\nexport class Game {\n    id: number;\n    title: string;\n    age: number;\n    category: Category;\n    author: Author;\n}\n

    Como ves, el juego tiene dos objetos para mapear categor\u00eda y autor.

    "},{"location":"develop/filtered/angular/#anadir-el-punto-de-entrada","title":"A\u00f1adir el punto de entrada","text":"

    A\u00f1adimos la ruta al men\u00fa para que podamos navegar a esta pantalla:

    app-routing.module.ts
    import { NgModule } from '@angular/core';\nimport { Routes, RouterModule } from '@angular/router';\nimport { AuthorListComponent } from './author/author-list/author-list.component';\nimport { CategoryListComponent } from './category/category-list/category-list.component';\nimport { GameListComponent } from './game/game-list/game-list.component';\n\n\nconst routes: Routes = [\n    { path: '', redirectTo: '/games', pathMatch: 'full'},\n    { path: 'categories', component: CategoryListComponent },\n    { path: 'authors', component: AuthorListComponent },\n    { path: 'games', component: GameListComponent },\n];\n\n@NgModule({\n    imports: [RouterModule.forRoot(routes)],\n    exports: [RouterModule]\n})\nexport class AppRoutingModule { }\n

    Adem\u00e1s, hemos a\u00f1adido una regla adicional con el path vac\u00edo para indicar que si no pone ruta, por defecto la p\u00e1gina inicial redirija al path /games, que es nuevo path que hemos a\u00f1adido.

    "},{"location":"develop/filtered/angular/#implementar-servicio","title":"Implementar servicio","text":"

    A continuaci\u00f3n implementamos el servicio y mockeamos datos de ejemplo:

    mock-games.tsgame.service.ts
    import { Game } from \"./Game\";\n\nexport const GAME_DATA: Game[] = [\n    { id: 1, title: 'Juego 1', age: 6, category: { id: 1, name: 'Categor\u00eda 1' }, author: { id: 1, name: 'Autor 1', nationality: 'Nacionalidad 1' } },\n    { id: 2, title: 'Juego 2', age: 8, category: { id: 1, name: 'Categor\u00eda 1' }, author: { id: 2, name: 'Autor 2', nationality: 'Nacionalidad 2' } },\n    { id: 3, title: 'Juego 3', age: 4, category: { id: 1, name: 'Categor\u00eda 1' }, author: { id: 3, name: 'Autor 3', nationality: 'Nacionalidad 3' } },\n    { id: 4, title: 'Juego 4', age: 10, category: { id: 2, name: 'Categor\u00eda 2' }, author: { id: 1, name: 'Autor 1', nationality: 'Nacionalidad 1' } },\n    { id: 5, title: 'Juego 5', age: 16, category: { id: 2, name: 'Categor\u00eda 2' }, author: { id: 2, name: 'Autor 2', nationality: 'Nacionalidad 2' } },\n    { id: 6, title: 'Juego 6', age: 16, category: { id: 2, name: 'Categor\u00eda 2' }, author: { id: 3, name: 'Autor 3', nationality: 'Nacionalidad 3' } },\n    { id: 7, title: 'Juego 7', age: 12, category: { id: 3, name: 'Categor\u00eda 3' }, author: { id: 1, name: 'Autor 1', nationality: 'Nacionalidad 1' } },\n    { id: 8, title: 'Juego 8', age: 14, category: { id: 3, name: 'Categor\u00eda 3' }, author: { id: 2, name: 'Autor 2', nationality: 'Nacionalidad 2' } },\n]\n
    import { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Game } from './model/Game';\nimport { GAME_DATA } from './model/mock-games';\n\n@Injectable({\n    providedIn: 'root'\n})\nexport class GameService {\n\n    constructor() { }\n\n    getGames(title?: String, categoryId?: number): Observable<Game[]> {\n        return of(GAME_DATA);\n    }\n\n    saveGame(game: Game): Observable<void> {\n        return of(null);\n    }\n\n}\n
    "},{"location":"develop/filtered/angular/#implementar-listado","title":"Implementar listado","text":"

    Ya tenemos las operaciones del servicio con datoos, as\u00ed que ahora vamos a por el listado filtrado.

    game-list.component.htmlgame-list.component.scssgame-list.component.ts
    <div class=\"container\">\n    <h1>Cat\u00e1logo de juegos</h1>\n\n    <div class=\"filters\">\n        <form>\n            <mat-form-field>\n                <mat-label>T\u00edtulo del juego</mat-label>\n                <input type=\"text\" matInput placeholder=\"T\u00edtulo del juego\" [(ngModel)]=\"filterTitle\" name=\"title\">\n            </mat-form-field>\n\n            <mat-form-field>\n                <mat-label>Categor\u00eda del juego</mat-label>\n                <mat-select disableRipple [(ngModel)]=\"filterCategory\" name=\"category\">\n                    <mat-option *ngFor=\"let category of categories\" [value]=\"category\">{{category.name}}</mat-option>\n                </mat-select>\n            </mat-form-field>    \n        </form>\n\n        <div class=\"buttons\">\n            <button mat-stroked-button (click)=\"onCleanFilter()\">Limpiar</button> \n            <button mat-stroked-button (click)=\"onSearch()\">Filtrar</button> \n        </div>   \n    </div>   \n\n    <div class=\"game-list\">\n        <app-game-item *ngFor=\"let game of games; let i = index;\" (click)=\"editGame(game)\">\n        </app-game-item>\n    </div>\n\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\" (click)=\"createGame()\">Nuevo juego</button>            \n    </div>   \n</div>\n
    .container {\n    margin: 20px;\n\n    .filters {\n        display: flex;\n\n        mat-form-field {\n            width: 300px;\n            margin-right: 20px;\n        }\n\n        .buttons {\n            flex: auto;\n            align-self: center;\n\n            button {\n                margin-left: 15px;\n            }\n        }\n    }\n\n    .game-list { \n        margin-top: 20px;\n        margin-bottom: 20px;\n\n        display: flex;\n        flex-flow: wrap;\n        overflow: auto;  \n    }\n\n    .buttons {\n        text-align: right;\n    }\n}\n\nbutton {\n    width: 125px;\n}\n
    import { Component, OnInit } from '@angular/core';\nimport { MatDialog } from '@angular/material/dialog';\nimport { CategoryService } from 'src/app/category/category.service';\nimport { Category } from 'src/app/category/model/Category';\nimport { GameEditComponent } from '../game-edit/game-edit.component';\nimport { GameService } from '../game.service';\nimport { Game } from '../model/Game';\n\n@Component({\n    selector: 'app-game-list',\n    templateUrl: './game-list.component.html',\n    styleUrls: ['./game-list.component.scss']\n})\nexport class GameListComponent implements OnInit {\n\n    categories : Category[];\n    games: Game[];\n    filterCategory: Category;\n    filterTitle: string;\n\n    constructor(\n        private gameService: GameService,\n        private categoryService: CategoryService,\n        public dialog: MatDialog,\n    ) { }\n\n    ngOnInit(): void {\n\n        this.gameService.getGames().subscribe(\n            games => this.games = games\n        );\n\n        this.categoryService.getCategories().subscribe(\n            categories => this.categories = categories\n        );\n    }\n\n    onCleanFilter(): void {\n        this.filterTitle = null;\n        this.filterCategory = null;\n\n        this.onSearch();\n    }\n\n    onSearch(): void {\n\n        let title = this.filterTitle;\n        let categoryId = this.filterCategory != null ? this.filterCategory.id : null;\n\n        this.gameService.getGames(title, categoryId).subscribe(\n            games => this.games = games\n        );\n    }\n\n    createGame() {    \n        const dialogRef = this.dialog.open(GameEditComponent, {\n            data: {}\n        });\n\n        dialogRef.afterClosed().subscribe(result => {\n            this.ngOnInit();\n        });    \n    }  \n\n    editGame(game: Game) {\n        const dialogRef = this.dialog.open(GameEditComponent, {\n            data: { game: game }\n        });\n\n        dialogRef.afterClosed().subscribe(result => {\n            this.onSearch();\n        });\n    }\n}\n

    Recuerda, de nuevo, que todos los componentes de Angular que utilicemos hay que importarlos en el m\u00f3dulo padre correspondiente para que se puedan precargar correctamente.

    game.module.ts
    import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { GameListComponent } from './game-list/game-list.component';\nimport { GameEditComponent } from './game-edit/game-edit.component';\nimport { GameItemComponent } from './game-list/game-item/game-item.component';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatOptionModule } from '@angular/material/core';\nimport { MatDialogModule } from '@angular/material/dialog';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatPaginatorModule } from '@angular/material/paginator';\nimport { MatSelectModule } from '@angular/material/select';\nimport { MatTableModule } from '@angular/material/table';\nimport { MatCardModule } from '@angular/material/card';\n\n\n@NgModule({\ndeclarations: [\n    GameListComponent,\n    GameEditComponent,\n    GameItemComponent\n],\nimports: [\n    CommonModule,\n    MatTableModule,\n    MatIconModule, \n    MatButtonModule,\n    MatDialogModule,\n    MatFormFieldModule,\n    MatInputModule,\n    FormsModule,\n    ReactiveFormsModule,\n    MatPaginatorModule,\n    MatOptionModule,\n    MatSelectModule,\n    MatCardModule,\n]\n})\nexport class GameModule { }\n

    Con todos estos cambios y si refrescamos el navegador, deber\u00eda verse una pantalla similar a esta:

    Tenemos una pantalla con una secci\u00f3n de filtros en la parte superior, donde podemos introducir un texto o seleccionar una categor\u00eda de un dropdown, un listado que de momento tiene todos los componentes b\u00e1sicos en una fila uno detr\u00e1s del otro, y un bot\u00f3n para crear juegos nuevos.

    Dropdown

    El componente Dropdown es uno de los componentes m\u00e1s utilizados en las pantallas y formularios de Angular. Ves familiariz\u00e1ndote con \u00e9l porque lo vas a usar mucho. Es bastante potente y medianamente sencillo de utilizar. Los datos del listado pueden ser din\u00e1micos (desde servidor) o est\u00e1ticos (si los valores ya los tienes prefijados).

    "},{"location":"develop/filtered/angular/#implementar-detalle-del-item","title":"Implementar detalle del item","text":"

    Ahora vamos a implementar el detalle de cada uno de los items que forman el listado. Para ello lo primero que haremos ser\u00e1 pasarle la informaci\u00f3n del juego a cada componente como un dato de entrada Input hacia el componente.

    game-list.component.html
    <div class=\"container\">\n    <h1>Cat\u00e1logo de juegos</h1>\n\n    <div class=\"filters\">\n        <form>\n            <mat-form-field>\n                <mat-label>T\u00edtulo del juego</mat-label>\n                <input type=\"text\" matInput placeholder=\"T\u00edtulo del juego\" [(ngModel)]=\"filterTitle\" name=\"title\">\n            </mat-form-field>\n\n            <mat-form-field>\n                <mat-label>Categor\u00eda del juego</mat-label>\n                <mat-select disableRipple [(ngModel)]=\"filterCategory\" name=\"category\">\n                    <mat-option *ngFor=\"let category of categories\" [value]=\"category\">{{category.name}}</mat-option>\n                </mat-select>\n            </mat-form-field>    \n        </form>\n\n        <div class=\"buttons\">\n            <button mat-stroked-button (click)=\"onCleanFilter()\">Limpiar</button> \n            <button mat-stroked-button (click)=\"onSearch()\">Filtrar</button> \n        </div>   \n    </div>   \n\n    <div class=\"game-list\">\n        <app-game-item *ngFor=\"let game of games; let i = index;\" (click)=\"editGame(game)\" [game]=\"game\">\n        </app-game-item>\n    </div>\n\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\" (click)=\"createGame()\">Nuevo juego</button>            \n    </div>   \n</div>\n

    Tambi\u00e9n vamos a necesitar una foto de ejemplo para poner dentro de la tarjeta detalle de los juegos. Vamos a utilizar esta imagen:

    Desc\u00e1rgala y d\u00e9jala dentro del proyecto en assets/foto.png. Y ya para terminar, implementamos el componente de detalle:

    game-item.component.htmlgame-item.component.scssgame-item.component.ts
    <div class=\"container\">\n    <mat-card>\n        <div class=\"photo\">\n            <img src=\"./assets/foto.png\">\n        </div>\n        <div class=\"detail\">\n            <div class=\"title\">{{game.title}}</div>\n            <div class=\"properties\">\n                <div><i>Edad recomendada: </i>+{{game.age}}</div>\n                <div><i>Categor\u00eda: </i>{{game.category.name}}</div>\n                <div><i>Autor: </i>{{game.author.name}}</div>\n                <div><i>Nacionalidad: </i>{{game.author.nationality}}</div>\n            </div>\n        </div>\n    </mat-card>\n</div>\n
    .container {\n    display: flex;\n    width: 325px;\n\n    mat-card {\n        width: 100%;\n        margin: 10px;\n        display: flex;\n\n        .photo {\n            margin-right: 10px;\n\n            img {\n                width: 80px;\n                height: 80px;\n            }\n        }\n\n        .detail {\n            .title {\n                font-size: 14px;\n                font-weight: bold;\n            }\n\n            .properties {\n                font-size: 11px;\n\n                div {\n                    height: 15px;\n                }                \n            }\n        }\n    }\n}    \n
    import { Component, OnInit, Input } from '@angular/core';\nimport { Game } from '../../model/Game';\n\n@Component({\n    selector: 'app-game-item',\n    templateUrl: './game-item.component.html',\n    styleUrls: ['./game-item.component.scss']\n})\nexport class GameItemComponent implements OnInit {\n\n    @Input() game: Game;\n\n    constructor() { }\n\n    ngOnInit(): void {\n    }\n\n}\n

    Ahora si que deber\u00eda quedar algo similar a esta pantalla:

    "},{"location":"develop/filtered/angular/#implementar-dialogo-de-edicion","title":"Implementar dialogo de edici\u00f3n","text":"

    Ya solo nos falta el \u00faltimo paso, implementar el cuadro de edici\u00f3n / alta de un nuevo juego. Pero tenemos un peque\u00f1o problema, y es que al crear o editar un juego debemos seleccionar una Categor\u00eda y un Autor.

    Para la Categor\u00eda no tenemos ning\u00fan problema, pero para el Autor no tenemos un servicio que nos devuelva todos los autores, solo tenemos un servicio que nos devuelve una Page de autores.

    As\u00ed que lo primero que haremos ser\u00e1 implementar una operaci\u00f3n getAllAuthors para poder recuperar una lista.

    mock-authors-list.tsauthor.service.ts
    import { Author } from \"./Author\";\n\nexport const AUTHOR_DATA_LIST : Author[] = [\n    { id: 1, name: 'Klaus Teuber', nationality: 'Alemania' },\n    { id: 2, name: 'Matt Leacock', nationality: 'Estados Unidos' },\n    { id: 3, name: 'Keng Leong Yeo', nationality: 'Singapur' },\n    { id: 4, name: 'Gil Hova', nationality: 'Estados Unidos'},\n    { id: 5, name: 'Kelly Adams', nationality: 'Estados Unidos' },\n]    \n
    import { HttpClient } from '@angular/common/http';\nimport { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Pageable } from '../core/model/page/Pageable';\nimport { Author } from './model/Author';\nimport { AuthorPage } from './model/AuthorPage';\nimport { AUTHOR_DATA_LIST } from './model/mock-authors-list';\n\n@Injectable({\n    providedIn: 'root'\n})\nexport class AuthorService {\n\n    constructor(\n        private http: HttpClient\n    ) { }\n\n    getAuthors(pageable: Pageable): Observable<AuthorPage> {\n        return this.http.post<AuthorPage>('http://localhost:8080/author', {pageable:pageable});\n    }\n\n    saveAuthor(author: Author): Observable<void> {\n\n        let url = 'http://localhost:8080/author';\n        if (author.id != null) url += '/'+author.id;\n\n        return this.http.put<void>(url, author);\n    }\n\n    deleteAuthor(idAuthor : number): Observable<void> {\n        return this.http.delete<void>('http://localhost:8080/author/'+idAuthor);\n    }    \n\n    getAllAuthors(): Observable<Author[]> {\n        return of(AUTHOR_DATA_LIST);\n    }\n\n}\n

    Ahora s\u00ed que tenemos todo listo para implementar el cuadro de dialogo para dar de alta o editar juegos.

    game-edit.component.htmlgame-edit.component.scssgame-edit.component.ts
    <div class=\"container\">\n    <h1 *ngIf=\"game.id == null\">Crear juego</h1>\n    <h1 *ngIf=\"game.id != null\">Modificar juego</h1>\n\n    <form>\n        <mat-form-field>\n            <mat-label>Identificador</mat-label>\n            <input type=\"text\" matInput placeholder=\"Identificador\" [(ngModel)]=\"game.id\" name=\"id\" disabled>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>T\u00edtulo</mat-label>\n            <input type=\"text\" matInput placeholder=\"T\u00edtulo del juego\" [(ngModel)]=\"game.title\" name=\"title\" required>\n            <mat-error>El t\u00edtulo no puede estar vac\u00edo</mat-error>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Edad recomendada</mat-label>\n            <input type=\"number\" matInput placeholder=\"Edad recomendada\" [(ngModel)]=\"game.age\" name=\"age\" required>\n            <mat-error>La edad no puede estar vac\u00eda</mat-error>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Categor\u00eda</mat-label>\n            <mat-select disableRipple [(ngModel)]=\"game.category\" name=\"category\" required>\n                <mat-option *ngFor=\"let category of categories\" [value]=\"category\">{{category.name}}</mat-option>\n            </mat-select>\n            <mat-error>La categor\u00eda no puede estar vac\u00eda</mat-error>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Autor</mat-label>\n            <mat-select disableRipple [(ngModel)]=\"game.author\" name=\"author\" required>\n                <mat-option *ngFor=\"let author of authors\" [value]=\"author\">{{author.name}}</mat-option>\n            </mat-select>\n            <mat-error>El autor no puede estar vac\u00edo</mat-error>\n        </mat-form-field>\n    </form>\n\n    <div class=\"buttons\">\n        <button mat-stroked-button (click)=\"onClose()\">Cerrar</button>\n        <button mat-flat-button color=\"primary\" (click)=\"onSave()\">Guardar</button>\n    </div>\n</div>\n
    .container {\n    min-width: 350px;\n    max-width: 500px;\n    padding: 20px;\n\n    form {\n        display: flex;\n        flex-direction: column;\n        margin-bottom:20px;\n    }\n\n    .buttons {\n        text-align: right;\n\n        button {\n            margin-left: 10px;\n        }\n    }\n}\n
    import { Component, Inject, OnInit } from '@angular/core';\nimport { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';\nimport { AuthorService } from 'src/app/author/author.service';\nimport { Author } from 'src/app/author/model/Author';\nimport { CategoryService } from 'src/app/category/category.service';\nimport { Category } from 'src/app/category/model/Category';\nimport { GameService } from '../game.service';\nimport { Game } from '../model/Game';\n\n@Component({\n    selector: 'app-game-edit',\n    templateUrl: './game-edit.component.html',\n    styleUrls: ['./game-edit.component.scss']\n})\nexport class GameEditComponent implements OnInit {\n\n    game: Game; \n    authors: Author[];\n    categories: Category[];\n\n    constructor(\n        public dialogRef: MatDialogRef<GameEditComponent>,\n        @Inject(MAT_DIALOG_DATA) public data: any,\n        private gameService: GameService,\n        private categoryService: CategoryService,\n        private authorService: AuthorService,\n    ) { }\n\n    ngOnInit(): void {\n        if (this.data.game != null) {\n            this.game = Object.assign({}, this.data.game);\n        }\n        else {\n            this.game = new Game();\n        }\n\n        this.categoryService.getCategories().subscribe(\n            categories => {\n                this.categories = categories;\n\n                if (this.game.category != null) {\n                    let categoryFilter: Category[] = categories.filter(category => category.id == this.data.game.category.id);\n                    if (categoryFilter != null) {\n                        this.game.category = categoryFilter[0];\n                    }\n                }\n            }\n        );\n\n        this.authorService.getAllAuthors().subscribe(\n            authors => {\n                this.authors = authors\n\n                if (this.game.author != null) {\n                    let authorFilter: Author[] = authors.filter(author => author.id == this.data.game.author.id);\n                    if (authorFilter != null) {\n                        this.game.author = authorFilter[0];\n                    }\n                }\n            }\n        );\n    }\n\n    onSave() {\n        this.gameService.saveGame(this.game).subscribe(result => {\n            this.dialogRef.close();\n        });    \n    }  \n\n    onClose() {\n        this.dialogRef.close();\n    }\n\n}\n

    Como puedes ver, para rellenar los componentes seleccionables de dropdown, hemos realizado una consulta al servicio para recuperar todos los autores y categorias, y en la respuesta de cada uno de ellos, hemos buscado en los resultados cual es el que coincide con el ID enviado desde el listado, y ese es el que hemos fijado en el objeto Game.

    De esta forma, no estamos cogiendo directamente los datos del listado, sino que no estamos asegurando que los datos de autor y de categor\u00eda son los que vienen del servicio, siempre filtrando por su ID.

    "},{"location":"develop/filtered/angular/#conectar-con-backend","title":"Conectar con Backend","text":"

    Antes de seguir

    Antes de seguir con este punto, debes implementar el c\u00f3digo de backend en la tecnolog\u00eda que quieras (Springboot o Nodejs). Si has empezado este cap\u00edtulo implementando el frontend, por favor accede a la secci\u00f3n correspondiente de backend para poder continuar con el tutorial. Una vez tengas implementadas todas las operaciones para este listado, puedes volver a este punto y continuar con Angular.

    Una vez implementado front y back, lo que nos queda es modificar el servicio del front para que conecte directamente con las operaciones ofrecidas por el back.

    author-service.tsgame-service.ts
    import { HttpClient } from '@angular/common/http';\nimport { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Pageable } from '../core/model/page/Pageable';\nimport { Author } from './model/Author';\nimport { AuthorPage } from './model/AuthorPage';\n\n@Injectable({\n    providedIn: 'root'\n})\nexport class AuthorService {\n\n    constructor(\n        private http: HttpClient\n    ) { }\n\n    getAuthors(pageable: Pageable): Observable<AuthorPage> {\n        return this.http.post<AuthorPage>('http://localhost:8080/author', {pageable:pageable});\n    }\n\n    saveAuthor(author: Author): Observable<void> {\n\n        let url = 'http://localhost:8080/author';\n        if (author.id != null) url += '/'+author.id;\n\n        return this.http.put<void>(url, author);\n    }\n\n    deleteAuthor(idAuthor : number): Observable<void> {\n        return this.http.delete<void>('http://localhost:8080/author/'+idAuthor);\n    }    \n\n    getAllAuthors(): Observable<Author[]> {\n        return this.http.get<Author[]>('http://localhost:8080/author');\n    }\n\n}\n
    import { HttpClient } from '@angular/common/http';\nimport { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Game } from './model/Game';\n\n@Injectable({\n    providedIn: 'root'\n})\nexport class GameService {\n\n    constructor(\n        private http: HttpClient\n    ) { }\n\n    getGames(title?: String, categoryId?: number): Observable<Game[]> {            \n        return this.http.get<Game[]>(this.composeFindUrl(title, categoryId));\n    }\n\n    saveGame(game: Game): Observable<void> {\n        let url = 'http://localhost:8080/game';\n\n        if (game.id != null) {\n            url += '/'+game.id;\n        }\n\n        return this.http.put<void>(url, game);\n    }\n\n    private composeFindUrl(title?: String, categoryId?: number) : string {\n        let params = '';\n\n        if (title != null) {\n            params += 'title='+title;\n        }\n\n        if (categoryId != null) {\n            if (params != '') params += \"&\";\n            params += \"idCategory=\"+categoryId;\n        }\n\n        let url = 'http://localhost:8080/game'\n\n        if (params == '') return url;\n        else return url + '?'+params;\n    }\n}\n

    Y ahora si, podemos navegar por la web y ver el resultado completo.

    "},{"location":"develop/filtered/angular17/","title":"Listado filtrado - Angular","text":"

    En este punto ya tenemos dos listados, uno b\u00e1sico y otro paginado. Ahora vamos a implementar un listado un poco diferente, con filtros y con una presentaci\u00f3n un tanto distinta.

    Como ya conocemos como se debe desarrollar, en este ejemplo vamos a ir m\u00e1s r\u00e1pidos y nos vamos a centrar \u00fanicamente en las novedades.

    "},{"location":"develop/filtered/angular17/#crear-componentes","title":"Crear componentes","text":"

    Vamos a desarrollar el listado de Juegos. Este listado es un tanto peculiar, porque no tiene una tabla como tal, sino que tiene una tabla con \"tiles\" para cada uno de los juegos. Necesitaremos un componente para el listado y otro componente para el detalle del juego. Tambi\u00e9n necesitaremos otro componente para el di\u00e1logo de edici\u00f3n / alta.

    Manos a la obra:

    ng generate component game/game-list --type=page\nng generate component game/game-list/game-item\nng generate component game/game-edit\n\nng generate service game/game\n
    "},{"location":"develop/filtered/angular17/#crear-el-modelo","title":"Crear el modelo","text":"

    Lo primero que vamos a hacer es crear el modelo en game/model/Game.ts con todas las propiedades necesarias para trabajar con un juego:

    Game.ts
    import { Author } from \"../../author/model/Author\";\nimport { Category } from \"../../category/model/Category\";\n\nexport class Game {\n    id: number;\n    title: string;\n    age: number;\n    category: Category;\n    author: Author;\n}\n

    Como ves, el juego tiene dos objetos para mapear categor\u00eda y autor.

    "},{"location":"develop/filtered/angular17/#anadir-el-punto-de-entrada","title":"A\u00f1adir el punto de entrada","text":"

    A\u00f1adimos la ruta al men\u00fa para que podamos navegar a esta pantalla:

    app.routes.ts
    import { Routes } from '@angular/router';\n\nexport const routes: Routes = [\n    { path: '', redirectTo: '/games', pathMatch: 'full'},\n    { path: 'categories', loadComponent: () => import('../category/category-list/category-list.page').then(m => m.CategoryListPage)},\n    { path: 'authors', loadComponent: () => import('../author/author-list/author-list.page').then(m => m.AuthorListPage)},\n    { path: 'games', loadComponent: () => import('../game/game-list/game-list.page').then(m => m.GameListPage)}\n];\n

    Adem\u00e1s, hemos a\u00f1adido una regla adicional con el path vac\u00edo para indicar que si no pone ruta, por defecto la p\u00e1gina inicial redirija al path /games, que es nuevo path que hemos a\u00f1adido.

    "},{"location":"develop/filtered/angular17/#implementar-servicio","title":"Implementar servicio","text":"

    A continuaci\u00f3n implementamos el servicio y mockeamos datos de ejemplo:

    mock-games.tsgame.service.ts
    import { Game } from \"./Game\";\n\nexport const GAME_DATA: Game[] = [\n    { id: 1, title: 'Juego 1', age: 6, category: { id: 1, name: 'Categor\u00eda 1' }, author: { id: 1, name: 'Autor 1', nationality: 'Nacionalidad 1' } },\n    { id: 2, title: 'Juego 2', age: 8, category: { id: 1, name: 'Categor\u00eda 1' }, author: { id: 2, name: 'Autor 2', nationality: 'Nacionalidad 2' } },\n    { id: 3, title: 'Juego 3', age: 4, category: { id: 1, name: 'Categor\u00eda 1' }, author: { id: 3, name: 'Autor 3', nationality: 'Nacionalidad 3' } },\n    { id: 4, title: 'Juego 4', age: 10, category: { id: 2, name: 'Categor\u00eda 2' }, author: { id: 1, name: 'Autor 1', nationality: 'Nacionalidad 1' } },\n    { id: 5, title: 'Juego 5', age: 16, category: { id: 2, name: 'Categor\u00eda 2' }, author: { id: 2, name: 'Autor 2', nationality: 'Nacionalidad 2' } },\n    { id: 6, title: 'Juego 6', age: 16, category: { id: 2, name: 'Categor\u00eda 2' }, author: { id: 3, name: 'Autor 3', nationality: 'Nacionalidad 3' } },\n    { id: 7, title: 'Juego 7', age: 12, category: { id: 3, name: 'Categor\u00eda 3' }, author: { id: 1, name: 'Autor 1', nationality: 'Nacionalidad 1' } },\n    { id: 8, title: 'Juego 8', age: 14, category: { id: 3, name: 'Categor\u00eda 3' }, author: { id: 2, name: 'Autor 2', nationality: 'Nacionalidad 2' } },\n]\n
    import { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Game } from './model/Game';\nimport { GAME_DATA } from './model/mock-games';\n\n@Injectable({\n    providedIn: 'root'\n})\nexport class GameService {\n\n    constructor() { }\n\n    getGames(title?: string, categoryId?: number): Observable<Game[]> {\n        return of(GAME_DATA);\n    }\n\n    saveGame(game: Game): Observable<void> {\n        return of(null);\n    }\n\n}\n
    "},{"location":"develop/filtered/angular17/#implementar-listado","title":"Implementar listado","text":"

    Ya tenemos las operaciones del servicio con datos, as\u00ed que ahora vamos a por el listado filtrado.

    game-list.component.htmlgame-list.component.scssgame-list.component.ts
    <div class=\"container\">\n    <h1>Cat\u00e1logo de juegos</h1>\n\n    <div class=\"filters\">\n        <form>\n            <mat-form-field>\n                <mat-label>T\u00edtulo del juego</mat-label>\n                <input\n                    type=\"text\"\n                    matInput\n                    placeholder=\"T\u00edtulo del juego\"\n                    [(ngModel)]=\"filterTitle\"\n                    name=\"title\"\n                />\n            </mat-form-field>\n\n            <mat-form-field>\n                <mat-label>Categor\u00eda del juego</mat-label>\n                <mat-select disableRipple [(ngModel)]=\"filterCategory\" name=\"category\">\n                    @for (category of categories(); track category.id) {\n                        <mat-option [value]=\"category\">{{ category.name }}</mat-option>\n                    }\n                </mat-select>\n            </mat-form-field>\n        </form>\n\n        <div class=\"buttons\">\n            <button mat-stroked-button (click)=\"onCleanFilter()\">Limpiar</button>\n            <button mat-stroked-button (click)=\"onSearch()\">Filtrar</button>\n        </div>\n    </div>\n\n    <div class=\"game-list\">\n        @for (game of games(); track game.id) {\n            <app-game-item (click)=\"editGame(game)\" />\n        }\n    </div>\n\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\" (click)=\"createGame()\">\n            Nuevo juego\n        </button>\n    </div>\n</div>\n
    .container {\n    margin: 20px;\n\n    .filters {\n        display: flex;\n\n        mat-form-field {\n            width: 300px;\n            margin-right: 20px;\n        }\n\n        .buttons {\n            flex: auto;\n            align-self: center;\n\n            button {\n                margin-left: 15px;\n            }\n        }\n    }\n\n    .game-list { \n        margin-top: 20px;\n        margin-bottom: 20px;\n\n        display: flex;\n        flex-flow: wrap;\n        overflow: auto;  \n    }\n\n    .buttons {\n        text-align: right;\n    }\n}\n\nbutton {\n    width: 125px;\n}\n
    import { Component, OnInit, inject, signal } from '@angular/core';\nimport { MatDialog } from '@angular/material/dialog';\nimport { GameEditComponent } from '../game-edit/game-edit.component';\nimport { GameService } from '../game.service';\nimport { Game } from '../model/Game';\nimport { CategoryService } from '../../category/category.service';\nimport { Category } from '../../category/model/Category';\nimport { CommonModule } from '@angular/common';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatTableModule } from '@angular/material/table';\nimport { FormsModule } from '@angular/forms';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatSelectModule } from '@angular/material/select';\nimport { GameItemComponent } from './game-item/game-item.component';\n\n@Component({\n    selector: 'app-game-list',\n    standalone: true,\n    imports: [\n        MatButtonModule,\n        MatIconModule,\n        MatTableModule,\n        CommonModule,\n        FormsModule,\n        MatFormFieldModule,\n        MatInputModule,\n        MatSelectModule,\n        GameItemComponent\n    ],\n    templateUrl: './game-list.component.html',\n    styleUrl: './game-list.component.scss',\n})\nexport class GameListComponent implements OnInit {\n    protected readonly categories = signal<Category[]>([]);\n    protected readonly games = signal<Game[]>([]);\n    protected readonly filterCategory = signal<Category | null>(null);\n    protected readonly filterTitle = signal<string>('');\n\n    protected readonly gameService = inject(GameService);\n    protected readonly categoryService = inject(CategoryService);\n    protected readonly dialog = inject(MatDialog);\n\n    ngOnInit(): void {\n        this.gameService.getGames().subscribe((games) => this.games.set(games));\n\n        this.categoryService\n            .getCategories()\n            .subscribe((categories) => this.categories.set(categories));\n    }\n\n    onCleanFilter(): void {\n        this.filterTitle.set('');\n        this.filterCategory.set(null);\n        this.onSearch();\n    }\n\n    onSearch(): void {\n        const title = this.filterTitle();\n        const categoryId =\n            this.filterCategory() != null ? this.filterCategory().id : null;\n\n        this.gameService\n            .getGames(title, categoryId)\n            .subscribe((games) => this.games.set(games));\n    }\n\n    createGame() {\n        const dialogRef = this.dialog.open(GameEditComponent, {\n            data: {},\n        });\n\n        dialogRef.afterClosed().subscribe((result) => {\n            if(!result) return;\n            this.onSearch();\n        });\n    }\n\n    editGame(game: Game) {\n        const dialogRef = this.dialog.open(GameEditComponent, {\n            data: { game: game },\n        });\n\n        dialogRef.afterClosed().subscribe((result) => {\n            if(!result) return;\n            this.onSearch();\n        });\n    }\n}\n

    Con todos estos cambios y si refrescamos el navegador, deber\u00eda verse una pantalla similar a esta:

    Tenemos una pantalla con una secci\u00f3n de filtros en la parte superior, donde podemos introducir un texto o seleccionar una categor\u00eda de un dropdown, un listado que de momento tiene todos los componentes b\u00e1sicos en una fila, uno detr\u00e1s del otro, y un bot\u00f3n para crear juegos nuevos.

    Dropdown

    El componente Dropdown es uno de los componentes m\u00e1s utilizados en las pantallas y formularios de Angular. Ves familiariz\u00e1ndote con \u00e9l porque lo vas a usar mucho. Es bastante potente y medianamente sencillo de utilizar. Los datos del listado pueden ser din\u00e1micos (desde servidor) o est\u00e1ticos (si los valores ya los tienes prefijados).

    "},{"location":"develop/filtered/angular17/#implementar-detalle-del-item","title":"Implementar detalle del item","text":"

    Ahora vamos a implementar el detalle de cada uno de los items que forman el listado. Para ello lo primero que haremos ser\u00e1 pasarle la informaci\u00f3n del juego a cada componente como un dato de entrada Input hacia el componente.

    game-list.component.html
    <div class=\"container\">\n    <h1>Cat\u00e1logo de juegos</h1>\n\n    <div class=\"filters\">\n        <form>\n            <mat-form-field>\n                <mat-label>T\u00edtulo del juego</mat-label>\n                <input\n                    type=\"text\"\n                    matInput\n                    placeholder=\"T\u00edtulo del juego\"\n                    [(ngModel)]=\"filterTitle\"\n                    name=\"title\"\n                />\n            </mat-form-field>\n\n            <mat-form-field>\n                <mat-label>Categor\u00eda del juego</mat-label>\n                <mat-select disableRipple [(ngModel)]=\"filterCategory\" name=\"category\">\n                    @for (category of categories(); track category.id) {\n                        <mat-option [value]=\"category\">{{ category.name }}</mat-option>\n                    }\n                </mat-select>\n            </mat-form-field>\n        </form>\n\n        <div class=\"buttons\">\n            <button mat-stroked-button (click)=\"onCleanFilter()\">Limpiar</button>\n            <button mat-stroked-button (click)=\"onSearch()\">Filtrar</button>\n        </div>\n    </div>\n\n    <div class=\"game-list\">\n        @for (game of games(); track game.id) {\n            <app-game-item (click)=\"editGame(game)\" [game]=\"game\" />\n        }\n    </div>\n\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\" (click)=\"createGame()\">\n            Nuevo juego\n        </button>\n    </div>\n</div>\n

    Tambi\u00e9n vamos a necesitar una foto de ejemplo para poner dentro de la tarjeta detalle de los juegos. Vamos a utilizar esta imagen:

    Desc\u00e1rgala y d\u00e9jala dentro del proyecto en public/img/foto.png. Y ya para terminar, implementamos el componente de detalle:

    game-item.component.htmlgame-item.component.scssgame-item.component.ts
    <div class=\"container\">\n    <mat-card>\n        <div class=\"photo\">\n            <img src=\"img/foto.png\">\n        </div>\n        <div class=\"detail\">\n            <div class=\"title\">{{game().title}}</div>\n            <div class=\"properties\">\n                <div><i>Edad recomendada: </i>+{{game().age}}</div>\n                <div><i>Categor\u00eda: </i>{{game().category.name}}</div>\n                <div><i>Autor: </i>{{game().author.name}}</div>\n                <div><i>Nacionalidad: </i>{{game().author.nationality}}</div>\n            </div>\n        </div>\n    </mat-card>\n</div>\n
    .container {\n    display: flex;\n    width: 325px;\n\n    mat-card {\n        width: 100%;\n        margin: 10px;\n        display: flex;\n        padding: 1rem;\n\n        .photo {\n            margin-right: 10px;\n\n            img {\n                width: 80px;\n                height: 80px;\n            }\n        }\n\n        .detail {\n            .title {\n                font-size: 14px;\n                font-weight: bold;\n            }\n\n            .properties {\n                font-size: 11px;\n\n                div {\n                    height: 15px;\n                }                \n            }\n        }\n    }\n}    \n
    import { Component, input } from '@angular/core';\nimport { Game } from '../../model/Game';\nimport {MatCardModule} from '@angular/material/card';\n\n@Component({\n    selector: 'app-game-item',\n    standalone: true,\n    imports: [MatCardModule],\n    templateUrl: './game-item.component.html',\n    styleUrl: './game-item.component.scss'\n})\nexport class GameItemComponent {\n    protected readonly game = input.required<Game>();\n}\n

    Ahora s\u00ed que deber\u00eda quedar algo similar a esta pantalla:

    "},{"location":"develop/filtered/angular17/#implementar-dialogo-de-edicion","title":"Implementar di\u00e1logo de edici\u00f3n","text":"

    Ya solo nos falta el \u00faltimo paso, implementar el cuadro de edici\u00f3n / alta de un nuevo juego. Pero tenemos un peque\u00f1o problema, y es que al crear o editar un juego debemos seleccionar una Categor\u00eda y un Autor.

    Para la Categor\u00eda no tenemos ning\u00fan problema, pero para el Autor no tenemos un servicio que nos devuelva todos los autores, solo tenemos un servicio que nos devuelve una Page de autores.

    As\u00ed que lo primero que haremos ser\u00e1 implementar una operaci\u00f3n getAllAuthors para poder recuperar una lista.

    mock-authors-list.tsauthor.service.ts
    import { Author } from \"./Author\";\n\nexport const AUTHOR_DATA_LIST : Author[] = [\n    { id: 1, name: 'Klaus Teuber', nationality: 'Alemania' },\n    { id: 2, name: 'Matt Leacock', nationality: 'Estados Unidos' },\n    { id: 3, name: 'Keng Leong Yeo', nationality: 'Singapur' },\n    { id: 4, name: 'Gil Hova', nationality: 'Estados Unidos'},\n    { id: 5, name: 'Kelly Adams', nationality: 'Estados Unidos' },\n]    \n
    import { Injectable, inject } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Pageable } from '../core/model/page/Pageable';\nimport { Author } from './model/Author';\nimport { PaginatedData } from 'src/app/core/model/page/PaginatedData';\nimport { HttpClient } from '@angular/common/http';\nimport { AUTHOR_DATA_LIST } from './model/mock-authors-list';\n\n@Injectable({\n    providedIn: 'root',\n})\nexport class AuthorService {\n    protected readonly http = inject(HttpClient);\n\n    private baseUrl = 'http://localhost:8080/author';\n\n    getAuthors(pageable: Pageable): Observable<PaginatedData<Author>> {\n        return this.http.post<PaginatedData<Author>>(this.baseUrl, { pageable: pageable });\n    }\n\n    saveAuthor(author: Author): Observable<Author> {\n        const { id } = author;\n        const url = id ? `${this.baseUrl}/${id}` : this.baseUrl;\n        return this.http.put<Author>(url, author);\n    }\n\n    deleteAuthor(idAuthor: number): Observable<void> {\n        return this.http.delete<void>(`${this.baseUrl}/${idAuthor}`);\n    }\n\n    getAllAuthors(): Observable<Author[]> {\n        return of(AUTHOR_DATA_LIST);\n    }\n}\n

    Ahora s\u00ed que tenemos todo listo para implementar el cuadro de di\u00e1logo para dar de alta o editar juegos.

    game-edit.component.htmlgame-edit.component.scssgame-edit.component.ts
    <div class=\"container\">\n    @if (id()) {\n        <h1>Modificar juego</h1>\n    } @else {\n        <h1>Crear juego</h1>\n    }\n\n    <form>\n        <mat-form-field>\n            <mat-label>Identificador</mat-label>\n            <input type=\"text\" matInput placeholder=\"Identificador\" [(ngModel)]=\"id\" name=\"id\" disabled>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>T\u00edtulo</mat-label>\n            <input type=\"text\" matInput placeholder=\"T\u00edtulo del juego\" [(ngModel)]=\"title\" name=\"title\" required>\n            <mat-error>El t\u00edtulo no puede estar vac\u00edo</mat-error>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Edad recomendada</mat-label>\n            <input type=\"number\" matInput placeholder=\"Edad recomendada\" [(ngModel)]=\"age\" name=\"age\" required>\n            <mat-error>La edad no puede estar vac\u00eda</mat-error>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Categor\u00eda</mat-label>\n            <mat-select disableRipple [(ngModel)]=\"categoryId\" name=\"category\" required>\n                @for (cat of categories(); track cat.id) {\n                    <mat-option [value]=\"cat.id\">{{cat.name}}</mat-option>\n                }\n            </mat-select>\n            <mat-error>La categor\u00eda no puede estar vac\u00eda</mat-error>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Autor</mat-label>\n            <mat-select disableRipple [(ngModel)]=\"authorId\" name=\"author\" required>\n                @for (aut of authors(); track aut.id) {\n                    <mat-option [value]=\"aut.id\">{{aut.name}}</mat-option>\n                }\n            </mat-select>\n            <mat-error>El autor no puede estar vac\u00edo</mat-error>\n        </mat-form-field>\n    </form>\n\n    <div class=\"buttons\">\n        <button mat-stroked-button (click)=\"onClose()\">Cerrar</button>\n        <button mat-flat-button color=\"primary\" (click)=\"onSave()\">Guardar</button>\n    </div>\n</div>\n
    .container {\n    min-width: 350px;\n    max-width: 500px;\n    padding: 20px;\n\n    form {\n        display: flex;\n        flex-direction: column;\n        margin-bottom:20px;\n    }\n\n    .buttons {\n        text-align: right;\n\n        button {\n            margin-left: 10px;\n        }\n    }\n}\n
    import { Component, inject, OnInit, signal } from '@angular/core';\nimport { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';\nimport { GameService } from '../game.service';\nimport { Game } from '../model/Game';\nimport { AuthorService } from '../../author/author.service';\nimport { Author } from '../../author/model/Author';\nimport { CategoryService } from '../../category/category.service';\nimport { Category } from '../../category/model/Category';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatSelectModule } from '@angular/material/select';\n\n@Component({\n    selector: 'app-game-edit',\n    standalone: true,\n    imports: [FormsModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatButtonModule, MatSelectModule ],\n    templateUrl: './game-edit.component.html',\n    styleUrl: './game-edit.component.scss',\n})\nexport class GameEditComponent implements OnInit {\n    protected readonly id = signal<number | null>(null);\n    protected readonly title = signal<string | null>(null);\n    protected readonly age = signal<number | null>(null);\n    protected readonly categoryId = signal<number | null>(null);\n    protected readonly authorId = signal<number | null>(null);\n    protected readonly categories = signal<Category[]>([]);\n    protected readonly authors = signal<Author[]>([]);\n\n    protected readonly dialogRef = inject(MatDialogRef<GameEditComponent>);\n    protected readonly data = inject(MAT_DIALOG_DATA);\n    protected readonly gameService = inject(GameService);\n    protected readonly categoryService = inject(CategoryService);\n    protected readonly authorService = inject(AuthorService);\n\n    ngOnInit(): void {\n        this.loadFormData(this.data.game ?? null);\n    }\n\n    loadFormData(initialData: Game | null): void {\n        this.id.set(initialData?.id ?? null);\n        this.title.set(initialData?.title ?? null);\n        this.age.set(initialData?.age ?? null);\n\n        this.categoryService.getCategories().subscribe((cats) => {\n            this.categories.set(cats);\n            this.categoryId.set(initialData?.category?.id ?? null);\n        });\n\n        this.authorService.getAllAuthors().subscribe((auts) => {\n            this.authors.set(auts);\n            this.authorId.set(initialData?.author?.id ?? null);\n        });\n    }\n\n    onSave() {\n        const game: Game = {\n            id: this.id(),\n            title: this.title(),\n            age: this.age(),\n            category: this.categories().find(c => c.id === this.categoryId()) ?? null,\n            author: this.authors().find(a => a.id === this.authorId()) ?? null,\n        };\n        this.gameService.saveGame(game).subscribe(() => {\n            this.dialogRef.close(true);\n        });\n    }\n\n    onClose() {\n        this.dialogRef.close();\n    }\n}\n

    Como puedes ver, para rellenar los componentes seleccionables de dropdown, hemos realizado una consulta al servicio para recuperar todos los autores y categor\u00edas, y en la respuesta de cada uno de ellos, hemos buscado en los resultados cu\u00e1l es el que coincide con el ID enviado desde el listado, y ese es el que hemos fijado en el objeto Game.

    De esta forma, no estamos cogiendo directamente los datos del listado, sino que no estamos asegurando que los datos de autor y de categor\u00eda son los que vienen del servicio, siempre filtrando por su ID.

    "},{"location":"develop/filtered/angular17/#conectar-con-backend","title":"Conectar con Backend","text":"

    Antes de seguir

    Antes de seguir con este punto, debes implementar el c\u00f3digo de backend en la tecnolog\u00eda que quieras (Springboot o Nodejs). Si has empezado este cap\u00edtulo implementando el frontend, por favor accede a la secci\u00f3n correspondiente de backend para poder continuar con el tutorial. Una vez tengas implementadas todas las operaciones para este listado, puedes volver a este punto y continuar con Angular.

    Una vez implementado front y back, lo que nos queda es modificar el servicio del front para que conecte directamente con las operaciones ofrecidas por el back.

    author-service.tsgame-service.ts
    import { Injectable, inject } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Pageable } from '../core/model/page/Pageable';\nimport { Author } from './model/Author';\nimport { PaginatedData } from 'src/app/core/model/page/PaginatedData';\nimport { HttpClient } from '@angular/common/http';\n\n@Injectable({\n    providedIn: 'root',\n})\nexport class AuthorService {\n    protected readonly http = inject(HttpClient);\n\n    private baseUrl = 'http://localhost:8080/author';\n\n    getAuthors(pageable: Pageable): Observable<PaginatedData<Author>> {\n        return this.http.post<PaginatedData<Author>>(this.baseUrl, { pageable: pageable });\n    }\n\n    saveAuthor(author: Author): Observable<Author> {\n        const { id } = author;\n        const url = id ? `${this.baseUrl}/${id}` : this.baseUrl;\n        return this.http.put<Author>(url, author);\n    }\n\n    deleteAuthor(idAuthor: number): Observable<void> {\n        return this.http.delete<void>(`${this.baseUrl}/${idAuthor}`);\n    }\n\n    getAllAuthors(): Observable<Author[]> {\n        return this.http.get<Author[]>(this.baseUrl);\n    }\n}\n
    import { Injectable, inject } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Game } from './model/Game';\nimport { HttpClient } from '@angular/common/http';\n\n@Injectable({\nprovidedIn: 'root',\n})\nexport class GameService {\n    protected readonly http = inject(HttpClient);\n\n    private baseUrl = 'http://localhost:8080/game';\n\n    getGames(title?: string, categoryId?: number): Observable<Game[]> {\n        return this.http.get<Game[]>(this.composeFindUrl(title, categoryId));\n    }\n\n    saveGame(game: Game): Observable<void> {\n        const { id } = game;\n        const url = id ? `${this.baseUrl}/${id}` : this.baseUrl;\n\n        return this.http.put<void>(url, game);\n    }\n\n    private composeFindUrl(title?: string, categoryId?: number): string {\n        const params = new URLSearchParams();\n        if (title) {\n          params.set('title', title);\n        }  \n        if (categoryId) {\n            params.set('idCategory', categoryId.toString());\n        }\n        const queryString = params.toString();\n        return queryString ? `${this.baseUrl}?${queryString}` : this.baseUrl;\n    }\n}\n

    Y ahora si, podemos navegar por la web y ver el resultado completo.

    "},{"location":"develop/filtered/nodejs/","title":"Listado simple - Nodejs","text":"

    En este punto ya tenemos dos listados, uno b\u00e1sico y otro paginado. Ahora vamos a implementar un listado un poco diferente, este listado va a tener filtros de b\u00fasqueda.

    Como ya conocemos como se debe desarrollar, en este ejemplo vamos a ir m\u00e1s r\u00e1pidos y nos vamos a centrar \u00fanicamente en las novedades.

    "},{"location":"develop/filtered/nodejs/#crear-modelos","title":"Crear Modelos","text":"

    Lo primero que vamos a hacer es crear el modelo de author para trabajar con BBDD. En la carpeta schemas creamos el archivo game.schema.js:

    game.schema.js
    import mongoose from \"mongoose\";\nconst { Schema, model } = mongoose;\nimport normalize from 'normalize-mongoose';\n\nconst gameSchema = new Schema({\n    title: {\n        type: String,\n        require: true\n    },\n    age: {\n        type: Number,\n        require: true,\n        max: 99,\n        min: 0\n    },\n    category: {\n        type: Schema.Types.ObjectId,\n        ref: 'Category',\n        required: true\n    },\n    author: {\n        type: Schema.Types.ObjectId,\n        ref: 'Author',\n        required: true\n    }\n});\n\ngameSchema.plugin(normalize);\nconst GameModel = model('Game', gameSchema);\n\nexport default GameModel;\n

    Lo m\u00e1s novedoso aqu\u00ed es que ahora cada juego va a tener una categor\u00eda y un autor asociados. Para ello simplemente en el tipo del dato Category y Author tenemos que hacer referencia al id del esquema deseado.

    "},{"location":"develop/filtered/nodejs/#implementar-el-service","title":"Implementar el Service","text":"

    Creamos el service correspondiente game.service.js:

    game.service.js
    import GameModel from '../schemas/game.schema.js';\n\nexport const getGames = async (title, category) => {\n    try {\n        const regexTitle = new RegExp(title, 'i');\n        const find = category? { $and: [{ title: regexTitle }, { category: category }] } : { title: regexTitle };\n        return await GameModel.find(find).sort('id').populate('category').populate('author');\n    } catch(e) {\n        throw Error('Error fetching games');\n    }\n}\n\nexport const createGame = async (data) => {\n    try {\n        const game = new GameModel({\n            ...data,\n            category: data.category.id,\n            author: data.author.id,\n        });\n        return await game.save();\n    } catch (e) {\n        throw Error('Error creating game');\n    }\n}\n\nexport const updateGame = async (id, data) => {\n    try {\n        const game = await GameModel.findById(id);\n        if (!game) {\n            throw Error('There is no game with that Id');\n        }    \n        const gameToUpdate = {\n            ...data,\n            category: data.category.id,\n            author: data.author.id,\n        };\n        return await GameModel.findByIdAndUpdate(id, gameToUpdate, { new: false });\n    } catch (e) {\n        throw Error(e);\n    }\n}\n

    En este caso recibimos en el m\u00e9todo para recuperar juegos dos par\u00e1metros, el titulo del juego y la categor\u00eda. Aqu\u00ed vamos a utilizar una expresi\u00f3n regular para que podamos encontrar cualquier juego que contenga el titulo que pasemos en su nombre. Con la categor\u00eda tiene que ser el valor exacto de su id. El m\u00e9todo populate lo que hace es traernos toda la informaci\u00f3n de la categor\u00eda y del autor. Sino lo us\u00e1semos solo nos recuperar\u00eda el id.

    "},{"location":"develop/filtered/nodejs/#implementar-el-controller","title":"Implementar el Controller","text":"

    Creamos el controlador game.controller.js:

    game.controller.js
    import * as GameService from '../services/game.service.js';\n\nexport const getGames = async (req, res) => {\n    try {\n        const titleToFind = req.query?.title || '';\n        const categoryToFind = req.query?.idCategory || null;\n        const games = await GameService.getGames(titleToFind, categoryToFind);\n        res.status(200).json(games);\n    } catch(err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n\nexport const createGame = async (req, res) => {\n    try {\n        const game = await GameService.createGame(req.body);\n        res.status(200).json({\n            game\n        });\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n\nexport const updateGame = async (req, res) => {\n    const gameId = req.params.id;\n    try {\n        await GameService.updateGame(gameId, req.body);\n        res.status(200).json(1);\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n

    Los m\u00e9todos son muy parecidos al resto de los controllers. En este caso para recuperar los datos del filtro tendremos que hacerlo con req.query para leer los datos que nos lleguen como query params en la url. Por ejemplo: http://localhost:8080/game?title=trivial&category=1

    "},{"location":"develop/filtered/nodejs/#implementar-las-rutas","title":"Implementar las Rutas","text":"

    Y por \u00faltimo creamos nuestro archivo de rutas game.routes.js:

    game.routes.js
    import { Router } from 'express';\nimport { check } from 'express-validator';\nimport validateFields from '../middlewares/validateFields.js';\nimport { createGame, getGames, updateGame } from '../controllers/game.controller.js';\nconst gameRouter = Router();\n\ngameRouter.put('/:id', [\n    check('title').not().isEmpty(),\n    check('age').not().isEmpty(),\n    check('age').isNumeric(),\n    check('category.id').not().isEmpty(),\n    check('author.id').not().isEmpty(),\n    validateFields\n], updateGame);\n\ngameRouter.put('/', [\n    check('title').not().isEmpty(),\n    check('age').not().isEmpty(),\n    check('age').isNumeric(),\n    check('category.id').not().isEmpty(),\n    check('author.id').not().isEmpty(),\n    validateFields\n], createGame);\n\ngameRouter.get('/', getGames);\ngameRouter.get('/:query', getGames);\n\nexport default gameRouter;\n

    En este caso hemos tenido que meter dos rutas para get, una para cuando se informen los filtros y otra para cuando no vayan informados. Si lo hici\u00e9ramos con una \u00fanica ruta nos fallar\u00eda en el otro caso.

    Finalmente en nuestro archivo index.js vamos a a\u00f1adir el nuevo router:

    index.js
    ...\n\nimport gameRouter from './src/routes/game.routes.js';\n\n...\n\napp.use('/game', gameRouter);\n\n...\n
    "},{"location":"develop/filtered/nodejs/#probar-las-operaciones","title":"Probar las operaciones","text":"

    Y ahora que tenemos todo creado, ya podemos probarlo con Postman:

    Por un lado creamos juegos con:

    ** PUT /game **

    ** PUT /game/{id} **

    {\n    \"title\": \"Nuevo juego\",\n    \"age\": \"18\",\n    \"category\": {\n        \"id\": \"63e8b795f7dae4b980b63202\"\n },\n    \"author\": {\n        \"id\": \"63e8bda064c208e065667bfa\"\n    }\n}\n

    Tambi\u00e9n podemos filtrar y recuperar informaci\u00f3n:

    ** GET /game **

    ** GET /game?title=xxx **

    ** GET /game?idCategory=xxx **

    "},{"location":"develop/filtered/nodejs/#implementar-validaciones","title":"Implementar validaciones","text":"

    Ahora que ya tenemos todos nuestros CRUDs creados vamos a introducir unas peque\u00f1as validaciones.

    "},{"location":"develop/filtered/nodejs/#validacion-en-borrado","title":"Validaci\u00f3n en borrado","text":"

    La primera validaci\u00f3n sera para que no podamos borrar categor\u00edas ni autores que tengan un juego asociado. Para ello primero tendremos que crear un m\u00e9todo en el servicio de juegos para buscar los juegos que correspondan con un campo dado. En game.service.js a\u00f1adimos:

    game.service.js
    ...\nexport const getGame = async (field) => {\n    try {\n        return await GameModel.find(field);\n    } catch (e) {\n        throw Error('Error fetching games');\n    }\n}\n...\n

    Y ahora en category.service.js importamos el m\u00e9todo creado y modificamos el m\u00e9todo para borrar categor\u00edas:

    category.service.js
    ...\nimport { getGame } from './game.service.js';\n...\n\n...\nexport const deleteCategory = async (id) => {\n    try {\n        const category = await CategoryModel.findById(id);\n        if (!category) {\n            throw 'There is no category with that Id';\n        }\n        const games = await getGame({category});\n        if(games.length > 0) {\n            throw 'There are games related to this category';\n        }\n        return await CategoryModel.findByIdAndDelete(id);\n    } catch (e) {\n        throw Error(e);\n    }\n}\n...\n

    De este modo si encontramos alg\u00fan juego con esta categor\u00eda no nos dejar\u00e1 borrarla.

    Por \u00faltimo, hacemos lo mismo en author.service.js:

    author.service.js
    ...\nimport { getGame } from './game.service.js';\n...\n\n...\nexport const deleteAuthor = async (id) => {\n    try {\n        const author = await AuthorModel.findById(id);\n        if (!author) {\n            throw 'There is no author with that Id';\n        }\n        const games = await getGame({author});\n        if(games.length > 0) {\n            throw 'There are games related to this author';\n        }\n        return await AuthorModel.findByIdAndDelete(id);\n    } catch (e) {\n        throw Error(e);\n    }\n}\n...\n
    "},{"location":"develop/filtered/nodejs/#validacion-en-creacion","title":"Validaci\u00f3n en creaci\u00f3n","text":"

    En las creaciones es conveniente validad la existencia de las entidades relacionadas para garantizar la integridad de la BBDD.

    Para esto vamos a introducir una validaci\u00f3n en la creaci\u00f3n y edici\u00f3n de los juegos para garantizar que la categor\u00eda y el autor proporcionados existen.

    En primer lugar vamos a crear los servicios de consulta de categor\u00eda y autor:

    category.service.js
    ...\nexport const getCategory = async (id) => {\n    try {\n        return await CategoryModel.findById(id);\n    } catch (e) {\n        throw Error('There is no category with that Id');\n    }\n}\n...\n
    author.service.js
    ...\nexport const getAuthor = async (id) => {\n    try {\n        return await AuthorModel.findById(id);\n    } catch (e) {\n        throw Error('There is no author with that Id');\n    }\n}\n...\n

    Teniendo los servicios ya disponibles, vamos a a\u00f1adir las validaciones a los servicios de creaci\u00f3n y edici\u00f3n:

    game.service.js
    ...\nimport { getCategory } from './category.service.js';\nimport { getAuthor } from './author.service.js';\n...\n\n...\nexport const createGame = async (data) => {\n    try {\n        const category = await getCategory(data.category.id);\n        if (!category) {\n            throw Error('There is no category with that Id');\n        }\n\n        const author = await getAuthor(data.author.id);\n        if (!author) {\n            throw Error('There is no author with that Id');\n        }\n\n        const game = new GameModel({\n            ...data,\n            category: data.category.id,\n            author: data.author.id,\n        });\n        return await game.save();\n    } catch (e) {\n        throw Error(e);\n    }\n}\n...\n\n...\nexport const updateGame = async (id, data) => {\n    try {\n        const game = await GameModel.findById(id);\n        if (!game) {\n            throw Error('There is no game with that Id');\n        }\n\n        const category = await getCategory(data.category.id);\n        if (!category) {\n            throw Error('There is no category with that Id');\n        }\n\n        const author = await getAuthor(data.author.id);\n        if (!author) {\n            throw Error('There is no author with that Id');\n        }\n\n        const gameToUpdate = {\n            ...data,\n            category: data.category.id,\n            author: data.author.id,\n        };\n        return await GameModel.findByIdAndUpdate(id, gameToUpdate, { new: false });\n    } catch (e) {\n        throw Error(e);\n    }\n}\n...\n

    Con esto ya tendr\u00edamos acabado nuestro CRUD.

    "},{"location":"develop/filtered/react/","title":"Listado filtrado - Angular","text":"

    En este punto ya tenemos dos listados, uno b\u00e1sico y otro paginado. Ahora vamos a implementar un listado un poco diferente, con filtros y con una presentaci\u00f3n un tanto distinta.

    Como ya conocemos como se debe desarrollar, en este ejemplo vamos a ir m\u00e1s r\u00e1pidos y nos vamos a centrar \u00fanicamente en las novedades.

    Vamos a desarrollar el listado de Juegos. Este listado es un tanto peculiar, porque no tiene una tabla como tal, sino que vamos a mostrar los juegos como cards. Ya tenemos creado nuestro componentes pagina pero vamos a necesitar un componente para mostrar cada uno de los juegos y otro para crear y editar los juegos.

    "},{"location":"develop/filtered/react/#crear-componente-game","title":"Crear componente game","text":"

    Manos a la obra:

    Creamos el fichero Game.ts dentro de la carpeta types:

    Game.ts
    import { Category } from \"./Category\";\nimport { Author } from \"./Author\";\n\nexport interface Game {\n  id: string;\n  title: string;\n  age: number;\n  category?: Category;\n  author?: Author;\n}\n

    Modificamos nuestra api de Toolkit para a\u00f1adir los endpoints de juegos y aparte creamos un endpoint para recuperar los autores que necesitaremos para crear un nuevo juego, el fichero completo quedar\u00eda de esta manera:

    import { createApi, fetchBaseQuery } from \"@reduxjs/toolkit/query/react\";\nimport { Game } from \"../../types/Game\";\nimport { Category } from \"../../types/Category\";\nimport { Author, AuthorResponse } from \"../../types/Author\";\n\nexport const ludotecaAPI = createApi({\n  reducerPath: \"ludotecaApi\",\n  baseQuery: fetchBaseQuery({\n    baseUrl: \"http://localhost:8080\",\n  }),\n  tagTypes: [\"Category\", \"Author\", \"Game\"],\n  endpoints: (builder) => ({\n    getCategories: builder.query<Category[], null>({\n      query: () => \"category\",\n      providesTags: [\"Category\"],\n    }),\n    createCategory: builder.mutation({\n      query: (payload) => ({\n        url: \"/category\",\n        method: \"PUT\",\n        body: payload,\n        headers: {\n          \"Content-type\": \"application/json; charset=UTF-8\",\n        },\n      }),\n      invalidatesTags: [\"Category\"],\n    }),\n    deleteCategory: builder.mutation({\n      query: (id: string) => ({\n        url: `/category/${id}`,\n        method: \"DELETE\",\n      }),\n      invalidatesTags: [\"Category\"],\n    }),\n    updateCategory: builder.mutation({\n      query: (payload: Category) => ({\n        url: `category/${payload.id}`,\n        method: \"PUT\",\n        body: payload,\n      }),\n      invalidatesTags: [\"Category\"],\n    }),\n    getAllAuthors: builder.query<Author[], null>({\n      query: () => \"author\",\n      providesTags: [\"Author\"],\n    }),\n    getAuthors: builder.query<\n      AuthorResponse,\n      { pageNumber: number; pageSize: number }\n    >({\n      query: ({ pageNumber, pageSize }) => {\n        return {\n          url: \"author\",\n          method: \"POST\",\n          body: {\n            pageable: {\n              pageNumber,\n              pageSize,\n            },\n          },\n        };\n      },\n      providesTags: [\"Author\"],\n    }),\n    createAuthor: builder.mutation({\n      query: (payload) => ({\n        url: \"/author\",\n        method: \"PUT\",\n        body: payload,\n        headers: {\n          \"Content-type\": \"application/json; charset=UTF-8\",\n        },\n      }),\n      invalidatesTags: [\"Author\"],\n    }),\n    deleteAuthor: builder.mutation({\n      query: (id: string) => ({\n        url: `/author/${id}`,\n        method: \"DELETE\",\n      }),\n      invalidatesTags: [\"Author\"],\n    }),\n    updateAuthor: builder.mutation({\n      query: (payload: Author) => ({\n        url: `author/${payload.id}`,\n        method: \"PUT\",\n        body: payload,\n      }),\n      invalidatesTags: [\"Author\", \"Game\"],\n    }),\n    getGames: builder.query<Game[], { title: string; idCategory: string }>({\n      query: ({ title, idCategory }) => {\n        return {\n          url: \"game/\",\n          params: { title, idCategory },\n        };\n      },\n      providesTags: [\"Game\"],\n    }),\n    createGame: builder.mutation({\n      query: (payload: Game) => ({\n        url: \"/game\",\n        method: \"PUT\",\n        body: { ...payload },\n        headers: {\n          \"Content-type\": \"application/json; charset=UTF-8\",\n        },\n      }),\n      invalidatesTags: [\"Game\"],\n    }),\n    updateGame: builder.mutation({\n      query: (payload: Game) => ({\n        url: `game/${payload.id}`,\n        method: \"PUT\",\n        body: { ...payload },\n      }),\n      invalidatesTags: [\"Game\"],\n    }),\n\n  }),\n});\n\nexport const {\n  useGetCategoriesQuery,\n  useCreateCategoryMutation,\n  useDeleteCategoryMutation,\n  useUpdateCategoryMutation,\n  useCreateAuthorMutation,\n  useDeleteAuthorMutation,\n  useGetAllAuthorsQuery,\n  useGetAuthorsQuery,\n  useUpdateAuthorMutation,\n  useCreateGameMutation,\n  useGetGamesQuery,\n  useUpdateGameMutation\n} = ludotecaAPI;\n

    Creamos una nueva carpeta components dentro de src/pages/Game y dentro creamos un archivo llamado CreateGame.tsx con el siguiente contenido:

    CreateGame.tsx
    import { ChangeEvent, useContext, useEffect, useState } from \"react\";\nimport Button from \"@mui/material/Button\";\nimport MenuItem from \"@mui/material/MenuItem\";\nimport TextField from \"@mui/material/TextField\";\nimport Dialog from \"@mui/material/Dialog\";\nimport DialogActions from \"@mui/material/DialogActions\";\nimport DialogContent from \"@mui/material/DialogContent\";\nimport DialogTitle from \"@mui/material/DialogTitle\";\nimport {\n  useGetAllAuthorsQuery,\n  useGetCategoriesQuery,\n} from \"../../../redux/services/ludotecaApi\";\nimport { LoaderContext } from \"../../../context/LoaderProvider\";\nimport { Game } from \"../../../types/Game\";\nimport { Category } from \"../../../types/Category\";\nimport { Author } from \"../../../types/Author\";\n\ninterface Props {\n  game: Game | null;\n  closeModal: () => void;\n  create: (game: Game) => void;\n}\n\nconst initialState = {\n  id: \"\",\n  title: \"\",\n  age: 0,\n  category: undefined,\n  author: undefined,\n};\n\nexport default function CreateGame(props: Props) {\n  const [form, setForm] = useState<Game>(initialState);\n  const loader = useContext(LoaderContext);\n  const { data: categories, isLoading: isLoadingCategories } =\n    useGetCategoriesQuery(null);\n  const { data: authors, isLoading: isLoadingAuthors } =\n    useGetAllAuthorsQuery(null);\n\n  useEffect(() => {\n    setForm({\n      id: props.game?.id || \"\",\n      title: props.game?.title || \"\",\n      age: props.game?.age || 0,\n      category: props.game?.category,\n      author: props.game?.author,\n    });\n  }, [props?.game]);\n\n  useEffect(() => {\n    loader.showLoading(isLoadingCategories || isLoadingAuthors);\n  }, [isLoadingCategories, isLoadingAuthors]);\n\n  const handleChangeForm = (\n    event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n  ) => {\n    setForm({\n      ...form,\n      [event.target.id]: event.target.value,\n    });\n  };\n\n  const handleChangeSelect = (\n    event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n  ) => {\n    const values = event.target.name === \"category\" ? categories : authors;\n    setForm({\n      ...form,\n      [event.target.name]: values?.find((val) => val.id === event.target.value),\n    });\n  };\n\n  return (\n    <div>\n      <Dialog open={true} onClose={props.closeModal}>\n        <DialogTitle>\n          {props.game ? \"Actualizar Juego\" : \"Crear Juego\"}\n        </DialogTitle>\n        <DialogContent>\n          {props.game && (\n            <TextField\n              margin=\"dense\"\n              disabled\n              id=\"id\"\n              label=\"Id\"\n              fullWidth\n              value={props.game.id}\n              variant=\"standard\"\n            />\n          )}\n          <TextField\n            margin=\"dense\"\n            id=\"title\"\n            label=\"Titulo\"\n            fullWidth\n            variant=\"standard\"\n            onChange={handleChangeForm}\n            value={form.title}\n          />\n          <TextField\n            margin=\"dense\"\n            id=\"age\"\n            label=\"Edad Recomendada\"\n            fullWidth\n            type=\"number\"\n            variant=\"standard\"\n            onChange={handleChangeForm}\n            value={form.age}\n          />\n          <TextField\n            id=\"category\"\n            select\n            label=\"Categor\u00eda\"\n            defaultValue=\"''\"\n            fullWidth\n            variant=\"standard\"\n            name=\"category\"\n            value={form.category ? form.category.id : \"\"}\n            onChange={handleChangeSelect}\n          >\n            {categories &&\n              categories.map((option: Category) => (\n                <MenuItem key={option.id} value={option.id}>\n                  {option.name}\n                </MenuItem>\n              ))}\n          </TextField>\n          <TextField\n            id=\"author\"\n            select\n            label=\"Autor\"\n            defaultValue=\"''\"\n            fullWidth\n            variant=\"standard\"\n            name=\"author\"\n            value={form.author ? form.author.id : \"\"}\n            onChange={handleChangeSelect}\n          >\n            {authors &&\n              authors.map((option: Author) => (\n                <MenuItem key={option.id} value={option.id}>\n                  {option.name}\n                </MenuItem>\n              ))}\n          </TextField>\n        </DialogContent>\n        <DialogActions>\n          <Button onClick={props.closeModal}>Cancelar</Button>\n          <Button\n            onClick={() =>\n              props.create({\n                id: \"\",\n                title: form.title,\n                age: form.age,\n                category: form.category,\n                author: form.author,\n              })\n            }\n            disabled={\n              !form.title || !form.age || !form.category || !form.author\n            }\n          >\n            {props.game ? \"Actualizar\" : \"Crear\"}\n          </Button>\n        </DialogActions>\n      </Dialog>\n    </div>\n  );\n}\n

    Ahora en esa misma carpeta crearemos el componente GameCard.tsx para mostrar nuestros juegos con un dise\u00f1o de carta:

    GameCard.tsx
    import Card from \"@mui/material/Card\";\nimport CardContent from \"@mui/material/CardContent\";\nimport CardMedia from \"@mui/material/CardMedia\";\nimport CardHeader from \"@mui/material/CardHeader\";\nimport List from \"@mui/material/List\";\nimport ListItem from \"@mui/material/ListItem\";\nimport ListItemAvatar from \"@mui/material/ListItemAvatar\";\nimport ListItemText from \"@mui/material/ListItemText\";\nimport Avatar from \"@mui/material/Avatar\";\nimport PersonIcon from \"@mui/icons-material/Person\";\nimport LanguageIcon from \"@mui/icons-material/Language\";\nimport CardActionArea from \"@mui/material/CardActionArea\";\nimport red from \"@mui/material/colors/red\";\nimport imageGame from \"./../../../assets/foto.png\";\nimport { Game } from \"../../../types/Game\";\n\ninterface GameCardProps {\n  game: Game;\n}\n\nexport default function GameCard(props: GameCardProps) {\n  const { title, age, category, author } = props.game;\n  return (\n    <Card sx={{ maxWidth: 265 }}>\n      <CardHeader\n        sx={{\n          \".MuiCardHeader-title\": {\n            fontSize: \"20px\",\n          },\n        }}\n        avatar={\n          <Avatar sx={{ bgcolor: red[500] }} aria-label=\"age\">\n            +{age}\n          </Avatar>\n        }\n        title={title}\n        subheader={category?.name}\n      />\n      <CardActionArea>\n        <CardMedia\n          component=\"img\"\n          height=\"140\"\n          image={imageGame}\n          alt=\"game image\"\n        />\n        <CardContent>\n          <List dense={true}>\n            <ListItem>\n              <ListItemAvatar>\n                <Avatar>\n                  <PersonIcon />\n                </Avatar>\n              </ListItemAvatar>\n              <ListItemText primary={`Autor: ${author?.name}`} />\n            </ListItem>\n            <ListItem>\n              <ListItemAvatar>\n                <Avatar>\n                  <LanguageIcon />\n                </Avatar>\n              </ListItemAvatar>\n              <ListItemText primary={`Nacionalidad: ${author?.nationality}`} />\n            </ListItem>\n          </List>\n        </CardContent>\n      </CardActionArea>\n    </Card>\n  );\n}\n

    En la carpeta src/pages/game vamos a crear un fichero para los estilos llamado Game.module.css:

    Game.module.css
    .filter {\n    display: flex;\n    align-items: center;\n}\n\n.cards {\n    display: flex;\n    gap: 20px;\n    padding: 10px;\n    flex-wrap: wrap;\n}\n\n.card {\n    cursor: pointer;\n}\n\n@media (max-width: 800px) {\n    .cards {\n        display: flex;\n        flex-direction: column;\n        align-items: center;\n    }\n\n    .filter {\n        display: flex;\n        flex-direction: column;\n    }\n}\n

    Y por \u00faltimo modificamos nuestro componente p\u00e1gina Game y lo dejamos de esta manera:

    Game.tsx
    import { useState, useContext, useEffect } from \"react\";\nimport MenuItem from \"@mui/material/MenuItem\";\nimport FormControl from \"@mui/material/FormControl\";\nimport TextField from \"@mui/material/TextField\";\nimport Button from \"@mui/material/Button\";\nimport GameCard from \"./components/GameCard\";\nimport styles from \"./Game.module.css\";\nimport {\n  useCreateGameMutation,\n  useGetCategoriesQuery,\n  useGetGamesQuery,\n  useUpdateGameMutation,\n} from \"../../redux/services/ludotecaApi\";\nimport CreateGame from \"./components/CreateGame\";\nimport { LoaderContext } from \"../../context/LoaderProvider\";\nimport { useAppDispatch } from \"../../redux/hooks\";\nimport { setMessage } from \"../../redux/features/messagesSlice\";\nimport { Game as GameModel } from \"../../types/Game\";\nimport { Category } from \"../../types/Category\";\n\nexport const Game = () => {\n  const [openCreate, setOpenCreate] = useState(false);\n  const [filterTitle, setFilterTitle] = useState(\"\");\n  const [filterCategory, setFilterCategory] = useState(\"\");\n  const [gameToUpdate, setGameToUpdate] = useState<GameModel | null>(null);\n  const loader = useContext(LoaderContext);\n  const dispatch = useAppDispatch();\n\n  const { data, error, isLoading, isFetching } = useGetGamesQuery({\n    title: filterTitle,\n    idCategory: filterCategory,\n  });\n\n  const [updateGameApi, { isLoading: isLoadingUpdate, error: errorUpdate }] =\n    useUpdateGameMutation();\n\n  const { data: categories } = useGetCategoriesQuery(null);\n\n  const [createGameApi, { isLoading: isLoadingCreate, error: errorCreate }] =\n    useCreateGameMutation();\n\n  useEffect(() => {\n    loader.showLoading(\n      isLoadingCreate || isLoadingUpdate || isLoading || isFetching\n    );\n  }, [isLoadingCreate, isLoadingUpdate, isLoading, isFetching]);\n\n  useEffect(() => {\n    if (errorCreate || errorUpdate) {\n      setMessage({\n        text: \"Se ha producido un error al realizar la operaci\u00f3n\",\n        type: \"error\",\n      });\n    }\n  }, [errorUpdate, errorCreate]);\n\n  if (error) return <p>Error cargando!!!</p>;\n\n  const createGame = (game: GameModel) => {\n    setOpenCreate(false);\n    if (gameToUpdate) {\n      updateGameApi({\n        ...game,\n        id: gameToUpdate.id,\n      })\n        .then(() => {\n          dispatch(\n            setMessage({\n              text: \"Juego actualizado correctamente\",\n              type: \"ok\",\n            })\n          );\n          setGameToUpdate(null);\n        })\n        .catch((err) => console.log(err));\n    } else {\n      createGameApi(game)\n        .then(() => {\n          dispatch(\n            setMessage({\n              text: \"Juego creado correctamente\",\n              type: \"ok\",\n            })\n          );\n          setGameToUpdate(null);\n        })\n        .catch((err) => console.log(err));\n    }\n  };\n\n  return (\n    <div className=\"container\">\n      <h1>Cat\u00e1logo de juegos</h1>\n      <div className={styles.filter}>\n        <FormControl variant=\"standard\" sx={{ m: 1, minWidth: 220 }}>\n          <TextField\n            margin=\"dense\"\n            id=\"title\"\n            label=\"Titulo\"\n            fullWidth\n            value={filterTitle}\n            variant=\"standard\"\n            onChange={(event) => setFilterTitle(event.target.value)}\n          />\n        </FormControl>\n        <FormControl variant=\"standard\" sx={{ m: 1, minWidth: 220 }}>\n          <TextField\n            id=\"category\"\n            select\n            label=\"Categor\u00eda\"\n            defaultValue=\"''\"\n            fullWidth\n            variant=\"standard\"\n            name=\"author\"\n            value={filterCategory}\n            onChange={(event) => setFilterCategory(event.target.value)}\n          >\n            {categories &&\n              categories.map((option: Category) => (\n                <MenuItem key={option.id} value={option.id}>\n                  {option.name}\n                </MenuItem>\n              ))}\n          </TextField>\n        </FormControl>\n        <Button\n          variant=\"outlined\"\n          onClick={() => {\n            setFilterCategory(\"\");\n            setFilterTitle(\"\");\n          }}\n        >\n          Limpiar\n        </Button>\n      </div>\n      <div className={styles.cards}>\n        {data?.map((card) => (\n          <div\n            key={card.id}\n            className={styles.card}\n            onClick={() => {\n              setGameToUpdate(card);\n              setOpenCreate(true);\n            }}\n          >\n            <GameCard game={card} />\n          </div>\n        ))}\n      </div>\n      <div className=\"newButton\">\n        <Button variant=\"contained\" onClick={() => setOpenCreate(true)}>\n          Nuevo juego\n        </Button>\n      </div>\n      {openCreate && (\n        <CreateGame\n          create={createGame}\n          game={gameToUpdate}\n          closeModal={() => {\n            setGameToUpdate(null);\n            setOpenCreate(false);\n          }}\n        />\n      )}\n    </div>\n  );\n};\n

    Y por \u00faltimo descargamos la siguiente imagen y la guardamos en la carpeta src/assets.

    En este listado realizamos el filtro de manera din\u00e1mica, en el momento en que cambiamos el valor de la categor\u00eda o el t\u00edtulo a filtrar, como estas variables est\u00e1n asociadas al estado de nuestro componente, se vuelve a renderizar y por lo tanto se actualiza el valor de \"data\" modificando as\u00ed los resultados.

    El resto es muy parecido a lo que ya hemos realizado antes. Aqu\u00ed no tenemos una tabla, sino que mostramos nuestros juegos como Cards y si pulsamos sobre cualquier Card se mostrar\u00e1 el formulario de edici\u00f3n del juego.

    Si ahora arrancamos el proyecto y nos vamos a la pagina de juegos podremos crear y ver nuestros juegos.

    "},{"location":"develop/filtered/springboot/","title":"Listado filtrado - Spring Boot","text":"

    En este punto ya tenemos dos listados, uno b\u00e1sico y otro paginado. Ahora vamos a implementar un listado un poco diferente, este listado va a tener filtros de b\u00fasqueda.

    Como ya conocemos como se debe desarrollar, en este ejemplo vamos a ir m\u00e1s r\u00e1pidos y nos vamos a centrar \u00fanicamente en las novedades.

    "},{"location":"develop/filtered/springboot/#crear-modelos","title":"Crear Modelos","text":"

    Lo primero que vamos a hacer es crear los modelos para trabajar con BBDD y con peticiones hacia el front. Adem\u00e1s, tambi\u00e9n tenemos que a\u00f1adir datos al script de inicializaci\u00f3n de BBDD.

    Game.javaGameDto.javadata.sql
    package com.ccsw.tutorial.game.model;\n\nimport com.ccsw.tutorial.author.model.Author;\nimport com.ccsw.tutorial.category.model.Category;\n\nimport jakarta.persistence.*;\n\n\n/**\n * @author ccsw\n *\n */\n@Entity\n@Table(name = \"game\")\npublic class Game {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"id\", nullable = false)\n    private Long id;\n\n    @Column(name = \"title\", nullable = false)\n    private String title;\n\n    @Column(name = \"age\", nullable = false)\n    private String age;\n\n    @ManyToOne\n    @JoinColumn(name = \"category_id\", nullable = false)\n    private Category category;\n\n    @ManyToOne\n    @JoinColumn(name = \"author_id\", nullable = false)\n    private Author author;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return title\n     */\n    public String getTitle() {\n\n        return this.title;\n    }\n\n    /**\n     * @param title new value of {@link #getTitle}.\n     */\n    public void setTitle(String title) {\n\n        this.title = title;\n    }\n\n    /**\n     * @return age\n     */\n    public String getAge() {\n\n        return this.age;\n    }\n\n    /**\n     * @param age new value of {@link #getAge}.\n     */\n    public void setAge(String age) {\n\n        this.age = age;\n    }\n\n    /**\n     * @return category\n     */\n    public Category getCategory() {\n\n        return this.category;\n    }\n\n    /**\n     * @param category new value of {@link #getCategory}.\n     */\n    public void setCategory(Category category) {\n\n        this.category = category;\n    }\n\n    /**\n     * @return author\n     */\n    public Author getAuthor() {\n\n        return this.author;\n    }\n\n    /**\n     * @param author new value of {@link #getAuthor}.\n     */\n    public void setAuthor(Author author) {\n\n        this.author = author;\n    }\n\n}\n
    package com.ccsw.tutorial.game.model;\n\nimport com.ccsw.tutorial.author.model.AuthorDto;\nimport com.ccsw.tutorial.category.model.CategoryDto;\n\n/**\n * @author ccsw\n *\n */\npublic class GameDto {\n\n    private Long id;\n\n    private String title;\n\n    private String age;\n\n    private CategoryDto category;\n\n    private AuthorDto author;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return title\n     */\n    public String getTitle() {\n\n        return this.title;\n    }\n\n    /**\n     * @param title new value of {@link #getTitle}.\n     */\n    public void setTitle(String title) {\n\n        this.title = title;\n    }\n\n    /**\n     * @return age\n     */\n    public String getAge() {\n\n        return this.age;\n    }\n\n    /**\n     * @param age new value of {@link #getAge}.\n     */\n    public void setAge(String age) {\n\n        this.age = age;\n    }\n\n    /**\n     * @return category\n     */\n    public CategoryDto getCategory() {\n\n        return this.category;\n    }\n\n    /**\n     * @param category new value of {@link #getCategory}.\n     */\n    public void setCategory(CategoryDto category) {\n\n        this.category = category;\n    }\n\n    /**\n     * @return author\n     */\n    public AuthorDto getAuthor() {\n\n        return this.author;\n    }\n\n    /**\n     * @param author new value of {@link #getAuthor}.\n     */\n    public void setAuthor(AuthorDto author) {\n\n        this.author = author;\n    }\n\n}\n
    INSERT INTO category(name) VALUES ('Eurogames');\nINSERT INTO category(name) VALUES ('Ameritrash');\nINSERT INTO category(name) VALUES ('Familiar');\n\nINSERT INTO author(name, nationality) VALUES ('Alan R. Moon', 'US');\nINSERT INTO author(name, nationality) VALUES ('Vital Lacerda', 'PT');\nINSERT INTO author(name, nationality) VALUES ('Simone Luciani', 'IT');\nINSERT INTO author(name, nationality) VALUES ('Perepau Llistosella', 'ES');\nINSERT INTO author(name, nationality) VALUES ('Michael Kiesling', 'DE');\nINSERT INTO author(name, nationality) VALUES ('Phil Walker-Harding', 'US');\n\nINSERT INTO game(title, age, category_id, author_id) VALUES ('On Mars', '14', 1, 2);\nINSERT INTO game(title, age, category_id, author_id) VALUES ('Aventureros al tren', '8', 3, 1);\nINSERT INTO game(title, age, category_id, author_id) VALUES ('1920: Wall Street', '12', 1, 4);\nINSERT INTO game(title, age, category_id, author_id) VALUES ('Barrage', '14', 1, 3);\nINSERT INTO game(title, age, category_id, author_id) VALUES ('Los viajes de Marco Polo', '12', 1, 3);\nINSERT INTO game(title, age, category_id, author_id) VALUES ('Azul', '8', 3, 5);\n

    Relaciones anidadas

    F\u00edjate que tanto la Entity como el Dto tienen relaciones con Author y Category. Gracias a Spring JPA se pueden resolver de esta forma y tener toda la informaci\u00f3n de las relaciones hijas dentro del objeto padre. Muy importante recordar que en el mundo entity las relaciones ser\u00e1n con objetos Entity mientras que en el mundo dto las relaciones deben ser siempre con objetos Dto. La utilidad beanMapper ya har\u00e1 las conversiones necesarias, siempre que tengan el mismo nombre de propiedades.

    "},{"location":"develop/filtered/springboot/#tdd-pruebas","title":"TDD - Pruebas","text":"

    Para desarrollar todas las operaciones, empezaremos primero dise\u00f1ando las pruebas y luego implementando el c\u00f3digo necesario que haga funcionar correctamente esas pruebas. Para ir m\u00e1s r\u00e1pido vamos a poner todas las pruebas de golpe, pero realmente se deber\u00edan crear una a una e ir implementando el c\u00f3digo necesario para esa prueba. Para evitar tantas iteraciones en el tutorial las haremos todas de golpe.

    Vamos a pararnos a pensar un poco que necesitamos en la pantalla. En este caso solo tenemos dos operaciones:

    • Una consulta filtrada, que reciba datos de filtro opcionales (t\u00edtulo e idCategor\u00eda) y devuelva los datos ya filtrados
    • Una operaci\u00f3n de guardado y modificaci\u00f3n

    De nuevo tendremos que desglosar esto en varios casos de prueba:

    • Buscar un juego sin filtros
    • Buscar un t\u00edtulo que exista
    • Buscar una categor\u00eda que exista
    • Buscar un t\u00edtulo y una categor\u00eda que existan
    • Buscar un t\u00edtulo que no exista
    • Buscar una categor\u00eda que no exista
    • Buscar un t\u00edtulo y una categor\u00eda que no existan
    • Crear un juego nuevo (en realidad deber\u00edamos probar diferentes combinaciones y errores)
    • Modificar un juego que exista
    • Modificar un juego que no exista

    Tambi\u00e9n crearemos una clase GameController dentro del package de com.ccsw.tutorial.game con la implementaci\u00f3n de los m\u00e9todos vac\u00edos, para que no falle la compilaci\u00f3n.

    \u00a1Vamos a implementar test!

    GameController.javaGameIT.java
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.game.model.Game;\nimport com.ccsw.tutorial.game.model.GameDto;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Tag(name = \"Game\", description = \"API of Game\")\n@RequestMapping(value = \"/game\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class GameController {\n\n    /**\n     * M\u00e9todo para recuperar una lista de {@link Game}\n     *\n     * @param title t\u00edtulo del juego\n     * @param idCategory PK de la categor\u00eda\n     * @return {@link List} de {@link GameDto}\n     */\n    @Operation(summary = \"Find\", description = \"Method that return a filtered list of Games\")\n    @RequestMapping(path = \"\", method = RequestMethod.GET)\n    public List<GameDto> find(@RequestParam(value = \"title\", required = false) String title,\n                              @RequestParam(value = \"idCategory\", required = false) Long idCategory) {\n\n        return null;\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Game}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Game\")\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody GameDto dto) {\n\n    }\n\n}\n
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.author.model.AuthorDto;\nimport com.ccsw.tutorial.category.model.CategoryDto;\nimport com.ccsw.tutorial.game.model.GameDto;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.test.web.client.TestRestTemplate;\nimport org.springframework.boot.test.web.server.LocalServerPort;\nimport org.springframework.core.ParameterizedTypeReference;\nimport org.springframework.http.HttpEntity;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.test.annotation.DirtiesContext;\nimport org.springframework.web.util.UriComponentsBuilder;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)\n@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)\npublic class GameIT {\n\n    public static final String LOCALHOST = \"http://localhost:\";\n    public static final String SERVICE_PATH = \"/game\";\n\n    public static final Long EXISTS_GAME_ID = 1L;\n    public static final Long NOT_EXISTS_GAME_ID = 0L;\n    private static final String NOT_EXISTS_TITLE = \"NotExists\";\n    private static final String EXISTS_TITLE = \"Aventureros\";\n    private static final String NEW_TITLE = \"Nuevo juego\";\n    private static final Long NOT_EXISTS_CATEGORY = 0L;\n    private static final Long EXISTS_CATEGORY = 3L;\n\n    private static final String TITLE_PARAM = \"title\";\n    private static final String CATEGORY_ID_PARAM = \"idCategory\";\n\n    @LocalServerPort\n    private int port;\n\n    @Autowired\n    private TestRestTemplate restTemplate;\n\n    ParameterizedTypeReference<List<GameDto>> responseType = new ParameterizedTypeReference<List<GameDto>>(){};\n\n    private String getUrlWithParams(){\n    return UriComponentsBuilder.fromHttpUrl(LOCALHOST + port + SERVICE_PATH)\n    .queryParam(TITLE_PARAM, \"{\" + TITLE_PARAM +\"}\")\n    .queryParam(CATEGORY_ID_PARAM, \"{\" + CATEGORY_ID_PARAM +\"}\")\n    .encode()\n    .toUriString();\n    }\n\n    @Test\n    public void findWithoutFiltersShouldReturnAllGamesInDB() {\n\n          int GAMES_WITH_FILTER = 6;\n\n          Map<String, Object> params = new HashMap<>();\n          params.put(TITLE_PARAM, null);\n          params.put(CATEGORY_ID_PARAM, null);\n\n          ResponseEntity<List<GameDto>> response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n\n          assertNotNull(response);\n          assertEquals(GAMES_WITH_FILTER, response.getBody().size());\n    }\n\n    @Test\n    public void findExistsTitleShouldReturnGames() {\n\n          int GAMES_WITH_FILTER = 1;\n\n          Map<String, Object> params = new HashMap<>();\n          params.put(TITLE_PARAM, EXISTS_TITLE);\n          params.put(CATEGORY_ID_PARAM, null);\n\n          ResponseEntity<List<GameDto>> response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n\n          assertNotNull(response);\n          assertEquals(GAMES_WITH_FILTER, response.getBody().size());\n    }\n\n    @Test\n    public void findExistsCategoryShouldReturnGames() {\n\n          int GAMES_WITH_FILTER = 2;\n\n          Map<String, Object> params = new HashMap<>();\n          params.put(TITLE_PARAM, null);\n          params.put(CATEGORY_ID_PARAM, EXISTS_CATEGORY);\n\n          ResponseEntity<List<GameDto>> response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n\n          assertNotNull(response);\n          assertEquals(GAMES_WITH_FILTER, response.getBody().size());\n    }\n\n    @Test\n    public void findExistsTitleAndCategoryShouldReturnGames() {\n\n          int GAMES_WITH_FILTER = 1;\n\n          Map<String, Object> params = new HashMap<>();\n          params.put(TITLE_PARAM, EXISTS_TITLE);\n          params.put(CATEGORY_ID_PARAM, EXISTS_CATEGORY);\n\n          ResponseEntity<List<GameDto>> response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n\n          assertNotNull(response);\n          assertEquals(GAMES_WITH_FILTER, response.getBody().size());\n    }\n\n    @Test\n    public void findNotExistsTitleShouldReturnEmpty() {\n\n          int GAMES_WITH_FILTER = 0;\n\n          Map<String, Object> params = new HashMap<>();\n          params.put(TITLE_PARAM, NOT_EXISTS_TITLE);\n          params.put(CATEGORY_ID_PARAM, null);\n\n          ResponseEntity<List<GameDto>> response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n\n          assertNotNull(response);\n          assertEquals(GAMES_WITH_FILTER, response.getBody().size());\n    }\n\n    @Test\n    public void findNotExistsCategoryShouldReturnEmpty() {\n\n          int GAMES_WITH_FILTER = 0;\n\n          Map<String, Object> params = new HashMap<>();\n          params.put(TITLE_PARAM, null);\n          params.put(CATEGORY_ID_PARAM, NOT_EXISTS_CATEGORY);\n\n          ResponseEntity<List<GameDto>> response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n\n          assertNotNull(response);\n          assertEquals(GAMES_WITH_FILTER, response.getBody().size());\n    }\n\n    @Test\n    public void findNotExistsTitleOrCategoryShouldReturnEmpty() {\n\n          int GAMES_WITH_FILTER = 0;\n\n          Map<String, Object> params = new HashMap<>();\n          params.put(TITLE_PARAM, NOT_EXISTS_TITLE);\n          params.put(CATEGORY_ID_PARAM, NOT_EXISTS_CATEGORY);\n\n          ResponseEntity<List<GameDto>> response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n          assertNotNull(response);\n          assertEquals(GAMES_WITH_FILTER, response.getBody().size());\n\n          params.put(TITLE_PARAM, NOT_EXISTS_TITLE);\n          params.put(CATEGORY_ID_PARAM, EXISTS_CATEGORY);\n\n          response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n          assertNotNull(response);\n          assertEquals(GAMES_WITH_FILTER, response.getBody().size());\n\n          params.put(TITLE_PARAM, EXISTS_TITLE);\n          params.put(CATEGORY_ID_PARAM, NOT_EXISTS_CATEGORY);\n\n          response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n          assertNotNull(response);\n          assertEquals(GAMES_WITH_FILTER, response.getBody().size());\n    }\n\n    @Test\n    public void saveWithoutIdShouldCreateNewGame() {\n\n          GameDto dto = new GameDto();\n          AuthorDto authorDto = new AuthorDto();\n          authorDto.setId(1L);\n\n          CategoryDto categoryDto = new CategoryDto();\n          categoryDto.setId(1L);\n\n          dto.setTitle(NEW_TITLE);\n          dto.setAge(\"18\");\n          dto.setAuthor(authorDto);\n          dto.setCategory(categoryDto);\n\n          Map<String, Object> params = new HashMap<>();\n          params.put(TITLE_PARAM, NEW_TITLE);\n          params.put(CATEGORY_ID_PARAM, null);\n\n          ResponseEntity<List<GameDto>> response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n\n          assertNotNull(response);\n          assertEquals(0, response.getBody().size());\n\n          restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.PUT, new HttpEntity<>(dto), Void.class);\n\n          response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n\n          assertNotNull(response);\n          assertEquals(1, response.getBody().size());\n    }\n\n    @Test\n    public void modifyWithExistIdShouldModifyGame() {\n\n          GameDto dto = new GameDto();\n          AuthorDto authorDto = new AuthorDto();\n          authorDto.setId(1L);\n\n          CategoryDto categoryDto = new CategoryDto();\n          categoryDto.setId(1L);\n\n          dto.setTitle(NEW_TITLE);\n          dto.setAge(\"18\");\n          dto.setAuthor(authorDto);\n          dto.setCategory(categoryDto);\n\n          Map<String, Object> params = new HashMap<>();\n          params.put(TITLE_PARAM, NEW_TITLE);\n          params.put(CATEGORY_ID_PARAM, null);\n\n          ResponseEntity<List<GameDto>> response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n\n          assertNotNull(response);\n          assertEquals(0, response.getBody().size());\n\n          restTemplate.exchange(LOCALHOST + port + SERVICE_PATH + \"/\" + EXISTS_GAME_ID, HttpMethod.PUT, new HttpEntity<>(dto), Void.class);\n\n          response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n\n          assertNotNull(response);\n          assertEquals(1, response.getBody().size());\n          assertEquals(EXISTS_GAME_ID, response.getBody().get(0).getId());\n    }\n\n    @Test\n    public void modifyWithNotExistIdShouldThrowException() {\n\n          GameDto dto = new GameDto();\n          dto.setTitle(NEW_TITLE);\n\n          ResponseEntity<?> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH + \"/\" + NOT_EXISTS_GAME_ID, HttpMethod.PUT, new HttpEntity<>(dto), Void.class);\n\n          assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());\n    }\n\n}\n

    B\u00fasquedas en BBDD

    Siempre deber\u00edamos buscar a los hijos por primary keys, nunca hay que hacerlo por una descripci\u00f3n libre, ya que el usuario podr\u00eda teclear el mismo nombre de diferentes formas y no habr\u00eda manera de buscar correctamente el resultado. As\u00ed que siempre que haya un dropdown, se debe filtrar por su ID.

    Si ahora ejecutas los jUnits, ver\u00e1s que en este caso hemos construido 10 pruebas, para cubrir los casos b\u00e1sicos del Controller, y todas ellas fallan la ejecuci\u00f3n. Vamos a seguir implementando el resto de capas para hacer que los test funcionen.

    "},{"location":"develop/filtered/springboot/#controller","title":"Controller","text":"

    De nuevo para poder compilar esta capa, nos hace falta delegar sus operaciones de l\u00f3gica de negocio en un Service as\u00ed que lo crearemos al mismo tiempo que lo vamos necesitando.

    GameService.javaGameController.java
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.game.model.Game;\nimport com.ccsw.tutorial.game.model.GameDto;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\npublic interface GameService {\n\n    /**\n     * Recupera los juegos filtrando opcionalmente por t\u00edtulo y/o categor\u00eda\n     *\n     * @param title t\u00edtulo del juego\n     * @param idCategory PK de la categor\u00eda\n     * @return {@link List} de {@link Game}\n     */\n    List<Game> find(String title, Long idCategory);\n\n    /**\n     * Guarda o modifica un juego, dependiendo de si el identificador est\u00e1 o no informado\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    void save(Long id, GameDto dto);\n\n}\n
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.game.model.Game;\nimport com.ccsw.tutorial.game.model.GameDto;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.modelmapper.ModelMapper;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * @author ccsw\n *\n */\n@Tag(name = \"Game\", description = \"API of Game\")\n@RequestMapping(value = \"/game\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class GameController {\n\n    @Autowired\n    GameService gameService;\n\n    @Autowired\n    ModelMapper mapper;\n\n    /**\n     * M\u00e9todo para recuperar una lista de {@link Game}\n     *\n     * @param title t\u00edtulo del juego\n     * @param idCategory PK de la categor\u00eda\n     * @return {@link List} de {@link GameDto}\n     */\n    @Operation(summary = \"Find\", description = \"Method that return a filtered list of Games\")\n    @RequestMapping(path = \"\", method = RequestMethod.GET)\n    public List<GameDto> find(@RequestParam(value = \"title\", required = false) String title,\n                              @RequestParam(value = \"idCategory\", required = false) Long idCategory) {\n\n        List<Game> games = gameService.find(title, idCategory);\n\n        return games.stream().map(e -> mapper.map(e, GameDto.class)).collect(Collectors.toList());\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Game}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Game\")\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody GameDto dto) {\n\n        gameService.save(id, dto);\n    }\n\n}\n

    En esta ocasi\u00f3n, para el m\u00e9todo de b\u00fasqueda hemos decidido utilizar par\u00e1metros en la URL de tal forma que nos quedar\u00e1 algo as\u00ed http://localhost:8080/game/?title=xxx&idCategoria=yyy. Queremos recuperar el recurso Game que es el raiz de la ruta, pero filtrado por cero o varios par\u00e1metros.

    "},{"location":"develop/filtered/springboot/#service","title":"Service","text":"

    Siguiente paso, la capa de l\u00f3gica de negocio, es decir el Service, que por tanto har\u00e1 uso de un Repository.

    GameServiceImpl.javaGameRepository.java
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.game.model.Game;\nimport com.ccsw.tutorial.game.model.GameDto;\nimport jakarta.transaction.Transactional;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\n\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Service\n@Transactional\npublic class GameServiceImpl implements GameService {\n\n    @Autowired\n    GameRepository gameRepository;\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public List<Game> find(String title, Long idCategory) {\n\n        return (List<Game>) this.gameRepository.findAll();\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void save(Long id, GameDto dto) {\n\n        Game game;\n\n        if (id == null) {\n            game = new Game();\n        } else {\n            game = this.gameRepository.findById(id).orElse(null);\n        }\n\n        BeanUtils.copyProperties(dto, game, \"id\", \"author\", \"category\");\n\n        this.gameRepository.save(game);\n    }\n\n}\n
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.game.model.Game;\nimport org.springframework.data.repository.CrudRepository;\n\n/**\n * @author ccsw\n *\n */\npublic interface GameRepository extends CrudRepository<Game, Long> {\n\n}\n

    Este servicio tiene dos peculiaridades, remarcadas en amarillo en la clase anterior. Por un lado tenemos la consulta, que no es un listado completo ni un listado paginado, sino que es un listado con filtros. Luego veremos como se hace eso, de momento lo dejaremos como un m\u00e9todo que recibe los dos filtros.

    La segunda peculiaridad es que de cliente nos est\u00e1 llegando un GameDto, que internamente tiene un AuthorDto y un CategoryDto, pero nosotros lo tenemos que traducir a entidades de BBDD. No sirve con copiar las propiedades tal cual, ya que entonces Spring lo que har\u00e1 ser\u00e1 crear un objeto nuevo y persistir ese objeto nuevo de Author y de Category. Adem\u00e1s, de cliente generalmente tan solo nos llega el ID de esos objetos hijo, y no el resto de informaci\u00f3n de la entidad. Por esos motivos lo hemos ignorado del copyProperties.

    Pero de alguna forma tendremos que asignarle esos valores a la entidad Game. Si conocemos sus ID que es lo que generalmente llega, podemos recuperar esos objetos de BBDD y asignarlos en el objeto Game. Si recuerdas las reglas b\u00e1sicas, un Repository debe pertenecer a un solo Service, por lo que en lugar de llamar a m\u00e9todos de los AuthorRepository y CategoryRepository desde nuestro GameServiceImpl, debemos llamar a m\u00e9todos expuestos en AuthorService y CategoryService, que son los que gestionan sus repositorios. Para ello necesitaremos crear esos m\u00e9todos get en los otros Services.

    Y ya sabes, para implementar nuevos m\u00e9todos, antes se deben hacer las pruebas jUnit, que en este caso, por variar, cubriremos con pruebas unitarias. Recuerda que los test van en src/test/java

    AuthorTest.javaAuthorService.javaAuthorServiceImpl.java
    package com.ccsw.tutorial.author;\n\nimport com.ccsw.tutorial.author.model.Author;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\nimport java.util.Optional;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\n@ExtendWith(MockitoExtension.class)\npublic class AuthorTest {\n\n    public static final Long EXISTS_AUTHOR_ID = 1L;\n    public static final Long NOT_EXISTS_AUTHOR_ID = 0L;\n\n    @Mock\n    private AuthorRepository authorRepository;\n\n    @InjectMocks\n    private AuthorServiceImpl authorService;\n\n    @Test\n    public void getExistsAuthorIdShouldReturnAuthor() {\n\n          Author author = mock(Author.class);\n          when(author.getId()).thenReturn(EXISTS_AUTHOR_ID);\n          when(authorRepository.findById(EXISTS_AUTHOR_ID)).thenReturn(Optional.of(author));\n\n          Author authorResponse = authorService.get(EXISTS_AUTHOR_ID);\n\n          assertNotNull(authorResponse);\n\n          assertEquals(EXISTS_AUTHOR_ID, authorResponse.getId());\n    }\n\n    @Test\n    public void getNotExistsAuthorIdShouldReturnNull() {\n\n          when(authorRepository.findById(NOT_EXISTS_AUTHOR_ID)).thenReturn(Optional.empty());\n\n          Author author = authorService.get(NOT_EXISTS_AUTHOR_ID);\n\n          assertNull(author);\n    }\n\n}\n
    package com.ccsw.tutorial.author;\n\nimport com.ccsw.tutorial.author.model.Author;\nimport com.ccsw.tutorial.author.model.AuthorDto;\nimport com.ccsw.tutorial.author.model.AuthorSearchDto;\nimport org.springframework.data.domain.Page;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\npublic interface AuthorService {\n\n    /**\n     * Recupera un {@link Author} a trav\u00e9s de su ID\n     *\n     * @param id PK de la entidad\n     * @return {@link Author}\n     */\n    Author get(Long id);\n\n    /**\n     * M\u00e9todo para recuperar un listado paginado de {@link Author}\n     *\n     * @param dto dto de b\u00fasqueda\n     * @return {@link Page} de {@link Author}\n     */\n    Page<Author> findPage(AuthorSearchDto dto);\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    void save(Long id, AuthorDto dto);\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     */\n    void delete(Long id) throws Exception;\n\n}\n
    package com.ccsw.tutorial.author;\n\nimport com.ccsw.tutorial.author.model.Author;\nimport com.ccsw.tutorial.author.model.AuthorDto;\nimport com.ccsw.tutorial.author.model.AuthorSearchDto;\nimport jakarta.transaction.Transactional;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.domain.Page;\nimport org.springframework.stereotype.Service;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Service\n@Transactional\npublic class AuthorServiceImpl implements AuthorService {\n\n    @Autowired\n    AuthorRepository authorRepository;\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public Author get(Long id) {\n\n        return this.authorRepository.findById(id).orElse(null);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public Page<Author> findPage(AuthorSearchDto dto) {\n\n        return this.authorRepository.findAll(dto.getPageable().getPageable());\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void save(Long id, AuthorDto data) {\n\n        Author author;\n\n        if (id == null) {\n            author = new Author();\n        } else {\n            author = this.get(id);\n        }\n\n        BeanUtils.copyProperties(data, author, \"id\");\n\n        this.authorRepository.save(author);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void delete(Long id) throws Exception {\n\n        if(this.get(id) == null){\n            throw new Exception(\"Not exists\");\n        }\n\n        this.authorRepository.deleteById(id);\n    }\n\n}\n

    Y lo mismo para categor\u00edas.

    CategoryTest.javaCategoryService.javaCategoryServiceImpl.java
    public static final Long NOT_EXISTS_CATEGORY_ID = 0L;\n\n@Test\npublic void getExistsCategoryIdShouldReturnCategory() {\n\n      Category category = mock(Category.class);\n      when(category.getId()).thenReturn(EXISTS_CATEGORY_ID);\n      when(categoryRepository.findById(EXISTS_CATEGORY_ID)).thenReturn(Optional.of(category));\n\n      Category categoryResponse = categoryService.get(EXISTS_CATEGORY_ID);\n\n      assertNotNull(categoryResponse);\n      assertEquals(EXISTS_CATEGORY_ID, category.getId());\n}\n\n@Test\npublic void getNotExistsCategoryIdShouldReturnNull() {\n\n      when(categoryRepository.findById(NOT_EXISTS_CATEGORY_ID)).thenReturn(Optional.empty());\n\n      Category category = categoryService.get(NOT_EXISTS_CATEGORY_ID);\n\n      assertNull(category);\n}\n
    package com.ccsw.tutorial.category;\n\nimport com.ccsw.tutorial.category.model.Category;\nimport com.ccsw.tutorial.category.model.CategoryDto;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\npublic interface CategoryService {\n\n    /**\n     * Recupera una {@link Category} a partir de su ID\n     *\n     * @param id PK de la entidad\n     * @return {@link Category}\n     */\n    Category get(Long id);\n\n    /**\n     * M\u00e9todo para recuperar todas las {@link Category}\n     *\n     * @return {@link List} de {@link Category}\n     */\n    List<Category> findAll();\n\n    /**\n     * M\u00e9todo para crear o actualizar una {@link Category}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    void save(Long id, CategoryDto dto);\n\n    /**\n     * M\u00e9todo para borrar una {@link Category}\n     *\n     * @param id PK de la entidad\n     */\n    void delete(Long id) throws Exception;\n\n}\n
    package com.ccsw.tutorial.category;\n\nimport com.ccsw.tutorial.category.model.Category;\nimport com.ccsw.tutorial.category.model.CategoryDto;\nimport jakarta.transaction.Transactional;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Service\n@Transactional\npublic class CategoryServiceImpl implements CategoryService {\n\n    @Autowired\n    CategoryRepository categoryRepository;\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public Category get(Long id) {\n\n          return this.categoryRepository.findById(id).orElse(null);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public List<Category> findAll() {\n\n          return (List<Category>) this.categoryRepository.findAll();\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void save(Long id, CategoryDto dto) {\n\n          Category category;\n\n          if (id == null) {\n             category = new Category();\n          } else {\n             category = this.get(id);\n          }\n\n          category.setName(dto.getName());\n\n          this.categoryRepository.save(category);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void delete(Long id) throws Exception {\n\n          if(this.get(id) == null){\n             throw new Exception(\"Not exists\");\n          }\n\n          this.categoryRepository.deleteById(id);\n    }\n\n}\n

    Clean Code

    A la hora de implementar m\u00e9todos nuevos, ten siempre presente el Clean Code. \u00a1No dupliques c\u00f3digo!, es muy importante de cara al futuro mantenimiento. Si en nuestro m\u00e9todo save hac\u00edamos uso de una operaci\u00f3n findById y ahora hemos creado una nueva operaci\u00f3n get, hagamos uso de esta nueva operaci\u00f3n y no repitamos el c\u00f3digo.

    Y ahora que ya tenemos los m\u00e9todos necesarios, ya podemos implementar correctamente nuestro GameServiceImpl.

    GameServiceImpl.java
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.author.AuthorService;\nimport com.ccsw.tutorial.category.CategoryService;\nimport com.ccsw.tutorial.game.model.Game;\nimport com.ccsw.tutorial.game.model.GameDto;\nimport jakarta.transaction.Transactional;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\n\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Service\n@Transactional\npublic class GameServiceImpl implements GameService {\n\n    @Autowired\n    GameRepository gameRepository;\n\n    @Autowired\n    AuthorService authorService;\n\n    @Autowired\n    CategoryService categoryService;\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public List<Game> find(String title, Long idCategory) {\n\n        return this.gameRepository.findAll();\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void save(Long id, GameDto dto) {\n\n        Game game;\n\n        if (id == null) {\n            game = new Game();\n        } else {\n            game = this.gameRepository.findById(id).orElse(null);\n        }\n\n        BeanUtils.copyProperties(dto, game, \"id\", \"author\", \"category\");\n\n        game.setAuthor(authorService.get(dto.getAuthor().getId()));\n        game.setCategory(categoryService.get(dto.getCategory().getId()));\n\n        this.gameRepository.save(game);\n    }\n\n}\n

    Ahora si que tenemos la capa de l\u00f3gica de negocio terminada, podemos pasar a la siguiente capa.

    "},{"location":"develop/filtered/springboot/#repository","title":"Repository","text":"

    Y llegamos a la \u00faltima capa donde, si recordamos, ten\u00edamos un m\u00e9todo que recibe dos par\u00e1metros. Necesitamos traducir esto en una consulta a la BBDD.

    Vamos a necesitar un listado filtrado por t\u00edtulo o por categor\u00eda, as\u00ed que necesitaremos pasarle esos datos y filtrar la query. Para el t\u00edtulo vamos a buscar por una cadena contenida, as\u00ed que el par\u00e1metro ser\u00e1 de tipo String, mientras que para la categor\u00eda vamos a buscar por su primary key, as\u00ed que el par\u00e1metro ser\u00e1 de tipo Long.

    Existen varias estrategias para abordar esta implementaci\u00f3n. Podr\u00edamos utilizar los QueryMethods para que Spring JPA haga su magia, pero en esta ocasi\u00f3n ser\u00eda bastante complicado encontrar un predicado correcto.

    Tambi\u00e9n podr\u00edamos hacer una implementaci\u00f3n de la interface y hacer la consulta directamente con Criteria.

    Por otro lado se podr\u00eda hacer uso de la anotaci\u00f3n @Query. Esta anotaci\u00f3n nos permite definir una consulta en SQL nativo o en JPQL (Java Persistence Query Language) y Spring JPA se encargar\u00e1 de realizar todo el mapeo y conversi\u00f3n de los datos de entrada y salida. Pero esta opci\u00f3n no es la m\u00e1s recomendable.

    "},{"location":"develop/filtered/springboot/#specifications","title":"Specifications","text":"

    En este caso vamos a hacer uso de las Specifications que es la opci\u00f3n m\u00e1s robusta y no presenta acoplamientos con el tipo de BBDD.

    Haciendo un resumen muy r\u00e1pido y con poco detalle, las Specifications sirven para generar de forma robusta las clausulas where de una consulta SQL. Estas clausulas se generar\u00e1n mediante Predicate (predicados) que realizar\u00e1n operaciones de comparaci\u00f3n entre un campo y un valor.

    En el siguiente ejemplo podemos verlo m\u00e1s claro: en la sentencia select * fromTablewherename = 'b\u00fasqueda' tenemos un solo predicado que es name = 'b\u00fasqueda'. En ese predicado diferenciamos tres etiquetas:

    • name \u2192 es el campo sobre el que hacemos el predicado
    • = \u2192 es la operaci\u00f3n que realizamos
    • 'b\u00fasqueda' \u2192 es el valor con el que realizamos la operaci\u00f3n

    Lo que trata de hacer Specifications es agregar varios predicados con AND o con OR de forma tipada en c\u00f3digo. Y \u00bfqu\u00e9 intentamos conseguir con esta forma de programar?, pues f\u00e1cil, intentamos hacer que si cambiamos alg\u00fan tipo o el nombre de alguna propiedad involucrada en la query, nos salte un fallo en tiempo de compilaci\u00f3n y nos demos cuenta de donde est\u00e1 el error. Si utiliz\u00e1ramos queries construidas directamente con String, al cambiar alg\u00fan tipo o el nombre de alguna propiedad involucrada, no nos dar\u00edamos cuenta hasta que saltara un fallo en tiempo de ejecuci\u00f3n.

    Por este motivo hay que programar con Specifications, porque son robustas ante cambios de c\u00f3digo y tenemos que tratar de evitar las construcciones a trav\u00e9s de cadenas de texto.

    Dicho esto, \u00a1vamos a implementar!

    Lo primero que necesitaremos ser\u00e1 una clase que nos permita guardar la informaci\u00f3n de un Predicate para luego generar facilmente la construcci\u00f3n. Para ello vamos a crear una clase que guarde informaci\u00f3n de los criterios de filtrado (campo, operaci\u00f3n y valor), por suerte esta clase ser\u00e1 gen\u00e9rica y la podremos usar en toda la aplicaci\u00f3n, as\u00ed que la vamos a crear en el paquete com.ccsw.tutorial.common.criteria

    SearchCriteria.java
    package com.ccsw.tutorial.common.criteria;\n\npublic class SearchCriteria {\n\n    private String key;\n    private String operation;\n    private Object value;\n\n    public SearchCriteria(String key, String operation, Object value) {\n\n        this.key = key;\n        this.operation = operation;\n        this.value = value;\n    }\n\n    public String getKey() {\n        return key;\n    }\n\n    public void setKey(String key) {\n        this.key = key;\n    }\n\n    public String getOperation() {\n        return operation;\n    }\n\n    public void setOperation(String operation) {\n        this.operation = operation;\n    }\n\n    public Object getValue() {\n        return value;\n    }\n\n    public void setValue(Object value) {\n        this.value = value;\n    }\n\n}\n

    Hecho esto pasamos a definir el Specification de nuestra clase la cual contendr\u00e1 la construcci\u00f3n de la consulta en funci\u00f3n de los criterios que se le proporcionan. No queremos construir los predicados directamente en nuestro Service ya que duplicariamos mucho c\u00f3digo, mucho mejor si hacemos una clase para centralizar la construcci\u00f3n de predicados.

    De esta forma vamos a crear una clase Specification por cada una de las Entity que queramos consultar. En nuestro caso solo vamos a generar queries para Game, as\u00ed que solo crearemos un GameSpecification donde construirmos los predicados.

    GameSpecification.java
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.common.criteria.SearchCriteria;\nimport com.ccsw.tutorial.game.model.Game;\nimport jakarta.persistence.criteria.*;\nimport org.springframework.data.jpa.domain.Specification;\n\n\npublic class GameSpecification implements Specification<Game> {\n\n    private static final long serialVersionUID = 1L;\n\n    private final SearchCriteria criteria;\n\n    public GameSpecification(SearchCriteria criteria) {\n\n        this.criteria = criteria;\n    }\n\n    @Override\n    public Predicate toPredicate(Root<Game> root, CriteriaQuery<?> query, CriteriaBuilder builder) {\n        if (criteria.getOperation().equalsIgnoreCase(\":\") && criteria.getValue() != null) {\n            Path<String> path = getPath(root);\n            if (path.getJavaType() == String.class) {\n                return builder.like(path, \"%\" + criteria.getValue() + \"%\");\n            } else {\n                return builder.equal(path, criteria.getValue());\n            }\n        }\n        return null;\n    }\n\n    private Path<String> getPath(Root<Game> root) {\n        String key = criteria.getKey();\n        String[] split = key.split(\"[.]\", 0);\n\n        Path<String> expression = root.get(split[0]);\n        for (int i = 1; i < split.length; i++) {\n            expression = expression.get(split[i]);\n        }\n\n        return expression;\n    }\n\n}\n

    Voy a tratar de explicar con calma cada una de las l\u00edneas marcadas, ya que son conceptos dificiles de entender hasta que no se utilizan.

    • Las dos primeras l\u00edneas marcadas hacen referencia a que cuando se crea un Specification, esta debe generar un predicado, con lo que necesita unos criterios de filtrado para poder generarlo. En el constructor le estamos pasando esos criterios de filtrado que luego utilizaremos.

    • La tercera l\u00ednea marcada est\u00e1 seleccionando el tipo de operaci\u00f3n. En nuestro caso solo vamos a utilizar operaciones de comparaci\u00f3n. Por convenio las operaciones de comparaci\u00f3n se marcan como \":\" ya que el s\u00edmbolo = est\u00e1 reservado. Aqu\u00ed es donde podr\u00edamos a\u00f1adir otro tipo de operaciones como \">\" o \"<>\" o cualquiera que queramos implementar. Gu\u00e1rdate esa informaci\u00f3n que te servir\u00e1 en el ejercicio final .

    • Las dos siguientes l\u00edneas, las de return est\u00e1n construyendo un Predicate al ser de tipo comparaci\u00f3n, si es un texto har\u00e1 un like y si no es texto (que es un n\u00famero o fecha) har\u00e1 un equals.

    • Por \u00faltimo, tenemos un m\u00e9todo getPath que invocamos dentro la generaci\u00f3n del predicado y que implementamos m\u00e1s abajo. Esta funci\u00f3n nos permite explorar las sub-entidades para realizar consultas sobre los atributos de estas. Por ejemplo, si queremos navegar hasta game.author.name, lo que har\u00e1 la exploraci\u00f3n ser\u00e1 recuperar el atributo name del objeto author de la entidad game.

    Una vez implementada nuestra clase de Specification, que lo \u00fanico que hace es recoger un criterio de filtrado y construir un predicado, y que en principio solo permite generar comparaciones de igualdad, vamos a utilizarlo dentro de nuestro Service:

    GameServiceImpl.javaGameRepository.java
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.author.AuthorService;\nimport com.ccsw.tutorial.category.CategoryService;\nimport com.ccsw.tutorial.common.criteria.SearchCriteria;\nimport com.ccsw.tutorial.game.model.Game;\nimport com.ccsw.tutorial.game.model.GameDto;\nimport jakarta.transaction.Transactional;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.jpa.domain.Specification;\nimport org.springframework.stereotype.Service;\n\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Service\n@Transactional\npublic class GameServiceImpl implements GameService {\n\n    @Autowired\n    GameRepository gameRepository;\n\n    @Autowired\n    AuthorService authorService;\n\n    @Autowired\n    CategoryService categoryService;\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public List<Game> find(String title, Long idCategory) {\n\n        GameSpecification titleSpec = new GameSpecification(new SearchCriteria(\"title\", \":\", title));\n        GameSpecification categorySpec = new GameSpecification(new SearchCriteria(\"category.id\", \":\", idCategory));\n\n        Specification<Game> spec = Specification.where(titleSpec).and(categorySpec);\n        // Desde la versi\u00f3n 3.5.0 de Spring Boot, la nueva manera es\n        Specification<Game> spec = titleSpec.and(categorySpec);\n\n        return this.gameRepository.findAll(spec);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void save(Long id, GameDto dto) {\n\n        Game game;\n\n        if (id == null) {\n            game = new Game();\n        } else {\n            game = this.gameRepository.findById(id).orElse(null);\n        }\n\n        BeanUtils.copyProperties(dto, game, \"id\", \"author\", \"category\");\n\n        game.setAuthor(authorService.get(dto.getAuthor().getId()));\n        game.setCategory(categoryService.get(dto.getCategory().getId()));\n\n        this.gameRepository.save(game);\n    }\n\n}\n
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.game.model.Game;\nimport org.springframework.data.jpa.domain.Specification;\nimport org.springframework.data.jpa.repository.EntityGraph;\nimport org.springframework.data.jpa.repository.JpaSpecificationExecutor;\nimport org.springframework.data.repository.CrudRepository;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\npublic interface GameRepository extends CrudRepository<Game, Long>, JpaSpecificationExecutor<Game> {\n\n}\n

    Lo que hemos hecho es crear los dos criterios de filtrado que necesit\u00e1bamos. En nuestro caso eran title, que es un atributo de la entidad Game y por otro lado el identificador de categor\u00eda, que en este caso, ya no es un atributo directo de la entidad, si no, de la categor\u00eda asociada, por lo que debemos navegar hasta el atributo id a trav\u00e9s del atributo category (para esto utilizamos el getPath que hemos visto anteriormente).

    A partir de estos dos predicados, podemos generar el Specification global para la consulta, uniendo los dos predicados mediante el operador AND.

    Una vez construido el Specification ya podemos usar el m\u00e9todo por defecto que nos proporciona Spring Data para dicho fin, tan solo tenemos que decirle a nuestro GameRepository que adem\u00e1s extender de CrudRepository debe extender de JpaSpecificationExecutor, para que pueda ejecutarlas.

    "},{"location":"develop/filtered/springboot/#mejoras-rendimiento","title":"Mejoras rendimiento","text":"

    Finalmente, de cara a mejorar el rendimiento de nuestros servicios vamos a hacer foco en la generaci\u00f3n de transacciones con la base de datos. Si ejecut\u00e1ramos esta petici\u00f3n tal cual lo tenemos implementado ahora mismo, en la consola ver\u00edamos lo siguiente:

    Hibernate: select g1_0.id,g1_0.age,g1_0.author_id,g1_0.category_id,g1_0.title from game g1_0\nHibernate: select a1_0.id,a1_0.name,a1_0.nationality from author a1_0 where a1_0.id=?\nHibernate: select c1_0.id,c1_0.name from category c1_0 where c1_0.id=?\nHibernate: select a1_0.id,a1_0.name,a1_0.nationality from author a1_0 where a1_0.id=?\nHibernate: select c1_0.id,c1_0.name from category c1_0 where c1_0.id=?\nHibernate: select a1_0.id,a1_0.name,a1_0.nationality from author a1_0 where a1_0.id=?\nHibernate: select a1_0.id,a1_0.name,a1_0.nationality from author a1_0 where a1_0.id=?\nHibernate: select a1_0.id,a1_0.name,a1_0.nationality from author a1_0 where a1_0.id=?\n

    Esto es debido a que no le hemos dado indicaciones a Spring Data de como queremos que construya las consultas con relaciones y por defecto est\u00e1 configurado para generar sub-consultas cuando tenemos tablas relacionadas.

    En nuestro caso la tabla Game est\u00e1 relacionada con Author y Category. Al realizar la consulta a Game realiza las sub-consultas por cada uno de los registros relacionados con los resultados Game.

    Para evitar tantas consultas contra la BBDD y realizar esto de una forma mucho m\u00e1s \u00f3ptima, podemos decirle a Spring Data el comportamiento que queremos, que en nuestro caso ser\u00e1 que haga una \u00fanica consulta y haga las sub-consultas mediante los join correspondientes.

    Para ello a\u00f1adimos una sobre-escritura del m\u00e9todo findAll, que ya ten\u00edamos implementado en JpaSpecificationExecutor y que utlizamos de forma heredada, pero en este caso le a\u00f1adimos la anotaci\u00f3n @EntityGraph con los atributos que queremos que se incluyan dentro de la consulta principal mediante join:

    GameRepository.java
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.game.model.Game;\nimport org.springframework.data.jpa.domain.Specification;\nimport org.springframework.data.jpa.repository.EntityGraph;\nimport org.springframework.data.jpa.repository.JpaSpecificationExecutor;\nimport org.springframework.data.repository.CrudRepository;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\npublic interface GameRepository extends CrudRepository<Game, Long>, JpaSpecificationExecutor<Game> {\n\n    @Override\n    @EntityGraph(attributePaths = {\"category\", \"author\"})\n    List<Game> findAll(Specification<Game> spec);\n\n}\n

    Tras realizar este cambio, podemos observar que la nueva consulta generada es la siguiente:

    Hibernate: select g1_0.id,g1_0.age,a1_0.id,a1_0.name,a1_0.nationality,c1_0.id,c1_0.name,g1_0.title from game g1_0 join author a1_0 on a1_0.id=g1_0.author_id join category c1_0 on c1_0.id=g1_0.category_id\n

    Como podemos observar, ahora se realiza una \u00fanica consulta con la correspondiente transacci\u00f3n con la BBDD, y se trae todos los datos necesarios de Game, Author y Category sin lanzar m\u00faltiples queries.

    "},{"location":"develop/filtered/springboot/#prueba-de-las-operaciones","title":"Prueba de las operaciones","text":"

    Si ahora ejecutamos de nuevo los jUnits, vemos que todos los que hemos desarrollado en GameIT ya funcionan correctamente, e incluso el resto de test de la aplicaci\u00f3n tambi\u00e9n funcionan correctamente.

    Pruebas jUnit

    Cada vez que desarrollemos un caso de uso nuevo, debemos relanzar todas las pruebas autom\u00e1ticas que tenga la aplicaci\u00f3n. Es muy com\u00fan que al implementar alg\u00fan desarrollo nuevo, interfiramos de alguna forma en el funcionamiento de otra funcionalidad. Si lanzamos toda la bater\u00eda de pruebas, nos daremos cuenta si algo ha dejado de funcionar y podremos solucionarlo antes de llevar ese error a Producci\u00f3n. Las pruebas jUnit son nuestra red de seguridad.

    Adem\u00e1s de las pruebas autom\u00e1ticas, podemos ver como se comporta la aplicaci\u00f3n y que respuesta nos ofrece, lanzando peticiones Rest con Postman, como hemos hecho en los casos anteriores. As\u00ed que podemos levantar la aplicaci\u00f3n y lanzar las operaciones:

    ** GET http://localhost:8080/game **

    ** GET http://localhost:8080/game?title=xxx **

    ** GET http://localhost:8080/game?idCategory=xxx **

    Nos devuelve un listado filtrado de Game. F\u00edjate bien en la petici\u00f3n donde enviamos los filtros y la respuesta que tiene los objetos Category y Author inclu\u00eddos.

    ** PUT http://localhost:8080/game ** ** PUT http://localhost:8080/game/{id} **

    {\n    \"title\": \"Nuevo juego\",\n    \"age\": \"18\",\n    \"category\": {\n        \"id\": 3\n    },\n    \"author\": {\n        \"id\": 1\n    }\n}\n

    Nos sirve para insertar un Game nuevo (si no tienen el id informado) o para actualizar un Game (si tienen el id informado). F\u00edjate que para enlazar Category y Author tan solo hace falta el id de cada no de ellos, ya que en el m\u00e9todo save se hace una consulta get para recuperarlos por su id. Adem\u00e1s que no tendr\u00eda sentido enviar toda la informaci\u00f3n de esas entidades ya que no est\u00e1s dando de alta una Category ni un Author.

    Rendimiento en las consultas JPA

    En este punto te recomiendo que visites el Anexo. Funcionamiento JPA para conocer un poco m\u00e1s como funciona por dentro JPA y alg\u00fan peque\u00f1o truco que puede mejorar el rendimiento.

    "},{"location":"develop/filtered/springboot/#implementar-listado-autores","title":"Implementar listado Autores","text":"

    Antes de poder conectar front con back, si recuerdas, en la edici\u00f3n de un Game, nos hac\u00eda falta un listado de Author y un listado de Category. El segundo ya lo tenemos ya que lo reutilizaremos del listado de categor\u00edas que implementamos. Pero el primero no lo tenemos, porque en la pantalla que hicimos, se mostraban de forma paginada.

    As\u00ed que necesitamos implementar esa funcionalidad, y como siempre vamos de la capa de testing hacia las siguientes capas. Deber\u00edamos a\u00f1adir los siguientes m\u00e9todos:

    AuthorIT.javaAuthorController.javaAuthorService.javaAuthorServiceImpl.java
    ...\n\nParameterizedTypeReference<List<AuthorDto>> responseTypeList = new ParameterizedTypeReference<List<AuthorDto>>(){};\n\n@Test\npublic void findAllShouldReturnAllAuthor() {\n\n      ResponseEntity<List<AuthorDto>> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.GET, null, responseTypeList);\n\n      assertNotNull(response);\n      assertEquals(TOTAL_AUTHORS, response.getBody().size());\n}\n\n...\n
    ...\n\n/**\n * Recupera un listado de autores {@link Author}\n *\n * @return {@link List} de {@link AuthorDto}\n */\n@Operation(summary = \"Find\", description = \"Method that return a list of Authors\")\n@RequestMapping(path = \"\", method = RequestMethod.GET)\npublic List<AuthorDto> findAll() {\n\n    List<Author> authors = this.authorService.findAll();\n\n    return authors.stream().map(e -> mapper.map(e, AuthorDto.class)).collect(Collectors.toList());\n}\n\n...\n
    ...\n\n/**\n * Recupera un listado de autores {@link Author}\n *\n * @return {@link List} de {@link Author}\n */\nList<Author> findAll();\n\n...\n
    ...\n\n/**\n * {@inheritDoc}\n */\n@Override\npublic List<Author> findAll() {\n\n    return (List<Author>) this.authorRepository.findAll();\n}\n\n\n...\n
    "},{"location":"develop/filtered/vuejs/","title":"Listado filtrado - VUE","text":"

    Aqu\u00ed vamos a volver a la pantalla de cat\u00e1logo para realizar un filtrado en la propia tabla.

    Empezaremos por modificar el template de la tabla que modificamos para a\u00f1adir el bot\u00f3n de a\u00f1adir nueva fila para a\u00f1adir tambi\u00e9n tres inputs: uno de texto para el nombre del juego y dos seleccionables para la categor\u00eda y el autor (les tendremos que asignar las opciones que haya en ese momento).

    Tambi\u00e9n a\u00f1adiremos un bot\u00f3n para que no se lance la petici\u00f3n cada vez que el usuario introduce una letra en el input de texto. Esto quedar\u00eda as\u00ed:

    <template v-slot:top>\n        <div class=\"q-table__title\">Cat\u00e1logo</div>\n        <q-btn flat round color=\"primary\" icon=\"add\" @click=\"showAdd = true\" />\n        <q-space />\n        <q-input dense v-model=\"filter.title\" placeholder=\"T\u00edtulo\">\n          <template v-slot:append>\n            <q-icon name=\"search\" />\n          </template>\n        </q-input>\n        <q-separator inset />\n        <div style=\"width: 10%\">\n          <q-select\n            dense\n            name=\"category\"\n            v-model=\"filter.category\"\n            :options=\"categories\"\n            emit-value\n            map-options\n            option-value=\"id\"\n            option-label=\"name\"\n            label=\"Categor\u00eda\"\n          />\n        </div>\n        <q-separator inset />\n        <div style=\"width: 10%\">\n          <q-select\n            dense\n            name=\"author\"\n            v-model=\"filter.author\"\n            :options=\"authors\"\n            emit-value\n            map-options\n            option-value=\"id\"\n            option-label=\"name\"\n            label=\"Autor\"\n          />\n        </div>\n        <q-separator inset />\n        <q-btn flat round color=\"primary\" icon=\"filter_alt\" @click=\"getGames\" />\n      </template>\n

    Adem\u00e1s, tambi\u00e9n vamos a a\u00f1adir un estado para todos los filtros juntos:

    const filter = ref({ title: '', category: '', author: '' });\n

    Por \u00faltimo, para no estar haciendo las tres peticiones (juegos, categor\u00edas y autores) las hemos extra\u00eddo en funciones diferentes de la siguiente manera:

    const getGames = () => {\n  const { data } = useFetch(url.value).get().json();\n  whenever(data, () => (catalogData.value = data.value));\n};\n\nconst getCategories = () => {\n  const { data: categoriesData } = useFetch('http://localhost:8080/category')\n    .get()\n    .json();\n  whenever(categoriesData, () => (categories.value = categoriesData.value));\n};\n\nconst getAuthors = () => {\n  const { data: authorsData } = useFetch('http://localhost:8080/author')\n    .get()\n    .json();\n  whenever(authorsData, () => (authors.value = authorsData.value));\n};\n\nconst firstLoad = () => {\n  getGames();\n  getCategories();\n  getAuthors();\n};\nfirstLoad();\n

    Y como podemos ver, ahora la petici\u00f3n de juegos no tiene la url. Esto es porque hemos hecho que sea una variable computada para a\u00f1adirle los par\u00e1metros de filtrado y ha quedado as\u00ed:

    const url = computed(() => {\n  const _url = new URL('http://localhost:8080/game');\n  _url.search = new URLSearchParams({\n    title: filter.value.title,\n    idCategory: filter.value.category ?? '',\n    idAuthor: filter.value.author ?? '',\n  });\n  return _url.toString();\n});\n
    "},{"location":"develop/paginated/angular/","title":"Listado paginado - Angular","text":"

    Ya tienes tu primer CRUD desarrollado. \u00bfHa sido sencillo, verdad?.

    Ahora vamos a implementar un CRUD un poco m\u00e1s complejo, este tiene datos paginados en servidor, esto quiere decir que no nos sirve un array de datos como en el anterior ejemplo. Para que un listado paginado en servidor funcione, el cliente debe enviar en cada petici\u00f3n que p\u00e1gina est\u00e1 solicitando y cual es el tama\u00f1o de la p\u00e1gina, para que el servidor devuelva solamente un subconjunto de datos, en lugar de devolver el listado completo.

    Como ya conocemos como se debe desarrollar, en este ejemplo vamos a ir m\u00e1s r\u00e1pidos y nos vamos a centrar \u00fanicamente en las novedades.

    "},{"location":"develop/paginated/angular/#crear-modulo-y-componentes","title":"Crear modulo y componentes","text":"

    Vamos a desarrollar el listado de Autores as\u00ed que, debemos crear los componentes:

    ng generate module author\nng generate component author/author-list\nng generate component author/author-edit\n\nng generate service author/author\n

    Este m\u00f3dulo lo vamos a a\u00f1adir a la aplicaci\u00f3n para que se cargue en el arranque. Abrimos el fichero app.module.ts y a\u00f1adimos el m\u00f3dulo:

    app.module.ts
    import { NgModule } from '@angular/core';\nimport { BrowserModule } from '@angular/platform-browser';\n\nimport { AppRoutingModule } from './app-routing.module';\nimport { AppComponent } from './app.component';\nimport { BrowserAnimationsModule } from '@angular/platform-browser/animations';\nimport { CoreModule } from './core/core.module';\nimport { CategoryModule } from './category/category.module';\nimport { AuthorModule } from './author/author.module';\n\n@NgModule({\ndeclarations: [\n    AppComponent\n],\nimports: [\n    BrowserModule,\n    AppRoutingModule,\n    CoreModule,\n    CategoryModule,\n    AuthorModule,\n    BrowserAnimationsModule\n],\nproviders: [],\nbootstrap: [AppComponent]\n})\nexport class AppModule { }\n
    "},{"location":"develop/paginated/angular/#crear-el-modelo","title":"Crear el modelo","text":"

    Creamos el modelo en author/model/Author.ts con las propiedades necesarias para trabajar con la informaci\u00f3n de un autor:

    Author.ts
    export class Author {\n    id: number;\n    name: string;\n    nationality: string;\n}\n
    "},{"location":"develop/paginated/angular/#anadir-el-punto-de-entrada","title":"A\u00f1adir el punto de entrada","text":"

    A\u00f1adimos la ruta al men\u00fa para que podamos acceder a la pantalla:

    app-routing.module.ts
    import { NgModule } from '@angular/core';\nimport { Routes, RouterModule } from '@angular/router';\nimport { CategoryListComponent } from './category/category-list/category-list.component';\nimport { AuthorListComponent } from './author/author-list/author-list.component';\n\nconst routes: Routes = [\n    { path: 'categories', component: CategoriesComponent },\n    { path: 'authors', component: AuthorListComponent },\n];\n\n@NgModule({\n    imports: [RouterModule.forRoot(routes)],\n    exports: [RouterModule]\n})\nexport class AppRoutingModule { }\n
    "},{"location":"develop/paginated/angular/#implementar-servicio","title":"Implementar servicio","text":"

    Y realizamos las diferentes implementaciones. Empezaremos por el servicio. En este caso, hay un cambio sustancial con el anterior ejemplo. Al tratarse de un listado paginado, la operaci\u00f3n getAuthors necesita informaci\u00f3n extra acerca de que p\u00e1gina de datos debe mostrar, adem\u00e1s de que el resultado ya no ser\u00e1 un listado sino una p\u00e1gina.

    Por defecto el esquema de datos de Spring para la paginaci\u00f3n es como el siguiente:

    Esquema de datos de paginaci\u00f3n
    {\n    \"content\": [ ... <listado con los resultados paginados> ... ],\n    \"pageable\": {\n        \"pageNumber\": <n\u00famero de p\u00e1gina empezando por 0>,\n        \"pageSize\": <tama\u00f1o de p\u00e1gina>,\n        \"sort\": [\n            { \n                \"property\": <nombre de la propiedad a ordenar>, \n                \"direction\": <direcci\u00f3n de la ordenaci\u00f3n ASC / DESC> \n            }\n        ]\n    },\n    \"totalElements\": <numero total de elementos en la tabla>\n}\n

    As\u00ed que necesitamos poder enviar y recuperar esa informaci\u00f3n desde Angular, nos hace falta crear esos objetos. Los objetos de paginaci\u00f3n al ser comunes a toda la aplicaci\u00f3n, vamos a crearlos en core/model/page, mientras que la paginaci\u00f3n de AuthorPage.ts la crear\u00e9 en su propio model dentro de author/model.

    SortPage.tsPageable.tsAuthorPage.ts
    export class SortPage {\n    property: String;\n    direction: String;\n}\n
    import { SortPage } from './SortPage';\n\nexport class Pageable {\n    pageNumber: number;\n    pageSize: number;\n    sort: SortPage[];\n}\n
    import { Pageable } from \"src/app/core/model/page/Pageable\";\nimport { Author } from \"./Author\";\n\nexport class AuthorPage {\n    content: Author[];\n    pageable: Pageable;\n    totalElements: number;\n}\n

    Con estos objetos creados ya podemos implementar el servicio y sus datos mockeados.

    mock-authors.tsauthor.service.ts
    import { AuthorPage } from \"./AuthorPage\";\n\nexport const AUTHOR_DATA: AuthorPage = {\n    content: [\n        { id: 1, name: 'Klaus Teuber', nationality: 'Alemania' },\n        { id: 2, name: 'Matt Leacock', nationality: 'Estados Unidos' },\n        { id: 3, name: 'Keng Leong Yeo', nationality: 'Singapur' },\n        { id: 4, name: 'Gil Hova', nationality: 'Estados Unidos'},\n        { id: 5, name: 'Kelly Adams', nationality: 'Estados Unidos' },\n        { id: 6, name: 'J. Alex Kavern', nationality: 'Estados Unidos' },\n        { id: 7, name: 'Corey Young', nationality: 'Estados Unidos' },\n    ],  \n    pageable : {\n        pageSize: 5,\n        pageNumber: 0,\n        sort: [\n            {property: \"id\", direction: \"ASC\"}\n        ]\n    },\n    totalElements: 7\n}\n
    import { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Pageable } from '../core/model/page/Pageable';\nimport { Author } from './model/Author';\nimport { AuthorPage } from './model/AuthorPage';\nimport { AUTHOR_DATA } from './model/mock-authors';\n\n@Injectable({\n    providedIn: 'root'\n})\nexport class AuthorService {\n\n    constructor() { }\n\n    getAuthors(pageable: Pageable): Observable<AuthorPage> {\n        return of(AUTHOR_DATA);\n    }\n\n    saveAuthor(author: Author): Observable<void> {\n        return of(null);\n    }\n\n    deleteAuthor(idAuthor : number): Observable<void> {\n        return of(null);\n    }    \n}\n
    "},{"location":"develop/paginated/angular/#implementar-listado","title":"Implementar listado","text":"

    Ya tenemos el servicio con los datos, ahora vamos a por el listado paginado.

    author-list.component.htmlauthor-list.component.scssauthor-list.component.ts
    <div class=\"container\">\n    <h1>Listado de Autores</h1>\n\n    <mat-table [dataSource]=\"dataSource\"> \n        <ng-container matColumnDef=\"id\">\n            <mat-header-cell *matHeaderCellDef> Identificador </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.id}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"name\">\n            <mat-header-cell *matHeaderCellDef> Nombre autor  </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.name}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"nationality\">\n            <mat-header-cell *matHeaderCellDef> Nacionalidad  </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.nationality}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"action\">\n            <mat-header-cell *matHeaderCellDef></mat-header-cell>\n            <mat-cell *matCellDef=\"let element\">\n                <button mat-icon-button color=\"primary\" (click)=\"editAuthor(element)\">\n                    <mat-icon>edit</mat-icon>\n                </button>\n                <button mat-icon-button color=\"accent\" (click)=\"deleteAuthor(element)\">\n                    <mat-icon>clear</mat-icon>\n                </button>\n            </mat-cell>\n        </ng-container>\n\n        <mat-header-row *matHeaderRowDef=\"displayedColumns; sticky: true\"></mat-header-row>\n        <mat-row *matRowDef=\"let row; columns: displayedColumns;\"></mat-row>\n    </mat-table> \n\n    <mat-paginator (page)=\"loadPage($event)\" [pageSizeOptions]=\"[5, 10, 20]\" [pageIndex]=\"pageNumber\" [pageSize]=\"pageSize\" [length]=\"totalElements\" showFirstLastButtons></mat-paginator>\n\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\" (click)=\"createAuthor()\">Nuevo autor</button> \n    </div>   \n</div>\n
    .container {\n    margin: 20px;\n\n    mat-table {\n        margin-top: 10px;\n        margin-bottom: 20px;\n\n        .mat-header-row {\n            background-color:#f5f5f5;\n\n            .mat-header-cell {\n                text-transform: uppercase;\n                font-weight: bold;\n                color: #838383;\n            }      \n        }\n\n        .mat-column-id {\n            flex: 0 0 20%;\n            justify-content: center;\n        }\n\n        .mat-column-action {\n            flex: 0 0 10%;\n            justify-content: center;\n        }\n    }\n\n    .buttons {\n        text-align: right;\n    }\n}\n
    import { Component, OnInit } from '@angular/core';\nimport { MatDialog } from '@angular/material/dialog';\nimport { PageEvent } from '@angular/material/paginator';\nimport { MatTableDataSource } from '@angular/material/table';\nimport { DialogConfirmationComponent } from 'src/app/core/dialog-confirmation/dialog-confirmation.component';\nimport { Pageable } from 'src/app/core/model/page/Pageable';\nimport { AuthorEditComponent } from '../author-edit/author-edit.component';\nimport { AuthorService } from '../author.service';\nimport { Author } from '../model/Author';\n\n@Component({\nselector: 'app-author-list',\ntemplateUrl: './author-list.component.html',\nstyleUrls: ['./author-list.component.scss']\n})\nexport class AuthorListComponent implements OnInit {\n\n    pageNumber: number = 0;\n    pageSize: number = 5;\n    totalElements: number = 0;\n\n    dataSource = new MatTableDataSource<Author>();\n    displayedColumns: string[] = ['id', 'name', 'nationality', 'action'];\n\n    constructor(\n        private authorService: AuthorService,\n        public dialog: MatDialog,\n    ) { }\n\n    ngOnInit(): void {\n        this.loadPage();\n    }\n\n    loadPage(event?: PageEvent) {\n\n        let pageable : Pageable =  {\n            pageNumber: this.pageNumber,\n            pageSize: this.pageSize,\n            sort: [{\n                property: 'id',\n                direction: 'ASC'\n            }]\n        }\n\n        if (event != null) {\n            pageable.pageSize = event.pageSize\n            pageable.pageNumber = event.pageIndex;\n        }\n\n        this.authorService.getAuthors(pageable).subscribe(data => {\n            this.dataSource.data = data.content;\n            this.pageNumber = data.pageable.pageNumber;\n            this.pageSize = data.pageable.pageSize;\n            this.totalElements = data.totalElements;\n        });\n\n    }  \n\n    createAuthor() {      \n        const dialogRef = this.dialog.open(AuthorEditComponent, {\n            data: {}\n        });\n\n        dialogRef.afterClosed().subscribe(result => {\n            this.ngOnInit();\n        });      \n    }  \n\n    editAuthor(author: Author) {    \n        const dialogRef = this.dialog.open(AuthorEditComponent, {\n            data: { author: author }\n        });\n\n        dialogRef.afterClosed().subscribe(result => {\n            this.ngOnInit();\n        });    \n    }\n\n    deleteAuthor(author: Author) {    \n        const dialogRef = this.dialog.open(DialogConfirmationComponent, {\n            data: { title: \"Eliminar autor\", description: \"Atenci\u00f3n si borra el autor se perder\u00e1n sus datos.<br> \u00bfDesea eliminar el autor?\" }\n        });\n\n        dialogRef.afterClosed().subscribe(result => {\n            if (result) {\n                this.authorService.deleteAuthor(author.id).subscribe(result =>  {\n                    this.ngOnInit();\n                }); \n            }\n        });\n    }  \n}\n

    F\u00edjate como hemos a\u00f1adido la paginaci\u00f3n.

    • Al HTML le hemos a\u00f1adido un componente nuevo mat-paginator, lo que nos va a obligar a a\u00f1adirlo al m\u00f3dulo tambi\u00e9n como dependencia. Ese componente le hemos definido un m\u00e9todo page que se ejecuta cada vez que la p\u00e1gina cambia, y unas propiedades con las que calcular\u00e1 la p\u00e1gina, el tama\u00f1o y el n\u00famero total de p\u00e1ginas.
    • Al Typescript le hemos tenido que a\u00f1adir esas variables y hemos creado un m\u00e9todo para cargar datos que lo que hace es construir un objeto pageable con los valores actuales del componente paginador y lanza la petici\u00f3n con esos datos en el body. Obviamente al ser un mock no funcionar\u00e1 el cambio de p\u00e1gina y dem\u00e1s.

    Como siempre, a\u00f1adimos las dependencias al m\u00f3dulo, vamos a intentar a\u00f1adir todas las que vamos a necesitar a futuro.

    author.module.ts
    import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { AuthorListComponent } from './author-list/author-list.component';\nimport { AuthorEditComponent } from './author-edit/author-edit.component';\nimport { MatTableModule } from '@angular/material/table';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatDialogModule, MAT_DIALOG_DATA } from '@angular/material/dialog';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatPaginatorModule } from '@angular/material/paginator';\n\n\n\n@NgModule({\ndeclarations: [\n    AuthorListComponent,\n    AuthorEditComponent\n],\nimports: [\n    CommonModule,\n    MatTableModule,\n    MatIconModule, \n    MatButtonModule,\n    MatDialogModule,\n    MatFormFieldModule,\n    MatInputModule,\n    FormsModule,\n    ReactiveFormsModule,\n    MatPaginatorModule,\n],\nproviders: [\n    {\n        provide: MAT_DIALOG_DATA,\n        useValue: {},\n    },\n]\n})\nexport class AuthorModule { }\n

    Deber\u00eda verse algo similar a esto:

    "},{"location":"develop/paginated/angular/#implementar-dialogo-edicion","title":"Implementar dialogo edici\u00f3n","text":"

    El \u00faltimo paso, es definir la pantalla de dialogo que realizar\u00e1 el alta y modificado de los datos de un Autor.

    author-edit.component.htmlauthor-edit.component.scssauthor-edit.component.ts
    <div class=\"container\">\n    <h1 *ngIf=\"author.id == null\">Crear autor</h1>\n    <h1 *ngIf=\"author.id != null\">Modificar autor</h1>\n\n    <form>\n        <mat-form-field>\n            <mat-label>Identificador</mat-label>\n            <input type=\"text\" matInput placeholder=\"Identificador\" [(ngModel)]=\"author.id\" name=\"id\" disabled>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Nombre</mat-label>\n            <input type=\"text\" matInput placeholder=\"Nombre del autor\" [(ngModel)]=\"author.name\" name=\"name\" required>\n            <mat-error>El nombre no puede estar vac\u00edo</mat-error>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Nacionalidad</mat-label>\n            <input type=\"text\" matInput placeholder=\"Nacionalidad del autor\" [(ngModel)]=\"author.nationality\" name=\"nationality\" required>\n            <mat-error>La nacionalidad no puede estar vac\u00eda</mat-error>\n        </mat-form-field>\n    </form>\n\n    <div class=\"buttons\">\n        <button mat-stroked-button (click)=\"onClose()\">Cerrar</button>\n        <button mat-flat-button color=\"primary\" (click)=\"onSave()\">Guardar</button>\n    </div>\n</div>\n
    .container {\n    min-width: 350px;\n    max-width: 500px;\n    padding: 20px;\n\n    form {\n        display: flex;\n        flex-direction: column;\n        margin-bottom:20px;\n    }\n\n    .buttons {\n    text-align: right;\n\n    button {\n        margin-left: 10px;\n    }\n    }\n}\n
    import { Component, Inject, OnInit } from '@angular/core';\nimport { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';\nimport { AuthorService } from '../author.service';\nimport { Author } from '../model/Author';\n\n@Component({\nselector: 'app-author-edit',\ntemplateUrl: './author-edit.component.html',\nstyleUrls: ['./author-edit.component.scss']\n})\nexport class AuthorEditComponent implements OnInit {\n\n    author : Author;\n\n    constructor(\n        public dialogRef: MatDialogRef<AuthorEditComponent>,\n        @Inject(MAT_DIALOG_DATA) public data: any,\n        private authorService: AuthorService\n    ) { }\n\n    ngOnInit(): void {\n        if (this.data.author != null) {\n            this.author = Object.assign({}, this.data.author);\n        }\n        else {\n            this.author = new Author();\n        }\n    }\n\n    onSave() {\n        this.authorService.saveAuthor(this.author).subscribe(result =>  {\n            this.dialogRef.close();\n        }); \n    }  \n\n    onClose() {\n        this.dialogRef.close();\n    }\n\n}\n

    Que deber\u00eda quedar algo as\u00ed:

    "},{"location":"develop/paginated/angular/#conectar-con-backend","title":"Conectar con Backend","text":"

    Antes de seguir

    Antes de seguir con este punto, debes implementar el c\u00f3digo de backend en la tecnolog\u00eda que quieras (Springboot o Nodejs). Si has empezado este cap\u00edtulo implementando el frontend, por favor accede a la secci\u00f3n correspondiente de backend para poder continuar con el tutorial. Una vez tengas implementadas todas las operaciones para este listado, puedes volver a este punto y continuar con Angular.

    Una vez implementado front y back, lo que nos queda es modificar el servicio del front para que conecte directamente con las operaciones ofrecidas por el back.

    author.service.ts
    import { HttpClient } from '@angular/common/http';\nimport { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Pageable } from '../core/model/page/Pageable';\nimport { Author } from './model/Author';\nimport { AuthorPage } from './model/AuthorPage';\n\n@Injectable({\nprovidedIn: 'root'\n})\nexport class AuthorService {\n\n    constructor(\n        private http: HttpClient\n    ) { }\n\n    getAuthors(pageable: Pageable): Observable<AuthorPage> {\n        return this.http.post<AuthorPage>('http://localhost:8080/author', {pageable:pageable});\n    }\n\n    saveAuthor(author: Author): Observable<void> {\n        let url = 'http://localhost:8080/author';\n        if (author.id != null) url += '/'+author.id;\n\n        return this.http.put<void>(url, author);\n    }\n\n    deleteAuthor(idAuthor : number): Observable<void> {\n        return this.http.delete<void>('http://localhost:8080/author/'+idAuthor);\n    }    \n}\n
    "},{"location":"develop/paginated/angular17/","title":"Listado paginado - Angular","text":"

    Ya tienes tu primer CRUD desarrollado. \u00bfHa sido sencillo, verdad?

    Ahora vamos a implementar un CRUD un poco m\u00e1s complejo, este tiene datos paginados en servidor, esto quiere decir que no nos sirve un array de datos como en el anterior ejemplo. Para que un listado paginado en servidor funcione, el cliente debe enviar en cada petici\u00f3n que p\u00e1gina est\u00e1 solicitando y cu\u00e1l es el tama\u00f1o de la p\u00e1gina, para que el servidor devuelva solamente un subconjunto de datos, en lugar de devolver el listado completo.

    Como ya conocemos como se debe desarrollar, en este ejemplo vamos a ir m\u00e1s r\u00e1pidos y nos vamos a centrar \u00fanicamente en las novedades.

    "},{"location":"develop/paginated/angular17/#crear-componentes","title":"Crear componentes","text":"

    Vamos a desarrollar el listado de Autores as\u00ed que, debemos crear los componentes:

    ng generate component author/author-list --type=page\nng generate component author/author-edit\n\nng generate service author/author\n
    "},{"location":"develop/paginated/angular17/#crear-el-modelo","title":"Crear el modelo","text":"

    Creamos el modelo en author/model/Author.ts con las propiedades necesarias para trabajar con la informaci\u00f3n de un autor:

    Author.ts
    export class Author {\n    id: number;\n    name: string;\n    nationality: string;\n}\n
    "},{"location":"develop/paginated/angular17/#anadir-el-punto-de-entrada","title":"A\u00f1adir el punto de entrada","text":"

    A\u00f1adimos la ruta al men\u00fa para que podamos acceder a la pantalla:

    app.routes.ts
    import { Routes } from '@angular/router';\n\nexport const routes: Routes = [\n    { path: 'categories', loadComponent: () => import('../category/category-list/category-list.page').then(m => m.CategoryListPage)},\n    { path: 'authors', loadComponent: () => import('../author/author-list/author-list.page').then(m => m.AuthorListPage)},\n];\n
    "},{"location":"develop/paginated/angular17/#implementar-servicio","title":"Implementar servicio","text":"

    Y realizamos las diferentes implementaciones. Empezaremos por el servicio. En este caso, hay un cambio sustancial con el anterior ejemplo. Al tratarse de un listado paginado, la operaci\u00f3n getAuthors necesita informaci\u00f3n extra acerca de que p\u00e1gina de datos debe mostrar, adem\u00e1s de que el resultado ya no ser\u00e1 un listado sino una p\u00e1gina.

    Por defecto, el esquema de datos de Spring para la paginaci\u00f3n es como el siguiente:

    Esquema de datos de paginaci\u00f3n
    {\n    \"content\": [ ... <listado con los resultados paginados> ... ],\n    \"pageable\": {\n        \"pageNumber\": <n\u00famero de p\u00e1gina empezando por 0>,\n        \"pageSize\": <tama\u00f1o de p\u00e1gina>,\n        \"sort\": [\n            { \n                \"property\": <nombre de la propiedad a ordenar>, \n                \"direction\": <direcci\u00f3n de la ordenaci\u00f3n ASC / DESC> \n            }\n        ]\n    },\n    \"totalElements\": <numero total de elementos en la tabla>\n}\n

    As\u00ed que necesitamos poder enviar y recuperar esa informaci\u00f3n desde Angular, nos hace falta crear esos objetos. Los objetos de paginaci\u00f3n, al ser comunes a toda la aplicaci\u00f3n, vamos a crearlos en core/model/page.

    \u00bfPor qu\u00e9 en core/model/page y no dentro del componente author?

    SortPage, Pageable y PaginatedData no pertenecen al dominio de negocio de Author; son contratos t\u00e9cnicos que describen c\u00f3mo se comunica el frontend con el backend cuando los datos vienen paginados.

    Cualquier otro listado de la aplicaci\u00f3n \u2014categor\u00edas, pr\u00e9stamos, juegos\u2026\u2014 necesitar\u00e1 exactamente estos mismos objetos. Si los meti\u00e9ramos dentro de author/, el d\u00eda que quisi\u00e9ramos paginar otro listado tendr\u00edamos dos opciones igual de malas: duplicar el c\u00f3digo o importar clases de un m\u00f3dulo que no tiene nada que ver con tu entidad.

    Colocarlos en core/ sigue el principio de reutilizaci\u00f3n y responsabilidad \u00fanica:

    • Un \u00fanico lugar donde mantener el contrato de paginaci\u00f3n.
    • Si el backend cambia el esquema (p. ej. renombra pageNumber a page), s\u00f3lo hay que tocar un fichero.
    • Cualquier desarrollador que llegue al proyecto sabr\u00e1 d\u00f3nde buscarlos sin tener que adivinar en qu\u00e9 m\u00f3dulo de negocio est\u00e1n escondidos.
    SortPage.tsPageable.tsPaginatedData.ts
    export class SortPage {\n    property: string;\n    direction: string;\n}\n
    import { SortPage } from './SortPage';\n\nexport class Pageable {\n    pageNumber: number;\n    pageSize: number;\n    sort: SortPage[];\n}\n
    import { Pageable } from \"src/app/core/model/page/Pageable\";\n\nexport class PaginatedData <TData>{\n    content: TData[];\n    pageable: Pageable;\n    totalElements: number;\n}\n

    \u00bfQu\u00e9 es <TData>?

    <TData> es un gen\u00e9rico de TypeScript (el equivalente a los generics de Java <T> o de C# <T>).

    Permite que PaginatedData sea una clase reutilizable con cualquier tipo de contenido, sin necesidad de crear una clase distinta para cada entidad:

    PaginatedData<Author>    // content ser\u00e1 Author[]\nPaginatedData<Category>  // content ser\u00e1 Category[]\nPaginatedData<Loan>      // content ser\u00e1 Loan[]\n

    TypeScript verifica el tipo en tiempo de compilaci\u00f3n, por lo que si intentas acceder a una propiedad que no existe en Author dentro de un PaginatedData<Author>, el compilador te avisar\u00e1 de inmediato. Obtienes reutilizaci\u00f3n y seguridad de tipos al mismo tiempo.

    Con estos objetos creados ya podemos implementar el servicio y sus datos mockeados.

    mock-authors.tsauthor.service.ts
    import { PaginatedData } from 'src/app/core/model/page/PaginatedData';\nimport { Author } from './Author';\n\nexport const AUTHOR_DATA: PaginatedData<Author> = {\n    content: [\n        { id: 1, name: 'Klaus Teuber', nationality: 'Alemania' },\n        { id: 2, name: 'Matt Leacock', nationality: 'Estados Unidos' },\n        { id: 3, name: 'Keng Leong Yeo', nationality: 'Singapur' },\n        { id: 4, name: 'Gil Hova', nationality: 'Estados Unidos' },\n        { id: 5, name: 'Kelly Adams', nationality: 'Estados Unidos' },\n        { id: 6, name: 'J. Alex Kavern', nationality: 'Estados Unidos' },\n        { id: 7, name: 'Corey Young', nationality: 'Estados Unidos' },\n    ],\n    pageable: {\n        pageSize: 5,\n        pageNumber: 0,\n        sort: [{ property: 'id', direction: 'ASC' }],\n    },\n    totalElements: 7,\n};\n
    import { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Pageable } from '../core/model/page/Pageable';\nimport { Author } from './model/Author';\nimport { PaginatedData } from 'src/app/core/model/page/PaginatedData';\nimport { AUTHOR_DATA } from './model/mock-authors';\n\n@Injectable({\n    providedIn: 'root',\n})\nexport class AuthorService {\n    constructor() {}\n\n    getAuthors(pageable: Pageable): Observable<PaginatedData<Author>> {\n        return of(AUTHOR_DATA);\n    }\n\n    saveAuthor(author: Author): Observable<void> {\n        return of(null);\n    }\n\n    deleteAuthor(idAuthor: number): Observable<void> {\n        return of(null);\n    }\n}\n
    "},{"location":"develop/paginated/angular17/#implementar-listado","title":"Implementar listado","text":"

    Ya tenemos el servicio con los datos, ahora vamos a por el listado paginado.

    author-list.component.htmlauthor-list.component.scssauthor-list.component.ts
    <div class=\"container\">\n    <h1>Listado de Autores</h1>\n\n    <mat-table [dataSource]=\"dataSource\"> \n        <ng-container matColumnDef=\"id\">\n            <mat-header-cell *matHeaderCellDef> Identificador </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.id}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"name\">\n            <mat-header-cell *matHeaderCellDef> Nombre autor  </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.name}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"nationality\">\n            <mat-header-cell *matHeaderCellDef> Nacionalidad  </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.nationality}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"action\">\n            <mat-header-cell *matHeaderCellDef></mat-header-cell>\n            <mat-cell *matCellDef=\"let element\">\n                <button mat-icon-button color=\"primary\" (click)=\"editAuthor(element)\">\n                    <mat-icon>edit</mat-icon>\n                </button>\n                <button mat-icon-button color=\"accent\" (click)=\"deleteAuthor(element)\">\n                    <mat-icon>clear</mat-icon>\n                </button>\n            </mat-cell>\n        </ng-container>\n\n        <mat-header-row *matHeaderRowDef=\"displayedColumns; sticky: true\"></mat-header-row>\n        <mat-row *matRowDef=\"let row; columns: displayedColumns;\"></mat-row>\n    </mat-table> \n\n    <mat-paginator (page)=\"loadPage($event)\" [pageSizeOptions]=\"[5, 10, 20]\" [pageIndex]=\"pageNumber\" [pageSize]=\"pageSize\" [length]=\"totalElements\" showFirstLastButtons></mat-paginator>\n\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\" (click)=\"createAuthor()\">Nuevo autor</button> \n    </div>   \n</div>\n
    .container {\n    margin: 20px;\n\n    mat-table {\n        margin-top: 10px;\n        margin-bottom: 20px;\n\n        .mat-header-row {\n            background-color:#f5f5f5;\n\n            .mat-header-cell {\n                text-transform: uppercase;\n                font-weight: bold;\n                color: #838383;\n            }      \n        }\n\n        .mat-column-id {\n            flex: 0 0 20%;\n            justify-content: center;\n        }\n\n        .mat-column-action {\n            flex: 0 0 10%;\n            justify-content: center;\n        }\n    }\n\n    .buttons {\n        text-align: right;\n    }\n}\n
    import { Component, OnInit } from '@angular/core';\nimport { MatDialog } from '@angular/material/dialog';\nimport { PageEvent } from '@angular/material/paginator';\nimport { MatTableDataSource, MatTableModule } from '@angular/material/table';\nimport { AuthorEditComponent } from '../author-edit/author-edit.component';\nimport { AuthorService } from '../author.service';\nimport { Author } from '../model/Author';\nimport { Pageable } from '../../core/model/page/Pageable';\nimport { DialogConfirmationComponent } from '../../core/dialog-confirmation/dialog-confirmation.component';\nimport { CommonModule } from '@angular/common';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatIconModule } from '@angular/material/icon';\n\n@Component({\n    selector: 'app-author-list',\n    standalone: true,\n    imports: [MatButtonModule, MatIconModule, MatTableModule, CommonModule],\n    templateUrl: './author-list.component.html',\n    styleUrl: './author-list.component.scss',\n})\nexport class AuthorListComponent implements OnInit {\n    pageNumber: number = 0;\n    pageSize: number = 5;\n    totalElements: number = 0;\n\n    dataSource = new MatTableDataSource<Author>();\n    displayedColumns: string[] = ['id', 'name', 'nationality', 'action'];\n\n    constructor(private authorService: AuthorService, public dialog: MatDialog) {}\n\n    ngOnInit(): void {\n        this.loadPage();\n    }\n\n    loadPage(event?: PageEvent) {\n        const pageable: Pageable = {\n            pageNumber: this.pageNumber,\n            pageSize: this.pageSize,\n            sort: [\n                {\n                    property: 'id',\n                    direction: 'ASC',\n                },\n            ],\n        };\n\n        if (event != null) {\n            pageable.pageSize = event.pageSize;\n            pageable.pageNumber = event.pageIndex;\n        }\n\n        this.authorService.getAuthors(pageable).subscribe((data) => {\n            this.dataSource.data = data.content;\n            this.pageNumber = data.pageable.pageNumber;\n            this.pageSize = data.pageable.pageSize;\n            this.totalElements = data.totalElements;\n        });\n    }\n\n    createAuthor() {\n        const dialogRef = this.dialog.open(AuthorEditComponent, {\n            data: {},\n        });\n\n        dialogRef.afterClosed().subscribe((result) => {\n            this.ngOnInit();\n        });\n    }\n\n    editAuthor(author: Author) {\n        const dialogRef = this.dialog.open(AuthorEditComponent, {\n            data: { author: author },\n        });\n\n        dialogRef.afterClosed().subscribe((result) => {\n            this.ngOnInit();\n        });\n    }\n\n    deleteAuthor(author: Author) {\n        const dialogRef = this.dialog.open(DialogConfirmationComponent, {\n            data: {\n                title: 'Eliminar autor',\n                description:\n                    'Atenci\u00f3n si borra el autor se perder\u00e1n sus datos.<br> \u00bfDesea eliminar el autor?',\n            },\n        });\n\n        dialogRef.afterClosed().subscribe((result) => {\n            if (result) {\n                this.authorService.deleteAuthor(author.id).subscribe((result) => {\n                    this.ngOnInit();\n                });\n            }\n        });\n    }\n}\n

    F\u00edjate como hemos a\u00f1adido la paginaci\u00f3n.

    • Al HTML le hemos a\u00f1adido un componente nuevo mat-paginator, lo que nos va a obligar a a\u00f1adirlo al array de imports tambi\u00e9n como dependencia. Ese componente le hemos definido un m\u00e9todo page que se ejecuta cada vez que la p\u00e1gina cambia, y unas propiedades con las que calcular\u00e1 la p\u00e1gina, el tama\u00f1o y el n\u00famero total de p\u00e1ginas.
    • Al Typescript le hemos tenido que a\u00f1adir esas variables y hemos creado un m\u00e9todo para cargar datos que lo que hace es construir un objeto pageable con los valores actuales del componente paginador y lanza la petici\u00f3n con esos datos en el body. Obviamente, al ser un mock no funcionar\u00e1 el cambio de p\u00e1gina y dem\u00e1s.

    Deber\u00eda verse algo similar a esto:

    "},{"location":"develop/paginated/angular17/#implementar-dialogo-edicion","title":"Implementar di\u00e1logo edici\u00f3n","text":"

    El \u00faltimo paso es definir la pantalla de di\u00e1logo que realizar\u00e1 el alta y modificado de los datos de un Autor.

    author-edit.component.htmlauthor-edit.component.scssauthor-edit.component.ts
    <div class=\"container\">\n    @if (author.id) {\n        <h1>Modificar autor</h1>\n    } @else {\n        <h1>Crear autor</h1>\n    }\n\n    <form>\n        <mat-form-field>\n            <mat-label>Identificador</mat-label>\n            <input type=\"text\" matInput placeholder=\"Identificador\" [(ngModel)]=\"id\" name=\"id\" disabled>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Nombre</mat-label>\n            <input type=\"text\" matInput placeholder=\"Nombre del autor\" [(ngModel)]=\"name\" name=\"name\" required>\n            <mat-error>El nombre no puede estar vac\u00edo</mat-error>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Nacionalidad</mat-label>\n            <input type=\"text\" matInput placeholder=\"Nacionalidad del autor\" [(ngModel)]=\"nationality\" name=\"nationality\" required>\n            <mat-error>La nacionalidad no puede estar vac\u00eda</mat-error>\n        </mat-form-field>\n    </form>\n\n    <div class=\"buttons\">\n        <button mat-stroked-button (click)=\"onClose()\">Cerrar</button>\n        <button mat-flat-button color=\"primary\" (click)=\"onSave()\">Guardar</button>\n    </div>\n</div>\n
    .container {\n    min-width: 350px;\n    max-width: 500px;\n    padding: 20px;\n\n    form {\n        display: flex;\n        flex-direction: column;\n        margin-bottom:20px;\n    }\n\n    .buttons {\n    text-align: right;\n\n    button {\n        margin-left: 10px;\n    }\n    }\n}\n
    import { Component, inject, OnInit, signal } from '@angular/core';\nimport { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';\nimport { AuthorService } from '../author.service';\nimport { Author } from '../model/Author';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\n\n@Component({\n    selector: 'app-author-edit',\n    standalone: true,\n    imports: [FormsModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatButtonModule ],\n    templateUrl: './author-edit.component.html',\n    styleUrl: './author-edit.component.scss',\n})\nexport class AuthorEditComponent implements OnInit {\n    protected readonly authorService = inject(AuthorService);\n    protected readonly dialogRef = inject(MatDialogRef<AuthorEditComponent>);\n    protected readonly data = inject(MAT_DIALOG_DATA);\n\n    protected readonly id = signal<number | null>(null);\n    protected readonly name = signal<string | null>(null);\n    protected readonly nationality = signal<string | null>(null);\n\n    loadFormData(initialData: Author | null) {\n        this.id.set(initialData.id ?? null);\n        this.name.set(initialData.name ?? null);\n        this.nationality.set(initialData.nationality ?? null);\n    }\n\n    ngOnInit(): void {\n        this.loadFormData(this.data.author ?? null);\n    }\n\n    onSave() {\n        this.authorService.saveAuthor(this.author).subscribe(() => {\n            this.dialogRef.close(true);\n        });\n    }\n\n    onClose() {\n        this.dialogRef.close(false);\n    }\n}\n

    Que deber\u00eda quedar algo as\u00ed:

    "},{"location":"develop/paginated/angular17/#conectar-con-backend","title":"Conectar con Backend","text":"

    Antes de seguir

    Antes de seguir con este punto, debes implementar el c\u00f3digo de backend en la tecnolog\u00eda que quieras (Springboot o Nodejs). Si has empezado este cap\u00edtulo implementando el frontend, por favor accede a la secci\u00f3n correspondiente de backend para poder continuar con el tutorial. Una vez tengas implementadas todas las operaciones para este listado, puedes volver a este punto y continuar con Angular.

    Una vez implementado front y back, lo que nos queda es modificar el servicio del front para que conecte directamente con las operaciones ofrecidas por el back.

    author.service.ts
    import { Injectable, inject } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Pageable } from '../core/model/page/Pageable';\nimport { Author } from './model/Author';\nimport { PaginatedData } from 'src/app/core/model/page/PaginatedData';\nimport { AUTHOR_DATA } from './model/mock-authors';\nimport { HttpClient } from '@angular/common/http';\n\n@Injectable({\n    providedIn: 'root',\n})\nexport class AuthorService {\n    protected readonly http = inject(HttpClient);\n\n    private baseUrl = 'http://localhost:8080/author';\n\n    getAuthors(pageable: Pageable): Observable<PaginatedData<Author>> {\n        return this.http.post<PaginatedData<Author>>(this.baseUrl, { pageable: pageable });\n    }\n\n    saveAuthor(author: Author): Observable<Author> {\n        const { id } = author;\n        const url = id ? `${this.baseUrl}/${id}` : this.baseUrl;\n        return this.http.put<Author>(url, author);\n    }\n\n    deleteAuthor(idAuthor: number): Observable<void> {\n        return this.http.delete<void>(`${this.baseUrl}/${idAuthor}`);\n    }\n}\n
    "},{"location":"develop/paginated/nodejs/","title":"Listado paginado - Nodejs","text":"

    Ahora vamos a implementar las operaciones necesarias para ayudar al front a cubrir la funcionalidad del CRUD paginado en servidor. Recuerda que para que un listado paginado en servidor funcione, el cliente debe enviar en cada petici\u00f3n que p\u00e1gina est\u00e1 solicitando y cual es el tama\u00f1o de la p\u00e1gina, para que el servidor devuelva solamente un subconjunto de datos, en lugar de devolver el listado completo.

    Como ya conocemos como se debe desarrollar, en este ejemplo vamos a ir m\u00e1s r\u00e1pidos y nos vamos a centrar \u00fanicamente en las novedades.

    "},{"location":"develop/paginated/nodejs/#crear-modelos","title":"Crear modelos","text":"

    Lo primero que vamos a hacer es crear el modelo de author para trabajar con BBDD. En la carpeta schemas creamos el archivo author.schema.js:

    author.schema.js
    import mongoose from \"mongoose\";\nimport normalize from 'normalize-mongoose';\nimport mongoosePaginate from 'mongoose-paginate-v2';\nconst { Schema, model } = mongoose;\n\nconst authorSchema = new Schema({\n    name: {\n        type: String,\n        require: true\n    },\n    nationality: {\n        type: String,\n        require: true\n    }\n});\nauthorSchema.plugin(normalize);\nauthorSchema.plugin(mongoosePaginate);\n\nconst AuthorModel = model('Author', authorSchema);\n\nexport default AuthorModel;\n
    "},{"location":"develop/paginated/nodejs/#implementar-el-service","title":"Implementar el Service","text":"

    Creamos el service correspondiente author.service.js:

    author.service.js
    import AuthorModel from '../schemas/author.schema.js';\n\nexport const getAuthors = async () => {\n    try {\n        return await AuthorModel.find().sort('id');\n    } catch (e) {\n        throw Error('Error fetching authors');\n    }\n}\n\nexport const createAuthor = async (data) => {\n    const { name, nationality } = data;\n    try {\n        const author = new AuthorModel({ name, nationality });\n        return await author.save();\n    } catch (e) {\n        throw Error('Error creating author');\n    }\n}\n\nexport const updateAuthor = async (id, data) => {\n    try {\n        const author = await AuthorModel.findById(id);\n        if (!author) {\n            throw Error('There is no author with that Id');\n        }    \n        return await AuthorModel.findByIdAndUpdate(id, data);\n    } catch (e) {\n        throw Error(e);\n    }\n}\n\nexport const deleteAuthor = async (id) => {\n    try {\n        const author = await AuthorModel.findById(id);\n        if (!author) {\n            throw Error('There is no author with that Id');\n        }\n        return await AuthorModel.findByIdAndDelete(id);\n    } catch (e) {\n        throw Error(e);\n    }\n}\n\nexport const getAuthorsPageable = async (page, limit, sort) => {\n    const sortObj = {\n        [sort?.property || 'name']: sort?.direction === 'desc' ? 'desc' : 'asc'\n    };\n    try {\n       const options = {\n            page: parseInt(page) + 1,\n            limit,\n            sort: sortObj\n        };\n\n        return await AuthorModel.paginate({}, options);\n    } catch (e) {\n        throw Error('Error fetching authors page');\n    }    \n}\n

    Como podemos observar es muy parecido al servicio de categor\u00edas, pero hemos incluido un nuevo m\u00e9todo getAuthorsPageable. Este m\u00e9todo tendr\u00e1 como par\u00e1metros de entrada la p\u00e1gina que queramos mostrar, el tama\u00f1o de esta y las propiedades de ordenaci\u00f3n. Moongose nos proporciona el m\u00e9todo paginate que es muy parecido a find salvo que adem\u00e1s podemos pasar las opciones de paginaci\u00f3n y el solo realizar\u00e1 todo el trabajo.

    "},{"location":"develop/paginated/nodejs/#implementar-el-controller","title":"Implementar el Controller","text":"

    Creamos el controlador author.controller.js:

    author.controller.js
    import * as AuthorService from '../services/author.service.js';\n\nexport const getAuthors = async (req, res) => {\n    try {\n        const authors = await AuthorService.getAuthors();\n        res.status(200).json(\n            authors\n        );\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n\nexport const createAuthor = async (req, res) => {\n    try {\n        const author = await AuthorService.createAuthor(req.body);\n        res.status(200).json({\n            author\n        });\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n\nexport const updateAuthor = async (req, res) => {\n    const authorId = req.params.id;\n    try {\n        await AuthorService.updateAuthor(authorId, req.body);\n        res.status(200).json(1);\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n\nexport const deleteAuthor = async (req, res) => {\n    const authorId = req.params.id;\n    try {\n        const deletedAuthor = await AuthorService.deleteAuthor(authorId);\n        res.status(200).json({\n            author: deletedAuthor\n        });\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n\nexport const getAuthorsPageable = async (req, res) => {\n    const page = req.body.pageable.pageNumber || 0;\n    const limit = req.body.pageable.pageSize || 5;\n    const sort = req.body.pageable.sort || null;\n\n    try {\n        const response = await AuthorService.getAuthorsPageable(page, limit, sort);\n        res.status(200).json({\n            content: response.docs,\n            pageable: {\n                pageNumber: response.page - 1,\n                pageSize: response.limit\n            },\n            totalElements: response.totalDocs\n        });\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n

    Y vemos que el m\u00e9todo getAuthorsPageable lee los datos de la request, se los pasa al servicio y por \u00faltimo transforma la response con los datos obtenidos.

    "},{"location":"develop/paginated/nodejs/#implementar-las-rutas","title":"Implementar las Rutas","text":"

    Creamos nuestro archivo de rutas author.routes.js:

    author.routes.js
    import { Router } from 'express';\nimport { check } from 'express-validator';\nimport validateFields from '../middlewares/validateFields.js';\nimport { createAuthor, deleteAuthor, getAuthors, updateAuthor, getAuthorsPageable } from '../controllers/author.controller.js';\nconst authorRouter = Router();\n\nauthorRouter.put('/:id', [\n    check('name').not().isEmpty(),\n    check('nationality').not().isEmpty(),\n    validateFields\n], updateAuthor);\n\nauthorRouter.put('/', [\n    check('name').not().isEmpty(),\n    check('nationality').not().isEmpty(),\n    validateFields\n], createAuthor);\n\nauthorRouter.get('/', getAuthors);\nauthorRouter.delete('/:id', deleteAuthor);\n\nauthorRouter.post('/', [\n    check('pageable').not().isEmpty(),\n    check('pageable.pageSize').not().isEmpty(),\n    check('pageable.pageNumber').not().isEmpty(),\n    validateFields\n], getAuthorsPageable)\n\nexport default authorRouter;\n

    Podemos observar que si hacemos una petici\u00f3n con get a /author nos devolver\u00e1 todos los autores. Pero si hacemos una petici\u00f3n post con el objeto pageable en el body realizaremos el listado paginado.

    Finalmente en nuestro archivo index.js vamos a a\u00f1adir el nuevo router:

    index.js
    ...\n\nimport authorRouter from './src/routes/author.routes.js';\n\n...\n\napp.use('/author', authorRouter);\n\n...\n
    "},{"location":"develop/paginated/nodejs/#probar-las-operaciones","title":"Probar las operaciones","text":"

    Y ahora que tenemos todo creado, ya podemos probarlo con Postman:

    Por un lado creamos autores con:

    ** PUT /author **

    ** PUT /author/{id} **

    {\n    \"name\" : \"Nuevo autor\",\n    \"nationality\" : \"Nueva nacionalidad\"\n}\n

    Nos sirve para insertar Autores nuevas (si no tienen el id informado) o para actualizar Autores (si tienen el id informado en la URL). F\u00edjate que los datos que se env\u00edan est\u00e1n en el body como formato JSON (parte izquierda de la imagen). Si no te dar\u00e1 un error.

    ** DELETE /author/{id} ** nos sirve eliminar Autores. F\u00edjate que el dato del ID que se env\u00eda est\u00e1 en el path.

    Luego recuperamos los autores con el m\u00e9todo GET (antes tienes que crear unos cuantos para poder ver un listado):

    Y por \u00faltimo listamos los autores paginados:

    ** POST /author **

    {\n    \"pageable\": {\n        \"pageSize\" : 4,\n        \"pageNumber\" : 0,\n        \"sort\" : [\n            {\n                \"property\": \"name\",\n                \"direction\": \"asc\"\n            }\n        ]\n    }\n}\n

    Importante: direction tiene que ir en min\u00fasculas

    "},{"location":"develop/paginated/react/","title":"Listado paginado - React","text":"

    Ya tienes tu primer CRUD desarrollado. \u00bfHa sido sencillo, verdad?.

    Ahora vamos a implementar un CRUD un poco m\u00e1s complejo, este tiene datos paginados en servidor, esto quiere decir que no nos sirve un array de datos como en el anterior ejemplo. Para que un listado paginado en servidor funcione, el cliente debe enviar en cada petici\u00f3n que p\u00e1gina est\u00e1 solicitando y cual es el tama\u00f1o de la p\u00e1gina, para que el servidor devuelva solamente un subconjunto de datos, en lugar de devolver el listado completo.

    Como ya conocemos como se debe desarrollar, en este ejemplo vamos a ir m\u00e1s r\u00e1pidos y nos vamos a centrar \u00fanicamente en las novedades.

    "},{"location":"develop/paginated/react/#crear-componente-author","title":"Crear componente author","text":"

    Lo primero que vamos a hacer es crear una carpeta llamada types dentro de src. Aqu\u00ed crearemos los tipos de typescript. Creamos un nuevo fichero llamado Author.ts cuyo contenido ser\u00e1 el siguiente:

    Author.ts
    export interface Author {\n    id: string,\n    name: string,\n    nationality: string\n}\n\nexport interface AuthorResponse {\n    content: Author[];\n    totalElements: number;\n}\n

    Ahora vamos a crear un archivo de estilos que ser\u00e1 solo utilizado por la p\u00e1gina de author. Para ello dentro de la carpeta Author creamos un archivo llamado Author.module.css. Al llamar al archivo de esta manera React reconoce este archivo como un archivo \u00fanico para un componente y hace que sus reglas css sean m\u00e1s prioritarias, aunque por ejemplo exista una clase con el mismo nombre en el archivo index.css.

    El contenido de nuestro archivo css ser\u00e1 el siguiente:

    index.css
    .tableActions {\n    margin-right: 20px;\n    display: flex;\n    justify-content: flex-end;\n    align-content: flex-start;\n    gap: 19px;\n}\n

    Al igual que hicimos con categor\u00edas vamos a crear un nuevo componente para el formulario de alta y edici\u00f3n, para ello creamos una nueva carpeta llamada components en src/pages/Author y dentro de esta carpeta crearemos un fichero llamado CreateAuthor.tsx:

    CreateAuthor.tsx
    import { ChangeEvent, useEffect, useState } from \"react\";\nimport Button from \"@mui/material/Button\";\nimport TextField from \"@mui/material/TextField\";\nimport Dialog from \"@mui/material/Dialog\";\nimport DialogActions from \"@mui/material/DialogActions\";\nimport DialogContent from \"@mui/material/DialogContent\";\nimport DialogTitle from \"@mui/material/DialogTitle\";\nimport { Author } from \"../../../types/Author\";\n\ninterface Props {\n  author: Author | null;\n  closeModal: () => void;\n  create: (author: Author) => void;\n}\n\nconst initialState = {\n  name: \"\",\n  nationality: \"\",\n};\n\nexport default function CreateAuthor(props: Props) {\n  const [form, setForm] = useState(initialState);\n\n  useEffect(() => {\n    setForm(props?.author || initialState);\n  }, [props?.author]);\n\n  const handleChangeForm = (\n    event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n  ) => {\n    setForm({\n      ...form,\n      [event.target.id]: event.target.value,\n    });\n  };\n\n  return (\n    <div>\n      <Dialog open={true} onClose={props.closeModal}>\n        <DialogTitle>\n          {props.author ? \"Actualizar Autor\" : \"Crear Autor\"}\n        </DialogTitle>\n        <DialogContent>\n          {props.author && (\n            <TextField\n              margin=\"dense\"\n              disabled\n              id=\"id\"\n              label=\"Id\"\n              fullWidth\n              value={props.author.id}\n              variant=\"standard\"\n            />\n          )}\n          <TextField\n            margin=\"dense\"\n            id=\"name\"\n            label=\"Nombre\"\n            fullWidth\n            variant=\"standard\"\n            onChange={handleChangeForm}\n            value={form.name}\n          />\n          <TextField\n            margin=\"dense\"\n            id=\"nationality\"\n            label=\"Nacionalidad\"\n            fullWidth\n            variant=\"standard\"\n            onChange={handleChangeForm}\n            value={form.nationality}\n          />\n        </DialogContent>\n        <DialogActions>\n          <Button onClick={props.closeModal}>Cancelar</Button>\n          <Button\n            onClick={() =>\n              props.create({\n                id: props.author ? props.author.id : \"\",\n                name: form.name,\n                nationality: form.nationality,\n              })\n            }\n            disabled={!form.name || !form.nationality}\n          >\n            {props.author ? \"Actualizar\" : \"Crear\"}\n          </Button>\n        </DialogActions>\n      </Dialog>\n    </div>\n  );\n}\n

    Como los autores tienen m\u00e1s campos hemos a\u00f1adido un poco de funcionalidad extra que no ten\u00edamos en el formulario de categor\u00edas, pero no es demasiado complicada.

    Vamos a a\u00f1adir los m\u00e9todos necesarios para el crud de autores en el fichero src/redux/services/ludotecaApi.ts:

    ludotecaApi.ts
        getAllAuthors: builder.query<Author[], null>({\n  query: () => \"author\",\n  providesTags: [\"Author\" ],\n}),\ngetAuthors: builder.query<\n  AuthorResponse,\n  { pageNumber: number; pageSize: number }\n>({\n  query: ({ pageNumber, pageSize }) => {\n    return {\n      url: \"author\",\n      method: \"POST\",\n      body: {\n        pageable: {\n          pageNumber,\n          pageSize,\n        },\n      },\n    };\n  },\n  providesTags: [\"Author\"],\n}),\ncreateAuthor: builder.mutation({\n  query: (payload) => ({\n    url: \"/author\",\n    method: \"PUT\",\n    body: payload,\n    headers: {\n      \"Content-type\": \"application/json; charset=UTF-8\",\n    },\n  }),\n  invalidatesTags: [\"Author\"],\n}),\ndeleteAuthor: builder.mutation({\n  query: (id: string) => ({\n    url: `/author/${id}`,\n    method: \"DELETE\",\n  }),\n  invalidatesTags: [\"Author\"],\n}),\nupdateAuthor: builder.mutation({\n  query: (payload: Author) => ({\n    url: `author/${payload.id}`,\n    method: \"PUT\",\n    body: payload,\n  }),\n  invalidatesTags: [\"Author\", \"Game\"],\n}),\n

    A\u00f1adimos tambi\u00e9n los imports, tags y exports necesarios y guardamos.

    import { Author, AuthorResponse } from \"../../types/Author\";\n\ntagTypes: [\"Category\", \"Author\", \"Game\"],\n\nexport const {\n  useGetCategoriesQuery,\n  useCreateCategoryMutation,\n  useDeleteCategoryMutation,\n  useUpdateCategoryMutation,\n  useCreateAuthorMutation,\n  useDeleteAuthorMutation,\n  useGetAllAuthorsQuery,\n  useGetAuthorsQuery,\n  useUpdateAuthorMutation,\n} = ludotecaAPI;\n

    Y por \u00faltimo el contenido de nuestro fichero Author.tsx quedar\u00eda as\u00ed:

    Author.tsx
    import { useEffect, useState, useContext } from \"react\";\nimport Button from \"@mui/material/Button\";\nimport TableHead from \"@mui/material/TableHead\";\nimport Table from \"@mui/material/Table\";\nimport TableBody from \"@mui/material/TableBody\";\nimport TableCell from \"@mui/material/TableCell\";\nimport TableContainer from \"@mui/material/TableContainer\";\nimport TableFooter from \"@mui/material/TableFooter\";\nimport TablePagination from \"@mui/material/TablePagination\";\nimport TableRow from \"@mui/material/TableRow\";\nimport Paper from \"@mui/material/Paper\";\nimport IconButton from \"@mui/material/IconButton\";\nimport EditIcon from \"@mui/icons-material/Edit\";\nimport ClearIcon from \"@mui/icons-material/Clear\";\nimport styles from \"./Author.module.css\";\nimport CreateAuthor from \"./components/CreateAuthor\";\nimport { ConfirmDialog } from \"../../components/ConfirmDialog\";\nimport { useAppDispatch } from \"../../redux/hooks\";\nimport { setMessage } from \"../../redux/features/messageSlice\";\nimport { BackError } from \"../../types/appTypes\";\nimport { Author as AuthorModel } from \"../../types/Author\";\nimport {\n  useDeleteAuthorMutation,\n  useGetAuthorsQuery,\n  useCreateAuthorMutation,\n  useUpdateAuthorMutation,\n} from \"../../redux/services/ludotecaApi\";\nimport { LoaderContext } from \"../../context/LoaderProvider\";\n\nexport const Author = () => {\n  const [pageNumber, setPageNumber] = useState(0);\n  const [pageSize, setPageSize] = useState(5);\n  const [total, setTotal] = useState(0);\n  const [authors, setAuthors] = useState<AuthorModel[]>([]);\n  const [openCreate, setOpenCreate] = useState(false);\n  const [idToDelete, setIdToDelete] = useState(\"\");\n  const [authorToUpdate, setAuthorToUpdate] = useState<AuthorModel | null>(\n    null\n  );\n\n  const dispatch = useAppDispatch();\n  const loader = useContext(LoaderContext);\n\n  const handleChangePage = (\n    _event: React.MouseEvent<HTMLButtonElement> | null,\n    newPage: number\n  ) => {\n    setPageNumber(newPage);\n  };\n\n  const handleChangeRowsPerPage = (\n    event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n  ) => {\n    setPageNumber(0);\n    setPageSize(parseInt(event.target.value, 10));\n  };\n\n  const { data, error, isLoading } = useGetAuthorsQuery({\n    pageNumber,\n    pageSize,\n  });\n\n  const [deleteAuthorApi, { isLoading: isLoadingDelete, error: errorDelete }] =\n    useDeleteAuthorMutation();\n\n  const [createAuthorApi, { isLoading: isLoadingCreate }] =\n    useCreateAuthorMutation();\n\n  const [updateAuthorApi, { isLoading: isLoadingUpdate }] =\n    useUpdateAuthorMutation();\n\n  useEffect(() => {\n    loader.showLoading(\n      isLoadingCreate || isLoading || isLoadingDelete || isLoadingUpdate\n    );\n  }, [isLoadingCreate, isLoading, isLoadingDelete, isLoadingUpdate]);\n\n  useEffect(() => {\n    if (data) {\n      setAuthors(data.content);\n      setTotal(data.totalElements);\n    }\n  }, [data]);\n\n  useEffect(() => {\n    if (errorDelete) {\n      if (\"status\" in errorDelete) {\n        dispatch(\n          setMessage({\n            text: (errorDelete?.data as BackError).msg,\n            type: \"error\",\n          })\n        );\n      }\n    }\n  }, [errorDelete, dispatch]);\n\n  useEffect(() => {\n    if (error) {\n      dispatch(setMessage({ text: \"Se ha producido un error\", type: \"error\" }));\n    }\n  }, [error]);\n\n  const createAuthor = (author: AuthorModel) => {\n    setOpenCreate(false);\n    if (author.id) {\n      updateAuthorApi(author)\n        .then(() => {\n          dispatch(\n            setMessage({\n              text: \"Autor actualizado correctamente\",\n              type: \"ok\",\n            })\n          );\n          setAuthorToUpdate(null);\n        })\n        .catch((err) => console.log(err));\n    } else {\n      createAuthorApi(author)\n        .then(() => {\n          dispatch(\n            setMessage({ text: \"Autor creado correctamente\", type: \"ok\" })\n          );\n          setAuthorToUpdate(null);\n        })\n        .catch((err) => console.log(err));\n    }\n  };\n\n  const deleteAuthor = () => {\n    deleteAuthorApi(idToDelete)\n      .then(() => {\n        setIdToDelete(\"\");\n      })\n      .catch((err) => console.log(err));\n  };\n\n  return (\n    <div className=\"container\">\n      <h1>Listado de Autores</h1>\n      <TableContainer component={Paper}>\n        <Table sx={{ minWidth: 500 }} aria-label=\"custom pagination table\">\n          <TableHead\n            sx={{\n              \"& th\": {\n                backgroundColor: \"lightgrey\",\n              },\n            }}\n          >\n            <TableRow>\n              <TableCell>Identificador</TableCell>\n              <TableCell>Nombre Autor</TableCell>\n              <TableCell>Nacionalidad</TableCell>\n              <TableCell align=\"right\"></TableCell>\n            </TableRow>\n          </TableHead>\n          <TableBody>\n            {authors.map((author: AuthorModel) => (\n              <TableRow key={author.id}>\n                <TableCell component=\"th\" scope=\"row\">\n                  {author.id}\n                </TableCell>\n                <TableCell style={{ width: 160 }}>{author.name}</TableCell>\n                <TableCell style={{ width: 160 }}>\n                  {author.nationality}\n                </TableCell>\n                <TableCell align=\"right\">\n                  <div className={styles.tableActions}>\n                    <IconButton\n                      aria-label=\"update\"\n                      color=\"primary\"\n                      onClick={() => {\n                        setAuthorToUpdate(author);\n                        setOpenCreate(true);\n                      }}\n                    >\n                      <EditIcon />\n                    </IconButton>\n                    <IconButton\n                      aria-label=\"delete\"\n                      color=\"error\"\n                      onClick={() => {\n                        setIdToDelete(author.id);\n                      }}\n                    >\n                      <ClearIcon />\n                    </IconButton>\n                  </div>\n                </TableCell>\n              </TableRow>\n            ))}\n          </TableBody>\n          <TableFooter>\n            <TableRow>\n              <TablePagination\n                rowsPerPageOptions={[5, 10, 25]}\n                colSpan={4}\n                count={total}\n                rowsPerPage={pageSize}\n                page={pageNumber}\n                SelectProps={{\n                  inputProps: {\n                    \"aria-label\": \"rows per page\",\n                  },\n                  native: true,\n                }}\n                onPageChange={handleChangePage}\n                onRowsPerPageChange={handleChangeRowsPerPage}\n              />\n            </TableRow>\n          </TableFooter>\n        </Table>\n      </TableContainer>\n      <div className=\"newButton\">\n        <Button variant=\"contained\" onClick={() => setOpenCreate(true)}>\n          Nuevo autor\n        </Button>\n      </div>\n      {openCreate && (\n        <CreateAuthor\n          create={createAuthor}\n          author={authorToUpdate}\n          closeModal={() => {\n            setAuthorToUpdate(null);\n            setOpenCreate(false);\n          }}\n        />\n      )}\n      {!!idToDelete && (\n        <ConfirmDialog\n          title=\"Eliminar Autor\"\n          text=\"Atenci\u00f3n si borra el autor se perder\u00e1n sus datos. \u00bfDesea eliminar el autor?\"\n          confirm={deleteAuthor}\n          closeModal={() => setIdToDelete(\"\")}\n        />\n      )}\n    </div>\n  );\n};\n

    Al tratarse de un listado paginado hemos creado dos nuevas variables en nuestro estado para almacenar la p\u00e1gina y el n\u00famero de registros a mostrar en la p\u00e1gina. Cuando cambiamos estos valores en el navegador como estas variables van como par\u00e1metro en nuestro hook para recuperar datos autom\u00e1ticamente el listado se va a modificar.

    El resto de funcionalidad es muy parecida a la de categor\u00edas.

    "},{"location":"develop/paginated/springboot/","title":"Listado paginado - Spring Boot","text":"

    Ahora vamos a implementar las operaciones necesarias para ayudar al front a cubrir la funcionalidad del CRUD paginado en servidor. Recuerda que para que un listado paginado en servidor funcione, el cliente debe enviar en cada petici\u00f3n que p\u00e1gina est\u00e1 solicitando y cu\u00e1l es el tama\u00f1o de la p\u00e1gina, para que el servidor devuelva solamente un subconjunto de datos, en lugar de devolver el listado completo.

    Como ya conocemos como se debe desarrollar, en este ejemplo vamos a ir m\u00e1s r\u00e1pidos y nos vamos a centrar \u00fanicamente en las novedades.

    "},{"location":"develop/paginated/springboot/#crear-modelos","title":"Crear modelos","text":"

    Lo primero que vamos a hacer es crear los modelos para trabajar con BBDD y con peticiones hacia el front. Adem\u00e1s, tambi\u00e9n tenemos que a\u00f1adir datos al script de inicializaci\u00f3n de BBDD, siempre respetando la nomenclatura que le hemos dado a la tabla y columnas de BBDD.

    Author.javaAuthorDto.javadata.sql
    package com.ccsw.tutorial.author.model;\n\nimport jakarta.persistence.*;\n\n/**\n * @author ccsw\n *\n */\n@Entity\n@Table(name = \"author\")\npublic class Author {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"id\", nullable = false)\n    private Long id;\n\n    @Column(name = \"name\", nullable = false)\n    private String name;\n\n    @Column(name = \"nationality\")\n    private String nationality;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return name\n     */\n    public String getName() {\n\n        return this.name;\n    }\n\n    /**\n     * @param name new value of {@link #getName}.\n     */\n    public void setName(String name) {\n\n        this.name = name;\n    }\n\n    /**\n     * @return nationality\n     */\n    public String getNationality() {\n\n        return this.nationality;\n    }\n\n    /**\n     * @param nationality new value of {@link #getNationality}.\n     */\n    public void setNationality(String nationality) {\n\n        this.nationality = nationality;\n    }\n\n}\n
    package com.ccsw.tutorial.author.model;\n\n/**\n * @author ccsw\n *\n */\npublic class AuthorDto {\n\n    private Long id;\n\n    private String name;\n\n    private String nationality;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return name\n     */\n    public String getName() {\n\n        return this.name;\n    }\n\n    /**\n     * @param name new value of {@link #getName}.\n     */\n    public void setName(String name) {\n\n        this.name = name;\n    }\n\n    /**\n     * @return nationality\n     */\n    public String getNationality() {\n\n        return this.nationality;\n    }\n\n    /**\n     * @param nationality new value of {@link #getNationality}.\n     */\n    public void setNationality(String nationality) {\n\n        this.nationality = nationality;\n    }\n\n}\n
    INSERT INTO category(name) VALUES ('Eurogames');\nINSERT INTO category(name) VALUES ('Ameritrash');\nINSERT INTO category(name) VALUES ('Familiar');\n\nINSERT INTO author(name, nationality) VALUES ('Alan R. Moon', 'US');\nINSERT INTO author(name, nationality) VALUES ('Vital Lacerda', 'PT');\nINSERT INTO author(name, nationality) VALUES ('Simone Luciani', 'IT');\nINSERT INTO author(name, nationality) VALUES ('Perepau Llistosella', 'ES');\nINSERT INTO author(name, nationality) VALUES ('Michael Kiesling', 'DE');\nINSERT INTO author(name, nationality) VALUES ('Phil Walker-Harding', 'US');\n
    "},{"location":"develop/paginated/springboot/#implementar-tdd-pruebas","title":"Implementar TDD - Pruebas","text":"

    Para desarrollar todas las operaciones, empezaremos primero dise\u00f1ando las pruebas y luego implementando el c\u00f3digo necesario que haga funcionar correctamente esas pruebas. Para ir m\u00e1s r\u00e1pido vamos a poner todas las pruebas de golpe, pero realmente se deber\u00edan crear una a una e ir implementando el c\u00f3digo necesario para esa prueba. Para evitar tantas iteraciones en el tutorial las haremos todas de golpe.

    Vamos a pararnos a pensar un poco que necesitamos en la pantalla. Ahora mismo nos sirve con:

    • Una consulta paginada, que reciba datos de la p\u00e1gina a consultar y devuelva los datos paginados
    • Una operaci\u00f3n de guardado y modificaci\u00f3n
    • Una operaci\u00f3n de borrado

    Para la primera prueba que hemos descrito (consulta paginada) se necesita un objeto que contenga los datos de la p\u00e1gina a consultar. As\u00ed que crearemos una clase AuthorSearchDto para utilizarlo como 'paginador'.

    Para ello, en primer lugar, deberemos a\u00f1adir una clase que vamos a utilizar como envoltorio para las peticiones de paginaci\u00f3n en el proyecto. Hacemos esto para desacoplar la interface de Spring Boot de nuestro contrato de entrada. Crearemos esta clase en el paquete com.ccsw.tutorial.common.pagination.

    PageableRequest.java
    package com.ccsw.tutorial.common.pagination;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport org.springframework.data.domain.*;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\npublic class PageableRequest implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private int pageNumber;\n\n    private int pageSize;\n\n    private List<SortRequest> sort;\n\n    public PageableRequest() {\n\n        sort = new ArrayList<>();\n    }\n\n    public PageableRequest(int pageNumber, int pageSize) {\n\n        this();\n        this.pageNumber = pageNumber;\n        this.pageSize = pageSize;\n    }\n\n    public PageableRequest(int pageNumber, int pageSize, List<SortRequest> sort) {\n\n        this();\n        this.pageNumber = pageNumber;\n        this.pageSize = pageSize;\n        this.sort = sort;\n    }\n\n    public int getPageNumber() {\n        return pageNumber;\n    }\n\n    public void setPageNumber(int pageNumber) {\n        this.pageNumber = pageNumber;\n    }\n\n    public int getPageSize() {\n        return pageSize;\n    }\n\n    public void setPageSize(int pageSize) {\n        this.pageSize = pageSize;\n    }\n\n    public List<SortRequest> getSort() {\n        return sort;\n    }\n\n    public void setSort(List<SortRequest> sort) {\n        this.sort = sort;\n    }\n\n    @JsonIgnore\n    public Pageable getPageable() {\n\n        return PageRequest.of(this.pageNumber, this.pageSize, Sort.by(sort.stream().map(e -> new Sort.Order(e.getDirection(), e.getProperty())).collect(Collectors.toList())));\n    }\n\n    public static class SortRequest implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        private String property;\n\n        private Sort.Direction direction;\n\n        protected String getProperty() {\n            return property;\n        }\n\n        protected void setProperty(String property) {\n            this.property = property;\n        }\n\n        protected Sort.Direction getDirection() {\n            return direction;\n        }\n\n        protected void setDirection(Sort.Direction direction) {\n            this.direction = direction;\n        }\n    }\n\n}\n

    Adicionalmente necesitaremos una clase para deserializar las respuestas de Page recibidas en los test que vamos a implementar. Para ello creamos la clase necesaria dentro de la fuente de la carpeta de los test en el paquete com.ccsw.tutorial.config. Esto solo hace falta porque necesitamos leer la respuesta paginada en el test, si no hicieramos test, no har\u00eda falta.

    ResponsePage.java
    package com.ccsw.tutorial.config;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport org.springframework.data.domain.PageImpl;\nimport org.springframework.data.domain.PageRequest;\nimport org.springframework.data.domain.Pageable;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@JsonIgnoreProperties(ignoreUnknown = true)\npublic class ResponsePage<T> extends PageImpl<T> {\n\n    private static final long serialVersionUID = 1L;\n\n    @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)\n    public ResponsePage(@JsonProperty(\"content\") List<T> content,\n                        @JsonProperty(\"number\") int number,\n                        @JsonProperty(\"size\") int size,\n                        @JsonProperty(\"totalElements\") Long totalElements,\n                        @JsonProperty(\"pageable\") JsonNode pageable,\n                        @JsonProperty(\"last\") boolean last,\n                        @JsonProperty(\"totalPages\") int totalPages,\n                        @JsonProperty(\"sort\") JsonNode sort,\n                        @JsonProperty(\"first\") boolean first,\n                        @JsonProperty(\"numberOfElements\") int numberOfElements) {\n\n        super(content, PageRequest.of(number, size), totalElements);\n    }\n\n    public ResponsePage(List<T> content, Pageable pageable, long total) {\n        super(content, pageable, total);\n    }\n\n    public ResponsePage(List<T> content) {\n        super(content);\n    }\n\n    public ResponsePage() {\n        super(new ArrayList<>());\n    }\n\n}\n

    Paginaci\u00f3n en Springframework

    Cuando utilicemos paginaci\u00f3n en Springframework, debemos recordar que ya vienen implementados algunos objetos que podemos utilizar y que nos facilitan la vida. Es el caso de Pageable y Page.

    • El objeto Pageable no es m\u00e1s que una interface que le permite a Spring JPA saber que p\u00e1gina se quiere buscar, cual es el tama\u00f1o de p\u00e1gina y cuales son las propiedades de ordenaci\u00f3n que se debe lanzar en la consulta.
    • El objeto PageRequest es una utilidad que permite crear objetos de tipo Pageable de forma sencilla. Se utiliza mucho para codificaci\u00f3n de test.
    • El objeto Page no es m\u00e1s que un contenedor que engloba la informaci\u00f3n b\u00e1sica de la p\u00e1gina que se est\u00e1 consultando (n\u00famero de p\u00e1gina, tama\u00f1o de p\u00e1gina, n\u00famero total de resultados) y el conjunto de datos de la BBDD que contiene esa p\u00e1gina una vez han sido buscados y ordenados.

    Tambi\u00e9n crearemos una clase AuthorController dentro del package de com.ccsw.tutorial.author con la implementaci\u00f3n de los m\u00e9todos vac\u00edos, para que no falle la compilaci\u00f3n.

    \u00a1Vamos a implementar test!

    AuthorSearchDto.javaAuthorController.javaAuthorIT.java
    package com.ccsw.tutorial.author.model;\n\nimport com.ccsw.tutorial.common.pagination.PageableRequest;\n\n/**\n * @author ccsw\n *\n */\npublic class AuthorSearchDto {\n\n    private PageableRequest pageable;\n\n    public PageableRequest getPageable() {\n        return pageable;\n    }\n\n    public void setPageable(PageableRequest pageable) {\n        this.pageable = pageable;\n    }\n}\n
    package com.ccsw.tutorial.author;\n\nimport com.ccsw.tutorial.author.model.Author;\nimport com.ccsw.tutorial.author.model.AuthorDto;\nimport com.ccsw.tutorial.author.model.AuthorSearchDto;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.springframework.data.domain.Page;\nimport org.springframework.web.bind.annotation.*;\n\n/**\n * @author ccsw\n *\n */\n@Tag(name = \"Author\", description = \"API of Author\")\n@RequestMapping(value = \"/author\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class AuthorController {\n\n    /**\n     * M\u00e9todo para recuperar un listado paginado de {@link Author}\n     *\n     * @param dto dto de b\u00fasqueda\n     * @return {@link Page} de {@link AuthorDto}\n     */\n    @Operation(summary = \"Find Page\", description = \"Method that return a page of Authors\")\n    @RequestMapping(path = \"\", method = RequestMethod.POST)\n    public Page<AuthorDto> findPage(@RequestBody AuthorSearchDto dto) {\n\n        return null;\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Author\")\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody AuthorDto dto) {\n\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     */\n    @Operation(summary = \"Delete\", description = \"Method that deletes a Author\")\n    @RequestMapping(path = \"/{id}\", method = RequestMethod.DELETE)\n    public void delete(@PathVariable(\"id\") Long id) throws Exception {\n\n    }\n\n}\n
    package com.ccsw.tutorial.author;\n\nimport com.ccsw.tutorial.author.model.AuthorDto;\nimport com.ccsw.tutorial.author.model.AuthorSearchDto;\nimport com.ccsw.tutorial.common.pagination.PageableRequest;\nimport com.ccsw.tutorial.config.ResponsePage;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.test.web.client.TestRestTemplate;\nimport org.springframework.boot.test.web.server.LocalServerPort;\nimport org.springframework.core.ParameterizedTypeReference;\nimport org.springframework.http.*;\nimport org.springframework.test.annotation.DirtiesContext;\n\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)\n@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)\npublic class AuthorIT {\n\n    public static final String LOCALHOST = \"http://localhost:\";\n    public static final String SERVICE_PATH = \"/author\";\n\n    public static final Long DELETE_AUTHOR_ID = 6L;\n    public static final Long MODIFY_AUTHOR_ID = 3L;\n    public static final String NEW_AUTHOR_NAME = \"Nuevo Autor\";\n    public static final String NEW_NATIONALITY = \"Nueva Nacionalidad\";\n\n    private static final int TOTAL_AUTHORS = 6;\n    private static final int PAGE_SIZE = 5;\n\n    @LocalServerPort\n    private int port;\n\n    @Autowired\n    private TestRestTemplate restTemplate;\n\n    ParameterizedTypeReference<ResponsePage<AuthorDto>> responseTypePage = new ParameterizedTypeReference<ResponsePage<AuthorDto>>(){};\n\n    @Test\n    public void findFirstPageWithFiveSizeShouldReturnFirstFiveResults() {\n\n          AuthorSearchDto searchDto = new AuthorSearchDto();\n          searchDto.setPageable(new PageableRequest(0, PAGE_SIZE));\n\n          ResponseEntity<ResponsePage<AuthorDto>> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.POST, new HttpEntity<>(searchDto), responseTypePage);\n\n          assertNotNull(response);\n          assertEquals(TOTAL_AUTHORS, response.getBody().getTotalElements());\n          assertEquals(PAGE_SIZE, response.getBody().getContent().size());\n    }\n\n    @Test\n    public void findSecondPageWithFiveSizeShouldReturnLastResult() {\n\n          int elementsCount = TOTAL_AUTHORS - PAGE_SIZE;\n\n          AuthorSearchDto searchDto = new AuthorSearchDto();\n          searchDto.setPageable(new PageableRequest(1, PAGE_SIZE));\n\n          ResponseEntity<ResponsePage<AuthorDto>> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.POST, new HttpEntity<>(searchDto), responseTypePage);\n\n          assertNotNull(response);\n          assertEquals(TOTAL_AUTHORS, response.getBody().getTotalElements());\n          assertEquals(elementsCount, response.getBody().getContent().size());\n    }\n\n    @Test\n    public void saveWithoutIdShouldCreateNewAuthor() {\n\n          long newAuthorId = TOTAL_AUTHORS + 1;\n          long newAuthorSize = TOTAL_AUTHORS + 1;\n\n          AuthorDto dto = new AuthorDto();\n          dto.setName(NEW_AUTHOR_NAME);\n          dto.setNationality(NEW_NATIONALITY);\n\n          restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.PUT, new HttpEntity<>(dto), Void.class);\n\n          AuthorSearchDto searchDto = new AuthorSearchDto();\n          searchDto.setPageable(new PageableRequest(0, (int) newAuthorSize));\n\n          ResponseEntity<ResponsePage<AuthorDto>> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.POST, new HttpEntity<>(searchDto), responseTypePage);\n\n          assertNotNull(response);\n          assertEquals(newAuthorSize, response.getBody().getTotalElements());\n\n          AuthorDto author = response.getBody().getContent().stream().filter(item -> item.getId().equals(newAuthorId)).findFirst().orElse(null);\n          assertNotNull(author);\n          assertEquals(NEW_AUTHOR_NAME, author.getName());\n    }\n\n    @Test\n    public void modifyWithExistIdShouldModifyAuthor() {\n\n          AuthorDto dto = new AuthorDto();\n          dto.setName(NEW_AUTHOR_NAME);\n          dto.setNationality(NEW_NATIONALITY);\n\n          restTemplate.exchange(LOCALHOST + port + SERVICE_PATH + \"/\" + MODIFY_AUTHOR_ID, HttpMethod.PUT, new HttpEntity<>(dto), Void.class);\n\n          AuthorSearchDto searchDto = new AuthorSearchDto();\n          searchDto.setPageable(new PageableRequest(0, PAGE_SIZE));\n\n          ResponseEntity<ResponsePage<AuthorDto>> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.POST, new HttpEntity<>(searchDto), responseTypePage);\n\n          assertNotNull(response);\n          assertEquals(TOTAL_AUTHORS, response.getBody().getTotalElements());\n\n          AuthorDto author = response.getBody().getContent().stream().filter(item -> item.getId().equals(MODIFY_AUTHOR_ID)).findFirst().orElse(null);\n          assertNotNull(author);\n          assertEquals(NEW_AUTHOR_NAME, author.getName());\n          assertEquals(NEW_NATIONALITY, author.getNationality());\n    }\n\n    @Test\n    public void modifyWithNotExistIdShouldThrowException() {\n\n          long authorId = TOTAL_AUTHORS + 1;\n\n          AuthorDto dto = new AuthorDto();\n          dto.setName(NEW_AUTHOR_NAME);\n\n          ResponseEntity<?> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH + \"/\" + authorId, HttpMethod.PUT, new HttpEntity<>(dto), Void.class);\n\n          assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());\n    }\n\n    @Test\n    public void deleteWithExistsIdShouldDeleteCategory() {\n\n          long newAuthorsSize = TOTAL_AUTHORS - 1;\n\n          restTemplate.exchange(LOCALHOST + port + SERVICE_PATH + \"/\" + DELETE_AUTHOR_ID, HttpMethod.DELETE, null, Void.class);\n\n          AuthorSearchDto searchDto = new AuthorSearchDto();\n          searchDto.setPageable(new PageableRequest(0, TOTAL_AUTHORS));\n\n          ResponseEntity<ResponsePage<AuthorDto>> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.POST, new HttpEntity<>(searchDto), responseTypePage);\n\n          assertNotNull(response);\n          assertEquals(newAuthorsSize, response.getBody().getTotalElements());\n    }\n\n    @Test\n    public void deleteWithNotExistsIdShouldThrowException() {\n\n          long deleteAuthorId = TOTAL_AUTHORS + 1;\n\n          ResponseEntity<?> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH + \"/\" + deleteAuthorId, HttpMethod.DELETE, null, Void.class);\n\n          assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());\n    }\n\n}\n

    Cuidado con las clases de Test

    Recuerda que el c\u00f3digo de aplicaci\u00f3n debe ir en src/main/java, mientras que las clases de test deben ir en src/test/java para que no se mezclen unas con otras y se empaquete todo en el artefacto final. En este caso AuthorIT.java va en el directorio de test src/test/java.

    Si ejecutamos los test, el resultado ser\u00e1 7 maravillosos test que fallan su ejecuci\u00f3n. Es normal, puesto que no hemos implementado nada de c\u00f3digo de aplicaci\u00f3n para corresponder esos test.

    "},{"location":"develop/paginated/springboot/#implementar-controller","title":"Implementar Controller","text":"

    Si recuerdas, esta capa de Controller es la que tiene los endpoints de entrada a la aplicaci\u00f3n. Nosotros ya tenemos definidas 3 operaciones, que hemos dise\u00f1ado directamente desde los tests. Ahora vamos a implementar esos m\u00e9todos con el c\u00f3digo necesario para que los test funcionen correctamente, y teniendo en mente que debemos apoyarnos en las capas inferiores Service y Repository para repartir l\u00f3gica de negocio y acceso a datos.

    AuthorController.javaAuthorService.java
    package com.ccsw.tutorial.author;\n\nimport com.ccsw.tutorial.author.model.Author;\nimport com.ccsw.tutorial.author.model.AuthorDto;\nimport com.ccsw.tutorial.author.model.AuthorSearchDto;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.modelmapper.ModelMapper;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.domain.Page;\nimport org.springframework.data.domain.PageImpl;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * @author ccsw\n *\n */\n@Tag(name = \"Author\", description = \"API of Author\")\n@RequestMapping(value = \"/author\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class AuthorController {\n\n    @Autowired\n    AuthorService authorService;\n\n    @Autowired\n    ModelMapper mapper;\n\n    /**\n     * M\u00e9todo para recuperar un listado paginado de {@link Author}\n     *\n     * @param dto dto de b\u00fasqueda\n     * @return {@link Page} de {@link AuthorDto}\n     */\n    @Operation(summary = \"Find Page\", description = \"Method that return a page of Authors\")\n    @RequestMapping(path = \"\", method = RequestMethod.POST)\n    public Page<AuthorDto> findPage(@RequestBody AuthorSearchDto dto) {\n\n        Page<Author> page = this.authorService.findPage(dto);\n\n        return new PageImpl<>(page.getContent().stream().map(e -> mapper.map(e, AuthorDto.class)).collect(Collectors.toList()), page.getPageable(), page.getTotalElements());\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Author\")\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody AuthorDto dto) {\n\n        this.authorService.save(id, dto);\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     */\n    @Operation(summary = \"Delete\", description = \"Method that deletes a Author\")\n    @RequestMapping(path = \"/{id}\", method = RequestMethod.DELETE)\n    public void delete(@PathVariable(\"id\") Long id) throws Exception {\n\n        this.authorService.delete(id);\n    }\n\n}\n
    package com.ccsw.tutorial.author;\n\nimport com.ccsw.tutorial.author.model.Author;\nimport com.ccsw.tutorial.author.model.AuthorDto;\nimport com.ccsw.tutorial.author.model.AuthorSearchDto;\nimport org.springframework.data.domain.Page;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\npublic interface AuthorService {\n\n    /**\n     * M\u00e9todo para recuperar un listado paginado de {@link Author}\n     *\n     * @param dto dto de b\u00fasqueda\n     * @return {@link Page} de {@link Author}\n     */\n    Page<Author> findPage(AuthorSearchDto dto);\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    void save(Long id, AuthorDto dto);\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     */\n    void delete(Long id) throws Exception;\n\n}\n

    Si te fijas, hemos trasladado toda la l\u00f3gica a llamadas al AuthorService que hemos inyectado, y para que no falle la compilaci\u00f3n hemos creado una interface con los m\u00e9todos necesarios.

    En la clase AuthorController es donde se hacen las conversiones de cara al cliente, pasaremos de un Page<Author> (modelo entidad) a un Page<AuthorDto> (modelo DTO) con la ayuda del beanMapper. Recuerda que al cliente no le deben llegar modelos entidades sino DTOs.

    Adem\u00e1s, el m\u00e9todo de carga findPage ya no es un m\u00e9todo de tipo GET, ahora es de tipo POST porque le tenemos que enviar los datos de la paginaci\u00f3n para que Spring JPA pueda hacer su magia.

    Ahora debemos implementar la siguiente capa.

    "},{"location":"develop/paginated/springboot/#implementar-service","title":"Implementar Service","text":"

    La siguiente capa que vamos a implementar es justamente la capa que contiene toda la l\u00f3gica de negocio, hace uso del Repository para acceder a los datos, y recibe llamadas generalmente de los Controller.

    AuthorServiceImpl.javaAuthorRepository.java
    package com.ccsw.tutorial.author;\n\nimport com.ccsw.tutorial.author.model.Author;\nimport com.ccsw.tutorial.author.model.AuthorDto;\nimport com.ccsw.tutorial.author.model.AuthorSearchDto;\nimport jakarta.transaction.Transactional;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.domain.Page;\nimport org.springframework.stereotype.Service;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Service\n@Transactional\npublic class AuthorServiceImpl implements AuthorService {\n\n    @Autowired\n    AuthorRepository authorRepository;\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public Page<Author> findPage(AuthorSearchDto dto) {\n\n        return this.authorRepository.findAll(dto.getPageable().getPageable());\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void save(Long id, AuthorDto data) {\n\n        Author author;\n\n        if (id == null) {\n            author = new Author();\n        } else {\n            author = this.authorRepository.findById(id).orElse(null);\n        }\n\n        BeanUtils.copyProperties(data, author, \"id\");\n\n        this.authorRepository.save(author);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void delete(Long id) throws Exception {\n\n        if(this.authorRepository.findById(id).orElse(null) == null){\n            throw new Exception(\"Not exists\");\n        }\n\n        this.authorRepository.deleteById(id);\n    }\n\n}\n
    package com.ccsw.tutorial.author;\n\nimport com.ccsw.tutorial.author.model.Author;\nimport org.springframework.data.domain.Page;\nimport org.springframework.data.domain.Pageable;\nimport org.springframework.data.repository.CrudRepository;\n\n/**\n * @author ccsw\n *\n */\npublic interface AuthorRepository extends CrudRepository<Author, Long> {\n\n}\n

    De nuevo pasa lo mismo que con la capa anterior, aqu\u00ed delegamos muchas operaciones de consulta y guardado de datos en AuthorRepository. Hemos tenido que crearlo como interface para que no falle la compilaci\u00f3n. Recuerda que cuando creamos un Repository es de gran ayuda hacerlo extender de CrudRepository<T, ID> ya que tiene muchos m\u00e9todos implementados de base que nos pueden servir, como el delete o el save.

    F\u00edjate tambi\u00e9n que cuando queremos copiar m\u00e1s de un dato de una clase a otra, tenemos una utilidad llamada BeanUtils que nos permite realizar esa copia (siempre que las propiedades de ambas clases se llamen igual). Adem\u00e1s, en nuestro ejemplo hemos ignorado el 'id' para que no nos copie un null a la clase destino.

    "},{"location":"develop/paginated/springboot/#implementar-repository","title":"Implementar Repository","text":"

    Y llegamos a la \u00faltima capa, la que est\u00e1 m\u00e1s cerca de los datos finales. Tenemos la siguiente interface:

    AuthorRepository.java
    package com.ccsw.tutorial.author;\n\nimport com.ccsw.tutorial.author.model.Author;\nimport org.springframework.data.domain.Page;\nimport org.springframework.data.domain.Pageable;\nimport org.springframework.data.repository.CrudRepository;\n\n/**\n * @author ccsw\n *\n */\npublic interface AuthorRepository extends CrudRepository<Author, Long> {\n\n    /**\n     * M\u00e9todo para recuperar un listado paginado de {@link Author}\n     *\n     * @param pageable pageable\n     * @return {@link Page} de {@link Author}\n     */\n    Page<Author> findAll(Pageable pageable);\n\n}\n

    Si te fijas, este Repository ya no est\u00e1 vac\u00edo como el anterior, no nos sirve con las operaciones b\u00e1sicas del CrudRepository en este caso hemos tenido que a\u00f1adir un m\u00e9todo nuevo al que pasandole un objeto de tipo Pageable nos devuelva una Page.

    Pues bien, resulta que la m\u00e1gina de Spring JPA en este caso har\u00e1 su trabajo y nosotros no necesitamos implementar ninguna query, Spring ya entiende que un findAll significa que debe recuperar todos los datos de la tabla Author (que es la tabla que tiene como generico en CrudRepository) y adem\u00e1s deben estar paginados ya que el m\u00e9todo devuelve un objeto tipo Page. Nos ahorra tener que generar una sql para buscar una p\u00e1gina concreta de datos y hacer un count de la tabla para obtener el total de resultados. Para ver otros ejemplos y m\u00e1s informaci\u00f3n, visita la p\u00e1gina de QueryMethods. Realmente se puede hacer much\u00edsimas cosas con solo escribir el nombre del m\u00e9todo, sin tener que pensar ni teclear ninguna sql.

    Con esto ya lo tendr\u00edamos todo.

    "},{"location":"develop/paginated/springboot/#probar-las-operaciones","title":"Probar las operaciones","text":"

    Si ahora ejecutamos los test jUnit, veremos que todos funcionan y est\u00e1n en verde. Hemos implementado todas nuestras pruebas y la aplicaci\u00f3n es correcta.

    Aun as\u00ed, debemos realizar pruebas con el postman para ver los resultados que nos ofrece el back. Para ello, tienes que levantar la aplici\u00f3n y ejecutar las siguientes operaciones:

    ** POST /author **

    {\n    \"pageable\": {\n        \"pageSize\" : 4,\n        \"pageNumber\" : 0,\n        \"sort\" : [\n            {\n                \"property\": \"name\",\n                \"direction\": \"ASC\"\n            }\n        ]\n    }\n}\n
    Nos devuelve un listado paginado de Autores. F\u00edjate que los datos que se env\u00edan est\u00e1n en el body como formato JSON (parte izquierda de la imagen). Si no env\u00edas datos con formato Pageable, te dar\u00e1 un error. Tambi\u00e9n f\u00edjate que la respuesta es de tipo Page. Prueba a jugar con los datos de paginaci\u00f3n e incluso de ordenaci\u00f3n. No hemos programado ninguna SQL pero Spring hace su magia.

    ** PUT /author **

    ** PUT /author/{id} **

    {\n    \"name\" : \"Nuevo autor\",\n    \"nationality\" : \"Nueva nacionalidad\"\n}\n
    Nos sirve para insertar Autores nuevas (si no tienen el id informado) o para actualizar Autores (si tienen el id informado en la URL). F\u00edjate que los datos que se env\u00edan est\u00e1n en el body como formato JSON (parte izquierda de la imagen). Si no te dar\u00e1 un error.

    ** DELETE /author/{id} ** nos sirve eliminar Autores. F\u00edjate que el dato del ID que se env\u00eda est\u00e1 en el path.

    "},{"location":"develop/paginated/vuejs/","title":"Listado paginado - VUE","text":"

    Ahora nos ponemos con la pantalla de autores y vamos a realizar los cambios para poder realizar un paginado en la tabla de autores, adem\u00e1s de realizar los cambios oportunos para poder a\u00f1adir, editar y borrar autores.

    "},{"location":"develop/paginated/vuejs/#acciones-posibles","title":"Acciones posibles","text":""},{"location":"develop/paginated/vuejs/#anadir-una-fila","title":"A\u00f1adir una fila","text":"

    Para poder a\u00f1adir una fila, vamos a tener que a\u00f1adir al componente de dialog de adici\u00f3n un nuevo campo que ser\u00e1 la nacionalidad habiendo quitado los que hab\u00edamos copiado del cat\u00e1logo dejando finalmente solo dos: el nombre y la nacionalidad.

    Veremos el estado del c\u00f3digo en el apartado de borrado.

    "},{"location":"develop/paginated/vuejs/#editar-una-fila","title":"Editar una fila","text":"

    A la hora de editar una fila, modificaremos la columna de \u201cedad\u201d para reutilizarla con la nacionalidad, reutilizaremos la columna de \u201cnombre\u201d tal cual est\u00e1 y borraremos las dem\u00e1s exceptuando la de opciones que ah\u00ed pondremos el bot\u00f3n para el borrado.

    Veremos el estado del c\u00f3digo en el apartado de borrado.

    "},{"location":"develop/paginated/vuejs/#borrar-una-fila","title":"Borrar una fila","text":"

    Y, por \u00faltimo, haremos lo mismo que hicimos en la pantalla de categor\u00edas, que es a\u00f1adir la funci\u00f3n delete despu\u00e9s del dialog de confirmaci\u00f3n.

    El estado del c\u00f3digo ahora mismo quedar\u00eda as\u00ed:

    <template>\n  <q-page padding>\n    <q-table\n      hide-bottom\n      :rows=\"authorsData\"\n      :columns=\"columns\"\n      v-model:pagination=\"pagination\"\n      title=\"Cat\u00e1logo\"\n      class=\"my-sticky-header-table\"\n      no-data-label=\"No hay resultados\"\n      row-key=\"id\"\n    >\n      <template v-slot:top>\n        <q-btn flat round color=\"primary\" icon=\"add\" @click=\"showAdd = true\" />\n      </template>\n      <template v-slot:body=\"props\">\n        <q-tr :props=\"props\">\n          <q-td key=\"id\" :props=\"props\">{{ props.row.id }}</q-td>\n          <q-td key=\"name\" :props=\"props\">\n            {{ props.row.name }}\n            <q-popup-edit\n              v-model=\"props.row.name\"\n              title=\"Cambiar nombre\"\n              v-slot=\"scope\"\n            >\n              <q-input\n                v-model=\"scope.value\"\n                dense\n                autofocus\n                counter\n                @keyup.enter=\"editRow(props, scope, 'name')\"\n              >\n                <template v-slot:append>\n                  <q-icon name=\"edit\" />\n                </template>\n              </q-input>\n            </q-popup-edit>\n          </q-td>\n          <q-td key=\"nationality\" :props=\"props\">\n            {{ props.row.nationality }}\n            <q-popup-edit\n              v-model=\"props.row.nationality\"\n              title=\"Cambiar nacionalidad\"\n              v-slot=\"scope\"\n            >\n              <q-input\n                v-model=\"scope.value\"\n                dense\n                autofocus\n                counter\n                @keyup.enter=\"editRow(props, scope, 'nationality')\"\n              >\n                <template v-slot:append>\n                  <q-icon name=\"edit\" />\n                </template>\n              </q-input>\n            </q-popup-edit>\n          </q-td>\n          <q-td key=\"options\" :props=\"props\">\n            <q-btn\n              flat\n              round\n              color=\"negative\"\n              icon=\"delete\"\n              @click=\"showDeleteDialog(props.row)\"\n            />\n          </q-td>\n        </q-tr>\n      </template>\n    </q-table>\n    <q-dialog v-model=\"showDelete\" persistent>\n      <q-card>\n        <q-card-section class=\"row items-center\">\n          <q-icon\n            name=\"delete\"\n            size=\"sm\"\n            color=\"negative\"\n            @click=\"showDelete = true\"\n          />\n          <span class=\"q-ml-sm\">\n            \u00bfEst\u00e1s seguro de que quieres borrar este elemento?\n          </span>\n        </q-card-section>\n\n        <q-card-actions align=\"right\">\n          <q-btn flat label=\"Cancelar\" color=\"primary\" v-close-popup />\n          <q-btn\n            flat\n            label=\"Confirmar\"\n            color=\"primary\"\n            v-close-popup\n            @click=\"deleteAuthor\"\n          />\n        </q-card-actions>\n      </q-card>\n    </q-dialog>\n    <q-dialog v-model=\"showAdd\">\n      <q-card style=\"width: 300px\" class=\"q-px-sm q-pb-md\">\n        <q-card-section>\n          <div class=\"text-h6\">Nuevo autor</div>\n        </q-card-section>\n\n        <q-item-label header>Nombre</q-item-label>\n        <q-item dense>\n          <q-item-section avatar>\n            <q-icon name=\"badge\" />\n          </q-item-section>\n          <q-item-section>\n            <q-input dense v-model=\"authorToAdd.name\" autofocus />\n          </q-item-section>\n        </q-item>\n\n        <q-item-label header>Nacionalidad</q-item-label>\n        <q-item dense>\n          <q-item-section avatar>\n            <q-icon name=\"flag\" />\n          </q-item-section>\n          <q-item-section>\n            <q-input\n              dense\n              v-model=\"authorToAdd.nationality\"\n              autofocus\n              @keyup.enter=\"addAuthor\"\n            />\n          </q-item-section>\n        </q-item>\n\n        <q-card-actions align=\"right\" class=\"text-primary\">\n          <q-btn flat label=\"Cancelar\" v-close-popup />\n          <q-btn flat label=\"A\u00f1adir autor\" v-close-popup @click=\"addAuthor\" />\n        </q-card-actions>\n      </q-card>\n    </q-dialog>\n  </q-page>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from 'vue';\nimport { useFetch, whenever } from '@vueuse/core';\n\nconst columns = [\n  { name: 'id', align: 'left', label: 'ID', field: 'id', sortable: true },\n  {\n    name: 'name',\n    align: 'left',\n    label: 'Nombre',\n    field: 'name',\n    sortable: true,\n  },\n  {\n    name: 'nationality',\n    align: 'left',\n    label: 'Nacionalidad',\n    field: 'nationality',\n    sortable: true,\n  },\n  { name: 'options', align: 'left', label: 'Options', field: 'options' },\n];\nconst pagination = {\n  page: 1,\n  rowsPerPage: 0,\n};\nconst newAuthor = {\n  name: '',\n  nationality: '',\n};\n\nconst authorsData = ref([]);\nconst showDelete = ref(false);\nconst showAdd = ref(false);\nconst selectedRow = ref({});\nconst authorToAdd = ref({ ...newAuthor });\n\nconst getAuthors = () => {\n  const { data } = useFetch('http://localhost:8080/author').get().json();\n  whenever(data, () => (authorsData.value = data.value));\n};\ngetAuthors();\n\nconst showDeleteDialog = (item: any) => {\n  selectedRow.value = item;\n  showDelete.value = true;\n};\n\nconst addAuthor = async () => {\n  await useFetch('http://localhost:8080/author', {\n    method: 'PUT',\n    redirect: 'manual',\n    headers: {\n      accept: '*/*',\n      origin: window.origin,\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify(authorToAdd.value),\n  })\n    .put()\n    .json();\n\n  getAuthors();\n  authorToAdd.value = newAuthor;\n  showAdd.value = false;\n};\n\nconst editRow = (props: any, scope: any, field: any) => {\n  const row = {\n    name: props.row.name,\n    nationality: props.row.nationality,\n  };\n  row[field] = scope.value;\n  scope.set();\n  editAuthor(props.row.id, row);\n};\n\nconst editAuthor = async (id: string, reqBody: any) => {\n  await useFetch(`http://localhost:8080/author/${id}`, {\n    method: 'PUT',\n    redirect: 'manual',\n    headers: {\n      accept: '*/*',\n      origin: window.origin,\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify(reqBody),\n  })\n    .put()\n    .json();\n\n  getAuthors();\n};\n\nconst deleteAuthor = async () => {\n  await useFetch(`http://localhost:8080/author/${selectedRow.value.id}`, {\n    method: 'DELETE',\n    redirect: 'manual',\n    headers: {\n      accept: '*/*',\n      origin: window.origin,\n      'Content-Type': 'application/json',\n    },\n  })\n    .delete()\n    .json();\n\n  getAuthors();\n};\n</script>\n

    "},{"location":"develop/paginated/vuejs/#paginado","title":"Paginado","text":"

    Lo primero que tenemos que hacer es usar las nuevas caracter\u00edsticas de nuestra tabla para poder a\u00f1adir datos y as\u00ed hacer funcionar el paginado correctamente.

    Lo primero que vamos a hacer es cambiar el objeto de paginaci\u00f3n para que tenga lo siguiente:

    const pagination = ref({\n  page: 0,\n  rowsPerPage: 5,\n  rowsNumber: 10,\n});\n

    Y debido a que la tabla y el back requieren de formatos diferentes para la paginaci\u00f3n, vamos a tener que realizar una funci\u00f3n que formatee el objeto para enviarlo al back. Esta funci\u00f3n ser\u00e1, m\u00e1s o menos, as\u00ed:

    const formatPageableBody = (props: any) => {\n  return {\n    pageable: {\n      pageSize:\n        props.pagination.rowsPerPage !== 0\n          ? props.pagination.rowsPerPage\n          : props.pagination.rowsNumber,\n      pageNumber: props.pagination.page - 1,\n      sort: [\n        {\n          property: 'name',\n          direction: 'ASC',\n        },\n      ],\n    },\n  };\n};\n

    Tal y como podemos ver, se realiza una condici\u00f3n en el formato ya que, si el usuario selecciona que quiere ver todas las filas de golpe el valor de dicha variable ser\u00e1 0 y el back necesitar\u00e1 el valor del n\u00famero m\u00e1ximo de filas para que nosotros recibamos todas.

    Y por \u00faltimo vamos a hacer que la funci\u00f3n de recibir los datos reciba por par\u00e1metro el paginado (siempre habr\u00e1 uno por defecto) y que cuando todo haya ido bien se actualice la paginaci\u00f3n local.

    const updateLocalPagination = (props: any) => {\n  pagination.value.page = props.pagination.page;\n  pagination.value.rowsPerPage = props.pagination.rowsPerPage;\n};\n\nconst getAuthors = (props: any = { pagination: pagination.value }) => {\n  const { data } = useFetch('http://localhost:8080/author', {\n    method: 'POST',\n    redirect: 'manual',\n    headers: {\n      accept: '*/*',\n      origin: window.origin,\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify(formatPageableBody(props)),\n  })\n    .post()\n    .json();\n  whenever(data, () => {\n    updateLocalPagination(props);\n    authorsData.value = data.value.content;\n    pagination.value.rowsNumber = data.value.totalElements;\n  });\n};\n

    Importante

    En la primera de las peticiones (y si quieres en las dem\u00e1s tambi\u00e9n) se ha de recoger el atributo de filas totales y setearlo en el objeto de paginaci\u00f3n con el nombre de rowsNumber. Esto se realiza en la zona subrayada anterior.

    Y por \u00faltimo, hacemos que se realicen peticiones siempre que el usuario cambie par\u00e1metros de la tabla, como el cambio de p\u00e1gina o el cambio de filas mostradas. Esto se realiza a\u00f1adiendo a la creaci\u00f3n de la tabla la siguiente l\u00ednea:

    <q-table\n      :rows=\"authorsData\"\n      :columns=\"columns\"\n      v-model:pagination=\"pagination\"\n      title=\"Autores\"\n      class=\"my-sticky-header-table\"\n      no-data-label=\"No hay resultados\"\n      row-key=\"id\"\n      @request=\"getAuthors\"\n    >\n

    Con estos cambios, la pantalla deber\u00eda funcionar correctamente con el paginado funcionando y todas sus funciones b\u00e1sicas.

    "},{"location":"install/angular/","title":"Entorno de desarrollo - Angular","text":""},{"location":"install/angular/#instalacion-de-herramientas","title":"Instalaci\u00f3n de herramientas","text":"

    Las herramientas b\u00e1sicas que vamos a utilizar para esta tecnolog\u00eda son:

    • Visual Studio Code
    • Scoop.sh
    • Nodejs
    • Angular CLI
    "},{"location":"install/angular/#visual-studio-code","title":"Visual Studio Code","text":"

    Lo primero de todo es instalar el IDE para el desarrollo front.

    Te recomiendo utilizar Visual Studio Code, en un IDE que a nosotros nos gusta mucho y tiene muchos plugins configurables. Puedes entrar en su p\u00e1gina y descargarte la versi\u00f3n estable.

    Si no tuvieras permisos para instalar la herramienta por restricciones en el port\u00e1til existe una alternativa para poder instalarlo, a trav\u00e9s del \"Portal de Empresa\" que tenemos instalado en nuestro port\u00e1til. Para ello teclea en el buscador de Windows (o en el men\u00fa de inicio) el texto \"Portal de empresa\". Deber\u00eda aparecerte una app instalada en tu ordenador, tan solo tendr\u00e1s que hacer click en ella:

    Una vez dentro del portal de empresa, ver\u00e1s una aplicaci\u00f3n que se llama \"Pre-Approved Catalogue\". Deber\u00e1s instalarla, de hecho cada vez que quieras acceder a ella, tendr\u00e1s que instalarla para que se descargue el nuevo cat\u00e1logo.

    Despu\u00e9s de unos minutos de instalaci\u00f3n, entrar\u00e1s en un listado de las aplicaciones que est\u00e1n pre-aprobadas por la empresa. Solo tendr\u00e1s que buscar \"Visual Studio Code\" e instalarla.

    Pasados unos minutos, ya tendr\u00e1s instalado el IDE en tu port\u00e1til.

    "},{"location":"install/angular/#scoopsh","title":"Scoop.sh","text":"

    Muchas de las herramientas que necesitar\u00e1s a lo largo de tu estancia en los proyectos, no podr\u00e1s instalarlas por temas de permisos de seguridad en nuestros port\u00e1tiles. Una forma de evitar estos permisos de seguridad y poder instalar herramientas (sobre todo a nivel de consola), es utilizando a Scoop.sh.

    Para instalar scoop.sh tan solo necesitas abrir un terminal de PowerShell (OJO que es PowerShell y no una consola de msdos) y ejecutar los siguientes comandos:

    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser\nInvoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression\n

    Esto nos instalar\u00e1 scoop.sh en nuestros port\u00e1tiles. Adem\u00e1s de la instalaci\u00f3n, y debido a las restricciones de seguridad que tenemos, necesitaremos activar el lessmsi para que las aplicaciones que necesitemos instalar no intenten ejecutar los .exe de instalaci\u00f3n, sino que descompriman el zip. Para ello deberemos ejecutar el comando:

    scoop config use_lessmsi true\n

    A partir de este punto, ya tenemos listo el port\u00e1til para instalar herramientas y aplicaciones.

    "},{"location":"install/angular/#nodejs","title":"Nodejs","text":"

    El siguiente paso ser\u00e1 instalar el motor de Nodejs. Para esto vamos a usar scoop.sh ya que lo tenemos instalado, y vamos a pedirle que nos instal\u00e9 el motor de Nodejs. Abriremos una consola de msdos y ejecutaremos el comando:

    scoop install main/nodejs\n

    Con esto, scoop ya nos instalar\u00e1 todo lo necesario.

    "},{"location":"install/angular/#angular-cli","title":"Angular CLI","text":"

    El siguiente pas\u00f3 ser\u00e1 instalar una capa de gesti\u00f3n por encima de Nodejs que nos ayudar\u00e1 en concreto con la funcionalidad de Angular. Si no indicamos nada se instalar\u00e1 la \u00faltima versi\u00f3n del CLI, pero si queremos podemos elegir una versi\u00f3n en concreto a\u00f1adiendo '@' y el n\u00famero de la versi\u00f3n correspondiente. Para poder instalarlo, tan solo hay que abrir una consola de msdos y ejecutar el comando y Nodejs ya har\u00e1 el resto:

    npm install -g @angular/cli\n\nnpm install -g @angular/cli@16\n

    Y con esto ya tendremos todo instalado, listo para empezar a crear los proyectos.

    Aviso para navegantes corporativos

    Si tienes alg\u00fan problema para ejecutar el comando ng ... puede deberse a que no se ha podido a\u00f1adir al PATH.

    Pero \u00a1\u00a1no te preocupes!! te explicamos c\u00f3mo puedes instal\u00e1rtelo paso a paso:

    1. Aseg\u00farate de que tienes instalado Git Bash, \u00bfc\u00f3mo?
      1. Clic derecho en una carpeta \u2013 la que sea
      2. \"M\u00e1s opciones\" / \"More options\"
      3. Si no te aparece, deber\u00e1s instal\u00e1rtelo desde el Portal de Empresa
    2. Elige una carpeta sobre la que tengas permisos como destino de la instalaci\u00f3n
      • npm install @angular/cli <- no ponemos -g
    3. Ahora viene lo confuso, pero te guiamos. Tienes que crear el siguiente alias
      • echo alias ng=\\'node RUTA_EN_LA_QUE_ESTAS/node_modules/@angular/cli/bin/ng.js\\' >> .bashrc <- esto crear\u00e1 el alias
      • source ~/.bashrc <- esto actualizar\u00e1 el perfil (resetea el diccionario con los alias disponibles)
    4. Ahora ya deber\u00edas poder ejecutar ng desde Git bash

    \u00bfTienes alg\u00fan problema en la instalaci\u00f3n? Cont\u00e1ctanos y te ayudaremos en la medida de lo que podamos

    "},{"location":"install/angular/#creacion-de-proyecto","title":"Creaci\u00f3n de proyecto","text":"

    La mayor\u00eda de los proyectos con Angular en los que trabajamos normalmente, suelen ser proyectos web usando las librer\u00edas mas comunes de angular, como Angular Material.

    Crear un proyecto de Angular es muy sencillo si tienes instalado el CLI de Angular. Lo primero abrir una consola de msdos y posicionarte en el directorio raiz donde quieres crear tu proyecto Angular, y ejecutamos lo siguiente:

    ng new tutorial --strict=false\n

    El propio CLI nos ir\u00e1 realizando una serie de preguntas que pueden cambiar dependiendo de la versi\u00f3n.

    Would you like to add Angular routing? (y/N)

    Preferiblemente: y

    Which stylesheet format would you like to use?

    Preferiblemente: SCSS

    Do you want to enable Server-Side Rendering (SSR)

    Preferiblemente: N

    En el caso del tutorial como vamos a tener dos proyectos para nuestra aplicaci\u00f3n (front y back), para poder seguir correctamente las explicaciones, voy a renombrar la carpeta para poder diferenciarla del otro proyecto. A partir de ahora se llamar\u00e1 client.

    Info

    Si durante el desarrollo del proyecto necesitas a\u00f1adir nuevos m\u00f3dulos al proyecto Angular, ser\u00e1 necesario resolver las dependencias antes de arrancar el servidor. Esto se puede realizar mediante el gestor de dependencias de Nodejs, directamente en consola ejecuta el comando npm update y descargar\u00e1 e instalar\u00e1 las nuevas dependencias.

    "},{"location":"install/angular/#arrancar-el-proyecto","title":"Arrancar el proyecto","text":"

    Para arrancar el proyecto, tan solo necesitamos ejecutar en consola el siguiente comando siempre dentro del directorio creado por Angular CLI:

    ng serve\n

    Angular compilar\u00e1 el c\u00f3digo fuente, levantar\u00e1 un servidor local al que podremos acceder por defecto mediante la URL: http://localhost:4200/

    Y ya podemos empezar a trabajar con Angular.

    Info

    Cuando se trata de un proyecto nuevo recien descargado de un repositorio, recuerda que ser\u00e1 necesario resolver las dependencias antes de arrancar el servidor. Esto se puede realizar mediante el gestor de dependencias de Nodejs, directamente en consola ejecuta el comando npm update y descargar\u00e1 e instalar\u00e1 las nuevas dependencias.

    Comandos de Angular CLI

    Si necesitas m\u00e1s informaci\u00f3n sobre los comandos que ofrece Angular CLI para poder crear aplicaciones, componentes, servicios, etc. los tienes disponibles en: https://angular.io/cli#command-overview

    "},{"location":"install/angular/#angular-17","title":"Angular 17+","text":"

    Con la llegada de Angular 17, se han introducido importantes novedades que impactan la manera en que se desarrollan aplicaciones web. A diferencia de las versiones anteriores, Angular 17 trae mejoras enfocadas en la simplicidad, el rendimiento y la flexibilidad del desarrollo. En este tutorial, te guiaremos a trav\u00e9s de estas nuevas caracter\u00edsticas, permiti\u00e9ndote elegir si deseas enfocarte en las versiones m\u00e1s recientes o adaptarlo a versiones anteriores.

    Las principales diferencias entre Angular 17+ y sus versiones anteriores incluyen:

    "},{"location":"install/angular/#componentes-standalone-por-defecto","title":"Componentes standalone por defecto","text":"

    Una de las novedades m\u00e1s importantes de Angular 17 es el uso de componentes standalone de forma predeterminada. En versiones anteriores, los m\u00f3dulos (NgModules) eran el n\u00facleo de la estructura de una aplicaci\u00f3n Angular. Ahora, con los componentes standalone, puedes crear y usar componentes sin necesidad de definir un m\u00f3dulo expl\u00edcito, lo que simplifica significativamente la configuraci\u00f3n inicial y mejora la modularidad. Esto facilita la creaci\u00f3n de aplicaciones m\u00e1s ligeras y modulares.

    "},{"location":"install/angular/#directivas-simplificadas","title":"Directivas Simplificadas","text":"

    En Angular 17, algunas de las directivas m\u00e1s utilizadas han sido actualizadas para simplificar su uso y mejorar la legibilidad del c\u00f3digo. Una de las principales mejoras es la introducci\u00f3n de @if, que reemplaza la tradicional ngIf. Esta nueva sintaxis hace que las condiciones sean m\u00e1s claras y f\u00e1ciles de aplicar en las plantillas. Del mismo modo, la directiva ngFor, utilizada para iterar sobre listas, tambi\u00e9n ha sido optimizada, ofreciendo una experiencia m\u00e1s fluida y mejor manejo de colecciones din\u00e1micas.

    Adem\u00e1s, se ha reducido la complejidad en el uso de otras directivas estructurales como ngSwitch y ngClass, haciendo m\u00e1s intuitivo el control del comportamiento y la apariencia de los elementos en las vistas. Con estas mejoras, Angular 17 ofrece una sintaxis m\u00e1s limpia y directa, permitiendo a los desarrolladores concentrarse en la l\u00f3gica de su aplicaci\u00f3n sin la sobrecarga de c\u00f3digo innecesario.

    "},{"location":"install/angular/#bloques-de-carga-deferred","title":"Bloques de carga deferred","text":"

    Los bloques de carga deferred (carga diferida) son una de las caracter\u00edsticas m\u00e1s esperadas en Angular 17. Esta funcionalidad permite retrasar la carga de ciertas partes de la aplicaci\u00f3n hasta que realmente sean necesarias, lo que optimiza el rendimiento al reducir el tama\u00f1o inicial del paquete que se descarga al navegador. Con esta t\u00e9cnica, es posible mejorar el tiempo de respuesta inicial de las aplicaciones y cargar los m\u00f3dulos o componentes bajo demanda, favoreciendo una mejor experiencia de usuario.

    "},{"location":"install/angular/#esbuild-y-vite","title":"ESBuild y Vite","text":"

    Otra de las mejoras clave de Angular 17 es la integraci\u00f3n de ESBuild y Vite como opciones de construcci\u00f3n (build). Estos dos motores de compilaci\u00f3n permiten una construcci\u00f3n mucho m\u00e1s r\u00e1pida y eficiente de aplicaciones, mejorando significativamente los tiempos de desarrollo y compilaci\u00f3n. ESBuild es un bundler de JavaScript que se enfoca en la velocidad, mientras que Vite proporciona una experiencia de desarrollo m\u00e1s \u00e1gil con recarga en caliente y un flujo de trabajo optimizado. Ambas herramientas ofrecen una alternativa moderna y r\u00e1pida a Webpack, especialmente para proyectos grandes o aplicaciones en tiempo real.

    "},{"location":"install/nodejs/","title":"Entorno de desarrollo - Nodejs","text":""},{"location":"install/nodejs/#instalacion-de-herramientas","title":"Instalaci\u00f3n de herramientas","text":"

    Las herramientas b\u00e1sicas que vamos a utilizar para esta tecnolog\u00eda son:

    • Visual Studio Code
    • Scoop.sh
    • Nodejs
    • MongoDB Atlas
    • Postman
    "},{"location":"install/nodejs/#visual-studio-code","title":"Visual Studio Code","text":"

    Lo primero de todo es instalar el IDE para el desarrollo en node si no lo has hecho previamente.

    Te recomiendo utilizar Visual Studio Code, en un IDE que a nosotros nos gusta mucho y tiene muchos plugins configurables. Puedes entrar en su p\u00e1gina y descargarte la versi\u00f3n estable.

    "},{"location":"install/nodejs/#scoopsh","title":"Scoop.sh","text":"

    Muchas de las herramientas que necesitar\u00e1s a lo largo de tu estancia en los proyectos, no podr\u00e1s instalarlas por temas de permisos de seguridad en nuestros port\u00e1tiles. Una forma de evitar estos permisos de seguridad y poder instalar herramientas (sobre todo a nivel de consola), es utilizando a Scoop.sh.

    Para instalar scoop.sh tan solo necesitas abrir un termina de PowerShell (OJO que es PowerShell y no una consola de msdos) y ejecutar los siguientes comandos:

    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser\nInvoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression\n

    Esto nos instalar\u00e1 scoop.sh en nuestros port\u00e1tiles. Adem\u00e1s de la instalaci\u00f3n, y debido a las restricciones de seguridad que tenemos, necesitaremos activar el lessmsi para que las aplicaciones que necesitemos instalar no intenten ejecutar los .exe de instalaci\u00f3n, sino que descompriman el zip. Para ello deberemos ejecutar el comando:

    scoop config use_lessmsi true\n

    A partir de este punto, ya tenemos listo el port\u00e1til para instalar herramientas y aplicaciones.

    "},{"location":"install/nodejs/#nodejs","title":"Nodejs","text":"

    El siguiente paso ser\u00e1 instalar el motor de Nodejs. Para esto vamos a usar scoop.sh ya que lo tenemos instalado, y vamos a pedirle que nos instal\u00e9 el motor de Nodejs. Abriremos una consola de msdos y ejecutaremos el comando:

    scoop install main/nodejs\n

    Con esto, scoop ya nos instalar\u00e1 todo lo necesario.

    "},{"location":"install/nodejs/#mongodb-atlas","title":"MongoDB Atlas","text":"

    Tambi\u00e9n necesitaremos crear una cuenta de MongoDB Atlas para crear nuestra base de datos MongoDB en la nube.

    Accede a la URL, registrate gr\u00e1tis con cualquier cuenta de correo y elige el tipo de cuenta gratuita \ud83d\ude0a:

    Configura el cluster a tu gusto (selecciona la opci\u00f3n gratuita en el cloud que m\u00e1s te guste) y ya tendr\u00edas una BBDD en cloud para hacer pruebas. Lo primero que se muestra es el dashboard que se ver\u00e1 algo similar a lo siguiente:

    A continuaci\u00f3n, pulsamos en la opci\u00f3n Database del men\u00fa y, sobre el Cluster0, pulsamos tambi\u00e9n el bot\u00f3n Connect. Se nos abrir\u00e1 el siguiente pop-up donde tendremos que elegir la opci\u00f3n Connect your application:

    En el siguiente paso es donde se nos muestra la url que tendremos que utilizar en nuestra aplicaci\u00f3n. La copiamos y guardamos para m\u00e1s tarde:

    Pulsamos Close y la BBDD ya estar\u00eda creada.

    Nota: Al crear la base de datos te aprecer\u00e1 un aviso para introducir tu IP en la whitelist, aseg\u00farate no estar en la VPN cuando lo hagas, de lo contrario no tendr\u00e1s conexi\u00f3n posteriormente.

    "},{"location":"install/nodejs/#herramientas-para-pruebas","title":"Herramientas para pruebas","text":"

    Para poder probar las operaciones de negocio que vamos a crear, lo mejor es utilizar una herramienta que permita realizar llamadas a API Rest. Para ello te propongo utilizar Postman, en su versi\u00f3n web o en su versi\u00f3n desktop, cualquiera de las dos sirve.

    Con esta herramienta se puede generar peticiones GET, POST, PUT, DELETE contra el servidor y pasarle par\u00e1metros de forma muy sencilla y visual. Lo usaremos durante el tutorial.

    "},{"location":"install/nodejs/#creacion-de-proyecto","title":"Creaci\u00f3n de proyecto","text":"

    Para la creaci\u00f3n de nuestro proyecto Node nos crearemos una carpeta con el nombre que deseemos y accederemos a ella con la consola de comandos de windows. Una vez dentro ejecutaremos el siguiente comando para inicializar nuestro proyecto con npm:

    npm init\n

    Cuando ejecutemos este comando nos pedir\u00e1 los valores para distintos par\u00e1metros de nuestro proyecto. Aconsejo solo cambiar el nombre y el resto dejarlo por defecto pulsando enter para cada valor. Una vez que hayamos terminado se nos habr\u00e1 generado un fichero package.json que contendr\u00e1 informaci\u00f3n b\u00e1sica de nuestro proyecto. Dentro de este fichero tendremos que a\u00f1adir un nuevo par\u00e1metro type con el valor module, esto nos permitir\u00e1 importar nuestros m\u00f3dulos con el est\u00e1ndar ES:

    {\n  \"name\": \"tutorialNode\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"type\": \"module\"\n}\n
    "},{"location":"install/nodejs/#instalar-dependencias","title":"Instalar dependencias","text":"

    En ese fichero aparte de la informaci\u00f3n de nuestro proyecto tambi\u00e9n tendremos que a\u00f1adir las dependencias que usara nuestra aplicaci\u00f3n.

    Para a\u00f1adir las dependencias, desde la consola de comandos y situados en la misma carpeta donde se haya creado el fichero package.json vamos a teclear los siguientes comandos:

    npm i express\nnpm i express-validator\nnpm i dotenv\nnpm i mongoose\nnpm i mongoose-paginate-v2\nnpm i normalize-mongoose\nnpm i cors\nnpm i nodemon --save-dev\n

    Tambi\u00e9n podr\u00edamos haber instalado todas a la vez en dos l\u00edneas:

    npm i express express-validator dotenv  mongoose mongoose-paginate-v2 normalize-mongoose cors\nnpm i nodemon --save-dev\n

    Las dependencias que acabamos de instalar son las siguientes:

    • Express es un framework de Node que nos facilitara mucho la tarea a la hora de crear nuestra aplicaci\u00f3n.
    • Dotenv es una librer\u00eda para usar variables de entorno.
    • Mongoose es una librer\u00eda ODM que nos ayudara a los accesos a BBDD.
    • Nodemon es una herramienta que nos ayuda reiniciando nuestro servidor cuando detecta un cambio en alguno de nuestros ficheros y as\u00ed no tener que hacerlo manualmente.
    • Cors es una herramienta que nos ayuda a configurar el CORS de nuestra app para que posteriormente podemos conectarlo al front.

    Ahora podemos fijarnos en nuestro fichero package.json donde se habr\u00e1n a\u00f1adido dos nuevos par\u00e1metros: dependencies y devDependencies. La diferencia est\u00e1 en que las devDependencies solo se utilizar en la fase de desarrollo de nuestro proyecto y las dependencies se utilizar\u00e1n en todo momento.

    "},{"location":"install/nodejs/#configurar-la-bbdd","title":"Configurar la BBDD","text":"

    A partir de aqu\u00ed ya podemos abrir Visual Studio Code, el IDE recomendado, y abrir la carpeta del proyecto para poder configurarlo y programarlo. Lo primero ser\u00e1 configurar el acceso con la BBDD.

    Para ello vamos a crear en la ra\u00edz de nuestro proyecto una carpeta config dentro de la cual crearemos un archivo llamado db.js. Este archivo exportar\u00e1 una funci\u00f3n que recibe una url de nuestra BBDD y la conectar\u00e1 con mongoose. El contenido de este archivo debe ser el siguiente:

    db.js
    import mongoose from 'mongoose';\n\nconst connectDB = async (url) => {\n\n    try {\n        await mongoose.connect(url);\n        console.log('BBDD connected');\n    } catch (error) {\n        throw new Error('Error initiating BBDD:' + error);\n    }\n}\n\nexport default connectDB;\n

    Ahora vamos a crear en la ra\u00edz de nuestro proyecto un archivo con el nombre .env. Este archivo tendr\u00e1 las variables de entorno de nuestro proyecto. Es aqu\u00ed donde pondremos la url que obtuvimos al crear nuestra BBDD. As\u00ed pues, crearemos una nueva variable y pegaremos la URL. Tambi\u00e9n vamos a configurar el puerto del servidor.

    .env
    MONGODB_URL='mongodb+srv://<user>:<pass>@<url>.mongodb.net/?retryWrites=true&w=majority'\nPORT='8080'\n
    "},{"location":"install/nodejs/#arrancar-el-proyecto","title":"Arrancar el proyecto","text":"

    Con toda esa configuraci\u00f3n, ahora ya podemos crear nuestra p\u00e1gina inicial. Dentro del fichero package.json, en concreto en el contenido de main vemos que nos indica el valor de index.js. Este ser\u00e1 el punto de entrada a nuestra aplicaci\u00f3n, pero este fichero todav\u00eda no existe, as\u00ed que lo crearemos con el siguiente contenido:

    index.js
    import express from 'express';\nimport cors from 'cors';\nimport connectDB from './config/db.js';\nimport { config } from 'dotenv';\n\nconfig();\nconnectDB(process.env.MONGODB_URL);\nconst app = express();\n\napp.use(cors({\n    origin: '*'\n}));\n\napp.listen(process.env.PORT, () => {\n    console.log(`Server running on port ${process.env.PORT}`);\n});\n

    El funcionamiento de este c\u00f3digo, resumiendo mucho, es el siguiente. Configurar la base de datos, configurar el CORS para que posteriormente podamos realizar peticiones desde el front y crea un servidor con express en el puerto 8080.

    Pero antes, para poder ejecutar nuestro servidor debemos modificar el fichero package.json, y a\u00f1adir un script de arranque. A\u00f1adiremos la siguiente l\u00ednea:

    \"dev\": \"nodemon ./index.js\"\n

    Y ahora s\u00ed, desde la consola de comando ya podemos ejecutar el siguiente comando:

    npm run dev\n

    y ya podremos ver en la consola como la aplicaci\u00f3n ha arrancado correctamente con el mensaje que le hemos a\u00f1adido.

    "},{"location":"install/react/","title":"Entorno de desarrollo - React","text":""},{"location":"install/react/#instalacion-de-herramientas","title":"Instalaci\u00f3n de herramientas","text":"

    Las herramientas b\u00e1sicas que vamos a utilizar para esta tecnolog\u00eda son:

    • Visual Studio Code
    • Scoop.sh
    • Nodejs
    "},{"location":"install/react/#visual-studio-code","title":"Visual Studio Code","text":"

    Lo primero de todo es instalar el IDE para el desarrollo front.

    Te recomiendo utilizar Visual Studio Code, en un IDE que a nosotros nos gusta mucho y tiene muchos plugins configurables. Puedes entrar en su p\u00e1gina y descargarte la versi\u00f3n estable.

    "},{"location":"install/react/#scoopsh","title":"Scoop.sh","text":"

    Muchas de las herramientas que necesitar\u00e1s a lo largo de tu estancia en los proyectos, no podr\u00e1s instalarlas por temas de permisos de seguridad en nuestros port\u00e1tiles. Una forma de evitar estos permisos de seguridad y poder instalar herramientas (sobre todo a nivel de consola), es utilizando a Scoop.sh.

    Para instalar scoop.sh tan solo necesitas abrir un termina de PowerShell (OJO que es PowerShell y no una consola de msdos) y ejecutar los siguientes comandos:

    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser\nInvoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression\n

    Esto nos instalar\u00e1 scoop.sh en nuestros port\u00e1tiles. Adem\u00e1s de la instalaci\u00f3n, y debido a las restricciones de seguridad que tenemos, necesitaremos activar el lessmsi para que las aplicaciones que necesitemos instalar no intenten ejecutar los .exe de instalaci\u00f3n, sino que descompriman el zip. Para ello deberemos ejecutar el comando:

    scoop config use_lessmsi true\n

    A partir de este punto, ya tenemos listo el port\u00e1til para instalar herramientas y aplicaciones.

    "},{"location":"install/react/#nodejs","title":"Nodejs","text":"

    El siguiente paso ser\u00e1 instalar el motor de Nodejs. Para esto vamos a usar scoop.sh ya que lo tenemos instalado, y vamos a pedirle que nos instal\u00e9 el motor de Nodejs. Abriremos una consola de msdos y ejecutaremos el comando:

    scoop install main/nodejs\n

    Con esto, scoop ya nos instalar\u00e1 todo lo necesario.

    "},{"location":"install/react/#creacion-de-proyecto","title":"Creaci\u00f3n de proyecto","text":"

    Hasta ahora para la generaci\u00f3n de un proyecto React se ha utilizado la herramienta \u201ccreate-react-app\u201d pero \u00faltimamente se usa m\u00e1s vite debido a su velocidad para desarrollar y su optimizaci\u00f3n en tiempos de construcci\u00f3n. En realidad, para realizar nuestro proyecto da igual una herramienta u otra m\u00e1s all\u00e1 de un poco de configuraci\u00f3n, pero para este proyecto elegiremos vite por su velocidad.

    Para generar nuestro proyecto react con Vite abrimos una consola de Windows y escribimos lo siguiente en la carpeta donde queramos localizar nuestro proyecto:

    npm create vite@latest\n

    Con esto se nos lanzara un wizard para la creaci\u00f3n de nuestro proyecto donde elegiremos el nombre del proyecto (en mi caso ludoteca-react), el framework (react evidentemente) y en la variante elegiremos typescript. Tras estos pasos instalaremos las dependencias base de nuestro proyecto. Primero accedemos a la ra\u00edz y despu\u00e9s ejecutaremos el comando install de npm.

    cd ludoteca-react\n
    npm install\n

    \u00f3

    npm i\n
    "},{"location":"install/react/#arrancar-el-proyecto","title":"Arrancar el proyecto","text":"

    Para arrancar el proyecto, tan solo necesitamos ejecutar en consola el siguiente comando siempre dentro del directorio creado por Vite:

    npm run dev\n

    Vite compilar\u00e1 el c\u00f3digo fuente, levantar\u00e1 un servidor local al que podremos acceder por defecto mediante la URL: http://localhost:5173/

    Y ya podemos empezar a trabajar en nuestro proyecto React.

    Info

    Cuando se trata de un proyecto nuevo recien descargado de un repositorio, recuerda que ser\u00e1 necesario resolver las dependencias antes de arrancar el servidor. Esto se puede realizar mediante el gestor de dependencias de Nodejs, directamente en consola ejecuta el comando npm update y descargar\u00e1 e instalar\u00e1 las nuevas dependencias.

    "},{"location":"install/springboot/","title":"Entorno de desarrollo - Spring Boot","text":""},{"location":"install/springboot/#instalacion-de-herramientas","title":"Instalaci\u00f3n de herramientas","text":"

    Las herramientas b\u00e1sicas que vamos a utilizar para esta tecnolog\u00eda son:

    • IntelliJ o Eclipse IDE (el que m\u00e1s te guste)
    • Java 17 o superior
    • Postman
    "},{"location":"install/springboot/#instalacion-de-intellij-idea","title":"Instalaci\u00f3n de IntelliJ IDEA","text":"

    Nuestra preferencia es utilizar IntelliJ ya que es un IDE m\u00e1s moderno que Eclise IDE, pero cualquiera de los dos es v\u00e1lido para hacer el tutorial. Debido a las restricciones que tenemos en nuestros port\u00e1tiles no ser\u00e1 posible descargarnos una versi\u00f3n de la web e instalarlo, aunque existe otra forma de hacerlo.

    Deberemos acceder al \"Portal de Empresa\" que tenemos instalado en nuestro port\u00e1til. Teclear en el buscador de Windows (o en el men\u00fa de inicio) el texto \"Portal de empresa\". Deber\u00eda aparecerte una app instalada en tu ordenador y hacer click en ella:

    Una vez dentro del portal de empresa, ver\u00e1s una aplicaci\u00f3n que se llama \"Pre-Approved Catalogue\". Deber\u00e1s instalarla, de hecho cada vez que quieras acceder a ella, tendr\u00e1s que instalarla para que se descargue el nuevo cat\u00e1logo.

    Despu\u00e9s de unos minutos de instalaci\u00f3n, entrar\u00e1s en un listado de las aplicaciones que est\u00e1n pre-aprobadas por la empresa. Solo tendr\u00e1s que buscar \"IntelliJ IDEA Community Edition\" e instalarla.

    Pasados unos minutos, ya tendr\u00e1s instalado el IDE en tu port\u00e1til.

    "},{"location":"install/springboot/#configuracion-del-ide","title":"Configuraci\u00f3n del IDE","text":"

    Como complemento al IntelliJ, con el fin de crear c\u00f3digo homog\u00e9neo y mantenible, vamos a configurar el formateador de c\u00f3digo autom\u00e1tico.

    Para ello de nuevo abrimos el men\u00fa Customize -> All Settings o el men\u00fa Settings si estamos en un proyecto, nos vamos a la secci\u00f3n Editor -> Code Style -> Java y aparecer\u00e1 una pantalla similar a esta:

    En el bot\u00f3n de opciones, nos permitir\u00e1 \"Importar esquema\" desde Intellij IDEA:

    Nos descargamos el fichero de Formmatter Profile IntelliJ y lo importamos en IntelliJ.

    Una vez cofigurado el nuevo formateador debemos activar que se aplique en el guardado. Para ello volvemos acceder a las preferencias de IntelliJ y nos dirigimos a la sub secci\u00f3n Tools -> Actions os Save. Es posible que esta secci\u00f3n solo est\u00e9 disponible cuando creemos o importemos un proyecto, as\u00ed que volveremos m\u00e1s adelante aqu\u00ed.

    Hay que activar la opci\u00f3n Reformat code y Optimize imports.

    "},{"location":"install/springboot/#obsoleto-instalacion-de-eclipse-ide","title":"(Obsoleto) Instalaci\u00f3n de Eclipse IDE","text":"

    Si no te gusta IntelliJ, puedes utilizar Eclipse IDE y la m\u00e1quina virtual de java necesaria para ejecutar el c\u00f3digo. Recomendamos Java 17 o superior, que es la versi\u00f3n con la que est\u00e1 desarrollado y probado el tutorial.

    Para instalar el IDE deber\u00e1s acceder a la web de Eclipse IDE y descargarte la \u00faltima versi\u00f3n del instalador. Una vez lo ejecutes te pedir\u00e1 el tipo de instalaci\u00f3n que deseas instalar. Por lo general con la de \"Eclipse IDE for Java Developers\" es suficiente. Con esta versi\u00f3n ya tiene integrado los plugins de Maven y Git.

    Pero recuerda que tendr\u00e1s que instalar una versi\u00f3n acorde de Java ya que Eclipse viene con una versi\u00f3n vieja.

    "},{"location":"install/springboot/#instalacion-de-java","title":"Instalaci\u00f3n de Java","text":"

    Si has instalado IntelliJ, te puedes saltar este punto.

    Si has instalado Eclise IDE, debes asegurarte que est\u00e1 usando por defecto la versi\u00f3n de Java 17 o superior y para ello deber\u00e1s instalarla. Desc\u00e1rgala del siguiente enlace. Es posible que te pida un registro de correo, utiliza el email que quieras (corporativo o personal). Revisa bien el enlace para buscar y descargar la versi\u00f3n 17 para Windows.

    OJO no instales el ejecutable .exe ya que no funcionar\u00e1 debido a nuestras medidas de seguridad. Debes descargarte el .zip y descomprimirlo en alg\u00fan directorio local.

    Ya solo queda a\u00f1adir Java al Eclipse. Para ello, abre el men\u00fa Window -> Preferences:

    y dentro de la secci\u00f3n Java - Installed JREs a\u00f1ade la versi\u00f3n que acabas de descargar, siempre pulsando el bot\u00f3n Add... y buscando el directorio home de la instalaci\u00f3n de Java. Adem\u00e1s, la debes marcar como default.

    "},{"location":"install/springboot/#configuracion-de-eclipse","title":"Configuraci\u00f3n de Eclipse","text":"

    Como complemento al Eclipse, con el fin de crear c\u00f3digo homog\u00e9neo y mantenible, vamos a configurar el formateador de c\u00f3digo autom\u00e1tico.

    Para ello de nuevo abrimos el men\u00fa Window -> Preferences, nos vamos a la secci\u00f3n Formatter de Java:

    Aqu\u00ed crearemos un nuevo perfil heredando la configuraci\u00f3n por defecto.

    En el nuevo perfil configuramos que se use espacios en vez de tabuladores con sangrado de 4 caracteres.

    Una vez cofigurado el nuevo formateador debemos activar que se aplique en el guardado. Para ello volvemos acceder a las preferencias de Eclipse y nos dirigimos a la sub secci\u00f3n Save Actions del la secci\u00f3n Editor nuevamente de Java.

    Aqu\u00ed aplicamos la configuraci\u00f3n deseada.

    "},{"location":"install/springboot/#herramientas-para-pruebas","title":"Herramientas para pruebas","text":"

    Para poder probar las operaciones de negocio que vamos a crear, lo mejor es utilizar una herramienta que permita realizar llamadas a API Rest. Para ello te propongo utilizar Postman, en su versi\u00f3n web o en su versi\u00f3n desktop, cualquiera de las dos sirve.

    Con esta herramienta se puede generar peticiones GET, POST, PUT, DELETE contra el servidor y pasarle par\u00e1metros de forma muy sencilla y visual. Lo usaremos durante el tutorial.

    "},{"location":"install/springboot/#creacion-de-proyecto","title":"Creaci\u00f3n de proyecto","text":"

    La mayor\u00eda de los proyectos Spring Boot en los que trabajamos normalmente, suelen ser proyectos web sencillos con pocas dependencias de terceros o incluso proyectos basados en micro-servicios que ejecutan pocas acciones. Ahora tienes que preparar el proyecto SpringBoot,

    "},{"location":"install/springboot/#crear-con-initilizr","title":"Crear con Initilizr","text":"

    Vamos a ver como configurar paso a paso un proyecto de cero, con las librer\u00edas que vamos a utilizar en el tutorial.

    "},{"location":"install/springboot/#como-usarlo","title":"\u00bfComo usarlo?","text":"

    Spring ha creado una p\u00e1gina interactiva que permite crear y configurar proyectos en diferentes lenguajes, con diferentes versiones de Spring Boot y a\u00f1adi\u00e9ndole los m\u00f3dulos que nosotros queramos.

    Esta p\u00e1gina est\u00e1 disponible desde Spring Initializr. Para seguir el ejemplo del tutorial, entraremos en la web y seleccionaremos los siguientes datos:

    • Tipo de proyecto: Maven
    • Lenguage: Java
    • Versi\u00f3n Spring boot: 3.2.4 (o alguna similar que no sea SNAPSHOPT y que sea 3.x)
    • Group: com.ccsw
    • ArtifactId: tutorial
    • Versi\u00f3n Java: 17 (o superior)
    • Dependencias: Spring Web, Spring Data JPA, H2 Database

    Esto nos generar\u00e1 un proyecto que ya vendr\u00e1 configurado con Spring Web, JPA y H2 para crear una BBDD en memoria de ejemplo con la que trabajaremos durante el tutorial.

    "},{"location":"install/springboot/#importar-en-intellij","title":"Importar en IntelliJ","text":"

    El siguiente paso, es descomprimir el proyecto generado e importarlo en el IDE. Abrimos IntelliJ, pulsamos en \"Open\" y buscamos la carpeta donde hemos descomprimido el proyecto.

    Una vez importado, recuerda darle al men\u00fa File \u2192 Settings y configurar las acciones de Actions on save que se explicar\u00f3n en el punto Configuraci\u00f3n del IDE.

    "},{"location":"install/springboot/#importar-en-eclipse","title":"Importar en Eclipse","text":"

    El siguiente paso, es descomprimir el proyecto generado e importarlo como proyecto Maven. Abrimos el eclipse, pulsamos en File \u2192 Import y seleccionamos Existing Maven Projects. Buscamos el proyecto y le damos a importar.

    "},{"location":"install/springboot/#configurar-las-dependencias","title":"Configurar las dependencias","text":"

    Lo primero que vamos a hacer es a\u00f1adir las dependencias a algunas librer\u00edas que vamos a utilizar. Abriremos el fichero pom.xml que nos ha generado el Spring Initilizr y a\u00f1adiremos las siguientes l\u00edneas:

    pom.xml
    <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\nxsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n<modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>3.2.4</version>\n        <relativePath/> <!-- lookup parent from repository -->\n    </parent>\n\n    <groupId>com.ccsw</groupId>\n    <artifactId>tutorial</artifactId>\n    <version>0.0.1-SNAPSHOT</version>\n    <name>tutorial</name>\n    <description>Tutorial project for Spring Boot</description>\n\n    <properties>\n        <java.version>17</java.version>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-jpa</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springdoc</groupId>\n            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>\n            <version>2.0.3</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.hibernate</groupId>\n            <artifactId>hibernate-validator</artifactId>\n            <version>8.0.0.Final</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.modelmapper</groupId>\n            <artifactId>modelmapper</artifactId>\n            <version>3.1.1</version>\n        </dependency>\n\n        <dependency>\n            <groupId>com.h2database</groupId>\n            <artifactId>h2</artifactId>\n            <scope>runtime</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n

    Hemos a\u00f1adido las dependencias de que nos permite utilizar Open API para documentar nuestras APIs. Adem\u00e1s de esa dependencia, hemos a\u00f1adido una utilidad para hacer mapeos entre objetos y para configurar los servicios Rest. M\u00e1s adelante veremos como se utilizan.

    "},{"location":"install/springboot/#configurar-librerias","title":"Configurar librer\u00edas","text":"

    El siguiente punto es crear las clases de configuraci\u00f3n para las librer\u00edas que hemos a\u00f1adido. Para ello vamos a crear un package de configuraci\u00f3n general de la aplicaci\u00f3n com.ccsw.tutorial.config donde crearemos una clase que llamaremos ModelMapperConfig y usaremos para configurar el bean de ModelMapper.

    ModelMapperConfig.java
    package com.ccsw.tutorial.config;\n\nimport org.modelmapper.ModelMapper;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n/**\n * @author ccsw\n *\n */\n@Configuration\npublic class ModelMapperConfig {\n\n    @Bean\n    public ModelMapper getModelMapper() {\n\n        return new ModelMapper();\n    }\n\n}\n

    Esta configuraci\u00f3n nos permitir\u00e1 luego hacer transformaciones entre objetos de forma muy sencilla. Ya lo iremos viendo m\u00e1s adelante. Listo, ya podemos empezar a desarrollar nuestros servicios.

    "},{"location":"install/springboot/#configurar-la-bbdd","title":"Configurar la BBDD","text":"

    Por \u00faltimo, vamos a dejar configurada la BBDD en memoria. Para ello crearemos un fichero, de momento en blanco, dentro de src/main/resources/:

    • data.sql \u2192 Ser\u00e1 el fichero que utilizaremos para rellenar con datos iniciales el esquema de BBDD

    Este fichero no puede estar vac\u00edo, ya que si no dar\u00e1 un error al arrancar. Puedes a\u00f1adirle la siguiente query (que no hace nada) para que pueda arrancar el proyecto.

    select 1 from dual;

    Y ahora le vamos a decir a Spring Boot que la BBDD ser\u00e1 en memoria, que use un motor de H2 y que la cree autom\u00e1ticamente desde el modelo y que utilice el fichero data.sql (por defecto) para cargar datos en esta. Para ello hay que configurar el fichero application.properties que est\u00e1 dentro de src/main/resources/:

    application.properties
      #Database\n  spring.datasource.url=jdbc:h2:mem:testdb\n  spring.datasource.username=sa\n  spring.datasource.password=sa\n  spring.datasource.driver-class-name=org.h2.Driver\n\n  spring.jpa.database-platform=org.hibernate.dialect.H2Dialect\n  spring.jpa.defer-datasource-initialization=true\n  spring.jpa.show-sql=true\n\n  spring.h2.console.enabled=true\n
    "},{"location":"install/springboot/#arrancar-el-proyecto","title":"Arrancar el proyecto","text":"

    Por \u00faltimo ya solo nos queda arrancar el proyecto creado. Para ello buscaremos la clase TutorialApplication.java (o la clase principal del proyecto) y con el bot\u00f3n derecho seleccionaremos Run As \u2192 Java Application. La aplicaci\u00f3n al estar basada en Spring Boot arrancar\u00e1 internamente un Tomcat embebido donde se despliega el proyecto.

    Si hab\u00e9is seguido el tutorial la aplicaci\u00f3n estar\u00e1 disponible en http://localhost:8080, aunque de momento a\u00fan no tenemos nada accesible y nos dar\u00e1 una p\u00e1gina de error Whitelabel Error Page, error 404. Eso significa que el Tomcat embedido nos ha contestado pero no sabe que devolvernos porque no hemos implementado todav\u00eda nada.

    "},{"location":"install/vuejs/","title":"Entorno de desarrollo - Vue.js","text":""},{"location":"install/vuejs/#instalacion-de-herramientas","title":"Instalaci\u00f3n de herramientas","text":"

    Las herramientas b\u00e1sicas que vamos a utilizar para esta tecnolog\u00eda son:

    • Visual Studio Code
    • Scoop.sh
    • Nodejs
    • Quasar CLI
    "},{"location":"install/vuejs/#visual-studio-code","title":"Visual Studio Code","text":"

    Lo primero de todo es instalar el IDE para el desarrollo front.

    Te recomiendo utilizar Visual Studio Code, en un IDE que a nosotros nos gusta mucho y tiene muchos plugins configurables. Puedes entrar en su p\u00e1gina y descargarte la versi\u00f3n estable.

    "},{"location":"install/vuejs/#scoopsh","title":"Scoop.sh","text":"

    Muchas de las herramientas que necesitar\u00e1s a lo largo de tu estancia en los proyectos, no podr\u00e1s instalarlas por temas de permisos de seguridad en nuestros port\u00e1tiles. Una forma de evitar estos permisos de seguridad y poder instalar herramientas (sobre todo a nivel de consola), es utilizando a Scoop.sh.

    Para instalar scoop.sh tan solo necesitas abrir un termina de PowerShell (OJO que es PowerShell y no una consola de msdos) y ejecutar los siguientes comandos:

    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser\nInvoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression\n

    Esto nos instalar\u00e1 scoop.sh en nuestros port\u00e1tiles. Adem\u00e1s de la instalaci\u00f3n, y debido a las restricciones de seguridad que tenemos, necesitaremos activar el lessmsi para que las aplicaciones que necesitemos instalar no intenten ejecutar los .exe de instalaci\u00f3n, sino que descompriman el zip. Para ello deberemos ejecutar el comando:

    scoop config use_lessmsi true\n

    A partir de este punto, ya tenemos listo el port\u00e1til para instalar herramientas y aplicaciones.

    "},{"location":"install/vuejs/#nodejs","title":"Nodejs","text":"

    El siguiente paso ser\u00e1 instalar el motor de Nodejs. Para esto vamos a usar scoop.sh ya que lo tenemos instalado, y vamos a pedirle que nos instal\u00e9 el motor de Nodejs. Abriremos una consola de msdos y ejecutaremos el comando:

    scoop install main/nodejs\n

    Con esto, scoop ya nos instalar\u00e1 todo lo necesario.

    "},{"location":"install/vuejs/#creacion-de-proyecto","title":"Creaci\u00f3n de proyecto","text":""},{"location":"install/vuejs/#generar-scaffolding","title":"Generar scaffolding","text":"

    Lo primero que haremos ser\u00e1 generar un proyecto mediante la librer\u00eda \"Quasar CLI\" para ello ejecutamos en consola el siguiente comando:

    npm init quasar\n

    Este comando detectar\u00e1 si tienes el CLI de Quasar instalado y en caso contrario te preguntar\u00e1 si deseas instalarlo. Debes responder que s\u00ed, que lo instale.

    Una vez instalado, aparecer\u00e1 un wizzard en el que se ir\u00e1n preguntando una serie de datos para crear la aplicaci\u00f3n:

    Y tendremos que elegir lo siguiente:

    What would you like to build?

    App with Quasar CLI, let's go!

    Project folder

    tutorial-vue

    Pick Quasar version

    Quasar v2 (Vue 3 | latest and greatest)

    Pick script type

    Typescript

    Pick Quasar App CLI variant

    Quasar App CLI with Vite

    Package name

    tutorial-vue

    Project product name

    Ludoceta Tan

    Project description

    Proyecto tutorial Ludoteca Tan

    Author

    <por defecto el email>

    Pick a Vue component style

    Composition API

    Pick your CSS preprocessor

    Sass with SCSS syntax

    Check the features needed for your project

    ESLint

    Pick an ESLint preset

    Prettier

    Install project dependencies?

    Yes, use npm

    "},{"location":"install/vuejs/#arrancar-el-proyecto","title":"Arrancar el proyecto","text":"

    Cuando todo ha terminado el propio scaffolding te dice lo que tienes que hacer para poner el proyecto en marcha y ver lo que te ha generado, solo tienes que seguir esos pasos.

    Accedes al directorio que acabas de crear y ejecutas

    npx quasar dev\n

    Esto arrancar\u00e1 el servidor y abrir\u00e1 un navegador en el puerto 9000 donde se mostrar\u00e1 la template creada.

    Tambi\u00e9n podemos navegar nosotros mismos a la URL http://localhost:9000/.

    Info

    Si durante el desarrollo del proyecto necesitas a\u00f1adir nuevos m\u00f3dulos al proyecto Vue.js, ser\u00e1 necesario resolver las dependencias antes de arrancar el servidor. Esto se puede realizar mediante el gestor de dependencias de Nodejs, directamente en consola ejecuta el comando npm install y descargar\u00e1 e instalar\u00e1 las nuevas dependencias..

    Proyecto descargado

    Cuando se trata de un proyecto nuevo recien descargado de un repositorio, recuerda que ser\u00e1 necesario resolver las dependencias antes de arrancar el servidor. Esto se puede realizar mediante el gestor de dependencias de Nodejs, directamente en consola ejecuta el comando npm install y descargar\u00e1 e instalar\u00e1 las nuevas dependencias.

    "},{"location":"specs/customers_free/","title":"Gesti\u00f3n de clientes (modelo gratuito)","text":"

    Warning

    Esta secci\u00f3n se encuentra en desarrollo \ud83d\udea7. NO se recomienda realizarla a menos que te lo hayan indicado expresamente.

    "},{"location":"specs/customers_free/#punto-de-partida","title":"Punto de partida","text":"

    Si has llegado hasta aqu\u00ed, entiendo que ya has le\u00eddo tanto la introducci\u00f3n como la instalaci\u00f3n del entorno. A partir de ahora voy a dar por hecho que partimos todos desde el mismo punto y que tenemos ya la estructura de directorios creada y los proyectos descargados.

    Dicho esto, nos vamos a la consola (o desde el terminal de tu IDE), nos situamos en el directorio ra\u00edz y lanzamos el inicializador de OpenSpec.

    openspec init\n

    Seleccionamos GitHub Copilot, pulsamos Enter para a\u00f1adirlo y despu\u00e9s Tab para validar la selecci\u00f3n.

    Esto instalar\u00e1 las plantillas necesarias para poder trabajar con OpenSpec + GitHub Copilot.

    "},{"location":"specs/customers_free/#requisitos-funcionales","title":"Requisitos funcionales","text":"
    • CRUD de clientes.
    • Entidad cliente con id y name.
    • Listado simple sin filtros ni paginaci\u00f3n.
    • Alta/edici\u00f3n en modal.
    • El nombre es el \u00fanico campo editable.
    • No se permite guardar clientes con nombre duplicado.
    "},{"location":"specs/customers_free/#estrategia-del-modo-gratuito","title":"Estrategia del modo gratuito","text":"

    Vamos a trabajar con un modelo gratuito, as\u00ed que es importante tener claras sus limitaciones:

    • El contexto es muy limitado
    • El n\u00famero de operaciones mensuales tambi\u00e9n lo es
    • El n\u00famero de operaciones por hora tambi\u00e9n

    As\u00ed que, te pido paciencia ya que posiblemente no puedas hacer todo el tutorial completo en el mismo d\u00eda, deber\u00e1s trocearlo por exceso de l\u00edmite de peticiones.

    Para intengar mitigar un poco esto, vamos a:

    • Dividir el trabajo en tareas peque\u00f1as
    • Ser muy expl\u00edcitos en los prompts
    • Dar m\u00e1s contexto \u201cartificial\u201d al modelo

    Con un modelo de pago podr\u00edamos plantear el ejercicio de forma m\u00e1s generalista y con menos fragmentaci\u00f3n.

    El modelo que utilizaremos ser\u00e1 Claude Haiku. Para ello debes:

    1. Hacer login con tu cuenta de GitHub
    2. Activar GitHub Copilot
    3. En el chat, abrir el tercer desplegable
    4. Seleccionar el modelo Claude Haiku

    En cualquier momento puedes ver el consumo mensual de tu cuenta pulsando el icono de la rana \ud83d\udc38 en la esquina inferior derecha. El contador se reinicia cada mes.

    Vamos a dividir el ejercicio en dos grandes bloques:

    1. Primero trabajaremos \u00fanicamente con el backend
    2. Despu\u00e9s abordaremos el frontend

    De esta forma limitamos el contexto a un solo proyecto y facilitamos el trabajo al modelo.

    Adem\u00e1s, recuerda que el comportamiento del modelo no es determinista. Si a ti te genera algo diferente a lo que ves aqu\u00ed, probablemente seguir\u00e1 siendo v\u00e1lido. No te frustres y ajusta los prompts si es necesario.

    "},{"location":"specs/customers_free/#flujo-de-trabajo-openspec","title":"Flujo de trabajo OpenSpec","text":"

    Seguiremos el ciclo completo de :

    1. Explore\n2. Propose\n3. Apply\n4. Archive\n
    "},{"location":"specs/customers_free/#generacion-de-backend","title":"Generaci\u00f3n de backend","text":""},{"location":"specs/customers_free/#explore","title":"Explore","text":"

    El objetivo de esta fase es analizar el sistema existente, sin modificar nada.

    Buscamos:

    • Entender la estructura actual
    • Identificar patrones reutilizables
    • Comprender c\u00f3mo se comunican frontend y backend

    Aspectos a revisar:

    Organizaci\u00f3n por dominios

    • Estructura de carpetas
    • Dominios existentes

    Angular

    • Componentes
    • Servicios
    • Modelos
    • Routing

    Spring Boot

    • Controller
    • Service
    • Repository
    • Entity
    • DTO

    Patr\u00f3n CRUD

    • Listados
    • Creaci\u00f3n / edici\u00f3n
    • Borrado

    Conexi\u00f3n frontend-backend

    • Endpoints
    • URLs
    • DTOs

    Reutilizaci\u00f3n

    • C\u00f3digo com\u00fan
    • Patrones repetidos

    \u26a0\ufe0f En esta fase:

    • NO se escribe c\u00f3digo
    • NO se dise\u00f1a la soluci\u00f3n
    • NO se inventan estructuras nuevas

    Solo se analiza el sistema actual.

    \ud83d\udcdc Prompt

    Lo que haremos ser\u00e1 escribir en el chat de Visual Studio Code el comando y las instrucciones que queramos darle. Recuerda haber elegido Claude Haiku.

    /opsx:explore\n\nAnaliza el proyecto actual que est\u00e1 en el directorio 'backend', es una aplicaci\u00f3n Spring Boot. Una vez analizado, responde:\n\n1. \u00bfC\u00f3mo est\u00e1n implementados los CRUD existentes?\n- Controller\n- Service\n- Repository\n\n2. \u00bfQu\u00e9 estructura siguen los dominios?\n\n3. \u00bfC\u00f3mo se implementan las operaciones?\n- Listado\n- Creaci\u00f3n/edici\u00f3n\n- Borrado\n\n4. \u00bfQu\u00e9 formato tienen los endpoints y que relaci\u00f3n tiene con los m\u00e9todos HTTP?\n\n5. \u00bfQu\u00e9 patrones o estructuras comunes se repiten en los CRUD existentes?\n- Clases reutilizables\n- L\u00f3gica repetida\n- Estructuras comunes entre dominios\n\n6. \u00bfExisten test unitarios y de integraci\u00f3n? \u00bfC\u00f3mo est\u00e1n implementados? \u00bfUtiliza algo especial al arrancar o al mockear?\n\nAnaliza \u00fanicamente la parte de backend (Spring Boot)\nNO propongas soluciones.\nNO dise\u00f1es nuevas funcionalidades.\nSolo analiza el sistema actual.\nEscr\u00edbe el resultado del contexto dentro del fichero backend-explore.md en el directorio de las specs para poder utilizarlo en siguientes fases.\n

    Este comando realizar\u00e1 un an\u00e1lisis exhaustivo de tu sistema y lo dejar\u00e1 escrito dentro de la carpeta specs en un fichero llamado backend-explore.md. Al final te mostrar\u00e1 un texto con el resumen del sistema y adem\u00e1s escribir\u00e1 el fichero de explore.

    Sobre los permisos

    Es posible que durante el an\u00e1lisis te pida permiso para hacer ciertas tareas. Le puedes ir dando permiso una a una o darle permiso en todo el workspace, eso lo dejamos a tu elecci\u00f3n.

    En cualquier momento puedes ver el consumo de la ventana de contexto para saber si todo el conocimiento del sistema est\u00e1 en memoria o no. En el icono de la gr\u00e1fica de pastel que est\u00e1 dentro del chat en la parte superior derecha.

    "},{"location":"specs/customers_free/#propose","title":"Propose","text":"

    En esta fase definimos qu\u00e9 vamos a construir, bas\u00e1ndonos estrictamente en el resultado del Explore.

    Durante esta fase debes especificar.

    Descripci\u00f3n funcional

    • Qu\u00e9 hace la funcionalidad
    • Qu\u00e9 problema resuelve

    Reglas de negocio

    • Validaciones
    • Restricciones
    • Comportamientos esperados

    Dise\u00f1o backend

    • Endpoints necesarios
    • Estructura del dominio (Entity, DTO, Service, Repository)
    • Tipo de operaciones (listado, creaci\u00f3n, edici\u00f3n, borrado)

    Dise\u00f1o frontend

    • Componentes necesarios
    • Flujo de usuario (listado, abrir modal, guardar, borrar)
    • Servicios Angular

    Decisiones t\u00e9cnicas

    • Qu\u00e9 patrones existentes se reutilizan
    • Qu\u00e9 se mantiene igual que en otros dominios
    • Qu\u00e9 diferencias introduce esta funcionalidad

    Plan de implementaci\u00f3n

    • Tareas ordenadas
    • Separaci\u00f3n backend / frontend

    Aqu\u00ed dejamos claro:

    • Qu\u00e9 funcionalidad se va a a\u00f1adir
    • Qu\u00e9 reglas de negocio existen
    • Qu\u00e9 piezas del sistema se ven afectadas
    • Qu\u00e9 tareas habr\u00e1 que ejecutar

    \u26a0\ufe0f En esta fase:

    • NO se implementa c\u00f3digo
    • NO se redefine el sistema

    \ud83d\udcdc Prompt

    Para nuestro ejemplo, lo que haremos ser\u00e1 escribir en el chat de Visual Studio Code el siguiente prompt:

    /opsx:propose manage-customers-backend\n\nDefine la funcionalidad de gesti\u00f3n de clientes bas\u00e1ndote en el sistema actual y en los patrones identificados en la fase Explore, tienes el resultado en el fichero \"backend-explore.md\".\n\nNos han pedido esta nueva funcionalidad.\n\nPor un lado necesita poder tener una base de datos de sus clientes. Para ello nos ha pedido que si podemos crearle una pantalla de CRUD sencilla, al igual que hicimos con las categor\u00edas donde \u00e9l pueda dar de alta a sus clientes.\n\nNos ha pasado un esquema muy sencillo de lo que quiere, tan solo quiere guardar un listado de los nombres de sus clientes para tenerlos fichados, y nos ha hecho un par de pantallas sencillas muy similares a Categor\u00edas.\n\nUn listado sin filtros de ning\u00fan tipo ni paginaci\u00f3n.\n\nUn formulario de edici\u00f3n / alta, cuyo \u00fanico dato editable sea el nombre. Adem\u00e1s, la \u00fanica restricci\u00f3n que nos ha pedido es que NO podamos dar de alta a un cliente con el mismo nombre que otro existente. As\u00ed que deberemos comprobar el nombre, antes de guardar el cliente.\n\nPara empezar te dar\u00e9 unos consejos:\n- Recuerda crear la tabla de la BBDD y sus datos\n- Intenta primero hacer el listado completo, en el orden que m\u00e1s te guste: frontend o backend.\n- Completa el listado conectando ambas capas.\n- Termina el caso de uso haciendo las funcionalidades de edici\u00f3n, nuevo y borrado. Presta atenci\u00f3n a la validaci\u00f3n a la hora de guardar un cliente, NO se puede guardar si el nombre ya existe.\n\n\nTe voy a dar otras directrices que pienso que te pueden servir:\n- Se necesita un CRUD de clientes\n- Un cliente solo tiene: id, name\n- El listado ser\u00e1 simple, sin filtros ni paginaci\u00f3n\n- Existir\u00e1 un formulario de alta / edici\u00f3n en modal\n- El \u00fanico campo editable ser\u00e1 el nombre\n- No se puede crear un cliente con un nombre ya existente, ser\u00e1 una validaci\u00f3n obligatoria que deberemos cumplir en el guardado\n\n\nNecesito que definas:\n\n1. Descripci\u00f3n de la funcionalidad\n\n2. Reglas de negocio\n\n3. Dise\u00f1o backend:\n- Endpoints necesarios\n- Estructura del dominio (Entity, DTO, Service, Repository)\n\n4. Decisiones t\u00e9cnicas:\n- Qu\u00e9 patrones del sistema actual se reutilizan\n\n\nNO implementes c\u00f3digo.\nNO analices de nuevo el proyecto.\nBasa la propuesta en los patrones detectados en la fase Explore.\nHaz la propuesta \u00fanicamente de backend.\nComo \u00faltima tarea a\u00f1ade al fichero de tasks generar un resumen del cambio realizado, con el contrato de los endpoints y la informaci\u00f3n necesaria para que luego el frontend pueda implementar sus llamadas de forma sencilla.\n\nTendr\u00e1s que escribir los ficheros de proposal, design, spec y tasks en la propuesta correspondiente.\n

    De nuevo al ser un modelo gratuito tenemos que delimitarle mucho las tareas y recordarle que debe generar los ficheros de proposal, design, spec y tasks, adem\u00e1s de basarse en el fichero de backend-explore. Tambi\u00e9n debemos centrarle para que SOLO genere la parte de backend.

    Por \u00faltimo si te fijas en el prompt hay una tarea que le indica claramente que genere un fichero con los contratos de los endpoints para poder implementar, en un futuro, la parte frontend. Esto es solamente una idea de generar un resumen para que el frontend sepa como comunicarse con el backend.

    Este paso debe generar una propuesta dentro de changes con los artefactos proposal, design, spec y tasks.

    Si necesitas recordar el papel de cada artefacto, revisa la introducci\u00f3n.

    Responsabilidades como developer IA

    En este punto la IA te ha hecho una propuesta que puede ser correcta o no, recordemos que se trata de un modelo matem\u00e1tico-probabil\u00edstico. Si hay algo de lo propuesto que no te encaja o es err\u00f3neo deber\u00edas comentarlo mediante el chat o corregirlo de forma manual en el fichero que corresponda. Por ejemplo si quieres a\u00f1adir una tarea porqu\u00e9 se te ha olvidado incluirla en el prompt original, deber\u00edas decirle al modelo que te incluya la nueva tarea.

    Una vez estemos de acuerdo con la propuesta que nos ha hecho la IA, podemos pasar al siguiente punto.

    "},{"location":"specs/customers_free/#apply","title":"Apply","text":"

    Una vez validada la propuesta, ejecutamos la implementaci\u00f3n:

    El objetivo de esta fase es transformar los artefactos generados (proposal.md, design.md, spec.md, tasks.md) en c\u00f3digo funcional, asegurando que:

    • Se respetan los requisitos funcionales definidos en spec.md
    • Se siguen las decisiones t\u00e9cnicas establecidas en design.md
    • Se ejecutan las tareas en el orden definido en tasks.md

    \ud83d\udcdc Prompt

    Esto es tan f\u00e1cil como escribir en el chat de Visual Studio Code el siguiente prompt:

    /opsx:apply\n

    El agente empezar\u00e1 a realizar un mont\u00f3n de tareas y pedirnos permisos. Es posible que algunas de esas tareas fallen y \u00e9l mismo lo reintente de otra forma. El resultado deber\u00eda ser el c\u00f3digo generado e implementado dentro de la carpeta de backend y un resumen de todas las tareas realizadas y checkeadas por la IA.

    "},{"location":"specs/customers_free/#verificacion-del-backend","title":"Verificaci\u00f3n del backend","text":"

    Un paso que no pertenece a OpenSpec pero que es altamente recomendable es probar los cambios realizados. Arranca el backend y verifica:

    • Que el servidor levanta
    • Que los endpoints existen y funcionan
    • Que los tests pasan

    Ojo no te fies

    Ojo no te fies de todo lo que construya la IA. Tu est\u00e1s al mando, tu debes decidir si el sistema est\u00e1 correctamente implementado o no. Es tu responsabilidad.

    Si NO est\u00e1s a gusto con la implementaci\u00f3n o se ha dejado algo por hacer, es el momento de escribirlo por el chat indic\u00e1ndole exactamente que es lo que falta. Cuanto m\u00e1s preciso y conciso seas, mejor implementar\u00e1 la IA.

    "},{"location":"specs/customers_free/#archive","title":"Archive","text":"

    Y llegamos a la \u00faltima etapa que nos define OpenSpec, donde se archiva el cambio y se da por finalizada la funcionalidad.

    El objetivo de esta fase es marcar la funcionalidad como completada, consolidar todos los artefactos generados durante el proceso y dejar el sistema en un estado estable, coherente y preparado para nuevas evoluciones.

    En esta fase se asegura que:

    • La funcionalidad ha sido correctamente implementada y validada
    • No existen incidencias cr\u00edticas pendientes
    • La documentaci\u00f3n asociada al cambio est\u00e1 completa y actualizada
    • Existe una trazabilidad entre requisitos, dise\u00f1o e implementaci\u00f3n

    Aunque parezca mentira, este paso es muy importante ya que nos servir\u00e1 para actualizar el contexto del sistema y archivar todos los cambios para futuras consultas.

    \ud83d\udcdc Prompt

    De nuevo nos vamos al chat de Visual Studio Code el siguiente prompt:

    /opsx:archive\n

    En este punto el sistema pedir\u00e1 confirmaci\u00f3n para sincronizar requisitos antes de archivar.

    Sincronizar es obligatorio si quieres mantener trazabilidad entre lo implementado y los specs oficiales.

    \ud83d\udcdc Actualizaci\u00f3n del contexto

    Adem\u00e1s, para forzar al modelo gratuito y dejarlo todo listo, es recomendable lanzar un \u00faltimo prompt que nos actualice el fichero de backend-explore.md

    Actualiza el fichero de backend-explore con los nuevos datos implementados\n
    "},{"location":"specs/customers_free/#generacion-de-frontend","title":"Generaci\u00f3n de frontend","text":"

    Bueno, pues ahora que ya tenemos el backend implementado, realizaremos de nuevo un ciclo completo de OpenSpec pero est\u00e1 vez para frontend:

    1. Explore\n2. Propose\n3. Apply\n4. Archive\n

    Es muy importante que cada nuevo cambio que hagamos, lo empecemos en un chat nuevo, para limpiar el contexto anterior y no arrastrar posibles errores o incoherencias.

    "},{"location":"specs/customers_free/#explore_1","title":"Explore","text":"

    De nuevo el objetivo de esta fase es analizar el sistema existente, sin modificar nada, pero esta vez nos centraremos en el frontend.

    \ud83d\udcdc Prompt

    Vamos a un nuevo chat de Visual Studio Code y escribimos el comando:

    opsx:explore\n\nAnaliza el proyecto actual que est\u00e1 en el directorio \"frontend\", es una aplicaci\u00f3n Angular. Ojo no escanees la carpeta de \"node_modules\" no tiene sentido. Una vez analizado, responde:\n\n1. \u00bfC\u00f3mo est\u00e1n implementados los CRUD existentes?\n- Componentes\n- Servicios\n- Modelos\n\n2. \u00bfQu\u00e9 estructura siguen los dominios?\n\n3. \u00bfC\u00f3mo se implementan las operaciones?\n- Listado\n- Creaci\u00f3n/edici\u00f3n\n- Borrado\n- C\u00f3mo funcionan las ventanas de creaci\u00f3n y edici\u00f3n (modales)\n\n4. \u00bfComo se comunican frontend con backend?\n- Servicios en Angular\n- Construcci\u00f3n de URLs\n\n5. \u00bfQu\u00e9 patrones o estructuras comunes se repiten en los CRUD existentes?\n- Clases reutilizables\n- L\u00f3gica repetida\n- Estructuras comunes entre dominios\n\n\nAnaliza \u00fanicamente la parte de frontend (Angular)\nNO propongas soluciones.\nNO dise\u00f1es nuevas funcionalidades.\nSolo analiza el sistema actual.\nEscr\u00edbe el resultado del contexto dentro del fichero frontend-explore.md en el directorio de las specs para poder utilizarlo en siguientes fases.\n

    Este comando realizar\u00e1 un an\u00e1lisis exhaustivo de tu sistema y lo dejar\u00e1 escrito dentro de la carpeta specs en un fichero llamado frontend-explore.md. Al final te mostrar\u00e1 un texto con el resumen del sistema y adem\u00e1s escribir\u00e1 el fichero de explore. Este an\u00e1lisis estar\u00e1 m\u00e1s centrado en el frontend y debes pedirle que compruebe como se comunica con el backend para que lo tenga en cuenta.

    "},{"location":"specs/customers_free/#propose_1","title":"Propose","text":"

    Una vez definido el an\u00e1lisis inicial, lo siguiente es pedirle una propuesta de lo que queremos construir.

    \ud83d\udcdc Prompt

    De nuevo en el chat de Visual Studio Code escribimos el comando:

    /opsx:propose manage-customers-frontend\n\nDefine la funcionalidad de gesti\u00f3n de clientes bas\u00e1ndote en el sistema actual y en los patrones identificados en la fase Explore, tienes el resultado en el fichero \"frontend-explore.md\". Adem\u00e1s tendr\u00e1s que ver el cambio realizado en la spec de \"manage-customers-backend\", sobre todo los endpoints generados que lo tienes definido en \"FRONTEND_API_CONTRACT.md\"\n\nNos han pedido esta nueva funcionalidad.\n\nPor un lado necesita poder tener una base de datos de sus clientes. Para ello nos ha pedido que si podemos crearle una pantalla de CRUD sencilla, al igual que hicimos con las categor\u00edas donde \u00e9l pueda dar de alta a sus clientes.\n\nNos ha pasado un esquema muy sencillo de lo que quiere, tan solo quiere guardar un listado de los nombres de sus clientes para tenerlos fichados, y nos ha hecho un par de pantallas sencillas muy similares a Categor\u00edas.\n\nUn listado sin filtros de ning\u00fan tipo ni paginaci\u00f3n.\n\nUn formulario de edici\u00f3n / alta, cuyo \u00fanico dato editable sea el nombre. Adem\u00e1s, la \u00fanica restricci\u00f3n que nos ha pedido es que NO podamos dar de alta a un cliente con el mismo nombre que otro existente. As\u00ed que deberemos comprobar el nombre, antes de guardar el cliente.\n\nPara empezar te dar\u00e9 unos consejos:\n- Recuerda crear la tabla de la BBDD y sus datos\n- Intenta primero hacer el listado completo, en el orden que m\u00e1s te guste: frontend o backend.\n- Completa el listado conectando ambas capas.\n- Termina el caso de uso haciendo las funcionalidades de edici\u00f3n, nuevo y borrado. Presta atenci\u00f3n a la validaci\u00f3n a la hora de guardar un cliente, NO se puede guardar si el nombre ya existe.\n\n\nTe voy a dar otras directrices que pienso que te pueden servir:\n- Se necesita un CRUD de clientes\n- Un cliente solo tiene: id, name\n- El listado ser\u00e1 simple, sin filtros ni paginaci\u00f3n\n- Existir\u00e1 un formulario de alta / edici\u00f3n en modal\n- El \u00fanico campo editable ser\u00e1 el nombre\n- No se puede crear un cliente con un nombre ya existente, ser\u00e1 una validaci\u00f3n obligatoria que deberemos cumplir en el guardado\n\n\nNecesito que definas:\n\n1. Descripci\u00f3n de la funcionalidad\n\n2. Reglas de negocio\n\n3. Dise\u00f1o frontend:\n- Componentes necesarios\n- Flujos de interacci\u00f3n (listado, abrir modal, guardar borrar)\n\n4. Uso de endpoints para llamar a backend\n\n5. Decisiones t\u00e9cnicas:\n- Qu\u00e9 patrones del sistema actual se reutilizan\n\n\nNO implementes c\u00f3digo.\nNO analices de nuevo el proyecto.\nBasa la propuesta en los patrones detectados en la fase Explore.\nHaz la propuesta \u00fanicamente de frontend.\nOlv\u00eddate de los test, en frontend no tenemos tests.\nA\u00f1ade el nuevo punto de men\u00fa en el header para que se pueda acceder.\nNo te inventes estilos, respeta los estilos de las pantallas (anchuras, alturas, colores, disposici\u00f3n de las tablas).\nUtiliza los alert dialog de Angular Material para las alertas, no uses la opci\u00f3n alert() del navegador.\n\nTendr\u00e1s que escribir los ficheros de proposal, design, spec y tasks en la propuesta correspondiente.\n

    Puntos a destacar de este prompt:

    • Adem\u00e1s del contexto inicial le hemos pedido que busque el fichero de FRONTEND_API_CONTRACT.md que es el fichero que generamos junto con el backend y que tiene las reglas de los endpoints del backend. Esto lo tenemos que hacer as\u00ed ya que el modelo es gratuito y tiene limitaciones, no puede analizar frontend y backend a la vez en el mismo contexto.
    • Tambi\u00e9n le hemos pedido que NO invente estilos y que se fije en las pantallas existentes, adem\u00e1s de decirle que utilice componentes de Angular Material. A veces los modelos gratuitos tienden a inventar mucho, por falta de contexto. De nuevo cuanto m\u00e1s concretos y precisos seamos, mejor implementar\u00e1.
    "},{"location":"specs/customers_free/#apply_1","title":"Apply","text":"

    Cuando estemos de acuerdo con la propuesta que nos ha hecho la IA y sobre todo con las tasks que propone realizar, lanzamos la fase de implementaci\u00f3n.

    \ud83d\udcdc Prompt

    Esto es tan f\u00e1cil como escribir en el chat de Visual Studio Code el siguiente prompt:

    /opsx:apply\n

    El agente empezar\u00e1 a realizar un mont\u00f3n de tareas y pedirnos permisos. Es posible que algunas de esas tareas fallen y \u00e9l mismo lo reintente de otra forma. El resultado deber\u00eda ser el c\u00f3digo generado e implementado dentro de la carpeta de frontend y un resumen de todas las tareas realizadas y checkeadas por la IA.

    "},{"location":"specs/customers_free/#verificacion-del-frontend","title":"Verificaci\u00f3n del frontend","text":"

    Ahora s\u00ed, prueba de \ud83d\udd25 fuego \ud83d\udd25. Es hora de levantar el sistema completo, frontend y backend, navegar por la aplicaci\u00f3n y comprobar que todo funciona.

    Si algo no encaja, es buen momento para conversarlo con la IA y que realice los cambios necesarios hasta conseguir que todo funcione perfectamente.

    "},{"location":"specs/customers_free/#archive_1","title":"Archive","text":"

    Una vez tengamos todo funcionando y perfectamente implementado, pasamos a la \u00faltima etapa para archivar y sincronizar nuestro cambio.

    \ud83d\udcdc Prompt

    De nuevo nos vamos al chat de Visual Studio Code el siguiente prompt:

    /opsx:archive\n

    En ese caso, el sistema solicita confirmaci\u00f3n para sincronizar los requisitos antes de archivar el cambio.

    \ud83d\udcdc Actualizaci\u00f3n del contexto

    Y por \u00faltimo forzamos una actualizaci\u00f3n del contexto inicial.

    Actualiza el fichero de frontend-explore con los nuevos datos implementados\n
    "},{"location":"specs/customers_paid/","title":"Gesti\u00f3n de clientes (modelo con licencia)","text":"

    Warning

    Esta secci\u00f3n se encuentra en desarrollo \ud83d\udea7. NO se recomienda realizarla a menos que te lo hayan indicado expresamente.

    "},{"location":"specs/customers_paid/#punto-de-partida","title":"Punto de partida","text":"

    Si has llegado hasta aqu\u00ed, entiendo que ya has le\u00eddo tanto la introducci\u00f3n como la instalaci\u00f3n del entorno. A partir de ahora voy a dar por hecho que partimos todos desde el mismo punto y que tenemos ya la estructura de directorios creada y los proyectos descargados.

    Dicho esto, nos vamos a la consola (o desde el terminal de tu IDE), nos situamos en el directorio ra\u00edz y lanzamos el inicializador de OpenSpec.

    openspec init\n

    Seleccionamos GitHub Copilot, pulsamos Enter para a\u00f1adirlo y despu\u00e9s Tab para validar la selecci\u00f3n.

    Esto instalar\u00e1 las plantillas necesarias para poder trabajar con OpenSpec + GitHub Copilot.

    "},{"location":"specs/customers_paid/#requisitos-funcionales","title":"Requisitos funcionales","text":"
    • CRUD de clientes.
    • Entidad cliente con id y name.
    • Listado simple sin filtros ni paginaci\u00f3n.
    • Alta/edici\u00f3n en modal.
    • El nombre es el \u00fanico campo editable.
    • No se permite guardar clientes con nombre duplicado.
    "},{"location":"specs/customers_paid/#estrategia-del-modo-con-licencia","title":"Estrategia del modo con licencia","text":"

    Vamos a trabajar con un modelo de pago, por lo que es importante entender qu\u00e9 ventajas nos ofrece frente al modelo gratuito.

    En este caso podremos:

    • Trabajar con mayor contexto
    • Analizar frontend y backend simult\u00e1neamente
    • Reducir la fragmentaci\u00f3n de tareas
    • Obtener propuestas e implementaciones m\u00e1s completas y robustas

    El modelo que utilizaremos ser\u00e1 Claude Sonnet 4.6. Para ello debes:

    1. Tener una cuenta premium con acceso a modelos de pago
    2. Hacer login con tu cuenta de GitHub
    3. Activar GitHub Copilot
    4. En el chat, abrir el tercer desplegable
    5. Seleccionar el modelo Claude Sonnet 4.6

    En cualquier momento puedes ver el consumo mensual de tu cuenta pulsando el icono de la rana \ud83d\udc38 en la esquina inferior derecha. El contador se reinicia cada mes.

    Vamos a abordar el ejercicio como un \u00fanico bloque de trabajo, analizando y construyendo la funcionalidad de forma simult\u00e1nea en backend y frontend.

    De esta manera aprovechamos el mayor contexto del modelo de pago, permitiendo:

    1. Analizar backend y frontend al mismo tiempo
    2. Dise\u00f1ar la funcionalidad de forma coherente en ambas capas desde el inicio

    Esto nos permite mantener una visi\u00f3n global del sistema durante todo el proceso y reducir la necesidad de dividir artificialmente el trabajo en fases independientes por capa.

    Adem\u00e1s, recuerda que el comportamiento del modelo no es determinista. Si a ti te genera algo diferente a lo que ves aqu\u00ed, probablemente seguir\u00e1 siendo v\u00e1lido. No te frustres y ajusta los prompts si es necesario.

    "},{"location":"specs/customers_paid/#flujo-de-trabajo-openspec","title":"Flujo de trabajo OpenSpec","text":"

    Seguiremos el ciclo completo de OpenSpec:

    1. Explore\n2. Propose\n3. Apply\n4. Archive\n
    "},{"location":"specs/customers_paid/#explore","title":"Explore","text":"

    El objetivo de esta fase es analizar el sistema existente, sin modificar nada.

    Buscamos:

    • Entender la estructura actual de la aplicaci\u00f3n
    • Identificar patrones y estructuras reutilizables
    • Comprender c\u00f3mo se comunican frontend y backend

    Aspectos a revisar:

    Organizaci\u00f3n por dominios

    • C\u00f3mo est\u00e1n estructurados los dominios existentes (category, author, game\u2026)
    • Qu\u00e9 carpetas existen en frontend y backend
    • C\u00f3mo se relacionan los dominios entre capas

    Angular

    • Componentes
    • Servicios
    • Modelos
    • Routing

    Spring Boot

    • Controller
    • Service
    • Repository
    • Entity
    • DTO

    Patr\u00f3n CRUD

    • C\u00f3mo se implementan los listados
    • C\u00f3mo se implementan las operaciones de creaci\u00f3n, edici\u00f3n y borrado
    • C\u00f3mo funcionan las ventanas de creaci\u00f3n y edici\u00f3n (modales)

    Conexi\u00f3n frontend-backend

    • C\u00f3mo Angular llama a los endpoints
    • C\u00f3mo se construyen las URLs
    • Qu\u00e9 DTOs se utilizan en la comunicaci\u00f3n

    Reutilizaci\u00f3n

    • C\u00f3digo com\u00fan entre dominios
    • Patrones repetidos
    • Estructuras compartidas entre diferentes funcionalidades

    \u26a0\ufe0f En esta fase:

    • NO se escribe c\u00f3digo
    • NO se dise\u00f1a la soluci\u00f3n
    • NO se inventan estructuras nuevas

    Solo se analiza el sistema actual.

    \ud83d\udcdc Prompt

    Lo que haremos ser\u00e1 escribir en el chat de Visual Studio Code el comando y las instrucciones que queramos darle. Recuerda haber elegido Claude Sonnet 4.6 y estar trabajando en modo Agent.

    En este caso, hemos a\u00f1adido las carpetas del proyecto frontend y backend al contexto, por lo que el an\u00e1lisis se realizar\u00e1 sobre el sistema completo.

    Para ello, desde el propio Chat de Copilot, pulsando el bot\u00f3n \u201c+\u201d, puedes seleccionar y a\u00f1adir tanto archivos individuales como directorios completos del proyecto. Tambi\u00e9n es posible a\u00f1adirlos arrastr\u00e1ndolos directamente al chat.

    /opsx:explore\n\nAnaliza el proyecto actual (Angular 17 + Spring Boot) que se ha a\u00f1adido al contexto. Una vez analizado, responde:\n\n1. \u00bfC\u00f3mo est\u00e1n implementados los CRUD existentes?\n- Backend: Controller, Service, Repository\n- Frontend: componentes, servicios y modelo\n\n2. \u00bfQu\u00e9 estructura siguen los dominios en backend y en frontend?\n\n3. \u00bfC\u00f3mo se implementan las operaciones?\n- Listado\n- Creaci\u00f3n/edici\u00f3n\n- Borrado\n- C\u00f3mo funcionan las ventanas de creaci\u00f3n y edici\u00f3n (modales)\n\n4. \u00bfC\u00f3mo se comunican frontend y backend?\n- Servicios Angular\n- Construcci\u00f3n de URLs\n\n5. \u00bfQu\u00e9 patrones o estructuras comunes se repiten en los CRUD existentes?\n- Clases reutilizables\n- L\u00f3gica repetida\n- Estructuras comunes entre dominios\n\nNO propongas soluciones.\nNO dise\u00f1es nuevas funcionalidades.\nSolo analiza el sistema actual.\n

    Este comando realizar\u00e1 un an\u00e1lisis exhaustivo de tu sistema que servir\u00e1 como base para definir la nueva funcionalidad en la siguiente fase.

    Sobre los permisos

    Es posible que durante el an\u00e1lisis te pida permiso para hacer ciertas tareas. Le puedes ir dando permiso una a una o darle permiso en todo el workspace, eso lo dejamos a tu elecci\u00f3n.

    En cualquier momento puedes ver el consumo de la ventana de contexto para saber si todo el conocimiento del sistema est\u00e1 en memoria o no. En el icono de la gr\u00e1fica circular que est\u00e1 situada en la parte inferior derecha del chat.

    "},{"location":"specs/customers_paid/#propose","title":"Propose","text":"

    Una vez analizado el sistema en la fase Explore, el siguiente paso es definir de forma clara y estructurada la nueva funcionalidad a implementar.

    En esta fase establecemos qu\u00e9 vamos a construir, bas\u00e1ndonos estrictamente en el resultado del Explore y aprovechando que el modelo dispone de una visi\u00f3n completa del sistema (frontend y backend).

    Esta fase act\u00faa como puente entre el an\u00e1lisis y la implementaci\u00f3n, permitiendo dise\u00f1ar la soluci\u00f3n antes de escribir c\u00f3digo y reduciendo el riesgo de errores durante el desarrollo.

    Durante esta fase debes especificar:

    Descripci\u00f3n funcional

    • Qu\u00e9 hace la funcionalidad
    • Qu\u00e9 problema resuelve

    Reglas de negocio

    • Validaciones
    • Restricciones
    • Comportamientos esperados

    Dise\u00f1o backend

    • Endpoints necesarios
    • Estructura del dominio (Entity, DTO, Service, Repository)
    • Tipo de operaciones (listado, creaci\u00f3n, edici\u00f3n, borrado)

    Dise\u00f1o frontend

    • Componentes necesarios
    • Flujo de usuario (listado, abrir modal, guardar, borrar)
    • Servicios Angular

    Decisiones t\u00e9cnicas

    • Qu\u00e9 patrones existentes se reutilizan
    • Qu\u00e9 se mantiene igual que en otros dominios
    • Qu\u00e9 diferencias introduce esta funcionalidad

    Plan de implementaci\u00f3n

    • Tareas ordenadas
    • Separaci\u00f3n backend / frontend

    Aqu\u00ed dejamos claro:

    • Qu\u00e9 funcionalidad se va a a\u00f1adir
    • Qu\u00e9 reglas de negocio existen
    • Qu\u00e9 piezas del sistema se ven afectadas
    • Qu\u00e9 tareas habr\u00e1 que ejecutar

    \u26a0\ufe0f En esta fase:

    • NO se implementa c\u00f3digo
    • NO se redefine el sistema

    \ud83d\udcdc Prompt

    Recuerda que seguimos trabajando en modo Agent, con las carpetas del proyecto frontend y backend a\u00f1adidas al contexto.

    Para nuestro ejemplo, lo que haremos ser\u00e1 escribir en el chat de Visual Studio Code el siguiente prompt:

    /opsx:propose client\n\nDefine la funcionalidad de gesti\u00f3n de clientes bas\u00e1ndote en el sistema actual (Angular 17 + Spring Boot) y en los patrones identificados en la fase Explore.\n\nRequisitos funcionales:\n- Se necesita un CRUD de clientes\n- Un cliente solo tiene: id, name\n- El listado ser\u00e1 simple, sin filtros ni paginaci\u00f3n\n- Existir\u00e1 un formulario de alta / edici\u00f3n en modal\n- El \u00fanico campo editable ser\u00e1 el nombre\n- No se puede crear un cliente con un nombre ya existente (validaci\u00f3n obligatoria)\n\nDefine:\n\n1. Descripci\u00f3n de la funcionalidad\n\n2. Reglas de negocio\n\n3. Dise\u00f1o backend:\n- Endpoints necesarios\n- Estructura del dominio (Entity, DTO, Service, Repository)\n\n4. Dise\u00f1o frontend:\n- Componentes necesarios\n- Flujo de interacci\u00f3n (listado, abrir modal, guardar, borrar)\n\n5. Decisiones t\u00e9cnicas:\n- Qu\u00e9 patrones del sistema actual se reutilizan\n\nNO implementes c\u00f3digo.\nNO analices de nuevo el proyecto.\nBasa la propuesta en los patrones detectados en la fase Explore.\n

    Este comando debe generar una propuesta en changes con los artefactos proposal, design, spec y tasks.

    Si necesitas recordar el papel de cada artefacto, revisa la introducci\u00f3n.

    Responsabilidades como developer IA

    En este punto la IA te ha hecho una propuesta que puede ser correcta o no, recordemos que se trata de un modelo matem\u00e1tico-probabil\u00edstico. Si hay algo de lo propuesto que no te encaja o es err\u00f3neo deber\u00edas comentarlo mediante el chat o corregirlo de forma manual en el fichero que corresponda. Por ejemplo si quieres a\u00f1adir una tarea porqu\u00e9 se te ha olvidado incluirla en el prompt original, deber\u00edas decirle al modelo que te incluya la nueva tarea.

    Una vez estemos de acuerdo con la propuesta que nos ha hecho la IA, podemos pasar al siguiente punto.

    "},{"location":"specs/customers_paid/#apply","title":"Apply","text":"

    Una vez validada la propuesta, ejecutamos la implementaci\u00f3n:

    El objetivo de esta fase es transformar los artefactos generados (proposal.md, design.md, spec.md, tasks.md) en c\u00f3digo funcional, asegurando que:

    • Se respetan los requisitos funcionales definidos en spec.md
    • Se siguen las decisiones t\u00e9cnicas establecidas en design.md
    • Se ejecutan las tareas en el orden definido en tasks.md

    \ud83d\udcdc Prompt

    Esto es tan f\u00e1cil como escribir en el chat de Visual Studio Code el siguiente prompt:

    /opsx:apply\n

    El agente empezar\u00e1 a realizar un mont\u00f3n de tareas y pedirnos permisos. Es posible que algunas de esas tareas fallen y \u00e9l mismo lo reintente de otra forma. El resultado deber\u00eda ser el c\u00f3digo generado e implementado tanto en la carpeta backend como en la carpeta frontend y un resumen de todas las tareas realizadas y checkeadas por la IA.

    "},{"location":"specs/customers_paid/#verificacion","title":"Verificaci\u00f3n","text":"

    Un paso que no pertenece a OpenSpec pero que es altamente recomendable es probar los cambios realizados.

    Arranca el backend y el frontend y verifica:

    • La aplicaci\u00f3n levanta correctamente
    • Las nuevas funcionalidades a\u00f1adidas est\u00e1n accesibles
    • Los flujos principales definidos en spec.md funcionan como se espera

    Ojo no te fies

    Ojo no te fies de todo lo que construya la IA. Tu est\u00e1s al mando, tu debes decidir si el sistema est\u00e1 correctamente implementado o no. Es tu responsabilidad.

    Si NO est\u00e1s a gusto con la implementaci\u00f3n o se ha dejado algo por hacer, es el momento de escribirlo por el chat indic\u00e1ndole exactamente que es lo que falta. Cuanto m\u00e1s preciso y conciso seas, mejor implementar\u00e1 la IA.

    "},{"location":"specs/customers_paid/#archive","title":"Archive","text":"

    Y llegamos a la \u00faltima etapa que nos define OpenSpec, donde se archiva el cambio y se da por finalizada la funcionalidad.

    El objetivo de esta fase es marcar la funcionalidad como completada, consolidar todos los artefactos generados durante el proceso y dejar el sistema en un estado estable, coherente y preparado para nuevas evoluciones.

    En esta fase se asegura que:

    • La funcionalidad ha sido correctamente implementada y validada
    • No existen incidencias cr\u00edticas pendientes
    • La documentaci\u00f3n asociada al cambio est\u00e1 completa y actualizada
    • Existe una trazabilidad entre requisitos, dise\u00f1o e implementaci\u00f3n

    Aunque parezca mentira, este paso es muy importante ya que nos servir\u00e1 para actualizar el contexto del sistema y archivar todos los cambios para futuras consultas.

    \ud83d\udcdc Prompt

    De nuevo nos vamos al chat de Visual Studio Code el siguiente prompt:

    /opsx:archive\n

    En este punto el sistema pedir\u00e1 confirmaci\u00f3n para sincronizar requisitos antes de archivar.

    Sincronizar es obligatorio si quieres mantener trazabilidad entre lo implementado y los specs oficiales.

    "},{"location":"specs/intro/","title":"Introducci\u00f3n a Spec-Driven Development","text":"

    Warning

    Esta secci\u00f3n se encuentra en desarrollo \ud83d\udea7. NO se recomienda realizarla a menos que te lo hayan indicado expresamente.

    "},{"location":"specs/intro/#contexto","title":"Contexto","text":"

    En este bloque del tutorial vamos a implementar nuevas funcionalidades usando Spec-Driven Development (SDD) con OpenSpec.

    El objetivo no es \u00fanicamente desarrollar nuevas funcionalidades, sino hacerlo siguiendo un proceso estructurado que permita separar claramente las distintas fases del desarrollo y garantizar trazabilidad entre an\u00e1lisis, definici\u00f3n, implementaci\u00f3n y validaci\u00f3n.

    Para llevar a cabo este enfoque de manera pr\u00e1ctica, se utiliza la metodolog\u00eda OpenSpec, que proporciona un flujo de trabajo claro y repetible para definir, implementar y consolidar cambios en el sistema.

    Las funcionalidades abordadas se organizan en dos bloques principales:

    • Gesti\u00f3n de clientes
    • Gesti\u00f3n de pr\u00e9stamos

    Ambas se implementan siguiendo las fases definidas por OpenSpec, reutilizando los patrones existentes del sistema y manteniendo coherencia t\u00e9cnica y funcional con el resto de la aplicaci\u00f3n.

    "},{"location":"specs/intro/#que-es-spec-driven-development","title":"\u00bfQu\u00e9 es Spec-Driven Development?","text":"

    Spec-Driven Development (SDD) es un enfoque de desarrollo en el que el comportamiento del sistema se define de forma expl\u00edcita antes de escribir c\u00f3digo.

    En lugar de comenzar directamente con la implementaci\u00f3n, SDD propone describir primero:

    • Qu\u00e9 debe hacer el sistema
    • Qu\u00e9 reglas y restricciones deben cumplirse
    • Qu\u00e9 comportamiento se espera en cada escenario

    Las especificaciones (specs) se convierten en el eje central del desarrollo y sirven como referencia com\u00fan durante todo el proceso.

    Este enfoque permite:

    • Reducir ambig\u00fcedades sobre el comportamiento esperado
    • Detectar errores de dise\u00f1o de forma temprana
    • Mantener coherencia en sistemas que evolucionan con el tiempo
    • Facilitar la comunicaci\u00f3n entre personas y herramientas implicadas en el desarrollo
    "},{"location":"specs/intro/#openspec-como-metodologia-de-trabajo","title":"OpenSpec como metodolog\u00eda de trabajo","text":"

    OpenSpec es una metodolog\u00eda que materializa el enfoque de Spec-Driven Development, proporcionando un flujo de trabajo estructurado para implementar cambios de forma controlada y trazable.

    OpenSpec organiza el desarrollo en una serie de fases bien definidas que permiten:

    • Analizar el contexto y el alcance del cambio
    • Definir el comportamiento funcional esperado
    • Implementar la soluci\u00f3n de forma alineada con lo definido
    • Cerrar y consolidar el cambio de manera ordenada

    A lo largo de esta gu\u00eda se utilizar\u00e1 OpenSpec como marco de trabajo para aplicar SDD en la implementaci\u00f3n de las funcionalidades de gesti\u00f3n de clientes y gesti\u00f3n de pr\u00e9stamos.

    SDD y agentes de IA

    Al trabajar con agentes de IA, el c\u00f3digo puede generarse r\u00e1pidamente, pero sin una gu\u00eda clara existe el riesgo de que la IA tome decisiones no deseadas para que \u201cel c\u00f3digo funcione\u201d.

    OpenSpec traslada el foco a las especificaciones, que definen expl\u00edcitamente el comportamiento esperado del sistema y sirven como contrato para la IA, reduciendo ambig\u00fcedades y asegurando trazabilidad entre lo definido y lo implementado.

    Es sumamente importante que se defina de forma concreta y muy concisa los requisitos y las reglas que debe seguir la IA a la hora de analizar y generar.

    "},{"location":"specs/intro/#fases-de-openspec","title":"Fases de OpenSpec","text":"

    El flujo de trabajo de OpenSpec se estructura en cuatro fases principales:

    1. Explore\n2. Propose\n3. Apply\n4. Archive\n

    Cada una de estas fases cumple un prop\u00f3sito espec\u00edfico y se apoya en la anterior, formando un ciclo completo de definici\u00f3n, implementaci\u00f3n y cierre del cambio.

    "},{"location":"specs/intro/#explore","title":"Explore","text":"

    Fase inicial orientada a comprender el contexto en el que se va a trabajar.

    Objetivo

    • Analizar la informaci\u00f3n disponible
    • Entender la necesidad, iniciativa o cambio a abordar
    • Identificar posibles limitaciones, dependencias y patrones existentes

    Esta fase no implica necesariamente la existencia de un sistema previo.

    Puede consistir en:

    • Analizar un sistema existente
    • Revisar documentaci\u00f3n o requisitos
    • Definir el contexto cuando se parte desde cero

    Resultado

    Una comprensi\u00f3n clara del punto de partida y del alcance del cambio a realizar.

    "},{"location":"specs/intro/#propose","title":"Propose","text":"

    Fase orientada a la definici\u00f3n de la soluci\u00f3n a implementar.

    Objetivo

    • Definir qu\u00e9 se va a construir
    • Delimitar el alcance del cambio (qu\u00e9 se incluye y qu\u00e9 queda fuera)
    • Establecer el comportamiento funcional esperado

    Resultado

    Una propuesta clara, estructurada y alineada con el objetivo del cambio, que servir\u00e1 como base para su implementaci\u00f3n. En esta fase se deber\u00edan generar 4 ficheros.

    \ud83d\udcc4 proposal.md

    Define la funcionalidad a alto nivel.

    Incluye:

    • El problema que se quiere resolver (Why)
    • Qu\u00e9 cambios se van a introducir (What Changes)
    • El alcance funcional
    • El impacto en la aplicaci\u00f3n

    Responde a: \u00bfQu\u00e9 se va a construir y por qu\u00e9?

    \ud83d\udcc4 design.md

    Describe el dise\u00f1o t\u00e9cnico de la soluci\u00f3n.

    Incluye:

    • Contexto del sistema actual
    • Objetivos (Goals / Non-Goals)
    • Decisiones t\u00e9cnicas y su justificaci\u00f3n
    • Alternativas consideradas
    • Riesgos y trade-offs

    Responde a: \u00bfC\u00f3mo se va a construir y por qu\u00e9 se ha elegido este enfoque?

    \ud83d\udcc4 spec.md

    Define el comportamiento funcional esperado.

    Incluye:

    • Requisitos funcionales
    • Casos de uso expresados como escenarios (WHEN / THEN)
    • Reglas de negocio
    • Validaciones y restricciones

    Responde a: \u00bfQu\u00e9 debe hacer el sistema?

    \ud83d\udcc4 tasks.md

    Descompone la implementaci\u00f3n en tareas ejecutables. Quiz\u00e1 es el fichero m\u00e1s importante.

    Incluye:

    • Lista ordenada de tareas
    • Pasos concretos para implementar la funcionalidad

    Responde a: \u00bfC\u00f3mo se implementa paso a paso?

    Relaci\u00f3n entre los artefactos

    Cada uno de los ficheros generados cumple un rol espec\u00edfico dentro del flujo de OpenSpec:

    • spec.md \u2192 define el comportamiento esperado (qu\u00e9 debe hacer el sistema)
    • design.md \u2192 define la soluci\u00f3n t\u00e9cnica (c\u00f3mo se va a construir)
    • proposal.md \u2192 aporta contexto y alcance (por qu\u00e9 se construye)
    • tasks.md \u2192 gu\u00eda la ejecuci\u00f3n paso a paso (c\u00f3mo se implementa)

    Esta separaci\u00f3n de responsabilidades permite:

    • Evitar mezclar requisitos con implementaci\u00f3n
    • Revisar cada nivel de forma independiente
    • Detectar errores e inconsistencias antes de escribir c\u00f3digo

    Estos artefactos constituyen la base para la siguiente fase.

    "},{"location":"specs/intro/#apply","title":"Apply","text":"

    Fase en la que se lleva a cabo la implementaci\u00f3n de la soluci\u00f3n definida en la fase Propose.

    Objetivo

    • Desarrollar la soluci\u00f3n definida
    • Asegurar la coherencia entre lo definido y lo implementado
    • Integrar y validar funcionalmente el resultado

    Resultado

    Una soluci\u00f3n implementada, coherente con la propuesta definida y preparada para su validaci\u00f3n final.

    "},{"location":"specs/intro/#archive","title":"Archive","text":"

    Fase final de cierre y consolidaci\u00f3n del cambio.

    Objetivo

    • Confirmar que el cambio est\u00e1 completo
    • Consolidar la documentaci\u00f3n generada durante el proceso
    • Garantizar la trazabilidad para futuras evoluciones

    Resultado

    Un cambio finalizado, validado y correctamente documentado. En esta fase se pedir\u00e1 sincronizar los requisitos antes de archivar y consolidar.

    \u00bfQu\u00e9 significa sincronizar?

    Al seleccionar la opci\u00f3n de sincronizaci\u00f3n:

    • Se integran los nuevos requisitos definidos en spec.md
    • Se crea o actualiza el spec definitivo
    • Los requisitos pasan a formar parte oficial del sistema

    Es decir, los requisitos pasan de ser un cambio temporal a formar parte permanente del sistema.

    \u00bfQu\u00e9 ocurre si no se sincroniza?

    Si se decide no sincronizar:

    • El c\u00f3digo permanece implementado
    • Los requisitos no se registran en los specs principales

    Esto puede provocar:

    • P\u00e9rdida de trazabilidad
    • Dificultad para futuras evoluciones
    • Desalineaci\u00f3n entre c\u00f3digo y documentaci\u00f3n

    Tras completar el proceso de Archive:

    • La funcionalidad queda documentada como completada
    • El cambio deja de formar parte de los cambios activos
    • Los requisitos quedan integrados definitivamente en el sistema (si se ha sincronizado)
    "},{"location":"specs/intro/#principios-de-calidad","title":"Principios de calidad","text":"

    SDD y agentes de IA

    Con agentes de IA se genera c\u00f3digo muy r\u00e1pido, pero la responsabilidad t\u00e9cnica sigue siendo tuya.

    Durante todo el proceso:

    • revisa siempre la propuesta antes de aplicar,
    • valida funcionalmente lo implementado,
    • corrige tareas o requisitos cuando detectes desviaciones,
    • no asumas que la primera respuesta de la IA es correcta.
    "},{"location":"specs/loans_free/","title":"Gesti\u00f3n de pr\u00e9stamos (modelo gratuito)","text":"

    Warning

    Esta secci\u00f3n se encuentra en desarrollo \ud83d\udea7. NO se recomienda realizarla a menos que te lo hayan indicado expresamente.

    "},{"location":"specs/loans_free/#punto-de-partida","title":"Punto de partida","text":"

    Si has llegado hasta aqu\u00ed, entiendo que ya has completado la funcionalidad de gesti\u00f3n de clientes utilizando el modelo gratuito.

    A partir de ahora vamos a dar por hecho que partimos de ese estado del sistema, donde:

    • Existe un CRUD funcional de clientes
    • La funcionalidad est\u00e1 implementada, validada y archivada
    • Los patrones de backend y frontend introducidos ya forman parte del sistema

    Una vez llegados a este punto, asumimos que el proyecto ya est\u00e1 descargado y configurado, y que hemos trabajado previamente sobre la funcionalidad de gesti\u00f3n de clientes.

    Por tanto, continuaremos utilizando los mismos proyectos y directorios, sin realizar ninguna instalaci\u00f3n ni configuraci\u00f3n adicional.

    En este tutorial seguiremos trabajando sobre:

    • server-springboot como backend
    • client-angular17 como frontend
    "},{"location":"specs/loans_free/#requisitos-funcionales","title":"Requisitos funcionales","text":"
    • Gesti\u00f3n de pr\u00e9stamos entre clientes y juegos.
    • Listado paginado con filtros por juego, cliente y fecha.
    • Alta/edici\u00f3n en modal con campos obligatorios (salvo identificador).
    • Validaciones de fechas y restricciones de solapamiento.
    • M\u00e1ximo 14 d\u00edas por pr\u00e9stamo.
    • Un juego no puede estar prestado a dos clientes en el mismo d\u00eda.
    • Un cliente no puede tener m\u00e1s de dos pr\u00e9stamos activos en el mismo d\u00eda.
    "},{"location":"specs/loans_free/#estrategia-del-modo-gratuito","title":"Estrategia del modo gratuito","text":"

    Continuaremos trabajando con un modelo gratuito, utilizando Claude Haiku y el mismo workspace que en la funcionalidad de gesti\u00f3n de clientes.

    Antes de comenzar, ten en cuenta lo siguiente:

    • Para cada nueva funcionalidad, es recomendable iniciar una nueva conversaci\u00f3n de chat dentro del mismo proyecto

    Esto ayuda a mantener el contexto limpio y a que el modelo se centre exclusivamente en la funcionalidad que vamos a abordar.

    Recuerda que en cualquier momento puedes ver el consumo mensual de tu cuenta pulsando el icono de la rana \ud83d\udc38 en la esquina inferior derecha. El contador se reinicia cada mes.

    Al igual que el ejercicio anterior, vamos a dividir el ejercicio en dos grandes bloques:

    1. Primero trabajaremos \u00fanicamente con el backend
    2. Despu\u00e9s abordaremos el frontend

    De esta forma limitamos el contexto a un solo proyecto y facilitamos el trabajo al modelo.

    Adem\u00e1s, recuerda que el comportamiento del modelo no es determinista. Si a ti te genera algo diferente a lo que ves aqu\u00ed, probablemente seguir\u00e1 siendo v\u00e1lido. No te frustres y ajusta los prompts si es necesario.

    "},{"location":"specs/loans_free/#flujo-de-trabajo-openspec","title":"Flujo de trabajo OpenSpec","text":"

    Seguiremos el ciclo completo de :

    1. Explore\n2. Propose\n3. Apply\n4. Archive\n
    "},{"location":"specs/loans_free/#generacion-de-backend","title":"Generaci\u00f3n de backend","text":"

    Aunque no es obligatorio, es altamente recomendable volver a ejecutar la fase de Explore. El sistema ha podido cambiar desde tu \u00faltimo cambio, alguien ha podido hacer modificaciones, etc. En tu caso no ser\u00eda necesario ya que est\u00e1s trabajando tu solo y no has cambiado nada, pero es buena pr\u00e1ctica hacerlo siempre.

    "},{"location":"specs/loans_free/#explore","title":"Explore","text":"

    El objetivo de esta fase es analizar el sistema existente, sin modificar nada.

    A diferencia de la gesti\u00f3n de clientes, este caso de uso introduce una mayor complejidad, principalmente por:

    • Relaciones entre entidades (cliente, juego, pr\u00e9stamo)
    • Uso de paginaci\u00f3n en los listados
    • Aplicaci\u00f3n de filtros combinados
    • Necesidad de validaciones de negocio m\u00e1s complejas

    En esta fase se analizar\u00e1 qu\u00e9 partes del sistema actual ya resuelven este tipo de problemas y pueden reutilizarse, y qu\u00e9 aspectos no est\u00e1n implementados y deber\u00e1n abordarse en fases posteriores.

    Aspectos a revisar:

    Paginaci\u00f3n

    • C\u00f3mo se implementa en backend (uso de Page)

    Filtros

    • C\u00f3mo se implementa en el cat\u00e1logo de juegos
    • C\u00f3mo se implementan filtros por rangos de fechas (si existen)
    • DTOs de filtro utilizados
    • Construcci\u00f3n de queries en backend
    • C\u00f3mo se construyen queries con condiciones combinadas y operadores distintos de igualdad

    Relaciones entre entidades

    • C\u00f3mo se modelan relaciones en JPA
    • Ejemplos existentes en el proyecto
    • C\u00f3mo se representan en DTOs
    • C\u00f3mo se cargan y exponen los datos relacionados

    Validaciones en backend

    • D\u00f3nde se implementan (Service)
    • C\u00f3mo se gestionan errores
    • C\u00f3mo se propagan al frontend
    • C\u00f3mo se implementan validaciones sobre rangos de fechas
    • C\u00f3mo se validan restricciones que dependen de registros existentes (solapamientos, l\u00edmites por cliente, etc.)

    \u26a0\ufe0f En esta fase:

    • NO se escribe c\u00f3digo
    • NO se dise\u00f1a la soluci\u00f3n
    • NO se inventan estructuras nuevas

    Solo se analiza el sistema actual.

    \ud83d\udcdc Prompt

    Lo que haremos ser\u00e1 escribir en el chat de Visual Studio Code el comando y las instrucciones que queramos darle. Recuerda haber elegido Claude Haiku y estar trabajando en modo Agent.

    /opsx:explore\n\nAnaliza el proyecto actual que est\u00e1 en el directorio \"backend\", es una aplicaci\u00f3n Spring Boot. Una vez analizado, responde:\n\n1. \u00bfC\u00f3mo est\u00e1n implementados los CRUD existentes?\n- Controller\n- Service\n- Repository\n- Paginaci\u00f3n y respuestas paginadas\n\n2. \u00bfQu\u00e9 estructura siguen los dominios?\n\n3. \u00bfC\u00f3mo se implementan las operaciones?\n- Listado\n- Creaci\u00f3n/edici\u00f3n\n- Borrado\n- DTOs de filtro utilizados y construcci\u00f3n de queries en backend\n- Uso de condiciones combinadas (no solo igualdad)\n- Ejemplos de filtros por rango de fechas (si existen)\n- Ejemplos de consultas donde una fecha debe estar contenida dentro de un rango (si existen)\n\n4. \u00bfC\u00f3mo se gestionan relaciones entre entidades?\n- Modelado en JPA\n- Ejemplos en el proyecto\n- C\u00f3mo se representan en DTOs\n- C\u00f3mo se exponen los datos relacionados\n\n5. \u00bfC\u00f3mo se implementan validaciones en backend?\n- D\u00f3nde se ubican (Service)\n- C\u00f3mo se gestionan errores\n- C\u00f3mo se propagan al frontend\n- Si existen validaciones que dependan de m\u00faltiples registros o condiciones\n- Si existen validaciones relacionadas con fechas o rangos\n- C\u00f3mo se validan restricciones basadas en datos existentes\n\n6. \u00bfQu\u00e9 formato tienen los endpoints y que relaci\u00f3n tiene con los m\u00e9todos HTTP?\n\n7. \u00bfQu\u00e9 patrones o estructuras comunes se repiten en los CRUD existentes?\n- Clases reutilizables\n- L\u00f3gica repetida\n- Estructuras comunes entre dominios\n\n8. \u00bfExisten test unitarios y de integraci\u00f3n? \u00bfC\u00f3mo est\u00e1n implementados? \u00bfUtiliza algo especial al arrancar o al mockear?\n\n\nAnaliza \u00fanicamente la parte de backend (Spring Boot)\nNO propongas soluciones.\nNO dise\u00f1es nuevas funcionalidades.\nSolo analiza el sistema actual.\nYa tienes un contexto previo en el fichero backend-explore.md en el directorio de las specs, utilizalo y lo actualizas con lo que analices y no est\u00e9.\n

    Si te fijas, le hemos indicado que aproveche el contexto previo generado en el ejercicio anterior y le hemos pedido que lo actualice con los cambios que considere.

    Este comando realizar\u00e1 un an\u00e1lisis exhaustivo de tu sistema que servir\u00e1 como base para definir la nueva funcionalidad en la siguiente fase.

    Sobre los permisos

    Es posible que durante el an\u00e1lisis te pida permiso para hacer ciertas tareas. Le puedes ir dando permiso una a una o darle permiso en todo el workspace, eso lo dejamos a tu elecci\u00f3n.

    "},{"location":"specs/loans_free/#propose","title":"Propose","text":"

    Una vez analizado el sistema en la fase Explore, el siguiente paso es definir de forma clara y estructurada la nueva funcionalidad a implementar.

    En esta fase establecemos qu\u00e9 vamos a construir, apoy\u00e1ndonos en el conocimiento ya consolidado del sistema y en el resultado del Explore.

    Esta fase act\u00faa como puente entre el an\u00e1lisis y la implementaci\u00f3n, permitiendo dise\u00f1ar la soluci\u00f3n antes de escribir c\u00f3digo y reduciendo el riesgo de errores durante el desarrollo.

    Durante esta fase debes especificar:

    Descripci\u00f3n funcional

    • Qu\u00e9 hace la funcionalidad
    • Qu\u00e9 problema resuelve

    Reglas de negocio

    • Validaciones sobre fechas:
      • La fecha de fin no podr\u00e1 ser anterior a la fecha de inicio
    • Restricciones de duraci\u00f3n del pr\u00e9stamo:
      • El per\u00edodo de pr\u00e9stamo m\u00e1ximo solo podr\u00e1 ser de 14 d\u00edas
    • Validaciones de solapamiento de pr\u00e9stamos:
      • El mismo juego no puede estar prestado a m\u00e1s de un cliente para ninguno de los d\u00edas incluidos en el rango del pr\u00e9stamo
    • L\u00edmites de pr\u00e9stamos simult\u00e1neos por cliente:
      • Un mismo cliente no puede tener m\u00e1s de 2 pr\u00e9stamos activos para ninguno de los d\u00edas incluidos en el rango del pr\u00e9stamo

    Dise\u00f1o backend

    • Endpoints necesarios
    • Estructura del dominio (Entity, DTO, Service, Repository)
    • Tipo de operaciones (listado, creaci\u00f3n, edici\u00f3n, borrado)
    • Estrategia para filtros por fecha dentro de rangos

    Decisiones t\u00e9cnicas

    • Qu\u00e9 patrones existentes se reutilizan
    • Qu\u00e9 partes deben extenderse
    • C\u00f3mo se gestionar\u00e1n los filtros de fecha y condiciones combinadas
    • C\u00f3mo se implementar\u00e1n validaciones basadas en m\u00faltiples registros (solapamientos y l\u00edmites)

    Plan de implementaci\u00f3n

    • Tareas ordenadas
    • Separaci\u00f3n backend / frontend
    • Prioridad de desarrollo (listado \u2192 filtros \u2192 validaciones)

    Aqu\u00ed dejamos claro:

    • Qu\u00e9 funcionalidad se va a a\u00f1adir
    • Qu\u00e9 reglas de negocio existen
    • Qu\u00e9 piezas del sistema se ven afectadas
    • Qu\u00e9 tareas habr\u00e1 que ejecutar

    \u26a0\ufe0f En esta fase:

    • NO se implementa c\u00f3digo
    • NO se redefine el sistema

    \ud83d\udcdc Prompt

    Para nuestro ejemplo, lo que haremos ser\u00e1 escribir en el chat de Visual Studio Code el siguiente prompt:

    /opsx:propose manage-loans-backend\n\nDefine la funcionalidad de gesti\u00f3n de pr\u00e9stamos de juegos bas\u00e1ndote en el sistema actual y en los patrones identificados en la fase Explore, tienes el resultado en el fichero \"backend-explore.md\".\n\nNos han pedido esta nueva funcionalidad.\n\nSe quiere hacer uso de su cat\u00e1logo de juegos y de sus clientes, y quiere saber que juegos ha prestado a cada cliente. Para ello nos ha pedido una p\u00e1gina bastante compleja donde se podr\u00e1 consultar diferente informaci\u00f3n y se permitir\u00e1 realizar el pr\u00e9stamo de los juegos.\n\nNos ha pasado el siguiente boceto y requisitos.\n\nLa pantalla tendr\u00e1 dos zonas:\n\n- Una zona de filtrado donde se permitir\u00e1 filtrar por:\n    - T\u00edtulo del juego, que deber\u00e1 ser un combo seleccionable con los juegos del cat\u00e1logo de la Ludoteca.\n    - Cliente, que deber\u00e1 ser un combo seleccionable con los clientes dados de alta en la aplicaci\u00f3n.\n    - Fecha, que deber\u00e1 ser de tipo Datepicker y que permitir\u00e1 elegir una fecha de b\u00fasqueda. Al elegir un d\u00eda nos deber\u00e1 mostrar que juegos est\u00e1n prestados para dicho d\u00eda. OJO que los pr\u00e9stamos son con fecha de inicio y de fin, si elijo un d\u00eda intermedio deber\u00eda aparecer el elemento en la tabla.\n- Una zona de listado paginado que deber\u00e1 mostrar\n    - El identificador del pr\u00e9stamo\n    - El nombre del juego prestado\n    - El nombre del cliente que lo solicit\u00f3\n    - La fecha de inicio del pr\u00e9stamo\n    - La fecha de fin del pr\u00e9stamo\n    - Un bot\u00f3n que permite eliminar el pr\u00e9stamo\n\n\nAl pulsar el bot\u00f3n de Nuevo pr\u00e9stamo se abrir\u00e1 una pantalla donde se podr\u00e1 ingresar la siguiente informaci\u00f3n, toda ella obligatoria:\n- Identificador, inicialmente vac\u00edo y en modo lectura\n- Nombre del cliente, mediante un combo seleccionable\n- Nombre del juego, mediante un combo seleccionable\n- Fechas del pr\u00e9stamo, donde se podr\u00e1 introducir dos fechas, de inicio y fin del pr\u00e9stamo.\n\nLas validaciones son sencillas aunque laboriosas:\n- La fecha de fin NO podr\u00e1 ser anterior a la fecha de inicio\n- El periodo de pr\u00e9stamo m\u00e1ximo solo podr\u00e1 ser de 14 d\u00edas. Si el usuario quiere un pr\u00e9stamo para m\u00e1s de 14 d\u00edas la aplicaci\u00f3n no debe permitirlo mostrando una alerta al intentar guardar.\n- El mismo juego no puede estar prestado a dos clientes distintos en un mismo d\u00eda. OJO que los pr\u00e9stamos tienen fecha de inicio y fecha fin, el juego no puede estar prestado a m\u00e1s de un cliente para ninguno de los d\u00edas que contemplan las fechas actuales del rango.\n- Un mismo cliente no puede tener prestados m\u00e1s de 2 juegos en un mismo d\u00eda. OJO que los pr\u00e9stamos tienen fecha de inicio y fecha fin, el cliente no puede tener m\u00e1s de dos pr\u00e9stamos para ninguno de los d\u00edas que contemplan las fechas actuales del rango.\n\nPara empezar te dar\u00e9 unos consejos:\n\n- Recuerda crear la tabla de la BBDD y sus datos\n- Intenta primero hacer el listado paginado sin filtros, en el orden que m\u00e1s te guste: frontend o backend. Recuerda que se trata de un listado paginado, as\u00ed que deber\u00e1s utilizar el objeto Page.\n- Completa el listado conectando ambas capas.\n- Ahora implementa los filtros, presta atenci\u00f3n al filtro de fecha, es el m\u00e1s complejo.\n- Para la paginaci\u00f3n filtrada solo tienes que mezclar los conceptos que hemos visto en los puntos del tutorial anteriores.\n- Si hiciste el backend en Springboot recuerda revisar Baeldung por si tienes dudas sobre las queries y recuerda que las Specifications son muy \u00fatiles, pero en este caso deber\u00e1s implementar otro tipo de operaciones, no te sirve solo con la operaci\u00f3n de igualdad :, que ya vimos en el tutorial.\n- Implementa la pantalla de alta de pr\u00e9stamo, sin ninguna validaci\u00f3n.\n- Cuando ya te funcione, intenta ir a\u00f1adiendo una a una las validaciones. Algunas de ellas pueden hacerse en frontend, mientras que otras deber\u00e1n validarse en backend\n- Os recordamos que han de poder crearse y editarse pr\u00e9stamos seg\u00fan las reglas de validaci\u00f3n indicadas anteriormente. Aplican las mismas reglas para ambas operaciones.\n- El Backend ha de validar siempre, independientemente de que el Frontend ya lo haya validado. Nunca conf\u00edes de manera exclusiva en terceras partes (Frontend o en otro Backend).\n\n\n\n\nTe voy a dar otras directrices que pienso que te pueden servir:\n- Se necesita un CRUD de prestamos\n- Debe tener una b\u00fasqueda y una paginaci\u00f3n, todo en el mismo endpoint\n- F\u00edjate en como est\u00e1n relacionadas las entidades del modelo ya que aqu\u00ed tendr\u00e1s que relacionar juego y cliente\n- Tienes que implementar las validaciones dentro del m\u00e9todo de guardado y creaci\u00f3n y, siempre que se pueda, la validaci\u00f3n se debe delegar en una query de BBDD.\n\n\nNecesito que definas:\n\n1. Descripci\u00f3n de la funcionalidad\n\n2. Reglas de negocio\n\n3. Dise\u00f1o backend:\n- Endpoints necesarios\n- Estructura del dominio (Entity, DTO, Service, Repository)\n\n4. Decisiones t\u00e9cnicas:\n- Qu\u00e9 patrones del sistema actual se reutilizan\n\n\nNO implementes c\u00f3digo.\nNO analices de nuevo el proyecto.\nBasa la propuesta en los patrones detectados en la fase Explore.\nHaz la propuesta \u00fanicamente de backend.\nComo \u00faltima tarea a\u00f1ade al fichero de tasks generar un resumen del cambio realizado, con el contrato de los endpoints y la informaci\u00f3n necesaria para que luego el frontend pueda implementar sus llamadas de forma sencilla.\n\nTendr\u00e1s que escribir los ficheros de proposal, design, spec y tasks en la propuesta correspondiente.\n

    Igual que en la gesti\u00f3n de clientes, este comando genera dentro del directorio changes la propuesta correspondiente, que incluye los siguientes ficheros: proposal.md, design.md, spec.md, tasks.md.

    Estos artefactos est\u00e1n adaptados a la funcionalidad de gesti\u00f3n de pr\u00e9stamos, incorporando las reglas de negocio, filtros y validaciones espec\u00edficas de este caso de uso.

    Constituyen la base para la siguiente fase: Apply, donde se ejecutar\u00e1 la implementaci\u00f3n siguiendo las tareas definidas.

    Responsabilidades como developer IA

    En este punto la IA te ha hecho una propuesta que puede ser correcta o no, recordemos que se trata de un modelo matem\u00e1tico-probabil\u00edstico. Si hay algo de lo propuesto que no te encaja o es err\u00f3neo deber\u00edas comentarlo mediante el chat o corregirlo de forma manual en el fichero que corresponda. Por ejemplo si quieres a\u00f1adir una tarea porqu\u00e9 se te ha olvidado incluirla en el prompt original, deber\u00edas decirle al modelo que te incluya la nueva tarea.

    Una vez estemos de acuerdo con la propuesta que nos ha hecho la IA, podemos pasar al siguiente punto.

    "},{"location":"specs/loans_free/#apply","title":"Apply","text":"

    Una vez validada la propuesta, ejecutamos la implementaci\u00f3n:

    El objetivo de esta fase es transformar los artefactos generados (proposal.md, design.md, spec.md, tasks.md) en c\u00f3digo funcional, asegurando que:

    • Se respetan los requisitos funcionales definidos en spec.md
    • Se siguen las decisiones t\u00e9cnicas establecidas en design.md
    • Se ejecutan las tareas en el orden definido en tasks.md

    \ud83d\udcdc Prompt

    Esto es tan f\u00e1cil como escribir en el chat de Visual Studio Code el siguiente prompt:

    /opsx:apply\n

    El agente empezar\u00e1 a realizar un mont\u00f3n de tareas y pedirnos permisos. Es posible que algunas de esas tareas fallen y \u00e9l mismo lo reintente de otra forma. El resultado deber\u00eda ser el c\u00f3digo generado e implementado dentro de la carpeta de backend y un resumen de todas las tareas realizadas y checkeadas por la IA.

    "},{"location":"specs/loans_free/#verificacion-del-backend","title":"Verificaci\u00f3n del backend","text":"

    Un paso que no pertenece a OpenSpec pero que es altamente recomendable es probar los cambios realizados. Arranca el backend y verifica:

    • Que el servidor levanta
    • Que los endpoints existen y funcionan
    • Que los tests pasan

    Ojo no te fies

    Ojo no te fies de todo lo que construya la IA. Tu est\u00e1s al mando, tu debes decidir si el sistema est\u00e1 correctamente implementado o no. Es tu responsabilidad.

    Si NO est\u00e1s a gusto con la implementaci\u00f3n o se ha dejado algo por hacer, es el momento de escribirlo por el chat indic\u00e1ndole exactamente que es lo que falta. Cuanto m\u00e1s preciso y conciso seas, mejor implementar\u00e1 la IA.

    "},{"location":"specs/loans_free/#archive","title":"Archive","text":"

    Y llegamos a la \u00faltima etapa que nos define OpenSpec, donde se archiva el cambio y se da por finalizada la funcionalidad.

    El objetivo de esta fase es marcar la funcionalidad como completada, consolidar todos los artefactos generados durante el proceso y dejar el sistema en un estado estable, coherente y preparado para nuevas evoluciones.

    \ud83d\udcdc Prompt

    De nuevo nos vamos al chat de Visual Studio Code el siguiente prompt:

    /opsx:archive\n

    Durante el proceso de Archive, el sistema solicitar\u00e1 confirmaci\u00f3n para sincronizar los requisitos antes de archivar el cambio.

    Recuerda que al sincronizar, los requisitos definidos en spec.md pasan de ser un cambio temporal a formar parte permanente del sistema.

    Si no se sincroniza, el c\u00f3digo queda implementado, pero los requisitos no se registran en los specs principales afectando a la trazabilidad y futuras evoluciones del sistema.

    \ud83d\udcdc Actualizaci\u00f3n del contexto

    Adem\u00e1s, para forzar al modelo gratuito y dejarlo todo listo, es recomendable lanzar un \u00faltimo prompt que nos actualice el fichero de backend-explore.md

    Actualiza el fichero de backend-explore con los nuevos datos implementados\n
    "},{"location":"specs/loans_free/#generacion-de-frontend","title":"Generaci\u00f3n de frontend","text":"

    Una vez implementado el backend, nos ponemos a trabajar con el frontend. De nuevo recordar que es muy importante que cada nuevo cambio que hagamos, lo empecemos en un chat nuevo, para limpiar el contexto anterior y no arrastrar posibles errores o incoherencias.

    "},{"location":"specs/loans_free/#explore_1","title":"Explore","text":"

    Al igual que en backend, aqu\u00ed tambi\u00e9n lanzamos una exploraci\u00f3n del sistema por si hubiera alg\u00fan cambio con respecto a la anterior versi\u00f3n.

    \ud83d\udcdc Prompt

    Vamos a un nuevo chat de Visual Studio Code y escribimos el comando:

    /opsx:explore\n\nAnaliza el proyecto actual que est\u00e1 en el directorio \"frontend\", es una aplicaci\u00f3n Angular. Ojo no escanees la carpeta de \"node_modules\" no tiene sentido. Una vez analizado, responde:\n\n1. \u00bfC\u00f3mo est\u00e1n implementados los CRUD existentes?\n- Componentes\n- Servicios\n- Modelos\n\n2. \u00bfQu\u00e9 estructura siguen los dominios?\n\n3. \u00bfC\u00f3mo se implementan las operaciones?\n- Listado\n- Creaci\u00f3n/edici\u00f3n\n- Borrado\n- C\u00f3mo funcionan las ventanas de creaci\u00f3n y edici\u00f3n (modales)\n\n4. \u00bfComo se comunican frontend con backend?\n- Servicios en Angular\n- Construcci\u00f3n de URLs\n\n5. \u00bfC\u00f3mo se implementa la paginaci\u00f3n?\n- Consumo de datos paginados\n- Integraci\u00f3n en tablas\n\n\n6. \u00bfC\u00f3mo se implementan los filtros en los listados?\n- Especialmente en el cat\u00e1logo de juegos\n- C\u00f3mo se env\u00edan los filtros desde Angular\n\n7. \u00bfC\u00f3mo se cargan datos en combos (selects) en frontend?\n- Servicios Angular utilizados\n- C\u00f3mo se obtienen los datos\n- Flujo de carga en componentes\n\n\n8. \u00bfQu\u00e9 patrones o estructuras comunes se repiten en los CRUD existentes?\n- Clases reutilizables\n- L\u00f3gica repetida\n- Estructuras comunes entre dominios\n\n\nAnaliza \u00fanicamente la parte de frontend (Angular)\nNO propongas soluciones.\nNO dise\u00f1es nuevas funcionalidades.\nSolo analiza el sistema actual.\nYa tienes un contexto previo en el fichero frontend-explore.md en el directorio de las specs, utilizalo y lo actualizas con lo que analices y no est\u00e9.\n

    Si te fijas en este explore hemos a\u00f1adido tanto la paginaci\u00f3n como los filtros. Al finalizar deber\u00eda actualizar el fichero explore de frontend y adem\u00e1s ofrecernos un resumen.

    "},{"location":"specs/loans_free/#propose_1","title":"Propose","text":"

    Una vez analizado el sistema en la fase Explore, el siguiente paso es definir de forma clara y estructurada la nueva funcionalidad a implementar.

    \ud83d\udcdc Prompt

    De nuevo en el chat de Visual Studio Code escribimos el siguiente prompt:

    /opsx:propose manage-loans-frontend\n\nDefine la funcionalidad de gesti\u00f3n de pr\u00e9stamos de juegos bas\u00e1ndote en el sistema actual y en los patrones identificados en la fase Explore, tienes el resultado en el fichero \"frontend-explore.md\". Adem\u00e1s tendr\u00e1s que ver el cambio realizado en la spec de \"manage-loans-backend\", sobre todo los endpoints generados. Por si acaso tambi\u00e9n deber\u00edas tener en cuenta el fichero de \"backend-explore.md\".\n\n\nNos han pedido esta nueva funcionalidad.\n\nSe quiere hacer uso de su cat\u00e1logo de juegos y de sus clientes, y quiere saber que juegos ha prestado a cada cliente. Para ello nos ha pedido una p\u00e1gina bastante compleja donde se podr\u00e1 consultar diferente informaci\u00f3n y se permitir\u00e1 realizar el pr\u00e9stamo de los juegos.\n\nNos ha pasado el siguiente boceto y requisitos.\n\nLa pantalla tendr\u00e1 dos zonas:\n\n- Una zona de filtrado donde se permitir\u00e1 filtrar por:\n    - T\u00edtulo del juego, que deber\u00e1 ser un combo seleccionable con los juegos del cat\u00e1logo de la Ludoteca.\n    - Cliente, que deber\u00e1 ser un combo seleccionable con los clientes dados de alta en la aplicaci\u00f3n.\n    - Fecha, que deber\u00e1 ser de tipo Datepicker y que permitir\u00e1 elegir una fecha de b\u00fasqueda. Al elegir un d\u00eda nos deber\u00e1 mostrar que juegos est\u00e1n prestados para dicho d\u00eda. OJO que los pr\u00e9stamos son con fecha de inicio y de fin, si elijo un d\u00eda intermedio deber\u00eda aparecer el elemento en la tabla.\n- Una zona de listado paginado que deber\u00e1 mostrar\n    - El identificador del pr\u00e9stamo\n    - El nombre del juego prestado\n    - El nombre del cliente que lo solicit\u00f3\n    - La fecha de inicio del pr\u00e9stamo\n    - La fecha de fin del pr\u00e9stamo\n    - Un bot\u00f3n que permite eliminar el pr\u00e9stamo\n\n\nAl pulsar el bot\u00f3n de Nuevo pr\u00e9stamo se abrir\u00e1 una pantalla donde se podr\u00e1 ingresar la siguiente informaci\u00f3n, toda ella obligatoria:\n- Identificador, inicialmente vac\u00edo y en modo lectura\n- Nombre del cliente, mediante un combo seleccionable\n- Nombre del juego, mediante un combo seleccionable\n- Fechas del pr\u00e9stamo, donde se podr\u00e1 introducir dos fechas, de inicio y fin del pr\u00e9stamo.\n\nLas validaciones son sencillas aunque laboriosas:\n- La fecha de fin NO podr\u00e1 ser anterior a la fecha de inicio\n- El periodo de pr\u00e9stamo m\u00e1ximo solo podr\u00e1 ser de 14 d\u00edas. Si el usuario quiere un pr\u00e9stamo para m\u00e1s de 14 d\u00edas la aplicaci\u00f3n no debe permitirlo mostrando una alerta al intentar guardar.\n- El mismo juego no puede estar prestado a dos clientes distintos en un mismo d\u00eda. OJO que los pr\u00e9stamos tienen fecha de inicio y fecha fin, el juego no puede estar prestado a m\u00e1s de un cliente para ninguno de los d\u00edas que contemplan las fechas actuales del rango.\n- Un mismo cliente no puede tener prestados m\u00e1s de 2 juegos en un mismo d\u00eda. OJO que los pr\u00e9stamos tienen fecha de inicio y fecha fin, el cliente no puede tener m\u00e1s de dos pr\u00e9stamos para ninguno de los d\u00edas que contemplan las fechas actuales del rango.\n\nPara empezar te dar\u00e9 unos consejos:\n\n- Recuerda crear la tabla de la BBDD y sus datos\n- Intenta primero hacer el listado paginado sin filtros, en el orden que m\u00e1s te guste: frontend o backend. Recuerda que se trata de un listado paginado, as\u00ed que deber\u00e1s utilizar el objeto Page.\n- Completa el listado conectando ambas capas.\n- Ahora implementa los filtros, presta atenci\u00f3n al filtro de fecha, es el m\u00e1s complejo.\n- Para la paginaci\u00f3n filtrada solo tienes que mezclar los conceptos que hemos visto en los puntos del tutorial anteriores.\n- Si hiciste el backend en Springboot recuerda revisar Baeldung por si tienes dudas sobre las queries y recuerda que las Specifications son muy \u00fatiles, pero en este caso deber\u00e1s implementar otro tipo de operaciones, no te sirve solo con la operaci\u00f3n de igualdad :, que ya vimos en el tutorial.\n- Implementa la pantalla de alta de pr\u00e9stamo, sin ninguna validaci\u00f3n.\n- Cuando ya te funcione, intenta ir a\u00f1adiendo una a una las validaciones. Algunas de ellas pueden hacerse en frontend, mientras que otras deber\u00e1n validarse en backend\n- Os recordamos que han de poder crearse y editarse pr\u00e9stamos seg\u00fan las reglas de validaci\u00f3n indicadas anteriormente. Aplican las mismas reglas para ambas operaciones.\n- El Backend ha de validar siempre, independientemente de que el Frontend ya lo haya validado. Nunca conf\u00edes de manera exclusiva en terceras partes (Frontend o en otro Backend).\n\n\n\n\nTe voy a dar otras directrices que pienso que te pueden servir:\n- Se necesita un CRUD de prestamos\n- Debe tener una b\u00fasqueda y una paginaci\u00f3n, as\u00ed que f\u00edjate en como est\u00e1 hecho en otras pantallas\n- Todo lo que se pueda tendr\u00e1 que estar con componentes de tipo dropdown\n- Es posible que tengas que implementar alg\u00fan nuevo endpoint para rellenar los componentes dropdown, dise\u00f1a eso tambi\u00e9n para el backend.\n\n\nNecesito que definas:\n\n1. Descripci\u00f3n de la funcionalidad\n\n2. Reglas de negocio\n\n3. Dise\u00f1o frontend:\n- Componentes necesarios\n- Flujos de interacci\u00f3n (listado, abrir modal, guardar borrar)\n\n4. Uso de endpoints para llamar a backend\n\n5. Decisiones t\u00e9cnicas:\n- Qu\u00e9 patrones del sistema actual se reutilizan\n\n\nNO implementes c\u00f3digo.\nNO analices de nuevo el proyecto.\nBasa la propuesta en los patrones detectados en la fase Explore.\nHaz la propuesta \u00fanicamente de frontend.\nOlv\u00eddate de los test, en frontend no tenemos tests.\nA\u00f1ade el nuevo punto de men\u00fa en el header para que se pueda acceder.\nNo te inventes estilos, respeta los estilos de las pantallas (anchuras, alturas, colores, disposici\u00f3n de las tablas).\nUtiliza los componentes de Angular Material para todo lo que puedas, no componentes nativos del navegador.\n\nTendr\u00e1s que escribir los ficheros de proposal, design, spec y tasks en la propuesta correspondiente.\n

    Aqu\u00ed es importante destacar que debe tener en cuenta:

    • debe coger el contexto generado anteriormente
    • adem\u00e1s, le debe sumar el contexto del \u00faltimo cambio de backend con los endpoints
    • debe respetar estilos y componentes de Angular material y no inventar
    • debe revisar como se rellenan los dropdown

    De nuevo este comando genera dentro del directorio changes la propuesta correspondiente, que incluye los siguientes ficheros: proposal.md, design.md, spec.md, tasks.md. Que deberemos revisar.

    No nos cansaremos de decirlo

    Esta es la fase m\u00e1s importante, aqu\u00ed es donde debes revisar toda la propuesta y si algo no te encaja o es err\u00f3neo deber\u00edas comentarlo mediante el chat o corregirlo de forma manual en el fichero que corresponda. Es t\u00fa responsabilidad.

    Una vez estemos de acuerdo con la propuesta que nos ha hecho la IA, podemos pasar al siguiente punto.

    "},{"location":"specs/loans_free/#apply_1","title":"Apply","text":"

    Una vez validado todo, pasamos a ejecutarlo.

    \ud83d\udcdc Prompt

    Esto es tan f\u00e1cil como escribir en el chat de Visual Studio Code el siguiente prompt:

    /opsx:apply\n

    El agente empezar\u00e1 a realizar un mont\u00f3n de tareas y pedirnos permisos. Es posible que algunas de esas tareas fallen y \u00e9l mismo lo reintente de otra forma. El resultado deber\u00eda ser el c\u00f3digo generado e implementado tanto en la carpeta backend como en la carpeta frontend y un resumen de todas las tareas realizadas y checkeadas por la IA.

    "},{"location":"specs/loans_free/#verificacion-del-frontend","title":"Verificaci\u00f3n del frontend","text":"

    Un paso que no pertenece a OpenSpec pero que es altamente recomendable es probar los cambios realizados.

    Arranca el backend y el frontend y verifica:

    • La aplicaci\u00f3n levanta correctamente
    • Las nuevas funcionalidades a\u00f1adidas est\u00e1n accesibles
    • Los flujos principales definidos en spec.md funcionan como se espera

    Ojo no te fies

    Ojo no te fies de todo lo que construya la IA. Tu est\u00e1s al mando, tu debes decidir si el sistema est\u00e1 correctamente implementado o no. Es tu responsabilidad.

    Si NO est\u00e1s a gusto con la implementaci\u00f3n o se ha dejado algo por hacer, es el momento de escribirlo por el chat indic\u00e1ndole exactamente que es lo que falta. Cuanto m\u00e1s preciso y conciso seas, mejor implementar\u00e1 la IA.

    "},{"location":"specs/loans_free/#archive_1","title":"Archive","text":"

    Y llegamos a la \u00faltima etapa que nos define OpenSpec, donde se archiva el cambio y se da por finalizada la funcionalidad.

    El objetivo de esta fase es marcar la funcionalidad como completada, consolidar todos los artefactos generados durante el proceso y dejar el sistema en un estado estable, coherente y preparado para nuevas evoluciones.

    \ud83d\udcdc Prompt

    De nuevo nos vamos al chat de Visual Studio Code el siguiente prompt:

    /opsx:archive\n

    Durante el proceso de Archive, el sistema solicitar\u00e1 confirmaci\u00f3n para sincronizar los requisitos antes de archivar el cambio.

    Recuerda que al sincronizar, los requisitos definidos en spec.md pasan de ser un cambio temporal a formar parte permanente del sistema.

    Si no se sincroniza, el c\u00f3digo queda implementado, pero los requisitos no se registran en los specs principales afectando a la trazabilidad y futuras evoluciones del sistema.

    "},{"location":"specs/loans_paid/","title":"Gesti\u00f3n de pr\u00e9stamos (modelo con licencia)","text":"

    Warning

    Esta secci\u00f3n se encuentra en desarrollo \ud83d\udea7. NO se recomienda realizarla a menos que te lo hayan indicado expresamente.

    "},{"location":"specs/loans_paid/#punto-de-partida","title":"Punto de partida","text":"

    Si has llegado hasta aqu\u00ed, entiendo que ya has completado la funcionalidad de gesti\u00f3n de clientes utilizando el modelo con licencia.

    A partir de ahora vamos a dar por hecho que partimos de ese estado del sistema, donde:

    • Existe un CRUD funcional de clientes
    • La funcionalidad est\u00e1 implementada, validada y archivada
    • Los patrones de backend y frontend introducidos ya forman parte del sistema

    Una vez llegados a este punto, asumimos que el proyecto ya est\u00e1 descargado y configurado, y que hemos trabajado previamente sobre la funcionalidad de gesti\u00f3n de clientes.

    Por tanto, continuaremos utilizando los mismos proyectos y directorios, sin realizar ninguna instalaci\u00f3n ni configuraci\u00f3n adicional.

    En este tutorial seguiremos trabajando sobre:

    • server-springboot como backend
    • client-angular17 como frontend
    "},{"location":"specs/loans_paid/#requisitos-funcionales","title":"Requisitos funcionales","text":"
    • Gesti\u00f3n de pr\u00e9stamos entre clientes y juegos.
    • Listado paginado con filtros por juego, cliente y fecha.
    • Alta/edici\u00f3n en modal con campos obligatorios (salvo identificador).
    • Validaciones de fechas y restricciones de solapamiento.
    • M\u00e1ximo 14 d\u00edas por pr\u00e9stamo.
    • Un juego no puede estar prestado a dos clientes en el mismo d\u00eda.
    • Un cliente no puede tener m\u00e1s de dos pr\u00e9stamos activos en el mismo d\u00eda.
    "},{"location":"specs/loans_paid/#estrategia-del-modo-con-licencia","title":"Estrategia del modo con licencia","text":"

    Continuaremos trabajando con un modelo de pago, utilizando Claude Sonnet 4.6 y el mismo workspace que en la funcionalidad de gesti\u00f3n de clientes.

    Antes de comenzar, ten en cuenta lo siguiente:

    • Para cada nueva funcionalidad, es recomendable iniciar una nueva conversaci\u00f3n de chat dentro del mismo proyecto

    Esto ayuda a mantener el contexto limpio y a que el modelo se centre exclusivamente en la funcionalidad que vamos a abordar.

    Recuerda que en cualquier momento puedes ver el consumo mensual de tu cuenta pulsando el icono de la rana \ud83d\udc38 en la esquina inferior derecha. El contador se reinicia cada mes.

    Vamos a abordar el ejercicio como un \u00fanico bloque de trabajo, analizando y construyendo la funcionalidad de forma simult\u00e1nea en backend y frontend.

    De esta manera aprovechamos el mayor contexto del modelo de pago, permitiendo:

    1. Analizar backend y frontend al mismo tiempo
    2. Dise\u00f1ar la funcionalidad de forma coherente en ambas capas desde el inicio

    Esto nos permite mantener una visi\u00f3n global del sistema durante todo el proceso y reducir la necesidad de dividir artificialmente el trabajo en fases independientes por capa.

    Adem\u00e1s, recuerda que el comportamiento del modelo no es determinista. Si a ti te genera algo diferente a lo que ves aqu\u00ed, probablemente seguir\u00e1 siendo v\u00e1lido. No te frustres y ajusta los prompts si es necesario.

    "},{"location":"specs/loans_paid/#flujo-de-trabajo-openspec","title":"Flujo de trabajo OpenSpec","text":"

    Seguiremos el ciclo completo de OpenSpec:

    1. Explore\n2. Propose\n3. Apply\n4. Archive\n
    "},{"location":"specs/loans_paid/#backend-y-frontend","title":"Backend y frontend","text":"

    Aunque no es obligatorio, es altamente recomendable volver a ejecutar la fase de Explore. El sistema ha podido cambiar desde tu \u00faltimo cambio, alguien ha podido hacer modificaciones, etc. En tu caso no ser\u00eda necesario ya que est\u00e1s trabajando tu solo y no has cambiado nada, pero es buena pr\u00e1ctica hacerlo siempre.

    "},{"location":"specs/loans_paid/#explore","title":"Explore","text":"

    El objetivo de esta fase es analizar el sistema existente, sin modificar nada.

    A diferencia de la gesti\u00f3n de clientes, este caso de uso introduce una mayor complejidad, principalmente por:

    • Relaciones entre entidades (cliente, juego, pr\u00e9stamo)
    • Uso de paginaci\u00f3n en los listados
    • Aplicaci\u00f3n de filtros combinados
    • Necesidad de validaciones de negocio m\u00e1s complejas

    En esta fase se analizar\u00e1 qu\u00e9 partes del sistema actual ya resuelven este tipo de problemas y pueden reutilizarse, y qu\u00e9 aspectos no est\u00e1n implementados y deber\u00e1n abordarse en fases posteriores.

    Aspectos a revisar:

    Paginaci\u00f3n

    • C\u00f3mo se implementa en backend (uso de Page)
    • C\u00f3mo se consume en frontend
    • C\u00f3mo se integra en tablas

    Filtros

    • C\u00f3mo se implementa en el cat\u00e1logo de juegos
    • C\u00f3mo se implementan filtros por rangos de fechas (si existen)
    • DTOs de filtro utilizados
    • Construcci\u00f3n de queries en backend
    • C\u00f3mo se construyen queries con condiciones combinadas y operadores distintos de igualdad
    • C\u00f3mo se env\u00edan los filtros desde Angular

    Relaciones entre entidades

    • C\u00f3mo se modelan relaciones en JPA
    • Ejemplos existentes en el proyecto
    • C\u00f3mo se representan en DTOs
    • C\u00f3mo se cargan y exponen los datos relacionados

    Validaciones en backend

    • D\u00f3nde se implementan (Service)
    • C\u00f3mo se gestionan errores
    • C\u00f3mo se propagan al frontend
    • C\u00f3mo se implementan validaciones sobre rangos de fechas
    • C\u00f3mo se validan restricciones que dependen de registros existentes (solapamientos, l\u00edmites por cliente, etc.)

    Combos (selects) en frontend - C\u00f3mo se cargan datos (clientes, juegos) - Uso de servicios Angular - Flujo de carga en componentes

    \u26a0\ufe0f En esta fase:

    • NO se escribe c\u00f3digo
    • NO se dise\u00f1a la soluci\u00f3n
    • NO se inventan estructuras nuevas

    Solo se analiza el sistema actual.

    \ud83d\udcdc Prompt

    Lo que haremos ser\u00e1 escribir en el chat de Visual Studio Code el comando y las instrucciones que queramos darle. Recuerda haber elegido Claude Sonnet 4.6 y estar trabajando en modo Agent.

    En este caso, hemos a\u00f1adido las carpetas del proyecto frontend y backend al contexto, por lo que el an\u00e1lisis se realizar\u00e1 sobre el sistema completo.

    Para ello, desde el propio Chat de Copilot, pulsando el bot\u00f3n \u201c+\u201d, puedes seleccionar y a\u00f1adir tanto archivos individuales como directorios completos del proyecto. Tambi\u00e9n es posible a\u00f1adirlos arrastr\u00e1ndolos directamente al chat.

    /opsx:explore\n\nAnaliza el proyecto actual (Angular 17 + Spring Boot) centr\u00e1ndote en las funcionalidades necesarias para implementar la gesti\u00f3n de pr\u00e9stamos y responde:\n\n1. \u00bfC\u00f3mo se implementa la paginaci\u00f3n?\n- Backend: uso de Page y construcci\u00f3n de respuestas paginadas\n- Frontend: consumo de datos paginados\n- Integraci\u00f3n en tablas\n\n2. \u00bfC\u00f3mo se implementan los filtros en los listados?\n- Especialmente en el cat\u00e1logo de juegos\n- DTOs de filtro utilizados\n- Construcci\u00f3n de queries en backend\n- Uso de condiciones combinadas (no solo igualdad)\n- Ejemplos de filtros por rango de fechas (si existen)\n- Ejemplos de consultas donde una fecha debe estar contenida dentro de un rango (si existen)\n- C\u00f3mo se env\u00edan los filtros desde Angular\n\n3. \u00bfC\u00f3mo se gestionan relaciones entre entidades?\n- Modelado en JPA\n- Ejemplos en el proyecto\n- C\u00f3mo se representan en DTOs\n- C\u00f3mo se exponen los datos relacionados\n\n4. \u00bfC\u00f3mo se implementan validaciones en backend?\n- D\u00f3nde se ubican (Service)\n- C\u00f3mo se gestionan errores\n- C\u00f3mo se propagan al frontend\n- Si existen validaciones que dependan de m\u00faltiples registros o condiciones\n- Si existen validaciones relacionadas con fechas o rangos\n- C\u00f3mo se validan restricciones basadas en datos existentes\n\n5. \u00bfC\u00f3mo se cargan datos en combos (selects) en frontend?\n- Servicios Angular utilizados\n- C\u00f3mo se obtienen los datos\n- Flujo de carga en componentes\n\nNO propongas soluciones.\nNO dise\u00f1es la funcionalidad de pr\u00e9stamos.\nNO repitas el an\u00e1lisis b\u00e1sico del sistema.\nNO incluyas c\u00f3digo completo. Resume la l\u00f3gica cuando sea necesario.\n\nC\u00e9ntrate \u00fanicamente en los aspectos necesarios para implementar la funcionalidad de gesti\u00f3n de pr\u00e9stamos.\n

    Este comando realizar\u00e1 un an\u00e1lisis exhaustivo de tu sistema que servir\u00e1 como base para definir la nueva funcionalidad en la siguiente fase.

    Sobre los permisos

    Es posible que durante el an\u00e1lisis te pida permiso para hacer ciertas tareas. Le puedes ir dando permiso una a una o darle permiso en todo el workspace, eso lo dejamos a tu elecci\u00f3n.

    En cualquier momento puedes ver el consumo de la ventana de contexto para saber si todo el conocimiento del sistema est\u00e1 en memoria o no. En el icono de la gr\u00e1fica circular que est\u00e1 situada en la parte inferior derecha del chat.

    "},{"location":"specs/loans_paid/#propose","title":"Propose","text":"

    Una vez analizado el sistema en la fase Explore, el siguiente paso es definir de forma clara y estructurada la nueva funcionalidad a implementar.

    En esta fase establecemos qu\u00e9 vamos a construir, apoy\u00e1ndonos en el conocimiento ya consolidado del sistema y en el resultado del Explore.

    Esta fase act\u00faa como puente entre el an\u00e1lisis y la implementaci\u00f3n, permitiendo dise\u00f1ar la soluci\u00f3n antes de escribir c\u00f3digo y reduciendo el riesgo de errores durante el desarrollo.

    Durante esta fase debes especificar:

    Descripci\u00f3n funcional

    • Qu\u00e9 hace la funcionalidad
    • Qu\u00e9 problema resuelve

    Reglas de negocio

    • Validaciones sobre fechas:
      • La fecha de fin no podr\u00e1 ser anterior a la fecha de inicio
    • Restricciones de duraci\u00f3n del pr\u00e9stamo:
      • El per\u00edodo de pr\u00e9stamo m\u00e1ximo solo podr\u00e1 ser de 14 d\u00edas
    • Validaciones de solapamiento de pr\u00e9stamos:
      • El mismo juego no puede estar prestado a m\u00e1s de un cliente para ninguno de los d\u00edas incluidos en el rango del pr\u00e9stamo
    • L\u00edmites de pr\u00e9stamos simult\u00e1neos por cliente:
      • Un mismo cliente no puede tener m\u00e1s de 2 pr\u00e9stamos activos para ninguno de los d\u00edas incluidos en el rango del pr\u00e9stamo

    Dise\u00f1o backend

    • Endpoints necesarios
    • Estructura del dominio (Entity, DTO, Service, Repository)
    • Tipo de operaciones (listado, creaci\u00f3n, edici\u00f3n, borrado)
    • Estrategia para filtros por fecha dentro de rangos

    Dise\u00f1o frontend

    • Componentes necesarios
    • Flujo de usuario (listado, abrir modal, guardar, borrar)
    • Servicios Angular
    • Gesti\u00f3n de combos (clientes y juegos)
    • Integraci\u00f3n de filtros y paginaci\u00f3n
    • Integraci\u00f3n de Datepicker para filtro por fecha
    • Estructura de pantalla:
      • Listado, seguir\u00e1 la estructura general de las pantallas ya existentes, reutilizando:
        • Patr\u00f3n de filtros de cat\u00e1logo. Para este caso, se permitir\u00e1 filtrar por:
          • T\u00edtulo del juego (combo)
          • Cliente (combo)
          • Fecha (Datepicker): la fecha seleccionada debe estar contenida entre la fecha de inicio y la fecha de fin del pr\u00e9stamo
        • Patr\u00f3n de paginaci\u00f3n del listado de autores
        • El orden de las columnas del listado ser\u00e1:
          • Identificador
          • Nombre del juego
          • Nombre del cliente
          • Fecha de pr\u00e9stamo
          • Fecha de devoluci\u00f3n
        • Las fechas se mostrar\u00e1n siempre en formato DD/MM/YYYY
      • Alta/edici\u00f3n:
        • El identificador aparecer\u00e1 vac\u00edo en creaci\u00f3n y en modo solo lectura
        • Debajo se mostrar\u00e1 el campo de nombre de cliente (combo seleccionable)
        • Debajo se mostrar\u00e1 el campo de nombre de juego (combo seleccionable)
        • Debajo se mostrar\u00e1 la secci\u00f3n de fechas de pr\u00e9stamo: la fecha de inicio y la fecha de fin estar\u00e1n en la misma fila
        • Todos los campos, salvo el identificador, ser\u00e1n obligatorios

    Decisiones t\u00e9cnicas

    • Qu\u00e9 patrones existentes se reutilizan
    • Qu\u00e9 partes deben extenderse
    • C\u00f3mo se gestionar\u00e1n los filtros de fecha y condiciones combinadas
    • C\u00f3mo se implementar\u00e1n validaciones basadas en m\u00faltiples registros (solapamientos y l\u00edmites)

    Plan de implementaci\u00f3n

    • Tareas ordenadas
    • Separaci\u00f3n backend / frontend
    • Prioridad de desarrollo (listado \u2192 filtros \u2192 validaciones)

    Aqu\u00ed dejamos claro:

    • Qu\u00e9 funcionalidad se va a a\u00f1adir
    • Qu\u00e9 reglas de negocio existen
    • Qu\u00e9 piezas del sistema se ven afectadas
    • Qu\u00e9 tareas habr\u00e1 que ejecutar

    \u26a0\ufe0f En esta fase:

    • NO se implementa c\u00f3digo
    • NO se redefine el sistema

    \ud83d\udcdc Prompt

    Recuerda que seguimos trabajando en modo Agent, con las carpetas del proyecto frontend y backend a\u00f1adidas al contexto.

    Para nuestro ejemplo, lo que haremos ser\u00e1 escribir en el chat de Visual Studio Code el siguiente prompt:

    /opsx:propose loan\n\nDefine la funcionalidad de gesti\u00f3n de pr\u00e9stamos bas\u00e1ndote en el sistema actual (Angular 17 + Spring Boot), en los patrones identificados en la fase Explore y en los requisitos funcionales indicados.\n\nRequisitos funcionales:\n- Se necesita una funcionalidad de gesti\u00f3n de pr\u00e9stamos\n\n- Un pr\u00e9stamo relaciona un cliente y un juego\n\n- El listado ser\u00e1 paginado\n\n- Existir\u00e1 una zona de filtros en la parte superior del listado\n\n- Se podr\u00e1 filtrar por:\n  - Juego (combo seleccionable)\n  - Cliente (combo seleccionable)\n  - Fecha (Datepicker)\n\n- La fecha seleccionada deber\u00e1 estar contenida entre la fecha de inicio y la fecha de fin del pr\u00e9stamo para que el registro aparezca en el listado\n\n- El listado deber\u00e1 mostrar:\n  - Identificador\n  - Nombre del juego\n  - Nombre del cliente\n  - Fecha de pr\u00e9stamo\n  - Fecha de devoluci\u00f3n\n\n- Las fechas se mostrar\u00e1n en formato DD/MM/YYYY\n\n- Existir\u00e1 una pantalla modal de alta / edici\u00f3n\n\n- En alta / edici\u00f3n:\n  - El identificador aparecer\u00e1 vac\u00edo en creaci\u00f3n y en modo solo lectura\n  - Se seleccionar\u00e1 cliente mediante combo\n  - Se seleccionar\u00e1 juego mediante combo\n  - Se introducir\u00e1n fecha de inicio y fecha de fin en la misma fila\n  - Todos los campos, salvo el identificador, ser\u00e1n obligatorios\n\nReglas de negocio:\n- La fecha de fin no podr\u00e1 ser anterior a la fecha de inicio\n- El per\u00edodo m\u00e1ximo del pr\u00e9stamo ser\u00e1 de 14 d\u00edas\n- El mismo juego no podr\u00e1 estar prestado a m\u00e1s de un cliente para ninguno de los d\u00edas incluidos en el rango del pr\u00e9stamo\n- Un mismo cliente no podr\u00e1 tener m\u00e1s de 2 pr\u00e9stamos activos para ninguno de los d\u00edas incluidos en el rango del pr\u00e9stamo\n- Las mismas validaciones aplican tanto en creaci\u00f3n como en edici\u00f3n\n- El backend deber\u00e1 validar siempre, aunque el frontend tambi\u00e9n realice validaciones\n\nDefine:\n\n1. Descripci\u00f3n de la funcionalidad\n\n2. Reglas de negocio\n\n3. Dise\u00f1o backend:\n- Endpoints necesarios\n- Estructura del dominio (Entity, DTO, Service, Repository)\n- Tipo de operaciones (listado, creaci\u00f3n, edici\u00f3n, borrado)\n- Estrategia para filtros por fecha dentro de rangos\n\n4. Dise\u00f1o frontend:\n- Componentes necesarios\n- Flujo de interacci\u00f3n (listado, abrir modal, guardar, borrar)\n- Servicios Angular\n- Gesti\u00f3n de combos (clientes y juegos)\n- Integraci\u00f3n de filtros y paginaci\u00f3n\n- Integraci\u00f3n de Datepicker para filtro por fecha\n- Estructura funcional del listado y del formulario de alta/edici\u00f3n\n\n5. Decisiones t\u00e9cnicas:\n- Qu\u00e9 patrones del sistema actual se reutilizan\n- Qu\u00e9 partes deben extenderse\n- C\u00f3mo se gestionar\u00e1n los filtros de fecha y condiciones combinadas\n- C\u00f3mo se implementar\u00e1n validaciones basadas en m\u00faltiples registros (solapamientos y l\u00edmites)\n\nNO implementes c\u00f3digo.\nNO analices de nuevo el proyecto.\nBasa la propuesta en los patrones detectados en la fase Explore.\n

    Igual que en la gesti\u00f3n de clientes, este comando genera dentro del directorio changes la propuesta correspondiente, que incluye los siguientes ficheros: proposal.md, design.md, spec.md, tasks.md.

    Estos artefactos est\u00e1n adaptados a la funcionalidad de gesti\u00f3n de pr\u00e9stamos, incorporando las reglas de negocio, filtros y validaciones espec\u00edficas de este caso de uso.

    Constituyen la base para la siguiente fase: Apply, donde se ejecutar\u00e1 la implementaci\u00f3n siguiendo las tareas definidas.

    Responsabilidades como developer IA

    En este punto la IA te ha hecho una propuesta que puede ser correcta o no, recordemos que se trata de un modelo matem\u00e1tico-probabil\u00edstico. Si hay algo de lo propuesto que no te encaja o es err\u00f3neo deber\u00edas comentarlo mediante el chat o corregirlo de forma manual en el fichero que corresponda. Por ejemplo si quieres a\u00f1adir una tarea porqu\u00e9 se te ha olvidado incluirla en el prompt original, deber\u00edas decirle al modelo que te incluya la nueva tarea.

    Una vez estemos de acuerdo con la propuesta que nos ha hecho la IA, podemos pasar al siguiente punto.

    "},{"location":"specs/loans_paid/#apply","title":"Apply","text":"

    Una vez validada la propuesta, ejecutamos la implementaci\u00f3n:

    El objetivo de esta fase es transformar los artefactos generados (proposal.md, design.md, spec.md, tasks.md) en c\u00f3digo funcional, asegurando que:

    • Se respetan los requisitos funcionales definidos en spec.md
    • Se siguen las decisiones t\u00e9cnicas establecidas en design.md
    • Se ejecutan las tareas en el orden definido en tasks.md

    \ud83d\udcdc Prompt

    Esto es tan f\u00e1cil como escribir en el chat de Visual Studio Code el siguiente prompt:

    /opsx:apply\n

    El agente empezar\u00e1 a realizar un mont\u00f3n de tareas y pedirnos permisos. Es posible que algunas de esas tareas fallen y \u00e9l mismo lo reintente de otra forma. El resultado deber\u00eda ser el c\u00f3digo generado e implementado tanto en la carpeta backend como en la carpeta frontend y un resumen de todas las tareas realizadas y checkeadas por la IA.

    "},{"location":"specs/loans_paid/#verificacion","title":"Verificaci\u00f3n","text":"

    Un paso que no pertenece a OpenSpec pero que es altamente recomendable es probar los cambios realizados.

    Arranca el backend y el frontend y verifica:

    • La aplicaci\u00f3n levanta correctamente
    • Las nuevas funcionalidades a\u00f1adidas est\u00e1n accesibles
    • Los flujos principales definidos en spec.md funcionan como se espera

    Ojo no te fies

    Ojo no te fies de todo lo que construya la IA. Tu est\u00e1s al mando, tu debes decidir si el sistema est\u00e1 correctamente implementado o no. Es tu responsabilidad.

    Si NO est\u00e1s a gusto con la implementaci\u00f3n o se ha dejado algo por hacer, es el momento de escribirlo por el chat indic\u00e1ndole exactamente que es lo que falta. Cuanto m\u00e1s preciso y conciso seas, mejor implementar\u00e1 la IA.

    "},{"location":"specs/loans_paid/#archive","title":"Archive","text":"

    Y llegamos a la \u00faltima etapa que nos define OpenSpec, donde se archiva el cambio y se da por finalizada la funcionalidad.

    El objetivo de esta fase es marcar la funcionalidad como completada, consolidar todos los artefactos generados durante el proceso y dejar el sistema en un estado estable, coherente y preparado para nuevas evoluciones.

    \ud83d\udcdc Prompt

    De nuevo nos vamos al chat de Visual Studio Code el siguiente prompt:

    /opsx:archive\n

    Durante el proceso de Archive, el sistema solicitar\u00e1 confirmaci\u00f3n para sincronizar los requisitos antes de archivar el cambio.

    Recuerda que al sincronizar, los requisitos definidos en spec.md pasan de ser un cambio temporal a formar parte permanente del sistema.

    Si no se sincroniza, el c\u00f3digo queda implementado, pero los requisitos no se registran en los specs principales afectando a la trazabilidad y futuras evoluciones del sistema.

    "},{"location":"specs/prepare/","title":"Preparaci\u00f3n del entorno","text":"

    Warning

    Esta secci\u00f3n se encuentra en desarrollo \ud83d\udea7. NO se recomienda realizarla a menos que te lo hayan indicado expresamente.

    En esta secci\u00f3n asumimos que ya completaste el tutorial base y que el entorno de Angular y Spring Boot est\u00e1 configurado.

    Tambi\u00e9n es recomendable haber hecho el ejercicio Ahora hazlo tu! para que el contexto funcional te resulte familiar.

    Partimos, por tanto, de un entorno con las herramientas b\u00e1sicas ya instaladas.

    Daremos por hecho que ya dispones de:

    • Visual Studio Code
    • Node.js
    • Angular CLI
    • Java (17 o superior)

    Estas herramientas son prerrequisitos y aqu\u00ed no repetiremos su instalaci\u00f3n en detalle.

    Info

    Si alguna de estas herramientas no est\u00e1 instalada o necesitas revisar el proceso completo de configuraci\u00f3n, puedes consultar los siguientes apartados del tutorial:

    • Entorno de desarrollo \u2013 Angular
    • Entorno de desarrollo \u2013 Spring Boot
    "},{"location":"specs/prepare/#prerrequisitos-tecnicos","title":"Prerrequisitos t\u00e9cnicos","text":"

    Vamos a preparar el entorno para trabajar con Spec-Driven Development usando OpenSpec desde Visual Studio Code, en un \u00fanico workspace con frontend, backend y especificaciones.

    "},{"location":"specs/prepare/#verificacion-de-nodejs","title":"Verificaci\u00f3n de Node.js","text":"

    OpenSpec se distribuye como una herramienta basada en Node.js, por lo que es necesario tener instalado Node.js 20.19.0 o superior.

    Para comprobar la versi\u00f3n instalada, ejecuta en una terminal:

    node --version\n

    Si no tienes Node.js instalado o tu versi\u00f3n es inferior, puedes descargarlo desde su web oficial.

    Se recomienda instalar la versi\u00f3n LTS m\u00e1s reciente.

    Si tienes restricciones de permisos en el port\u00e1til, tambi\u00e9n es posible instalar Node.js a trav\u00e9s del Portal de Empresa, siguiendo el mismo procedimiento utilizado durante la configuraci\u00f3n del Entorno de desarrollo para el tutorial:

    1. Accede al Portal de Empresa
    2. Entra en el cat\u00e1logo de aplicaciones pre\u2011aprobadas
    3. Busca Node.js
    4. Inst\u00e1lalo desde ah\u00ed

    Una vez finalizada la instalaci\u00f3n, vuelve a ejecutar el comando node --version para verificar que Node.js est\u00e1 correctamente instalado.

    "},{"location":"specs/prepare/#instalacion-de-openspec","title":"Instalaci\u00f3n de OpenSpec","text":"

    OpenSpec se puede instalar de forma global con cualquier gestor compatible con Node.js.

    Si utilizas npm, ejecuta el siguiente comando:

    npm install -g @fission-ai/openspec@latest\n

    Info

    OpenSpec tambi\u00e9n es compatible con pnpm, yarn o bun. En esta gu\u00eda usaremos npm por simplicidad.

    Una vez finalizada la instalaci\u00f3n, verifica que OpenSpec est\u00e1 correctamente instalado ejecutando:

    openspec --version\n

    Si el comando responde correctamente mostrando la versi\u00f3n instalada, el entorno ya est\u00e1 preparado para trabajar con Spec\u2011Driven Development utilizando OpenSpec.

    "},{"location":"specs/prepare/#convenciones-de-trabajo-aplican-a-todos-los-ejercicios","title":"Convenciones de trabajo (aplican a todos los ejercicios)","text":""},{"location":"specs/prepare/#github-copilot","title":"GitHub Copilot","text":"

    Necesitas una cuenta de GitHub con Copilot (gratuita o premium) y haber iniciado sesi\u00f3n en Visual Studio Code para usar el chat.

    "},{"location":"specs/prepare/#estructura-inicial-del-proyecto","title":"Estructura inicial del proyecto","text":"

    A partir de aqu\u00ed necesitas los proyectos base (sin el ejercicio hecho). Si no los tienes, puedes descargarlos en https://github.com/ccsw-csd/tutorial-proyectos.

    En esta gu\u00eda vamos a usar server-springboot y client-angular17. Ambos deben estar en el mismo directorio ra\u00edz. Para simplificar, durante todo el documento, los llamaremos:

    • backend
    • frontend

    La estructura deber\u00eda ser similar a esta:

    "},{"location":"specs/prepare/#reglas-generales-y-de-ejecucion","title":"Reglas generales y de ejecuci\u00f3n","text":"

    Durante todos los ejercicios:

    • Empieza cada cambio relevante en un chat nuevo para no arrastrar contexto innecesario.
    • Revisa siempre la propuesta antes de ejecutar la fase de Apply.
    • Valida manualmente los resultados funcionales despu\u00e9s de aplicar.
    "},{"location":"specs/prepare/#y-ahora-que","title":"\u00bfY ahora qu\u00e9?","text":"

    A partir de aqu\u00ed eliges ruta:

    • Con licencia de pago \ud83d\udcb0 tendr\u00e1s m\u00e1s contexto y menos fragmentaci\u00f3n.
    • Con licencia gratuita \ud83c\udd93 tendr\u00e1s menos contexto y m\u00e1s iteraciones. Adem\u00e1s es posible que superes las limitaciones diarias o de hora y tengas que esperar al d\u00eda siguiente para continuar con el tutorial.

    El flujo funcional es el mismo en ambos casos.

    Elige tu camino:

    • \ud83c\udd93 Gesti\u00f3n de clientes
    • \ud83c\udd93 Gesti\u00f3n de pr\u00e9stamos
    • \ud83d\udcb0 Gesti\u00f3n de clientes
    • \ud83d\udcb0 Gesti\u00f3n de pr\u00e9stamos
    "}]} \ No newline at end of file +{"config":{"lang":["es"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Bienvenido!","text":"

    Si est\u00e1s leyendo esto es porque tienes mucha fuerza de voluntad y unas enormes ganas de aprender a desarrollar con el stack tecnol\u00f3gico de CCA (Java Spring Boot, Nodejs, Angular, React, Vue) o porque te han mandando hacer este tutorial en tu etapa de formaci\u00f3n. En cualquier caso, te agradecemos el esfuerzo que est\u00e1s haciendo y te deseamos suerte .

    Por favor, si detectas que hay algo incorrecto en el tutorial, que no funciona o que est\u00e1 mal escrito, contacta con nosotros para que podamos solventarlo para futuras lecturas. Escr\u00edbenos un issue aqu\u00ed.

    "},{"location":"#que-vamos-a-hacer","title":"\u00bfQue vamos a hacer?","text":"

    Durante este tutorial, vamos a crear una aplicaci\u00f3n web paso a paso con Spring Boot o Nodejs para la parte servidora y con Angular o React para la parte frontal. Intentar\u00e9 comentar todo lo m\u00e1s detallado posible, pero si echas en falta alguna explicaci\u00f3n por favor, escr\u00edbenos un issue aqu\u00ed para que podamos a\u00f1adirla.

    "},{"location":"#como-lo-vamos-a-hacer","title":"\u00bfComo lo vamos a hacer?","text":"

    En primer lugar te comentar\u00e9 brevemente las herramientas que usaremos en el tutorial y la forma de instalarlas (altamente recomendado). Luego veremos un vistazo general de lo que vamos a construir para que tengas un contexto general de la aplicaci\u00f3n. Y por \u00faltimo desarrollaremos paso a paso el backend y el frontend de la aplicaci\u00f3n.

    Durante todo el tutorial intentar\u00e9 dar unas pautas y consejos de buenas pr\u00e1cticas que todos deber\u00edamos adoptar, en la medida de lo posible, para homogeneizar el desarrollo de todos los proyectos.

    Adem\u00e1s para cada uno de los cap\u00edtulos que lo requieran, voy a desdoblar el tutorial por cada una de las tecnolog\u00edas disponibles para que vayas construyendo con la que m\u00e1s c\u00f3modo te sientas.

    As\u00ed que antes de empezar debes elegir bien con que tecnolog\u00edas vas a comenzar de las que tengo disponibles. Puedes volver a este tutorial m\u00e1s adelante por si he ido a\u00f1adiendo nuevas tecnolog\u00edas.

    Elige UNA tecnolog\u00eda de backend y UNA tecnolog\u00eda de frontend y completa el tutorial con esas dos tecnolog\u00edas. No mezcles ni hagas todas las tecnolog\u00edas a la vez ya que si no, te vas a volver loco.

    "},{"location":"#hay-pre-requisitos","title":"\u00bfHay pre-requisitos?","text":"

    No es obligado tener ning\u00fan conocimiento previo, pero es altamente recomendable que al menos conozcas lo b\u00e1sico de las tecnolog\u00edas que vamos a ver en el tutorial. Si no tienes ni idea, ni has oido hablar de las tecnolog\u00edas que has seleccionado para el tutorial, te sugiero que visites los itinerarios formativos y realices los cursos de nivel Esencial. De momento tenemos estos itinerarios:

    • \ud83d\udd35 Frontend - Angular
    • \ud83d\udd35 Frontend - React
    • \ud83d\udd35 Frontend - Vue
    • \ud83d\udfe2 Backend - SpringBoot
    • \ud83d\udfe2 Backend - Nodejs

    Una vez hayas hecho los cursos esenciales, ya puedes volver y continuar con este tutorial. Repito que no es obligado, si ya tienes conocimientos previos de las tecnolog\u00edas no es necesario que hagas los cursos. Cuando termines el tutorial, ya puedes realizar el resto de cursos de otros niveles.

    "},{"location":"#y-luego-que","title":"\u00bfY luego qu\u00e9?","text":"

    Pues al final del tutorial, expondremos unos ejercicios pr\u00e1cticos para que los resuelvas tu mismo, aplicando los conocimientos adquiridos en el tutorial. Para ver si has comprendido correctamente todo lo aqu\u00ed descrito.

    No te preocupes, no es un examen

    "},{"location":"#recomendaciones","title":"Recomendaciones","text":"

    Te recomiendo que leas todo el tutorial, que no te saltes ning\u00fan punto y si se hace referencia a los anexos, que los visites y los leas tambi\u00e9n. Si tan solo copias y pegas, no ser\u00e1s capaz de hacer el \u00faltimo ejercicio por ti mismo. Debes leer y comprender lo que se est\u00e1 haciendo.

    Adem\u00e1s, los anexos est\u00e1n ah\u00ed por algo, sirven para completar informaci\u00f3n y para que conozcas los motivos por los que estamos programando as\u00ed el tutorial. Por favor, \u00e9chales un ojo tambi\u00e9n cuando te lo indique.

    "},{"location":"#por-ultimo-no-te-olvides","title":"Por \u00faltimo, \u00a1no te olvides!","text":"

    Cuando lo tengas todo listo, por favor no te olvides de subir los proyectos a alg\u00fan repositorio Github propio y av\u00edsanos para que podamos echarle un ojo y darte sugerencias y feedback .

    "},{"location":"exercise/","title":"Ahora hazlo tu!","text":"

    Ahora vamos a ver si has comprendido bien el tutorial. Voy a poner dos ejercicios uno m\u00e1s sencillo que el otro para ver si eres capaz de llevarlos a cabo. \u00a1Vamos alla, mucha suerte!

    Nuestro amigo Ernesto Esvida ya tiene disponible su web para gestionar su cat\u00e1logo de juegos, autores y categor\u00edas, pero todav\u00eda le falta un poco m\u00e1s para poder hacer buen uso de su ludoteca. As\u00ed que nos ha pedido dos funcionalidades extra.

    Ten en cuenta

    Solo te pedimos que realices las funcionalidades que se indican en los requisitos. No es necesario que completes los anexos antes de que revisemos los resultados.

    "},{"location":"exercise/#gestion-de-clientes","title":"Gesti\u00f3n de clientes","text":""},{"location":"exercise/#requisitos","title":"Requisitos","text":"

    Por un lado necesita poder tener una base de datos de sus clientes. Para ello nos ha pedido que si podemos crearle una pantalla de CRUD sencilla, al igual que hicimos con las categor\u00edas donde \u00e9l pueda dar de alta a sus clientes.

    Nos ha pasado un esquema muy sencillo de lo que quiere, tan solo quiere guardar un listado de los nombres de sus clientes para tenerlos fichados, y nos ha hecho un par de pantallas sencillas muy similares a Categor\u00edas.

    Un listado sin filtros de ning\u00fan tipo ni paginaci\u00f3n.

    Un formulario de edici\u00f3n / alta, cuyo \u00fanico dato editable sea el nombre. Adem\u00e1s, la \u00fanica restricci\u00f3n que nos ha pedido es que NO podamos dar de alta a un cliente con el mismo nombre que otro existente. As\u00ed que deberemos comprobar el nombre, antes de guardar el cliente.

    "},{"location":"exercise/#consejos","title":"Consejos","text":"

    Para empezar te dar\u00e9 unos consejos:

    • Recuerda crear la tabla de la BBDD y sus datos
    • Intenta primero hacer el listado completo, en el orden que m\u00e1s te guste: frontend o backend.
    • Completa el listado conectando ambas capas.
    • Termina el caso de uso haciendo las funcionalidades de edici\u00f3n, nuevo y borrado. Presta atenci\u00f3n a la validaci\u00f3n a la hora de guardar un cliente, NO se puede guardar si el nombre ya existe.
    "},{"location":"exercise/#gestion-de-prestamos","title":"Gesti\u00f3n de pr\u00e9stamos","text":""},{"location":"exercise/#requisitos_1","title":"Requisitos","text":"

    Por otro lado, quiere hacer uso de su cat\u00e1logo de juegos y de sus clientes, y quiere saber que juegos ha prestado a cada cliente. Para ello nos ha pedido una p\u00e1gina bastante compleja donde se podr\u00e1 consultar diferente informaci\u00f3n y se permitir\u00e1 realizar el pr\u00e9stamo de los juegos.

    Nos ha pasado el siguiente boceto y requisitos:

    Atenci\u00f3n

    Aunque no aparezca en el boceto, como en todas las listas que hemos visto, esperamos que puedan editarse los registros. El bot\u00f3n de \"Filtrar\" tendr\u00e1 el mismo aspecto visual que el bot\u00f3n de \"Nuevo pr\u00e9stamo\".

    La pantalla tendr\u00e1 dos zonas:

    • Una zona de filtrado donde se permitir\u00e1 filtrar por:
      • T\u00edtulo del juego, que deber\u00e1 ser un combo seleccionable con los juegos del cat\u00e1logo de la Ludoteca.
      • Cliente, que deber\u00e1 ser un combo seleccionable con los clientes dados de alta en la aplicaci\u00f3n.
      • Fecha, que deber\u00e1 ser de tipo Datepicker y que permitir\u00e1 elegir una fecha de b\u00fasqueda. Al elegir un d\u00eda nos deber\u00e1 mostrar que juegos est\u00e1n prestados para dicho d\u00eda. OJO que los pr\u00e9stamos son con fecha de inicio y de fin, si elijo un d\u00eda intermedio deber\u00eda aparecer el elemento en la tabla.
    • Una zona de listado paginado que deber\u00e1 mostrar
      • El identificador del pr\u00e9stamo
      • El nombre del juego prestado
      • El nombre del cliente que lo solicit\u00f3
      • La fecha de inicio del pr\u00e9stamo
      • La fecha de fin del pr\u00e9stamo
      • Un bot\u00f3n que permite eliminar el pr\u00e9stamo

    Al pulsar el bot\u00f3n de Nuevo pr\u00e9stamo se abrir\u00e1 una pantalla donde se podr\u00e1 ingresar la siguiente informaci\u00f3n, toda ella obligatoria:

    • Identificador, inicialmente vac\u00edo y en modo lectura
    • Nombre del cliente, mediante un combo seleccionable
    • Nombre del juego, mediante un combo seleccionable
    • Fechas del pr\u00e9stamo, donde se podr\u00e1 introducir dos fechas, de inicio y fin del pr\u00e9stamo.

    Las validaciones son sencillas aunque laboriosas:

    • La fecha de fin NO podr\u00e1 ser anterior a la fecha de inicio
    • El periodo de pr\u00e9stamo m\u00e1ximo solo podr\u00e1 ser de 14 d\u00edas. Si el usuario quiere un pr\u00e9stamo para m\u00e1s de 14 d\u00edas la aplicaci\u00f3n no debe permitirlo mostrando una alerta al intentar guardar.
    • El mismo juego no puede estar prestado a dos clientes distintos en un mismo d\u00eda. OJO que los pr\u00e9stamos tienen fecha de inicio y fecha fin, el juego no puede estar prestado a m\u00e1s de un cliente para ninguno de los d\u00edas que contemplan las fechas actuales del rango.
    • Un mismo cliente no puede tener prestados m\u00e1s de 2 juegos en un mismo d\u00eda. OJO que los pr\u00e9stamos tienen fecha de inicio y fecha fin, el cliente no puede tener m\u00e1s de dos pr\u00e9stamos para ninguno de los d\u00edas que contemplan las fechas actuales del rango.
    "},{"location":"exercise/#consejos_1","title":"Consejos","text":"

    Para empezar te dar\u00e9 unos consejos:

    • Recuerda crear la tabla de la BBDD y sus datos
    • Intenta primero hacer el listado paginado sin filtros, en el orden que m\u00e1s te guste: frontend o backend. Recuerda que se trata de un listado paginado, as\u00ed que deber\u00e1s utilizar el obtejo Page.
    • Completa el listado conectando ambas capas.
    • Ahora implementa los filtros, presta atenci\u00f3n al filtro de fecha, es el m\u00e1s complejo.
    • Para la paginaci\u00f3n filtrada solo tienes que mezclar los conceptos que hemos visto en los puntos del tutorial anteriores.
      • Si hiciste el backend en Springboot recuerda revisar Baeldung por si tienes dudas sobre las queries y recuerda que las Specifications son muy \u00fatiles, pero en este caso deber\u00e1s implementar otro tipo de operaciones, no te sirve solo con la operaci\u00f3n de igualdad :, que ya vimos en el tutorial.
    • Implementa la pantalla de alta de pr\u00e9stamo, sin ninguna validaci\u00f3n.
    • Cuando ya te funcione, intenta ir a\u00f1adiendo una a una las validaciones. Algunas de ellas pueden hacerse en frontend, mientras que otras deber\u00e1n validarse en backend
    • Os recordamos que han de poder crearse y editarse pr\u00e9stamos seg\u00fan las reglas de validaci\u00f3n indicadas anteriormente. Aplican las mismas reglas para ambas operaciones.
    • El Backend ha de validar siempre, independientemente de que el Frontend ya lo haya validado. Nunca conf\u00edes de manera exclusiva en terceras partes (Frontend o en otro Backend).
    "},{"location":"exercise/#ya-has-terminado","title":"\u00bfYa has terminado?","text":"

    Si has llegado a este punto es porque ya tienes terminado el tutorial. Por favor no te olvides de subir los proyectos a alg\u00fan repositorio Github propio (puedes revisar el anexo Tutorial b\u00e1sico de Git) y av\u00edsarnos para que podamos echarle un ojo y darte sugerencias y feedback .

    "},{"location":"thanks/","title":"Agradecimientos!","text":"

    Antes de empezar quer\u00edamos dar las gracias a todos los que hab\u00e9is participado de manera directa o indirecta en la elaboraci\u00f3n de este tutorial, y a todos aquellos que lo hab\u00e9is sufrido haciendolo.

    De verdad

                                    G R A C I A S\n
    "},{"location":"thanks/#colaboradores","title":"Colaboradores","text":"

    Menci\u00f3n especial a las personas que han participado en el tutorial ya sea como testers, como promotores o como desarrolladores, por orden temporal de colaboraci\u00f3n:

    • Felipe Garcia (@fgarciafer)
    • Armen Mirzoyan (@armirzoya)
    • Carlos Aguilar (@caaguila)
    • Jhonatan Core (@corevill)
    • Carlos Navarro (@DarkWarlord)
    • Cesar Cardona (@Cazs03)
    • Marina Valls (@mvalemany)
    • Jaume Segarra (@jaumesegarra)
    • Laura Medina (@larulirea)
    • Yolanda Ubeda
    • Pablo Jimenez (@pajimene)
    "},{"location":"usecases/","title":"Contexto de la aplicaci\u00f3n","text":""},{"location":"usecases/#introduccion","title":"Introducci\u00f3n","text":"

    Nuestro amigo Ernesto Esvida es muy aficionado a los juegos de mesa y desde muy peque\u00f1o ha ido coleccionando muchos juegos. Hasta tal punto que ha decidido regentar una Ludoteca.

    Como la colecci\u00f3n de juegos era suya personal, toda la informaci\u00f3n del cat\u00e1logo de juegos la ten\u00eda perfectamente clasificado en fichas de cart\u00f3n. Pero ahora que va abrir su propio negocio, necesita digitalizar esa informaci\u00f3n y hacerla m\u00e1s accesible.

    Como es un buen amigo de la infancia, hemos decidido ayudar a Ernesto y colaborar haciendo una peque\u00f1a aplicaci\u00f3n web que le sirva de cat\u00e1logo de juegos. Es m\u00e1s o menos el mismo sistema que estaba utilizando, pero esta vez en digital.

    Por cierto, la Ludoteca al final se va a llamar Ludoteca T\u00e1n.

    Info

    Las im\u00e1genes que aparecen a continuaci\u00f3n son mockups o dise\u00f1os de alambre de las pantallas que vamos a desarrollar durante el tutorial. No quiere decir que el estilo final de las pantallas deba ser as\u00ed, ni mucho menos. Es simplemente una forma sencilla de ejemplificar como debe quedar m\u00e1s o menos una pantalla.

    "},{"location":"usecases/#estructura-de-un-proyecto-web","title":"Estructura de un proyecto Web","text":"

    En todas las aplicaciones web modernas y los proyectos en los que trabajamos se pueden diferenciar, de forma general, tres grandes bloques funcionales, como se muestra en la imagen inferior.

    El funcionamiento es muy sencillo y difiere de las aplicaciones instalables que se ejecuta todo en una misma m\u00e1quina o servidor.

    • Con esta estructura, el usuario accede a la aplicaci\u00f3n mediante un navegador web instalado en su m\u00e1quina local.
    • Este navegador solicita informaci\u00f3n mediante una URL a un servidor de recursos est\u00e1ticos. Esto es lo que denominaremos un servidor frontend. Para programar servidores frontend se pueden usar muchas tecnolog\u00edas, en este tutorial lo desarrollaremos en Angular o en React. Este c\u00f3digo frontend se descarga y se ejecuta dentro del navegador, y contiene la representaci\u00f3n visual de las pantallas y ciertos comportamientos y navegaci\u00f3n entre componentes. Sin embargo, por lo general, no tiene datos ni ejecuta l\u00f3gica de negocio.
    • Para estas labores de obtener datos o ejecutar l\u00f3gica de negocio, el c\u00f3digo frontend necesita invocar endpoints de la segunda capa, el backend. Al igual que antes, el backend, puede estar desarrollado en muchas tecnolog\u00edas, en este tutorial se puede elegir entre Java-Springboot o Nodejs. Lo importante de esta capa es que es necesario exponer unos endpoints que sean invocados por la capa de frontend. T\u00edpicamente estos endpoints son operaciones API Rest que veremos m\u00e1s adelante.
    • Por \u00faltimo, el servidor backend / capa backend, necesitar\u00e1 leer y guardar datos de alg\u00fan sitio. Esto se hace utilizando la tercera capa, la capa de datos. Normalmente esta capa de datos ser\u00e1 una BBDD instalada en alg\u00fan servidor externo, aunque a veces como es el caso del tutorial de Springboot, podemos embeber el servidor en memoria de backend. Pero por norma general, esta capa es externa.

    As\u00ed pues el flujo normal de una aplicaci\u00f3n ser\u00eda el siguiente:

    • El usuario abre el navegador y solicita una web mediante una URL
    • El servidor frontend, le sirve los recursos (p\u00e1ginas web, javascript, im\u00e1genes, ...) y se cargan en el navegador
    • El navegador renderiza las p\u00e1ginas web, ejecuta los procesos javascript y realiza las navegaciones
    • Si en alg\u00fan momento se requiere invocar una operaci\u00f3n, el navegador lanzar\u00e1 una petici\u00f3n contra una URL del backend
    • El backend estar\u00e1 escuchando las peticiones y las ejecutar\u00e1 en el momento que le invoquen devulviendo un resultado al navegador
    • Si hiciera falta leer o guardar datos, el backend lo realizar\u00e1 lanzando consultas SQL contra la BBDD

    Dicho esto, por lo general necesitaremos un m\u00ednimo de dos proyectos para desarrollar una aplicaci\u00f3n:

    • Por un lado tendremos un proyecto Frontend que se ejecutar\u00e1 en un servidor web de ficheros est\u00e1ticos, tipo Apache. Este proyecto ser\u00e1 c\u00f3digo javascript, css y html, que se renderizar\u00e1 en el navegador Web y que realizar\u00e1 ciertas operaciones sencillas y validaciones en local y llamadas a nuestro servidor backend para ejecutar las operaciones de negocio.

    • Por otro lado tendremos un proyecto Backend que se ejecutar\u00e1 en un servidor de aplicaciones, tipo Tomcat o Node. Este proyecto tendr\u00e1 la l\u00f3gica de negocio de las operaciones, el acceso a los datos de la BBDD y cualquier integraci\u00f3n con servicios de terceros. La forma de exponer estas operaciones de negocio ser\u00e1 mediante endpoints de acceso, en concreto llamadas tipo REST.

    Pueden haber otros tipos de proyectos dentro de la aplicaci\u00f3n, sobretodo si est\u00e1n basados en microservicios o tienen componentes batch, pero estos proyectos no vamos a verlos en el tutorial.

    A partir de ahora, para que sea m\u00e1s sencillo acceder al tutorial, diferenciaremos las tecnolog\u00edas en el men\u00fa mediante los siguientes colores:

    • \ud83d\udd35 Frontend
    • \ud83d\udfe2 Backend

    Consejo

    Como norma cada uno de los proyectos que componen la aplicaci\u00f3n, deber\u00eda estar conectado a un repositorio de c\u00f3digo diferente para poder evolucionar y trabajar con cada uno de ellos de forma aislada sin afectar a los dem\u00e1s. As\u00ed adem\u00e1s podemos tener equipos aislados que trabajen con cada uno de los proyectos por separado.

    Info

    Durante todo el tutorial, voy a intentar separar la construcci\u00f3n del proyecto Frontend de la construcci\u00f3n del proyecto Backend. Elige una tecnolog\u00eda para cada una de las capas y utiliza siempre la misma en todos los apartados del tutorial.

    "},{"location":"usecases/#diseno-de-bd","title":"Dise\u00f1o de BD","text":"

    Para el proyecto que vamos a crear vamos a modelizar y gestionar 3 entidades: CATEGORY, AUTHOR y GAME.

    La entidad CATEGORY estar\u00e1 compuesta por los siguientes campos:

    • id (lo mismo que en GAME)
    • name

    La entidad AUTHOR estar\u00e1 compuesta por los siguientes campos:

    • id (lo mismo que en GAME)
    • name
    • nationality

    Para la entidad GAME, Ernesto nos ha comentado que la informaci\u00f3n que est\u00e1 guardando en sus fichas es la siguiente:

    • id (este dato no estaba originalmente en las fichas pero nos ser\u00e1 muy util para indexar y realizar b\u00fasquedas)
    • title
    • age
    • category
    • author

    Comenzaremos con un caso b\u00e1sico que cumpla las siguientes premisas: un juego pertenece a una categor\u00eda y ha sido creado por un \u00fanico autor.

    Modelando este contexto quedar\u00eda algo similar a esto:

    "},{"location":"usecases/#diseno-de-pantallas","title":"Dise\u00f1o de pantallas","text":"

    Deber\u00edamos construir tres pantallas de mantenimiento CRUD (Create, Read, Update, Delete) y una pantalla de Login general para activar las acciones de administrador. M\u00e1s o menos las pantallas deber\u00edan quedar as\u00ed:

    "},{"location":"usecases/#listado-de-categorias","title":"Listado de categor\u00edas","text":""},{"location":"usecases/#edicion-de-categoria","title":"Edici\u00f3n de categor\u00eda","text":""},{"location":"usecases/#listado-de-autores","title":"Listado de autores","text":""},{"location":"usecases/#edicion-de-autor","title":"Edici\u00f3n de autor","text":""},{"location":"usecases/#listado-de-juegos","title":"Listado de juegos","text":""},{"location":"usecases/#edicion-de-juego","title":"Edici\u00f3n de juego","text":""},{"location":"usecases/#diseno-funcional","title":"Dise\u00f1o funcional","text":"

    Por \u00faltimo vamos a definir un poco la funcionalidad b\u00e1sica que Ernesto necesita para iniciar su negocio.

    "},{"location":"usecases/#aspectos-generales","title":"Aspectos generales","text":"
    • El sistema tan solo tendr\u00e1 dos roles: ** usuario b\u00e1sico es el usuario an\u00f3nimo que accede a la web sin registrar. Solo tiene permisos para mostrar listados ** usuario administrador es el usuario que se registra en la aplicaci\u00f3n. Puede realizar las operaciones de alta, edici\u00f3n y borrado

    Por defecto cuando entras en la aplicaci\u00f3n tendr\u00e1s los privilegios de un usuario b\u00e1sico hasta que el usuario haga un login correcto con el usuario / password admin / admin. En ese momento pasara a ser un usuario administrador y podr\u00e1 realizar operaciones de alta, baja y modificaci\u00f3n.

    La estructura general de la aplicaci\u00f3n ser\u00e1:

    • Una cabecer\u00e1 superior que contendr\u00e1:
    • el logo y el nombre de la tienda
    • un enlace a cada uno de los CRUD del sistema
    • un bot\u00f3n de Sign in
    • Zona de trabajo, donde cargaremos las pantallas que el usuario vaya abriendo

    Al pulsar sobre la funcionalidad de Sign in aparecer\u00e1 una ventana modal que preguntar\u00e1 usuario y password. Esto realizar\u00e1 una llamada al backend, donde se validar\u00e1 si el usuario es correcto.

    • En caso de ser correcto, devolver\u00e1 un token jwt de acceso, que el cliente web deber\u00e1 guardar en sessionStorage para futuras peticiones
    • En caso de no ser correcto, devolver\u00e1 un error de Usuario y/o password incorrectos

    Todas las operaciones del backend que permitan crear, modificar o borrar datos, deber\u00e1n estar securizadas para que no puedan ser accedidas sin haberse autenticado previamente.

    "},{"location":"usecases/#crud-de-categorias","title":"CRUD de Categor\u00edas","text":"

    Al acceder a esta pantalla se mostrar\u00e1 un listado de las categor\u00edas que tenemos en la BD. La tabla no tiene filtros, puesto que tiene muy pocos registros. Tampoco estar\u00e1 paginada.

    En la tabla debe aparecer:

    • identificador de la categor\u00eda
    • nombre de la categor\u00eda
    • bot\u00f3n de editar (solo en el caso de que el usuario tenga permisos)
    • bot\u00f3n de borrar (solo en el caso de que el usuario tenga permisos)

    Debajo de la tabla aparecer\u00e1 un bot\u00f3n para crear nuevas categor\u00edas (solo en el caso de que el usuario tenga permisos).

    Crear

    Al pulsar el bot\u00f3n de crear se deber\u00e1 abrir una ventana modal con dos inputs:

    • Identificador. Este input deber\u00e1 ser de solo lectura y deber\u00e1 aparecer vac\u00edo, sin ning\u00fan valor. Con el placeholder de Identificador
    • Nombre. Este input es obligatorio, ser\u00e1 de escritura y deber\u00e1 aparecer vac\u00edo, sin ning\u00fan valor. Con el placeholder de Nombre

    Todos los datos obligatorios se deber\u00e1n comprobar que son v\u00e1lidos antes de guardarlo en BD. Dos botones en la parte inferior de la ventana permitir\u00e1n al usuario cerrar la ventana o guardar los datos en la BD.

    Editar

    Al pulsar el icono de editar se deber\u00e1 abrir una ventana modal utilizando el mismo componente que la ventana de Crear pero con los dos campos rellenados con los datos de BD.

    Borrar

    Si el usuario pulsa el bot\u00f3n de borrar, se deber\u00e1 comprobar si esa categor\u00eda tiene alg\u00fan Juego asociado. En caso de tenerlo se le informar\u00e1 al usuario de que dicha categor\u00eda no se puede eliminar por tener asociado un juego. En caso de no estar asociada, se le preguntar\u00e1 al usuario mediante un mensaje de confirmaci\u00f3n si desea eliminar la categor\u00eda. Solo en caso de que la respuesta sea afirmativa, se lanzar\u00e1 el borrado f\u00edsico de la categor\u00eda en BD.

    "},{"location":"usecases/#crud-de-autores","title":"CRUD de Autores","text":"

    Al acceder a esta pantalla se mostrar\u00e1 un listado de los autores que tenemos en la BD. La tabla no tiene filtros pero deber\u00e1 estar paginada en servidor.

    En la tabla debe aparecer:

    • identificador del autor
    • nombre del autor
    • nacionalidad del autor
    • bot\u00f3n de editar (solo en el caso de que el usuario tenga permisos)
    • bot\u00f3n de borrar (solo en el caso de que el usuario tenga permisos)

    Debajo de la tabla aparecer\u00e1 un bot\u00f3n para crear nuevos autores (solo en el caso de que el usuario tenga permisos).

    Crear

    Al pulsar el bot\u00f3n de crear se deber\u00e1 abrir una ventana modal con tres inputs:

    • Identificador. Este input deber\u00e1 ser de solo lectura y deber\u00e1 aparecer vac\u00edo, sin ning\u00fan valor. Con el placeholder de Identificador
    • Nombre. Este input es obligatorio, ser\u00e1 de escritura y deber\u00e1 aparecer vac\u00edo, sin ning\u00fan valor. Con el placeholder de Nombre
    • Nacionalidad. Este input es obligatorio, ser\u00e1 de escritura y deber\u00e1 aparecer vac\u00edo, sin ning\u00fan valor. Con el placeholder de Nacionalidad

    Todos los datos obligatorios se deber\u00e1n comprobar que son v\u00e1lidos antes de guardarlo en BD. Dos botones en la parte inferior de la ventana permitir\u00e1n al usuario cerrar la ventana o guardar los datos en la BD.

    Editar

    Al pulsar el icono de editar se deber\u00e1 abrir una ventana modal utilizando el mismo componente que la ventana de Crear pero con los tres campos rellenados con los datos de BD.

    Borrar

    Si el usuario pulsa el bot\u00f3n de borrar, se deber\u00e1 comprobar si ese autor tiene alg\u00fan Juego asociado. En caso de tenerlo se le informar\u00e1 al usuario de que dicho autor no se puede eliminar por tener asociado un juego. En caso de no estar asociado, se le preguntar\u00e1 al usuario mediante un mensaje de confirmaci\u00f3n si desea eliminar el autor. Solo en caso de que la respuesta sea afirmativa, se lanzar\u00e1 el borrado f\u00edsico de la categor\u00eda en BD.

    "},{"location":"usecases/#crud-de-juegos","title":"CRUD de Juegos","text":"

    Al acceder a esta pantalla se mostrar\u00e1 un listado de los juegos disponibles en el cat\u00e1logo de la BD. Esta tabla debe contener filtros en la parte superior, pero no debe estar paginada.

    Se debe poder filtrar por:

    • nombre del juego. Donde el usuario podr\u00e1 poner cualquier texto y el filtrado ser\u00e1 todos aquellos juegos que contengan el texto buscado
    • categor\u00eda del juego. Donde aparecer\u00e1 un desplegable que el usuario seleccionar de entre todas las categor\u00edas de juego que existan en la BD.

    Dos botones permitir\u00e1n realizar el filtrado de juegos (lanzando una nueva consulta a BD) o limpiar los filtros seleccionados (lanzando una consulta con los filtros vac\u00edos).

    En la tabla debe aparecer a modo de fichas. No hace falta que sea exactamente igual a la maqueta, no es un requisito determinar un ancho general de ficha por lo que pueden caber 2,3 o x fichas en una misma fila, depender\u00e1 del programador. Pero todas las fichas deben tener el mismo ancho:

    • Un espacio destinado a una foto (de momento no pondremos nada en ese espacio)
    • Una columna con la siguiente informaci\u00f3n:
      • T\u00edtulo del juego, resaltado de alguna forma
      • Edad recomendada
      • Categor\u00eda del juego, mostraremos su nombre
      • Autor del juego, mostraremos su nombre
      • Nacionalidad del juego, mostraremos la nacionalidad del autor del juego

    Los juegos no se pueden eliminar, pero si se puede editar si el usuario pulsa en alguna de las fichas (solo en el caso de que el usuario tenga permisos).

    Debajo de la tabla aparecer\u00e1 un bot\u00f3n para crear nuevos juegos (solo en el caso de que el usuario tenga permisos).

    Crear

    Al pulsar el bot\u00f3n de crear se deber\u00e1 abrir una ventana modal con cinco inputs:

    • Identificador. Este input deber\u00e1 ser de solo lectura y deber\u00e1 aparecer vac\u00edo, sin ning\u00fan valor. Con el placeholder de Identificador
    • T\u00edtulo. Este input es obligatorio, ser\u00e1 de escritura y deber\u00e1 aparecer vac\u00edo, sin ning\u00fan valor. Con el placeholder de T\u00edtulo
    • Edad. Este input es obligatorio, es de tipo num\u00e9rico de 0 a 99, ser\u00e1 de escritura y deber\u00e1 aparecer vac\u00edo, sin ning\u00fan valor. Con el placeholder de Edad
    • Categor\u00eda. Este input es obligatorio, ser\u00e1 un campo seleccionable donde aparecer\u00e1n todas las categor\u00edas de la BD, aparecer\u00e1 vac\u00edo por defecto. Con el placeholder de Categor\u00eda
    • Autor. Este input es obligatorio, ser\u00e1 un campo seleccionable donde aparecer\u00e1n todos los autores de la BD, aparecer\u00e1 vac\u00edo por defecto. Con el placeholder de Autor

    Todos los datos obligatorios se deber\u00e1n comprobar que son v\u00e1lidos antes de guardarlo en BD. Dos botones en la parte inferior de la ventana permitir\u00e1n al usuario cerrar la ventana o guardar los datos en la BD.

    Editar

    Al pulsar en una de las fichas con un click simple, se deber\u00e1 abrir una ventana modal utilizando el mismo componente que la ventana de Crear pero con los cinco campos rellenados con los datos de BD.

    "},{"location":"appendix/aws/","title":"AWS CLI","text":"

    AWS CLI (Command Line Interface) es una herramienta oficial proporcionada por Amazon Web Services que permite gestionar y automatizar servicios en la nube directamente desde la terminal, sin necesidad de acceder a la consola web. Con AWS CLI, los desarrolladores, administradores de sistemas y arquitectos pueden ejecutar comandos para crear, configurar y controlar recursos como instancias EC2, buckets S3, funciones Lambda, entre otros. Su uso es esencial en entornos donde se requiere eficiencia, repetibilidad y automatizaci\u00f3n, como en scripts de despliegue, tareas programadas o integraciones CI/CD. Adem\u00e1s, facilita el trabajo remoto y la administraci\u00f3n de m\u00faltiples cuentas o regiones de AWS de forma centralizada y segura.

    "},{"location":"appendix/aws/#pre-requisitos","title":"Pre-requisitos","text":"

    Vamos a instalar AWS CLI desde un sistema Linux, as\u00ed que lo primero que deber\u00e1s tener instalado en tu m\u00e1quina es un Linux o un WSL. Si no lo tienes, puedes revisar el tutorial de Subsistema de Linux para Windows.

    "},{"location":"appendix/aws/#instalacion-de-aws-cli","title":"Instalaci\u00f3n de AWS CLI","text":"

    Una vez tenemos Linux, si entramos en su consola de comandos, para instalar AWS CLI tan solo es necesario lanzar estos comandos:

    1. Update your Ubuntu packages

      sudo apt update\nsudo apt upgrade -y\n

    2. Install unzip if you don't have it already

      sudo apt install -y unzip curl\n

    3. Download the AWS CLI v2 installation package

      curl \"https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip\" -o \"awscliv2.zip\"\n

    4. Unzip the installer

      unzip awscliv2.zip\n

    5. Run the install program

      sudo ./aws/install\n

    6. Verify the installation

      aws --version\n

    Y eso es todo, ya tienes AWS CLI instalado.

    "},{"location":"appendix/dates/","title":"Fechas","text":"

    Uno de los elementos m\u00e1s problem\u00e1ticos en las aplicaciones son las fechas. M\u00e1s concretamente, c\u00f3mo las gestionamos.

    Pero podemos afrontarlo de una manera m\u00e1s clara. Vamos a empezar con unas definiciones b\u00e1sicas, veremos por qu\u00e9 hay problemas y la soluci\u00f3n que proponemos para que no te supongan un problema en tus aplicaciones.

    "},{"location":"appendix/dates/#definiciones-basicas","title":"Definiciones b\u00e1sicas","text":"

    Antes de nada, vamos a definir unos conceptos b\u00e1sicos que nos ayudar\u00e1n a entender mejor el problema.

    • Fecha (Date): Una fecha es una representaci\u00f3n de un d\u00eda concreto en el calendario. Por ejemplo, \"15 de junio de 2024\".
    • Hora (Time): La hora representa un momento espec\u00edfico dentro de un d\u00eda.
    • Zona horaria (Timezone): La zona horaria es una regi\u00f3n geogr\u00e1fica que tiene la misma hora est\u00e1ndar. Por ejemplo, \"CET\" (Central European Time) o \"GMT\" (Greenwich Mean Time).
    • Fecha y hora (DateTime): La combinaci\u00f3n de fecha y hora representa un momento espec\u00edfico en el tiempo, incluyendo la zona horaria. Por ejemplo, \"15 de junio de 2024 a las 14:30 CET\".
    • Timestamp: Un timestamp es una representaci\u00f3n num\u00e9rica de un momento espec\u00edfico en el tiempo, generalmente expresado en segundos o milisegundos desde una fecha de referencia (por ejemplo, el 1 de enero de 1970, conocido como la \"\u00e9poca Unix\").
    "},{"location":"appendix/dates/#problemas-comunes","title":"Problemas comunes","text":"

    Al trabajar con fechas y horas, pueden surgir varios problemas comunes:

    • Conversi\u00f3n de zonas horarias: Si una aplicaci\u00f3n maneja usuarios en diferentes zonas horarias, es crucial convertir correctamente las fechas y horas para evitar confusiones.
    • Formato inconsistente: Diferentes regiones utilizan diferentes formatos de fecha y hora, lo que puede llevar a errores de interpretaci\u00f3n. Ej. DD/MM/AAAA (Europa) vs MM/DD/AAAA (EE.UU.).
    • Diferencias en el horario de verano: Las fechas y horas pueden verse afectadas por el horario de verano, lo que puede causar confusiones si no se maneja correctamente.
    • Errores de c\u00e1lculo: Al realizar c\u00e1lculos con fechas y horas (por ejemplo, sumar d\u00edas o restar horas), es f\u00e1cil cometer errores si no se consideran todos los factores relevantes.
    • Me quita un d\u00eda: Es la m\u00e1s com\u00fan al principio, \u00bfpor qu\u00e9 ocurre esto? La respuesta est\u00e1 en c\u00f3mo se manejan las zonas horarias y el horario de verano. Cuando le pasamos una fecha, estamos indic\u00e1ndole un d\u00eda sin zona horaria, por lo que se detecta nuestra zona horaria local y se aplica el desfase correspondiente. GMT+1 en horario est\u00e1ndar y GMT+2 en horario de verano.

    Lo importante es recordar que un d\u00eda en una zona horaria puede no ser el mismo d\u00eda en otra zona horaria. Por ejemplo, el 15 de junio de 2024 en CET puede ser el 14 de junio de 2024 en GMT, que es por lo que ocurre un descuento de d\u00eda.

    "},{"location":"appendix/dates/#solucion-propuesta","title":"Soluci\u00f3n propuesta","text":"

    Para evitar estos problemas, os proponemos adheriros al est\u00e1ndar ISO 8601 para representar fechas y horas en vuestras aplicaciones. Este est\u00e1ndar define un formato claro y consistente para las fechas y horas, que incluye la zona horaria.

    El formato ISO 8601 para una fecha y hora completa con zona horaria es el siguiente:

    AAAA-MM-DDTHH:MM:SS.mmmZ\u00b1HH:MM\n

    Por ejemplo, \"2024-06-15T14:30:00.000Z+02:00\" representa el 15 de junio de 2024 a las 14:30 en la zona horaria GMT+2.

    "},{"location":"appendix/dates/#backend","title":"Backend","text":"

    En Java, para manejar fechas con ISO 8601 de forma segura, recomendamos el uso de Date, as\u00ed, siempre que le pasen una fecha en formato ISO 8601, se gestionar\u00e1 correctamente la zona horaria.

    import java.util.Date;\n\n// ...\n\n    private Date fecha;\n
    "},{"location":"appendix/dates/#frontend","title":"Frontend","text":"

    El uso de fechas lo gestionaremos con ISO Date

    // Para leer fechas en formato ISO 8601\nconst fecha = new Date(`2024-06-15T14:30:00.000Z`);\n\n// Para enviar fechas en formato ISO 8601\nconst fechaISO = fecha.toISOString();\n
    "},{"location":"appendix/dates/#buenas-practicas","title":"Buenas pr\u00e1cticas","text":"
    • Siempre utiliza el formato ISO 8601 para representar fechas y horas en tus aplicaciones.
    • Aseg\u00farate de manejar correctamente las zonas horarias al mostrar fechas y horas a los usuarios.
    • Realiza pruebas exhaustivas para verificar que las fechas y horas se manejan correctamente en diferentes escenarios y zonas horarias.
    • Nunca elimines las horas ni la zona horaria al manejar fechas. Siempre trabaja con fechas completas en formato ISO 8601.

    Si te cuestionan por qu\u00e9 pasan un d\u00eda y el servidor entiende otro, expl\u00edcales que es por la zona horaria y que la soluci\u00f3n es usar siempre ISO 8601. De esta manera la consistencia en los datos estar\u00e1 garantizada.

    "},{"location":"appendix/git/","title":"Tutorial b\u00e1sico de Git","text":"

    Cada vez se tiende m\u00e1s a utilizar repositorios de c\u00f3digo Git y, aunque no sea objeto de este tutorial Springboot-Angular, queremos hacer un resumen muy b\u00e1sico y sencillo de como utilizar Git.

    En el mercado existen multitud de herramientas para gestionar repositorios Git, podemos utilizar cualquiera de ellas, aunque desde devonfw se recomienda utilizar Git SCM. Adem\u00e1s, existen tambi\u00e9n multitud de servidores de c\u00f3digo que implementan repositorios Git, como podr\u00edan ser GitHub, GitLab, Bitbucket, etc. Todos ellos trabajan de la misma forma, as\u00ed que este resumen servir\u00e1 para todos ellos.

    Info

    Este anexo muestra un resumen muy sencillo y b\u00e1sico de los comandos m\u00e1s comunes que se utilizan en Git. Para ver detalles m\u00e1s avanzados o un tutorial completo te recomiendo que leas la guia de Atlassian.

    "},{"location":"appendix/git/#funcionamiento-basico","title":"Funcionamiento b\u00e1sico","text":"

    Existen dos conceptos en Git que debes tener muy claros: las ramas y los repositorios. Vamos a ver como funciona cada uno de ellos.

    "},{"location":"appendix/git/#ramas","title":"Ramas","text":"

    Por un lado tenemos las ramas de Git. El repositorio puede tener tantas ramas como se quiera, pero por lo general debe existir una rama maestra a menudo llamada develop o master, y luego muchas ramas con cada una de las funcionalidades desarrolladas.

    Las ramas siempre se deben crear a partir de una rama (en el ejemplo llamaremos develop), con una foto concreta y determinada de esa rama. Esta rama deber\u00e1 tener un nombre que describa lo que va a contener esa rama (en el ejemplo feature/xxx). Y por lo general, esa rama se mergear\u00e1 con otra rama del repositorio, que puede ser la rama de origen o cualquier otra (en el ejemplo ser\u00e1 con la rama origen develop).

    As\u00ed pues, podemos tener algo as\u00ed:

    Las acciones de crear ramas y mergear ramas est\u00e1n explicadas m\u00e1s abajo. En este punto solo es necesario que seas conocedor de:

    • existen ramas maestras --> que contienen el c\u00f3digo completo de la aplicaci\u00f3n
    • existen ramas de desarrollo --> que generalmente se crean de una rama maestra en un punto temporal concreto
    • en alg\u00fan momento esas ramas de desarrollo se deben mergear en una rama maestra
    • ojo cuidado, cuando hay varias personas en el equipo trabajando, habr\u00e1n varias ramas de desarrollo que nazcan de diferentes puntos temporales y que habr\u00e1 que tener en cuenta para posibles conflictos. Recuerda que no est\u00e1s solo programando, hay m\u00e1s gente modificando el c\u00f3digo.
    "},{"location":"appendix/git/#repositorios","title":"Repositorios","text":"

    El otro concepto que debe queda claro, es el de repositorios. Por defecto, en Git, se trabaja con el repositorio local, en el que puedes crear ramas, modificar c\u00f3digo, mergear, etc. pero todos esos cambios que se hagan, ser\u00e1n todos en local, nadie m\u00e1s tendr\u00e1 acceso.

    Tambi\u00e9n existe el repositorio remoto, tambi\u00e9n llamado origin. Este repositorio es el que todos los integrantes del equipo utilizan como referencia. Existen acciones de Git que permite sincronizar los repositorios.

    En este punto solo es necesario que seas conocedor de:

    • Los cambios que realices en local (en tu repositorio local) solo ser\u00e1n visibles por ti. Puedes crear ramas y borrarlas, pero solo tu las ver\u00e1s.
    • Los cambios que se suban al repositorio remoto ser\u00e1n visibles para todos. Pueden haber ramas protegidas para que no se puedan modificar desde el equipo, t\u00edpicamente las ramas maestras. Estas ramas solo pueden modificarse previa validaci\u00f3n y pull request o merge request (depende de la aplicaci\u00f3n usada para Git).
    • Existen acciones que permiten subir tus cambios de local a remoto. Recuerda que pueden existir ramas protegidas.
    • Existen acciones que permiten actualizar tus ramas locales con los cambios remotos.
    • Recuerda que no trabajas solo, es posible que tu repositorio local no est\u00e9 sincronizado, tus compa\u00f1eros han podido subir c\u00f3digo y deber\u00edas sincronizarte frecuentemente.
    "},{"location":"appendix/git/#acciones-mas-tipicas","title":"Acciones m\u00e1s t\u00edpicas","text":"

    En la Gu\u00eda r\u00e1pida puedes ver m\u00e1s detalle de estas acciones pero por lo general:

    • Lo primero es descargarse una copia del repositorio con todas sus ramas. Se descargar\u00eda de remoto a local. A partir de este momento se trabaja en local.
    • Cada nueva funcionalidad deber\u00eda tener su rama asociada, por tanto, lo l\u00f3gico es crear una rama de desarrollo (t\u00edpicamente feature/xxx) a partir de una rama maestra (t\u00edpicamente develop o master).
    • Se trabajar\u00eda de forma local con esa rama. Es buena pr\u00e1ctica que si llevas mucho tiempo con la rama creada, de vez en cuando, sincronices tu repositorio local con lo que exista en el repositorio remoto. Adem\u00e1s, como es posible que la rama maestra de la que part\u00eda haya cambiado, esos cambios deber\u00edas llevarlos tambi\u00e9n a tu rama en desarrollo. Con esto consigues que tu punto temporal sea m\u00e1s moderno y tengas menos conflictos. Recuerda que no est\u00e1s solo trabajando.
    • Cuando lo tengas listo y antes de subir nada, deber\u00edas realizar una \u00faltima sincronizaci\u00f3n de remoto a local. Despu\u00e9s deber\u00edas hacer un merge de tus ramas locales de desarrollo con las ramas maestras locales de las que partieron, por los posibles cambios que alguien hubiera podido subir.
    • Por \u00faltimo, una vez tengas todo actualizado, ya puedes subir el c\u00f3digo al repositorio remoto (tu rama de desarrollo), y solicitar un pull request o merge request contra la rama maestra que quieras modificar.
    • Alguien, diferente a ti, debe revisar esa solicitud y aprobarla antes de que se realice todo el merge correcto en remoto. Y vuelta a empezar.
    "},{"location":"appendix/git/#funcionamiento-avanzado","title":"Funcionamiento avanzado","text":"

    A continuaci\u00f3n vamos a describir estos mismos conceptos y acciones que hemos visto, pero m\u00e1s en profundidad para que veas como trabaja internamente Git. No es necesario que leas este punto, aunque es recomendable.

    "},{"location":"appendix/git/#estructuras-y-flujo-de-trabajo","title":"Estructuras y flujo de trabajo","text":"

    Lo primero que debes conocer de Git es su funcionamiento b\u00e1sico de flujo de trabajo. Tu repositorio local est\u00e1 compuesto por tres \"estructuras\" que contienen los archivos y los cambios de los ficheros del repositorio.

    • Working directory - Contiene los archivos con los que est\u00e1s trabajando localmente.
    • Staging Area - Es un \u00e1rea intermedia donde vamos a\u00f1adiendo ficheros para ir agrupando modificaciones.
    • Local Repository - Es el repositorio local donde tendr\u00e9mos el registro de todos los commits que hemos realizado. Por defecto apunta a HEAD que es el \u00faltimo commit registrado.

    Existen operaciones que nos permiten a\u00f1adir o borrar ficheros dentro de cada una de las estructuras desde otra estructura.

    As\u00ed pues, los comandos b\u00e1sicos dentro de nuestro repositorio local son los siguientes.

    "},{"location":"appendix/git/#add-y-commmit","title":"add y commmit","text":"

    Puedes registrar los cambios realizados en tu working directory y a\u00f1adirlos al staging area usando el comando

    git add <filename>\n
    o si quieres a\u00f1adir todos los ficheros modificados
    git add .\n

    Este es el primer paso en el flujo de trabajo b\u00e1sico. Una vez tenemos los cambios registrados en el staging area podemos hacer un commit y persistirlos dentro del local repository mediante el comando

    git commit -m \"<Commit message>\"\n

    A partir de ese momento, los ficheros modificados y a\u00f1adidos al local repository se han persistido y se han a\u00f1adido a tu HEAD, aunque todav\u00eda siguen estando el local, no lo has enviado a ning\u00fan repositorio remoto.

    "},{"location":"appendix/git/#reset","title":"reset","text":"

    De la misma manera que se han a\u00f1adido ficheros a staging area o a local repository, podemos retirarlos de estas estructuras y volver a recuperar los ficheros que ten\u00edamos anteriormente en el working directory. Por ejemplo, si nos hemos equivocado al incluir ficheros en un commit o simplemente queremos deshacer los cambios que hemos realizado bastar\u00eda con lanzar el comando

    git reset --hard\n
    o si queremos volver a un commit concreto
    git reset <COMMIT>\n

    "},{"location":"appendix/git/#trabajo-con-ramas","title":"Trabajo con ramas","text":"

    Para complicarlo todo un poco m\u00e1s, el trabajo con git siempre se realiza mediante ramas. Estas ramas nos sirven para desarrollar funcionalidades aisladas unas de otras y poder hacer mezclas de c\u00f3digo de unas ramas a otras. Las ramas m\u00e1s comunes dentro de git suelen ser:

    • master Esta ser\u00e1 la rama que contenga el c\u00f3digo fuente que tenemos en producci\u00f3n.
    • release Esta ser\u00e1 la rama que contenga el c\u00f3digo fuente de cada una de las entregas parciales, no tiene porqu\u00e9 coincidir con la rama master.
    • develop Esta ser\u00e1 la rama que contenga el c\u00f3digo fuente estable que est\u00e1 actualmente en desarrollo.
    • feature/xxxx Estas ser\u00e1nn la rama que contengan el c\u00f3digo fuente de desarrollo de cada una de las funcionalidades. Generalmente estas ramas las crea cada desarrollador, las mantiene en local, hasta que las sube a remoto para realizar un merge a la rama develop.

    Siempre que trabajes con ramas debes tener en cuenta que al empezar tu desarrollo debes partir de una versi\u00f3n actualizada de la rama develop, y al terminar tu desarrollo debes solicitar un merge contra develop, para que tu funcionalidad est\u00e9 incorporada en la rama de desarrollo.

    "},{"location":"appendix/git/#crear-ramas","title":"Crear ramas","text":"

    Crear ramas en local es tan sencillo como ejecutar este comando:

    git checkout -b <NOMBRE_RAMA>\n

    Eso nos crear\u00e1 una rama con el nombre que le hayamos dicho y mover\u00e1 el Working Directory a dicha rama.

    "},{"location":"appendix/git/#cambiar-de-rama","title":"Cambiar de rama","text":"

    Para cambiar de una rama a otra en local tan solo debemos ejecutar el comando:

    git checkout <NOMBRE_RAMA>\n

    La rama debe existir, sino se quejar\u00e1 de que no encuentra la rama. Este comando nos mover\u00e1 el Working Directory a la rama que le hayamos indicado. Si tenemos cambios en el Staging Area que no hayan sido movidos al Local Repository NO nos permitir\u00e1 movernos a la rama ya que perder\u00edamos los cambios. Antes de poder movernos debemos resetear los cambios o bien commitearlos.

    "},{"location":"appendix/git/#remote-repository","title":"Remote repository","text":"

    Hasta aqu\u00ed es todo m\u00e1s o menos sencillo, trabajamos con nuestro repositorio local, creamos ramas, commiteamos o reseteamos cambios de c\u00f3digo, pero todo esto lo hacemos en local. Ahora necesitamos que esos cambios se distribuyan y puedan leerlos el resto de integrantes de nuestro equipo.

    Aqu\u00ed es donde entra en juego los repositorios remotos.

    Aqu\u00ed debemos tener MUY en cuenta que el c\u00f3digo que vamos a publicar en remoto SOLO es posible publicarlo desde el Local Repository. Es decir que para poder subir c\u00f3digo a remote antes debemos a\u00f1adirlo a Staging Area y hacer un commit para persistirlo en el Local Repository.

    "},{"location":"appendix/git/#clone","title":"clone","text":"

    Antes de empezar a tocar c\u00f3digo del proyecto podemos crear un Local Repository vac\u00edo o bien bajarnos un proyecto que ya exista en un Remote Repository. Esta \u00faltima opci\u00f3n es la m\u00e1s normal.

    Para bajarnos un proyecto desde remoto tan solo hay que ejecutar el comando:

    git clone <REMOTE_URL>\n

    Esto te crear\u00e1 una carpeta con el nombre del proyecto y dentro se descargar\u00e1 la estructura completa del repositorio y te mover\u00e1 al Working Directory todo el c\u00f3digo de la rama por defecto para ese repositorio.

    "},{"location":"appendix/git/#envio-de-cambios","title":"env\u00edo de cambios","text":"

    El env\u00edo de datos a un Remote Repository tan solo es posible realizarlo desde Local Repository (por lo que antes deber\u00e1s commitear cambios all\u00ed), y se debe ejecutar el comando:

    git push origin\n
    "},{"location":"appendix/git/#actualizar-y-fusionar","title":"actualizar y fusionar","text":"

    En ocasiones (bastante habitual) ser\u00e1 necesario descargarse los cambios de un Remote Repository para poder trabajar con la \u00faltima versi\u00f3n. Para ello debemos ejecutar el comando:

    git pull\n

    El propio git realizar\u00e1 la fusi\u00f3n local del c\u00f3digo remoto con el c\u00f3digo de tu Working Directory. Pero en ocasiones, si se ha modificado el mismo fichero en remoto y en local, se puede producir un Conflicto. No pasa nada, tan solo tendr\u00e1s que abrir dicho fichero en conflicto y resolverlo manualmente dejando el c\u00f3digo mezclado correcto.

    Tambi\u00e9n es posible que el c\u00f3digo que queramos actualizar est\u00e9 en otra rama, si lo que necesitamos es fusionar el c\u00f3digo de otra rama con la rama actual, nos situaremos en la rama destino y ejecutaremos el comando:

    git merge <RAMA_ORIGEN>\n

    Esto har\u00e1 lo mismo que un pull en local y fusionar\u00e1 el c\u00f3digo de una rama en otra. Tambi\u00e9n es posible que se produzcan conflictos que deber\u00e1s resolver de forma manual.

    "},{"location":"appendix/git/#merge-request","title":"Merge Request","text":"

    Ya por \u00faltimo, como estamos trabajando con ramas, lo \u00fanico que hacemos es subir y bajar ramas, pero en alg\u00fan momento alguien debe fusionar el contenido de una rama en la rama develop, release o master, que son las ramas principales.

    Se podr\u00eda directamente usar el comando merge para eso, pero en la mayor\u00eda de los repositorios no esta permitido subir el c\u00f3digo de una rama principal, por lo que no podr\u00e1s hacer un merge y subirlo. Para eso existe otra opci\u00f3n que es la de Merge Request.

    Esta opci\u00f3n permite a un usuario solicitar que otro usuario verifique y valide que el c\u00f3digo de su rama es correcto y lo puede fusionar en Remote Repository con una rama principal. Al ser una operaci\u00f3n delicada, tan solo es posible ejecutarla a trav\u00e9s de la web del repositorio git.

    Por lo general existir\u00e1 una opci\u00f3n / bot\u00f3n que permitir\u00e1 hacer un Merge Request con una rama origen y una rama destino (generalmente una de las principales). A esa petici\u00f3n se le asignar\u00e1 un validador y se enviar\u00e1. El usuario validador verificar\u00e1 si es correcto o no y validar\u00e1 o rechazar\u00e1 la petici\u00f3n. En caso de validarla se fusionar\u00e1 autom\u00e1ticamente en remoto y todos los usuarios podr\u00e1n descargar los nuevos cambios desde la rama.

    \u00a1Cuidado!

    Siempre antes de solicitar un Merge Request debes comprobar que tienes actualizada la rama comparandola con la rama remota que queremos mergear, en nuestro ejemplo ser\u00e1 develop.

    Para actualizarla tu rama hay que seguir tres pasos muy sencillos:

    • Cambias a la rama develop y descargarnos los cambios del repositorio remoto (git pull)
    • Cambias a tu rama y ejecutar un merge desde develop hacia nuestra rama (git merge develop)
    • Subes tus cambios a remoto (git add, git commit y git push) y ya puedes solcitar el Merge Request
    "},{"location":"appendix/git/#guia-rapida","title":"Gu\u00eda r\u00e1pida","text":"

    Los pasos b\u00e1sicos de utilizaci\u00f3n de git son sencillos.

    • Primero nos bajamos el repositorio o lo creamos en local mediante los comandos
      git clone\n    o \ngit init\n
    • Una vez estamos trabajando con nuestro repositorio local, cada vez que vayamos a comenzar una funcionalidad nueva, debemos crear una rama nueva siempre partiendo desde una rama maestra mediante el comando: (en nuestro ejemplo la rama maestra ser\u00e1 develop)
      git checkout -b <rama>\n
    • Cuando tengamos implementados los cambios que queremos realizar, hay que subirlos al staging y luego persistirlos en nuestro repositorio local. Esto lo hacemos con el comando
      git add .\ngit commit -m \"<Commit message>\"\n
    • Siempre antes de subir los cambios al repositorio remoto, hay que comprobar que tenemos actualizada nuestra rama comparandola con la rama remota que queremos mergear, en nuestro ejemplo ser\u00e1 develop. Por tanto tenemos que cambiar a la rama develop, descargarnos los cambios del repositorio remoto, volver a cambiar a nuestra rama y ejecutar un merge desde develop hacia nuestra rama, ejecutando estos comandos
      git checkout develop\ngit pull\ngit checkout <rama>\ngit merge develop\n
    • Ahora que ya tenemos actualizadas las ramas, tan solo nos basta subir nuestra rama a remoto, con el comando
      git push --set-upstream origin <rama>\n
    • Por \u00faltimo accedemos al cliente web del repositorio y solicitamos un merge request contra develop. Para que sea validado y aprobado por otro compa\u00f1ero del equipo.
    • Si en alg\u00fan momento necesitamos modificar nuestro c\u00f3digo del merge request antes de que haya sido aprobado, nos basta con repetir los pasos anteriores
      git add .\ngit commit -m \"<Commit message>\"\ngit push origin\n
    • Una vez hayamos terminado el desarrollo y vayamos a empezar una nueva funcionalidad, volveremos al punto 2 de este listado y comenzaremos de nuevo los comando. Debemos recordad que tenemos que partir siempre de la rama develop y adem\u00e1s debe estar actualizada git pull.
    "},{"location":"appendix/jpa/","title":"Funcionamiento Spring Data","text":"

    Este anexo no pretende explicar el funcionamiento interno de Spring Data, simplemente conocer un poco como utilizarlo y algunos peque\u00f1os tips que pueden ser interesantes.

    "},{"location":"appendix/jpa/#funcionamiento-basico","title":"Funcionamiento b\u00e1sico","text":"

    Lo primero que deber\u00edas tener claro, es que hagas lo que hagas, al final todo termina lanzando una query nativa sobre la BBDD. Da igual que uses cualquier tipo de acelerador (luego veremos alguno), ya que al final Spring Data termina convirtiendo lo que hayas programado en una query nativa.

    Cuanta m\u00e1s informaci\u00f3n le proporciones a Spring Data, tendr\u00e1s m\u00e1s control sobre la query final, pero m\u00e1s dificil ser\u00e1 de mantener. Lo mejor es utilizar, siempre que se pueda, todos los automatismos y automagias posibles y dejar que Spring haga su faena. Habr\u00e1 ocasiones en que esto no nos sirva, en ese momento tendremos que decidir si queremos bajar el nivel de implementaci\u00f3n o queremos utilizar otra alternativa como procesos por streams.

    "},{"location":"appendix/jpa/#derived-query-methods","title":"Derived Query Methods","text":"

    Para la realizaci\u00f3n de consultas a la base de datos, Spring Data nos ofrece un sencillo mecanismo que consiste en crear definiciones de m\u00e9todos con una sintaxis especifica, para luego traducirlas autom\u00e1ticamente a consultas nativas, por parte de Spring Data.

    Esto es muy \u00fatil, ya que convierte a la aplicaci\u00f3n en agn\u00f3sticos de la tecnolog\u00eda de BBDD utilizada y podemos migrar con facilidad entre las muchas soluciones disponibles en el mercado, delegando esta tarea en Spring.

    Esta es la opci\u00f3n m\u00e1s indicada en la mayor\u00eda de los casos, siempre que puedas deber\u00edas utilizar esta forma de realizar las consultas. Como parte negativa, en algunos casos en consultas m\u00e1s complejas la definici\u00f3n de los m\u00e9todos puede extenderse demasiado dificultando la lectura del c\u00f3digo.

    De esto tenemos alg\u00fan ejemplo por el tutorial, en el repositorio de GameRepository.

    Siguiendo el ejemplo del tutorial, si tuvieramos que recuperar los Game por el nombre del juego, se podr\u00eda crear un m\u00e9todo en el GameRepository de esta forma:

    List<Game> findByName(String name);\n

    Spring Data entender\u00eda que quieres recuperar un listado de Game que est\u00e1n filtrados por su propiedad Name y generar\u00eda la consulta SQL de forma autom\u00e1tica, sin tener que implementar nada.

    Se pueden contruir muchos m\u00e9todos diferentes, te recomiendo que leas un peque\u00f1o tutorial de Baeldung y profundices con la documentaci\u00f3n oficial donde podr\u00e1s ver todas las opciones.

    "},{"location":"appendix/jpa/#anotacion-query","title":"Anotaci\u00f3n @Query","text":"

    Otra forma de realizar consultas, esta vez menos autom\u00e1tica y m\u00e1s cercana a SQL, es la anotaci\u00f3n @Query.

    Existen dos opciones a la hora de usar la anotaci\u00f3n @Query. Esta anotaci\u00f3n ya la hemos usado en el tutorial, dentro del GameRepository.

    En primer lugar tenemos las consultas JPQL. Estas guardan un parecido con el lenguaje SQL pero al igual que en el caso anterior, son traducidas por Spring Data a la consulta final nativa. Su uso no est\u00e1 recomendado ya que estamos a\u00f1adiendo un nivel de concreci\u00f3n y por tanto estamos aumentando la complejidad del c\u00f3digo. Aun as\u00ed, es otra forma de generar consultas.

    Por otra parte, tambi\u00e9n es posible generar consultas nativas directamente dentro de esta anotaci\u00f3n interactuando de forma directa con la base de datos. Esta pr\u00e1ctica es altamente desaconsejable ya que crea acoplamientos con la tecnolog\u00eda de la BBDD utilizada y es una fuente de errores.

    Puedes ver m\u00e1s informaci\u00f3n de esta anotaci\u00f3n desde este peque\u00f1o tutorial de Baeldung.

    "},{"location":"appendix/jpa/#acelerando-las-consultas","title":"Acelerando las consultas","text":"

    En muchas ocasiones necesitamos obtener informaci\u00f3n que no est\u00e1 en una \u00fanica tabla por motivos de dise\u00f1o de la base de datos. Debemos plasmar esta casu\u00edstica con cuidado a nuestro modelo relacional para obtener resultados \u00f3ptimos en cuanto al rendimiento.

    Para ilustrar el caso vamos a recuperar los objetos utilizados en el tutorial Author, Gategory y Game. Si recuerdas, tenemos que un Game tiene asociado un Author y tiene asociada una Gategory.

    Cuando utilizamos el m\u00e9todo de filtrado find que construimos en el GameRepository, vemos que Spring Data traduce la @Query que hab\u00edamos dise\u00f1ado en una query SQL para recuperar los juegos.

    @Query(\"select g from Game g where (:title is null or g.title like '%'||:title||'%') and (:category is null or g.category.id = :category)\")\nList<Game> find(@Param(\"title\") String title, @Param(\"category\") Long category);\n

    Esta @Query es la que utiliza Spring Data para traducir las propiedades a objetos de BBDD y mapear los resultados a objetos Java. Si tenemos activada la property spring.jpa.show-sql=true podremos ver las queries que est\u00e1 generando Spring Data. El resultado es el siguiente.

    Hibernate: select game0_.id as id1_2_, game0_.age as age2_2_, game0_.author_id as author_i4_2_, game0_.category_id as category5_2_, game0_.title as title3_2_ from game game0_ where (? is null or game0_.title like ('%'||?||'%')) and (? is null or game0_.category_id=?)\nHibernate: select author0_.id as id1_0_0_, author0_.name as name2_0_0_, author0_.nationality as national3_0_0_ from author author0_ where author0_.id=?\nHibernate: select category0_.id as id1_1_0_, category0_.name as name2_1_0_ from category category0_ where category0_.id=?\nHibernate: select author0_.id as id1_0_0_, author0_.name as name2_0_0_, author0_.nationality as national3_0_0_ from author author0_ where author0_.id=?\nHibernate: select category0_.id as id1_1_0_, category0_.name as name2_1_0_ from category category0_ where category0_.id=?\nHibernate: select author0_.id as id1_0_0_, author0_.name as name2_0_0_, author0_.nationality as national3_0_0_ from author author0_ where author0_.id=?\nHibernate: select author0_.id as id1_0_0_, author0_.name as name2_0_0_, author0_.nationality as national3_0_0_ from author author0_ where author0_.id=?\nHibernate: select author0_.id as id1_0_0_, author0_.name as name2_0_0_, author0_.nationality as national3_0_0_ from author author0_ where author0_.id=?\n

    Si te fijas ha generado una query SQL para filtrar los Game, pero luego cuando ha intentado construir los objetos Java, ha tenido que lanzar una serie de queries para recuperar los diferentes Author y Category a trav\u00e9s de sus id. Obviamente Spring Data es muy lista y cachea los resultados obtenidos para no tener que recuperarlos n veces, pero aun as\u00ed, lanza unas cuantas consultas. Esto penaliza el rendimiento de nuestra operaci\u00f3n, ya que tiene que lanzar n queries a BBDD que, aunque son muy \u00f3ptimas, incrementan unos milisegundos el tiempo total.

    Para evitar esta circunstancia, disponemos de la anotaci\u00f3n denominada @EnitityGraph la cual proporciona directrices a Spring Data sobre la forma en la que deseamos realizar la consulta, permitiendo que realice agrupaciones y uniones de tablas en una \u00fanica query que, aun siendo mas compleja, en muchos casos el rendimiento es mucho mejor que realizar m\u00faltiples interacciones con la BBDD.

    Siguiendo el ejemplo anterior podr\u00edamos utilizar la anotaci\u00f3n de esta forma:

    @Query(\"select g from Game g where (:title is null or g.title like '%'||:title||'%') and (:category is null or g.category.id = :category)\")\n@EntityGraph(attributePaths = {\"category\", \"author\"})\nList<Game> find(@Param(\"title\") String title, @Param(\"category\") Long category);\n

    Donde le estamos diciendo a Spring Data que cuando realice la query, haga el cruce con las propiedades category y author, que a su vez son entidades y por tanto mapean dos tablas de BBDD. El resultado es el siguiente:

    Hibernate: select game0_.id as id1_2_0_, category1_.id as id1_1_1_, author2_.id as id1_0_2_, game0_.age as age2_2_0_, game0_.author_id as author_i4_2_0_, game0_.category_id as category5_2_0_, game0_.title as title3_2_0_, category1_.name as name2_1_1_, author2_.name as name2_0_2_, author2_.nationality as national3_0_2_ from game game0_ left outer join category category1_ on game0_.category_id=category1_.id left outer join author author2_ on game0_.author_id=author2_.id where (? is null or game0_.title like ('%'||?||'%')) and (? is null or game0_.category_id=?)\n

    Una \u00fanica query, que es m\u00e1s compleja que la anterior, ya que hace dos cruces con tablas de BBDD, pero que nos evita tener que lanzar n queries diferentes para recuperar Author y Category.

    Generalmente, el uso de @EntityGraph acelera mucho los resultados y es muy recomendable utilizarlo para realizar los cruces inline. Se puede utilizar tanto con @Query como con Derived Query Methods. Puedes leer m\u00e1s informaci\u00f3n en este peque\u00f1o tutorial de Baeldung.

    "},{"location":"appendix/jpa/#alternativa-de-streams","title":"Alternativa de Streams","text":"

    A partir de Java 8 disponemos de los Java Streams. Se trata de una herramienta que nos permite multitud de opciones relativas tratamiento y trasformaci\u00f3n de los datos manejados.

    En este apartado \u00fanicamente se menciona debido a que en muchas ocasiones cuando nos enfrentamos a consultas complejas, puede ser beneficioso evitar ofuscar las consultas y realizar las trasformaciones necesarias mediante los Streams.

    Un ejemplo de uso pr\u00e1ctico podr\u00eda ser, evitar usar la cl\u00e1usula IN de SQL en una determinada consulta que podr\u00eda penalizar notablemente el rendimiento de las consultas. En vez de eso se podr\u00eda utilizar el m\u00e9todo de JAVA filter sobre el conjunto de elementos para obtener el mismo resultado.

    Puedes leer m\u00e1s informaci\u00f3n en el tutorial de Baeldung.

    "},{"location":"appendix/jpa/#specifications","title":"Specifications","text":"

    En algunos casos puede ocurrir que con las herramientas descritas anteriormente no tengamos suficiente alcance, bien porque las definiciones de los m\u00e9todos se complican y alargan demasiado o debido a que la consulta es demasiado gen\u00e9rica como para realizarlo de este modo.

    Para este caso se dispone de las Specifications que nos proveen de una forma de escribir consultas reutilizables mediante una API que ofrece una forma fluida de crear y combinar consultas complejas.

    Un ejemplo de caso de uso podr\u00eda ser un CRUD de una determinada entidad que debe poder filtrar por todos los atributos de esta, donde el tipo de filtrado viene especificado en la propia consulta y no siempre es requerido. En este caso no podr\u00edamos construir una consulta basada en definir un determinado m\u00e9todo ya no conocemos de ante mano que filtros ni que atributos vamos a recibir y deberemos recurrir al uso de las Specifications.

    Puedes leer m\u00e1s informaci\u00f3n en el tutorial de Baeldung.

    "},{"location":"appendix/multilanguage/","title":"Multidioma","text":"

    En las aplicaciones reales, un detalle de suma importancia es la capacidad de soportar m\u00faltiples idiomas para llegar a una audiencia m\u00e1s amplia. Afortunadamente, tanto en el frontend como en el backend, existen herramientas y bibliotecas que facilitan la implementaci\u00f3n de esta funcionalidad.

    En especial nos centraremos en el frontend.

    "},{"location":"appendix/multilanguage/#backend","title":"Backend","text":"

    La comunicaci\u00f3n del backend ha de ser agn\u00f3stica al idioma del cliente (frontend). Para conseguir este resultado, nos comunicaremos con c\u00f3digos de error (o \u00e9xito). Estableceremos un est\u00e1ndar de c\u00f3digos que el frontend interpretar\u00e1 y mostrar\u00e1 el mensaje adecuado en funci\u00f3n del idioma seleccionado por el usuario.

    Para cualquier informaci\u00f3n extra, por ejemplo, indicar el n\u00famero m\u00e1ximo de juegos que pueden prestarse. Acordaremos un campo asociado al c\u00f3digo de error que contendr\u00e1 dicha informaci\u00f3n adicional.

    "},{"location":"appendix/multilanguage/#frontend","title":"Frontend","text":"

    Para el frontend, existen varias bibliotecas que facilitan la implementaci\u00f3n de la funcionalidad multiling\u00fce. Usaremos el est\u00e1ndar i18n de Angular, que permite definir archivos de traducci\u00f3n para cada idioma soportado. (Gu\u00eda oficial de Internalizaci\u00f3n)[https://angular.dev/guide/i18n]

    Los t\u00e9rminos que has de tener en cuenta en esta nueva etapa son:

    • i18n: Abreviatura de \"internationalization\" (internacionalizaci\u00f3n), donde 18 representa el n\u00famero de letras entre la 'i' y la 'n'.
    • locale: Configuraci\u00f3n regional que define el idioma y las convenciones culturales (formato de fecha, moneda, etc.) para una regi\u00f3n espec\u00edfica. Ej. es-ES para espa\u00f1ol de Espa\u00f1a, en-US para ingl\u00e9s de Estados Unidos.
    "},{"location":"appendix/multilanguage/#material","title":"Material","text":"

    Angular Material tambi\u00e9n soporta la internacionalizaci\u00f3n. Para ello, es necesario importar los m\u00f3dulos de localizaci\u00f3n correspondientes y configurar el proveedor de localizaci\u00f3n en el m\u00f3dulo principal de la aplicaci\u00f3n.

    import { MAT_DATE_LOCALE } from \"@angular/material/core\";\n@NgModule({\n  providers: [\n    { provide: MAT_DATE_LOCALE, useValue: \"es-ES\" }, // Cambia 'es-ES' por el locale deseado\n  ],\n})\nexport class AppModule {}\n

    Esto evitar\u00e1 que tengamos un Items per page: 10 en ingl\u00e9s en las tablas de paginaci\u00f3n de Angular Material.

    "},{"location":"appendix/multilanguage/#estructura-de-un-fichero-de-traducciones","title":"Estructura de un fichero de traducciones","text":"

    Crearemos ficheros de traducciones que contendr\u00e1n keys (los c\u00f3digos de acceso) y sus correspondientes valores en cada idioma. Por ejemplo, podr\u00edamos tener un fichero en.json para ingl\u00e9s y otro es.json para espa\u00f1ol.

    • translations
      • en.json
      • es.json

    Aunque solemos preferir nombres m\u00e1s expl\u00edcitos:

    • translations
      • en-US.json
      • es-ES.json

    Un fichero de traducciones en Angular suele tener la siguiente estructura:

    {\n  \"VIEWS\": {\n    \"HOME\": {\n      \"TITLE\": \"T\u00edtulo de la aplicaci\u00f3n\",\n      \"WELCOME_MESSAGE\": \"Bienvenido a nuestra aplicaci\u00f3n\",\n      \"LOGIN\": \"Iniciar sesi\u00f3n\",\n      \"LOGOUT\": \"Cerrar sesi\u00f3n\"\n    },\n    \"DASHBOARD\": {\n      \"TITLE\": \"Panel de control\",\n      \"STATISTICS\": \"Estad\u00edsticas\",\n      \"SETTINGS\": \"Configuraciones\"\n    }\n  }\n}\n

    Ahora ya tienes todo lo necesario para crear aplicaciones multidioma. \u00a1Manos a la obra!

    "},{"location":"appendix/rest/","title":"Breve detalle sobre REST","text":"

    Antes de empezar vamos a hablar de operaciones REST. Estas operaciones son el punto de entrada a nuestra aplicaci\u00f3n y se pueden diferenciar dos claros elementos:

    • Ruta hacia el recurso, lo que viene siendo la URL.
    • Acci\u00f3n a realizar sobre el recurso, lo que viene siendo la operaci\u00f3n HTTP o el verbo.
    "},{"location":"appendix/rest/#ruta-del-recurso","title":"Ruta del recurso","text":"

    La ruta del recurso nos indica entre otras cosas, el endpoint y su posible jerarqu\u00eda sobre la que se va a realizar la operaci\u00f3n. Debe tener una ra\u00edz de recurso y si se requiere navegar por el recursos, la jerarqu\u00eda ir\u00e1 separada por barras. La URL nunca deber\u00eda tener verbos o acciones solamente recursos, identificadores o atributos. Por ejemplo en nuestro caso de Categor\u00edas, ser\u00edan correctas las siguientes rutas:

    • /category
    • /category/3
    • /category/?name=Dados

    Sin embargo, no ser\u00edan del todo correctas las rutas:

    • /getCategory
    • /findCategories
    • /saveCategory
    • /category/save

    A menudo, se integran datos identificadores o atributos de b\u00fasqueda dentro de la propia ruta. Podr\u00edamos definir la operaci\u00f3n category/3 para referirse a la Categor\u00eda con ID = 3, o category/?name=Dados para referirse a las categor\u00edas con nombre = Dados. A veces, estos datos tambi\u00e9n pueden ir como atributos en la URL o en el cuerpo de la petici\u00f3n, aunque se recomienda que siempre que sean identificadores vayan determinados en la propia URL.

    Si el dominio categor\u00eda tuviera hijos o relaciones con alg\u00fan otro dominio se podr\u00eda a\u00f1adir esas jerarqu\u00eda a la URL. Por ejemplo podr\u00edamos tener category/3/child/2 para referirnos al hijo de ID = 2 que tiene la Categor\u00eda de ID = 3, y as\u00ed sucesivamente.

    "},{"location":"appendix/rest/#accion-sobre-el-recurso","title":"Acci\u00f3n sobre el recurso","text":"

    La acci\u00f3n sobre el recurso se determina mediante la operaci\u00f3n o verbo HTTP que se utiliza en el endpoint. Los verbos m\u00e1s usados ser\u00edan:

    • GET. Cuando se quiere recuperar un recursos.
    • POST. Cuando se quiere crear un recurso. Aunque a menudo se utiliza para realizar otras acciones de b\u00fasqueda o validaci\u00f3n.
    • PUT. Cuando se quiere actualizar o modificar un recurso. Aunque a menudo se utiliza una sola operaci\u00f3n para crear o actualizar. En ese caso se utilizar\u00eda solamente POST.
    • DELETE. Cuando se quiere eliminar un recurso.

    De esta forma tendr\u00edamos:

    • GET /category/3. Realizar\u00eda un acceso para recuperar la categor\u00eda 3.
    • POST o PUT /category/3. Realizar\u00eda un acceso para crear o modificar la categor\u00eda 3. Los datos a modificar deber\u00edan ir en el body.
    • DELETE /category/3. Realizar\u00eda un acceso para borrar la categor\u00eda 3.
    • GET /category/?name=Dados. Realizar\u00eda un acceso para recuperar las categor\u00edas que tengan nombre = Dados.

    Excepciones a la regla

    A veces hay que ejecutar una operaci\u00f3n que no es 'estandar' en cuanto a verbos HTTP. Para ese caso, deberemos clarificar en la URL la acci\u00f3n que se debe realizar y si vamos a enviar datos deber\u00eda ser de tipo POST mientras que si simplemente se requiere una contestaci\u00f3n sin enviar datos ser\u00e1 de tipo GET. Por ejemplo POST /category/3/validate realizar\u00eda un acceso para ejecutar una validaci\u00f3n sobre los datos enviados en el body de la categor\u00eda 3.

    "},{"location":"appendix/tdd/","title":"TDD (Test Driven Development)","text":"

    Se trata de una pr\u00e1ctica de programaci\u00f3n que consiste en escribir primero las pruebas (generalmente unitarias), despu\u00e9s escribir el c\u00f3digo fuente que pase la prueba satisfactoriamente y, por \u00faltimo, refactorizar el c\u00f3digo escrito.

    Este ciclo se suele representar con la siguiente imagen:

    Con esta pr\u00e1ctica se consigue entre otras cosas: un c\u00f3digo m\u00e1s robusto, m\u00e1s seguro, m\u00e1s mantenible y una mayor rapidez en el desarrollo.

    Los pasos que se siguen son:

    1. Primero hay que escribir el test o los tests que cubran la funcionalidad que voy a implementar. Los test no solo deben probar los casos correctos, sino que deben probar los casos err\u00f3neos e incluso los casos en los que se provoca una excepci\u00f3n. Cuantos m\u00e1s test hagas, mejor probada y m\u00e1s robusta ser\u00e1 tu aplicaci\u00f3n.

      Adem\u00e1s, como efecto colateral, al escribir el test est\u00e1s pensando el dise\u00f1o de c\u00f3mo va a funcionar la aplicaci\u00f3n. En vez de liarte a programar como loco, te est\u00e1s forzando a pensar primero y ver cual es la mejor soluci\u00f3n. Por ejemplo para implementar una operaci\u00f3n de calculadora primero piensas en qu\u00e9 es lo que necesitar\u00e1s: una clase Calculadora con un m\u00e9todo que se llame Suma y que tenga dos par\u00e1metros.

    2. El segundo paso una vez tengo definido el test, que evidentemente fallar\u00e1 (e incluso a menudo ni siquiera compilar\u00e1), es implementar el c\u00f3digo necesario para que los tests funcionen. Aqu\u00ed muchas veces pecamos de querer implementar demasiadas cosas o pensando en que en un futuro necesitaremos modificar ciertas partes y lo dejamos ya preparado para ello. Hay que ir con mucho cuidado con las optimizaciones prematuras, a menudo no son necesarias y solo hacen que dificultar nuestro c\u00f3digo.

      Piensa en construir el m\u00ednimo c\u00f3digo que haga que tus tests funcionen correctamente. Adem\u00e1s, no es necesario que sea un c\u00f3digo demasiado purista y limpio.

    3. El \u00faltimo paso y a menudo el m\u00e1s olvidado es el Refactor. Una vez te has asegurado que tu c\u00f3digo funciona y que los tests funcionan correctamente (ojo no solo los tuyos sino todos los que ya existan en la aplicaci\u00f3n) llega el paso de sacarle brillo a tu c\u00f3digo.

      En este paso tienes que intentar mejorar tu c\u00f3digo, evitar duplicidades, evitar malos olores de programaci\u00f3n, eliminar posibles malos usos del lenguaje, etc. En definitiva que tu c\u00f3digo se lea y se entienda mejor.

    Si seguimos estos pasos a la hora de programar, nuestra aplicaci\u00f3n estar\u00e1 muy bien testada. Cada vez que hagamos un cambio tendremos una certeza muy elevada, de forma r\u00e1pida y sencilla, de si la aplicaci\u00f3n sigue funcionando o hemos roto algo. Y lo mejor de todo, las implementaciones que hagamos estar\u00e1n bien pensadas y dise\u00f1adas y acotadas realmente a lo que necesitamos.

    "},{"location":"appendix/docker/docudocker/","title":"M\u00d3DULO 2: \u00bfQU\u00c9 ES DOCKER?","text":"

    Docker es una plataforma de contenedorizaci\u00f3n de c\u00f3digo abierto que simplifica el despliegue de aplicaciones empaquetando el software y sus dependencias en una unidad estandarizada llamada contenedor. A diferencia de las m\u00e1quinas virtuales tradicionales , los contenedores Docker comparten el n\u00facleo del sistema operativo anfitri\u00f3n, lo que los hace m\u00e1s eficientes y ligeros. Los contenedores garantizan que una aplicaci\u00f3n se ejecute de la misma forma en entornos de desarrollo, pruebas y producci\u00f3n. Esto reduce los problemas de compatibilidad y mejora la portabilidad entre varias plataformas. Debido a su flexibilidad y escalabilidad, Docker se ha convertido en una herramienta crucial en los flujos de trabajo modernos de DevOps y desarrollo nativo en la nube.

    "},{"location":"appendix/docker/docudocker/#modulo-3-arquitectura-docker","title":"M\u00d3DULO 3: ARQUITECTURA DOCKER","text":"

    Docker opera en un modelo cliente-servidor, consistiendo en varios componentes clave que trabajan juntos sin problemas.

    "},{"location":"appendix/docker/docudocker/#motor-de-docker","title":"Motor de Docker","text":"

    En el n\u00facleo de Docker est\u00e1 el Motor de Docker, que incluye:

    • Daemon de Docker: El servicio de fondo que se ejecuta en el host y gestiona la construcci\u00f3n, ejecuci\u00f3n y distribuci\u00f3n de contenedores Docker.
    • CLI de Docker: La interfaz de l\u00ednea de comandos utilizada para interactuar con el daemon de Docker.
    • API REST: Permite que aplicaciones remotas interact\u00faen con el daemon de Docker.
    "},{"location":"appendix/docker/docudocker/#componentes-de-docker","title":"Componentes de Docker","text":"

    Docker utiliza varios objetos para construir y ejecutar aplicaciones:

    • Im\u00e1genes: Plantillas de solo lectura utilizadas para crear contenedores.
    • Contenedores: Instancias ejecutables de im\u00e1genes.
    • Networks: Redes que facilitan la comunicaci\u00f3n entre contenedores y el mundo exterior.
    • Vol\u00famenes: Almacenamiento de datos persistente para contenedores.

    Entender esta arquitectura permite visualizar c\u00f3mo interact\u00faan los diferentes componentes y ayuda en la resoluci\u00f3n de problemas potenciales.

    "},{"location":"appendix/docker/docudocker/#beneficios-de-la-arquitectura-de-docker","title":"Beneficios de la Arquitectura de Docker","text":"
    • Aislamiento: Los contenedores se ejecutan en entornos aislados, asegurando consistencia en diferentes sistemas.
    • Portabilidad: Las im\u00e1genes de Docker pueden ejecutarse en cualquier sistema que soporte Docker, independientemente del SO subyacente.
    • Eficiencia: Los contenedores comparten el kernel del SO host, haci\u00e9ndolos ligeros en comparaci\u00f3n con las VMs tradicionales.
    "},{"location":"appendix/docker/docudocker/#docker-hub","title":"Docker Hub","text":"

    Docker Hub es un servicio de registro basado en la nube donde puedes encontrar y compartir im\u00e1genes de contenedores. Es un excelente recurso para principiantes para explorar varias im\u00e1genes pre-construidas.

    "},{"location":"appendix/docker/docudocker/#modulo-4-docker-vs-vm","title":"M\u00d3DULO 4: DOCKER vs VM","text":"Factores Docker M\u00e1quina virtual Arranque

    En segundos

    En minutos

    Disponibilidad

    Los contenedores docker preconstruidos est\u00e1n f\u00e1cilmente disponibles

    Las m\u00e1quinas virtuales listas para usar son dif\u00edciles de encontrar

    Recursos

    Menor uso de recursos

    M\u00e1s uso de recursos

    Almacenamiento

    Los contenedores son algo m\u00e1s ligeros (KBs/MBs)

    Las m\u00e1quinas virtuales tienen unos pocos GB

    Sistema operativo

    Cada contenedor puede compartir el sistema operativo

    Cada m\u00e1quina virtual tiene un sistema operativo independiente

    Movilidad

    Los contenedores se destruyen y se vuelven a crear en lugar de moverse

    Las m\u00e1quinas virtuales pueden moverse a nuevos hosts f\u00e1cilmente

    Se ejecuta en

    Los dockers hacen uso del motor de ejecuci\u00f3n.

    Las m\u00e1quinas virtuales hacen uso del hipervisor.

    Uso

    Docker tiene un medio de operaci\u00f3n complejo que se compone de herramientas de terceros y administradas por Docker.

    Las herramientas son m\u00e1s sencillas de trabajar y f\u00e1ciles de usar.

    Gesti\u00f3n

    Los contenedores dejan de funcionar con la ejecuci\u00f3n del \"comando stop\"

    Las m\u00e1quinas virtuales siempre est\u00e1n en el estado de ejecuci\u00f3n de funcionamiento

    Control

    Las im\u00e1genes pueden ser interpretation controlled; tienen un registro original llamado Docker Hub.

    La VM no tiene un hub central; no est\u00e1n controlados

    Gesti\u00f3n de memoria Es m\u00e1s eficiente en la memoria. Es menos eficiente en la memoria. Aislamiento No dispone de un sistema de aislamiento, por lo que es muy propenso a los problemas. Cuenta con un eficiente mecanismo de aislamiento. Tiempo Es f\u00e1cil de implementar y lleva menos tiempo en comparaci\u00f3n con las m\u00e1quinas virtuales. Es un proceso largo. Por lo tanto, se necesita mucho tiempo para la implementaci\u00f3n. Facilidad de uso Es un poco dif\u00edcil de usar debido al complejo mecanismo de uso. Es f\u00e1cil de usar."},{"location":"appendix/docker/docudocker/#modulo-5-imagenes-docker-dockerfile","title":"M\u00d3DULO 5: IM\u00c1GENES DOCKER, DOCKERFILE","text":"

    Las im\u00e1genes Docker son los bloques de construcci\u00f3n fundamentales de los contenedores. Son plantillas inmutables, de s\u00f3lo lectura, que contienen todo lo necesario para ejecutar una aplicaci\u00f3n, incluido el sistema operativo, el c\u00f3digo de la aplicaci\u00f3n, el tiempo de ejecuci\u00f3n y las dependencias.

    Las im\u00e1genes se construyen utilizando un Dockerfile, que define las instrucciones para crear una imagen capa a capa.

    Las im\u00e1genes pueden almacenarse y recuperarse de registros de contenedores como Docker Hub.

    Aqu\u00ed tienes algunos comandos de ejemplo para trabajar con im\u00e1genes:

    docker build -t tu-nombre-de-imagen .: Genera una imagen y le da un nombre. docker image ls: Lista todas las im\u00e1genes disponibles en la m\u00e1quina local. docker pull nginx: Obt\u00e9n la \u00faltima imagen de Nginx de Docker Hub. docker rmi -f nginx: Eliminar una imagen de la m\u00e1quina local (forzado).

    "},{"location":"appendix/docker/docudocker/#modulo-6-contenedores-docker","title":"M\u00d3DULO 6: CONTENEDORES DOCKER","text":"

    Un contenedor Docker es una instancia en ejecuci\u00f3n de una imagen Docker. Cada contenedor tiene su propio sistema de archivos, red y espacio de procesos, pero comparte el n\u00facleo anfitri\u00f3n.

    Los contenedores siguen un ciclo de vida sencillo que incluye su creaci\u00f3n, inicio, parada y eliminaci\u00f3n. Aqu\u00ed tienes un desglose de los comandos comunes de gesti\u00f3n de contenedores:

    docker create o docker run: Crear un contenedor. docker start: Poner en marcha un contenedor. docker stop: Detener un contenedor. docker restart: Reiniciar un contenedor. docker rm: Borrar un contenedor. docker ps -a: Listar todos los contenedores.

    "},{"location":"appendix/docker/installdocker/","title":"M\u00d3DULO 1: INSTALACI\u00d3N DOCKER EN WINDOWS","text":"

    Para instalar la versi\u00f3n gratuita y open source de Docker Community Edition (CE) siga estos pasos:

    1. Instalar Ubuntu 24.04.1 LTS desde Microsoft Store: Como no est\u00e1 el WSL, al ejecutar, no funcionar\u00e1 y saldr\u00e1 un error, pero se solucionar\u00e1 en los pasos siguientes.
    2. Instalar PowerShell desde Microsoft Store.
    3. Instalar el Subsistema de Linux para Windows:
    4. Ejecuta el comando wsl --install
    5. Manda una petici\u00f3n para que te lo instalen. Cuando sea aprobada, deber\u00eda dejarte continuar. Ejemplo de lo que os sale, se elige la primera opci\u00f3n
    6. Despu\u00e9s

    7. Verificar la instalaci\u00f3n:

    8. Para ver si est\u00e1 instalado, ejecuta wsl \u2013version (es posible que necesites reiniciar el ordenador)

    9. Prueba wsl --status. Aqu\u00ed deber\u00eda indicar que \"Windows subsystem for Linux has no installed distributions\".

    10. Cuando abras Ubuntu, seguramente no funcione. De manera que tienes que reiniciar el PC y comprobar que ahora Ubuntu s\u00ed funciona.

    11. Introducir los siguientes comandos en Ubuntu.

    12. Si pide un usuario y contrase\u00f1a, poner la vuestra propia.

    13. Pod\u00e9is continuar con estos comandos:

      sudo apt update\nsudo apt install curl apt-transport-https ca-certificates software-properties-common\nsudo apt install docker.io -y\ncurl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg\necho \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null\nsudo apt update\nsudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\nsudo systemctl start docker  \n##? gpasswd -a $USER docker  \nsudo docker run hello-world\n

    14. En este o en el siguiente paso, te dar\u00e1 error por falta de permisos, no hay problema.

    15. Abrir Ubuntu y darle a ejecutar como administrador.
    16. Despu\u00e9s, tendr\u00e9is que pedir una solicitud para los permisos. sudo systemctl start docker sudo docker run hello-world

    17. Entrar en Ubuntu como administrador:

    18. Vuelve a ejecutar en Ubuntu sudo service docker start
    19. Ejecuta docker run hello-world

    20. Verificar Docker:

    21. Cuando ejecutes el comando anterior, deber\u00eda funcionar.
    22. Prueba docker ps -a para ver un listado de contenedores.
    "},{"location":"appendix/docker/summary/","title":"Resumen de la contenerizaci\u00f3n usando Docker","text":""},{"location":"appendix/docker/summary/#que-hemos-ganado-empleando-estas-herramientas","title":"\u00bfQu\u00e9 hemos ganado empleando estas herramientas?","text":"
    • Usar un lenguaje declarativo para facilitar las tareas de despliegue de aplicaciones.
    • Hacer m\u00e1s portables nuestros desarrollos en entorno local. Al menos estamos dando a los DevOps de preproducci\u00f3n, UAT y Producci\u00f3n una informaci\u00f3n adicional que les va a resultar \u00fatil cuando tengan que hacer los despliegues en sus entornos de producci\u00f3n.
    • Incrementar la productividad de los entornos de desarrollo locales a corto (porque es sencillo de usar), medio y largo plazo.
    • Facilitar y acortar la tarea de migraci\u00f3n de nuestros desarrollos a los entornos de alta productividad y econom\u00eda de escala en la nube.
    "},{"location":"appendix/docker/traindocker/","title":"M\u00d3DULO 7: HANDS-ON. \u00a1AHORA HAZLO T\u00da!","text":"

    Ahora vamos a construir im\u00e1genes de servicios y sobre estas im\u00e1genes lanzaremos contenedores en el entorno local del desarrollador.

    Teniendo ya instalados WSL, Docker Community y Docker Compose nos centramos en la parte pr\u00e1ctica.

    "},{"location":"appendix/docker/traindocker/#construir-imagenes-con-un-dockerfile","title":"Construir im\u00e1genes con un Dockerfile","text":"

    Empezamos creando una im\u00e1gen por cada servicio de nuestra aplicaci\u00f3n b\u00e1sica en microservicios basada en un backend con Spring Boot.

    Cada servicio requiere un fichero propio de nombre Dockerfile sin extensi\u00f3n, que queda situado en el directorio ra\u00edz del m\u00f3dulo y al mismo nivel que el fichero POM.

    Para el proyecto server-springboot-micros y m\u00f3dulo server-springboot-eureka un posible Dockerfile ser\u00eda:

    # Use the official Ubuntu 22.04 LTS base image\nFROM ubuntu:22.04\n\n# Install necessary packages\nRUN apt-get update && apt-get install -y \\\nopenjdk-19-jdk \\\nmaven \\\nwget \\\ncurl \\\ngnupg \\\n&& rm -rf /var/lib/apt/lists/*\n\n# Set the working directory\nWORKDIR /app\n\n# Copy the project files to the container\nCOPY . .\n\n# Build the project using Maven\nRUN mvn clean package\n\n# Expose the application port\nEXPOSE 8761\n\n# Run the Spring Boot application\nCMD [\"java\", \"-jar\", \"target/tutorial-eureka-0.0.1-SNAPSHOT.jar\"]\n
    Es un script b\u00e1sico para el service discovery de ejemplo, donde los comentarios de c\u00f3digo nos dan las explicaciones debidas.

    Para un m\u00f3dulo de reglas de negocio del mismo proyecto, su correspondiente Dockerfile b\u00e1sico podr\u00eda ser:

    # Use the official Ubuntu 22.04 LTS base image\nFROM ubuntu:22.04\n\n# Install necessary packages\nRUN apt-get update && apt-get install -y \\\n    openjdk-19-jdk \\\n    maven \\\n    wget \\\n    curl \\\n    gnupg \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Set the working directory\nWORKDIR /app\n\n# Copy the project files to the container\nCOPY . .\n\n# Build the project using Maven\nRUN mvn clean package\n\n# Expose the application port\nEXPOSE 8092\n\n# Run the Spring Boot application\nCMD [\"java\", \"-jar\", \"target/tutorial-author-0.0.1-SNAPSHOT.jar\"]\n
    Y de forma parecida el resto de los m\u00f3dulos del backend.

    Para el frontend, tomar en esta pr\u00e1ctica el m\u00f3dulo client-angular17 cuyo Dockerfile b\u00e1sico podr\u00eda ser:

    # Use the official Node.js image as the base image\nFROM node:18\n\n# Set the working directory inside the container\nWORKDIR /usr/src/app\n\n# Install Angular CLI globally\nRUN npm install -g @angular/cli@17\n\n# Copy package.json and package-lock.json to the working directory\nCOPY package*.json ./\n\n# Install project dependencies\nRUN npm install\n\n# Copy the rest of the application code to the working directory\nCOPY . .\n\n# Expose the port the app runs on\nEXPOSE 4200\n\n# Command to run the application in development mode\nCMD [\"ng\", \"serve\", \"--host\", \"0.0.0.0\"]\n
    "},{"location":"appendix/docker/traindocker/#desplegar-una-imagen-dentro-de-un-contenedor","title":"Desplegar una imagen dentro de un contenedor","text":"

    Teniendo en local las instalaciones hechas, no representa problema alguno, por ejemplo, para el caso del servicio eureka:

    docker build -t i-tutorial-eureka .\ndocker run -d -p 8761:8761 --name c-tutorial-eureka i-tutorial-eureka\n\ndocker logs c-tutorial-eureka\ndocker stop c-tutorial-eureka\ndocker start c-tutorial-eureka\ndocker rm c-tutorial-eureka\n

    que hace lo siguiente: 1. desde terminal situado en la ra\u00edz del m\u00f3dulo junto al Dockerfile del servicio, primero creamos la imagen i-tutorial-eureka, 2. lanzamos la creaci\u00f3n de su correspondiente contenedor de nombre c-tutorial-eureka y su ejecuci\u00f3n detached. 3. Por \u00faltimo, damos unos comandos para inspeccionar su log, pararlo, arrancarlo, eliminarlo cuando dejemos de necesitarlo.

    En el laptop corporativo, puede ocurrir que el build se detenga por timeout cuando descargue la imagen del SO. En tal caso, revise el estado de su VPN.

    Las im\u00e1genes y contenedores son ligeros para un servidor, pero no para un laptop corporativo, elimine los recursos que no est\u00e9 usando para no saturar su equipo.

    "},{"location":"appendix/docker/traindocker/#desplegar-un-conjunto-de-contenedores-que-se-comunican","title":"Desplegar un conjunto de contenedores que se comunican","text":"

    En lo sucesivo notar que en este tutorial estamos usando Version: 28.0.2 de Docker Community y Docker Compose version v2.34.0

    B\u00e1sicamente, si solo utilizamos m\u00f3dulos de negocio, cada uno en su Dockerfile correspondiente, no tenemos garantizado que todos ellos puedan comunicarse entre s\u00ed en la forma deseada.

    Adem\u00e1s de querer tener componentes separados que sean escalables, queremos asegurarnos de que los m\u00f3dulos puedan hablarse entre ellos, si deben hacerlo.

    Otro requerimiento que tenemos en este proyecto simple, es la necesidad de arrancar unos servicios antes que otros, por ejemplo: el service discovery debe iniciar primero, segundo el gateway, y luego el resto de los m\u00f3dulos con l\u00f3gica de negocio. Por \u00faltimo, si todos han arrancado bien y est\u00e1n perfectamente up, arrancamos el frontend.

    Para lograr estas necesidades, por otra parte, muy comunes en los proyectos basados en microservicios, tenemos disponible la herramienta docker-compose.

    docker-compose permite lanzar en un solo script todos los servicios/contenedores estableciendo el orden deseado de arranque y ejecuci\u00f3n, as\u00ed como, la configuraci\u00f3n de un network donde declarar qu\u00e9 m\u00f3dulos pueden hablar entre s\u00ed.

    Para el caso de nuestro proyecto:

    docker network create backend-network\ndocker compose up --build\n\ndocker network ls\ndocker compose ps\ndocker compose down\n
    Primero creamos una red donde puedan comunicarse los contenedores que deben comunicarse entre s\u00ed. Luego, en un comando ordenamos la interpretaci\u00f3n del script docker-compose.yml situado en la ra\u00edz del proyecto general, seguido del build de todas las im\u00e1genes declaradas, seguido del arranque en el orden dado, seguido del establecimiento de las comunicaciones declaradas, seguido del respaldo de los latidos solicitados. El resto son sencillos comandos para ver el estado de la red, de los contenedores, y para desarmar todos los contenedores pertenecientes al compose cuando dejemos de necesitarlos.

    A continuaci\u00f3n damos un posible docker-compose.yml de ejemplo, sigue siendo b\u00e1sico (aunque ya no tanto):

    services:\n  tutorial-eureka:\n    build:\n      context: ./server-springboot-micros/server-springboot-eureka\n      dockerfile: Dockerfile\n    ports:\n      - \"8761:8761\"\n    networks:\n      - backend-network\n    environment:\n      eureka.client.serviceUrl.defaultZone: http://tutorial-eureka:8761/eureka/\n    healthcheck:\n      test: \"curl -f http://tutorial-eureka:8761/actuator/health || exit 1\"\n      interval: 40s\n      timeout: 10s\n      retries: 3\n\n  tutorial-gateway:\n    build:\n      context: ./server-springboot-micros/server-springbbot-gateway\n      dockerfile: Dockerfile\n    ports:\n      - \"8080:8080\"\n    networks:\n      - backend-network\n    depends_on:\n      tutorial-eureka:\n        condition: service_healthy\n    environment:\n      eureka.client.serviceUrl.defaultZone: http://tutorial-eureka:8761/eureka/\n    healthcheck:\n      test: \"curl -f http://tutorial-gateway:8080/actuator/health || exit 1\"\n      interval: 40s\n      timeout: 10s\n      retries: 3\n\n  tutorial-category:\n    build:\n      context: ./server-springboot-micros/server-springboot-category\n      dockerfile: Dockerfile\n    ports:\n      - \"8091:8091\"\n    networks:\n      - backend-network\n    depends_on:\n      tutorial-eureka:\n        condition: service_healthy\n      tutorial-gateway:\n        condition: service_healthy\n    environment:\n      eureka.client.serviceUrl.defaultZone: http://tutorial-eureka:8761/eureka/\n    healthcheck:\n      test: \"curl -f http://tutorial-category:8091/actuator/health || exit 1\"\n      interval: 40s\n      timeout: 10s\n      retries: 3\n\n  tutorial-author:\n    build:\n      context: ./server-springboot-micros/server-springboot-author\n      dockerfile: Dockerfile\n    ports:\n      - \"8092:8092\"\n    networks:\n      - backend-network\n    depends_on:\n      tutorial-eureka:\n        condition: service_healthy\n      tutorial-gateway:\n        condition: service_healthy\n    environment:\n      eureka.client.serviceUrl.defaultZone: http://tutorial-eureka:8761/eureka/\n    healthcheck:\n      test: \"curl -f http://tutorial-author:8092/actuator/health || exit 1\"\n      interval: 40s\n      timeout: 10s\n      retries: 3\n\n  tutorial-game:\n    build:\n      context: ./server-springboot-micros/server-springboot-game\n      dockerfile: Dockerfile\n    ports:\n      - \"8093:8093\"\n    networks:\n      - backend-network\n    depends_on:\n      tutorial-eureka:\n        condition: service_healthy\n      tutorial-gateway:\n        condition: service_healthy\n      tutorial-category:\n        condition: service_healthy\n      tutorial-author:\n        condition: service_healthy\n    environment:\n      eureka.client.serviceUrl.defaultZone: http://tutorial-eureka:8761/eureka/\n    healthcheck:\n      test: \"curl -f http://tutorial-game:8093/actuator/health || exit 1\"\n      interval: 40s\n      timeout: 10s\n      retries: 3\n\n  tutorial-front:\n    build:\n      context: ./client-angular17\n      dockerfile: Dockerfile\n    ports:\n      - \"4200:4200\"\n    networks:\n      - backend-network\n    depends_on:\n      tutorial-eureka:\n        condition: service_healthy\n      tutorial-gateway:\n        condition: service_healthy\n      tutorial-category:\n        condition: service_healthy\n      tutorial-author:\n        condition: service_healthy\n      tutorial-game:\n        condition: service_healthy\n    environment:\n      eureka.client.serviceUrl.defaultZone: http://tutorial-eureka:8761/eureka/\n    healthcheck:\n      test: \"curl -f http://tutorial-front:4200/actuator/health || exit 1\"\n      interval: 40s\n      timeout: 10s\n      retries: 3\n\nnetworks:\n  backend-network:\n    driver: bridge\n
    Para lograr la orquestaci\u00f3n/sincronizaci\u00f3n/comunicaci\u00f3n deseada es mejor que todo est\u00e9 en su sitio con el orden debido.

    "},{"location":"appendix/docker/traindocker/#todo-y-practicar","title":"TODO y practicar","text":"
    1. Como pr\u00e1ctica, queremos duplicar nuestro frontend b\u00e1sico de manera que tengamos dos portales muy similares pero distintos.

    2. Es decir, que el Front1 ejecute en un contenedor y el Front2 ejecute en otro contenedor.

    3. Adem\u00e1s, ambos frontales se comunican con el mismo backend, el nuestro.
    4. Crea agentes \"cliente\" que solo accedan a uno de los frontales, crea un agente \"admin\" que acceda a la network que agrupa al resto de networks.

    5. [ ] Sugerencia: crea tres networks, una para los frontales, otra para el backend, y una tercera que agrupe las dos anteriores.

    "},{"location":"appendix/springbatch/clean/","title":"Limpieza - Spring Batch","text":"

    Ya tenemos todo configurado de los pasos anteriores asi que proseguimos con el \u00faltimo ejemplo.

    "},{"location":"appendix/springbatch/clean/#caso-de-uso","title":"Caso de Uso","text":"

    Este es un caso de uso nuevo para poner en pr\u00e1ctica el uso de Tasklet.

    "},{"location":"appendix/springbatch/clean/#que-vamos-a-hacer","title":"\u00bfQu\u00e9 vamos a hacer?","text":"

    Vamos a implementar un batch que limpie de ficheros un determinado directorio. Esta vez y dado que no necesitamos realizar ning\u00fan tipo de lectura ni trasformaci\u00f3n ni escritura y queremos hacerlo todo al mismo tiempo, es buen momento para utilizar un Tasklet.

    "},{"location":"appendix/springbatch/clean/#como-lo-vamos-a-hacer","title":"\u00bfC\u00f3mo lo vamos a hacer?","text":"

    A diferencia de los casos anteriores seguiremos el esquema de funcionamiento de tasklet de un proceso batch que hemos visto en la parte de introducci\u00f3n:

    • Tasklet: Eliminar\u00e1 todos los ficheros del directorio.
    • Step: El paso que contiene el tasklet que van a realizar la funcionalidad.
    • Job: La tarea que contiene los pasos definidos.
    "},{"location":"appendix/springbatch/clean/#codigo","title":"C\u00f3digo","text":""},{"location":"appendix/springbatch/clean/#tasklet","title":"Tasklet","text":"

    En primer lugar, vamos a crear CleanTasklet dentro del package com.ccsw.tutorialbatch.tasklet.

    CleanTasklet.java
    import java.io.File;\n\npublic class CleanTasklet implements Tasklet, InitializingBean {\n\n    private Resource directory;\n\n    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {\n        File dir = directory.getFile();\n\n        File[] files = dir.listFiles();\n\n        for (File file : files) {\n            boolean deleted = file.delete();\n            if (!deleted) {\n                throw new UnexpectedJobExecutionException(\"Could not delete file \" + file.getPath());\n            }\n        }\n        return RepeatStatus.FINISHED;\n    }\n\n    public void setDirectoryResource(Resource directory) {\n\n        this.directory = directory;\n    }\n\n    public void afterPropertiesSet() throws Exception {\n\n        if (directory == null) {\n            throw new UnexpectedJobExecutionException(\"Directory must be set\");\n        }\n    }\n}\n

    La implementaci\u00f3n de la interface Tasklet consiste en sobreescribir el m\u00e9todo execute de forma muy similar como lo hac\u00edamos en los Processors. En este m\u00e9todo emplazamos nuestra l\u00f3gica de negocio que b\u00e1sicamente consiste en borrar todos los ficheros que se encuentren en el directorio proporcionado como atributo.

    "},{"location":"appendix/springbatch/clean/#step-y-job","title":"Step y Job","text":"

    Posteriormente, como en el caso anterior, emplazamos la configuraci\u00f3n junto al resto de beans dentro del package com.ccsw.tutorialbatch.config.

    CleanBatchConfiguration.java
    package com.ccsw.tutorialbatch.config;\n\nimport com.ccsw.tutorialbatch.tasklet.CleanTasklet;\nimport org.springframework.batch.core.Job;\nimport org.springframework.batch.core.Step;\nimport org.springframework.batch.core.job.builder.JobBuilder;\nimport org.springframework.batch.core.launch.support.RunIdIncrementer;\nimport org.springframework.batch.core.repository.JobRepository;\nimport org.springframework.batch.core.step.builder.StepBuilder;\nimport org.springframework.batch.core.step.tasklet.Tasklet;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.io.FileSystemResource;\nimport org.springframework.transaction.PlatformTransactionManager;\n\n@Configuration\npublic class CleanBatchConfiguration {\n\n    @Bean\n    public Tasklet taskletClean() {\n        CleanTasklet tasklet = new CleanTasklet();\n\n        tasklet.setDirectoryResource(new FileSystemResource(\"target/test-outputs\"));\n\n        return tasklet;\n    }\n\n    @Bean\n    public Step step1Clean(JobRepository jobRepository, PlatformTransactionManager transactionManager, Tasklet taskletClean) {\n        return new StepBuilder(\"step1Clean\", jobRepository)\n                .tasklet(taskletClean, transactionManager)\n                .build();\n    }\n\n    @Bean\n    public Job jobClean(JobRepository jobRepository, Step step1Clean) {\n        return new JobBuilder(\"jobClean\", jobRepository)\n                .incrementer(new RunIdIncrementer())\n                .start(step1Clean)\n                .build();\n    }\n\n}\n
    • Tasklet: El bean del Tasklet que hemos creado anteriormente.
    • Step: La creaci\u00f3n del Step se realiza mediante \u00e9l StepBuilder al que \u00fanicamente le a\u00f1adimos el Tasklet que se va a ejecutar de forma at\u00f3mica.
    • Job: Finalmente, debemos definir \u00e9l Job que ser\u00e1 lo que se ejecute al lanzar nuestro proceso. La creaci\u00f3n se hace mediante el builder correspondiente como en los casos anteriores.
    "},{"location":"appendix/springbatch/clean/#pruebas","title":"Pruebas","text":"

    Ahora ya tenemos varios Jobs en nuestro batch por lo que debemos especificar en el arranque cu\u00e1l queremos ejecutar.

    Como en el caso anterior pasamos como VM option la siguiente propiedad en el arranque de la aplicaci\u00f3n:

    -Dspring.batch.job.name=jobClean\n

    Hecho esto y ejecutado el batch, podremos ver la traza de la ejecuci\u00f3n en nuestro log y que el fichero generado en el target del proyecto de la ejecuci\u00f3n del batch de autores ya no est\u00e1.

    Job: [SimpleJob: [name=jobClean]] launched with the following parameters: [{'run.id':'{value=1, type=class java.lang.Long, identifying=true}'}]\nExecuting step: [step1Clean]\nStep: [step1Clean] executed in 9ms\nJob: [SimpleJob: [name=jobClean]] completed with the following parameters: [{'run.id':'{value=1, type=class java.lang.Long, identifying=true}'}] and the following status: [COMPLETED] in 23ms\n
    "},{"location":"appendix/springbatch/exercise/","title":"Ahora hazlo t\u00fa!","text":"

    Ahora vamos a ver si has comprendido bien el tutorial. \u00a1Vamos alla!

    "},{"location":"appendix/springbatch/exercise/#exportacion-de-juegos-a-fichero","title":"Exportaci\u00f3n de juegos a fichero","text":""},{"location":"appendix/springbatch/exercise/#requisitos","title":"Requisitos","text":"

    En este ejercicio vamos a simular la exportaci\u00f3n de datos desde una tabla de base de datos a fichero.

    El objetivo es que en funci\u00f3n del n\u00famero de stock de un determinado juego, generemos un fichero con su nombre y si el juego est\u00e1 disponible.

    Par ello debemos tener una tabla de juegos con los siguientes atributos:

    • Identificador
    • T\u00edtulo
    • Edad recomendada
    • Stock

    El proceso batch debe consultar los registros y convertirlos a la siguiente estructura:

    • T\u00edtulo: T\u00edtulo del juego (el mismo que en la tabla de BBDD).
    • Disponibilidad: Si el stock es mayor que cero estar\u00e1 disponible y si es cero debera aparecer que no est\u00e1 disponible.

    Una vez realizada la conversion, se debe escribir dicha informaci\u00f3n a fichero y guardarlo en el target del proyecto.

    "},{"location":"appendix/springbatch/exercise/#consejos","title":"Consejos","text":"

    Para empezar te dar\u00e9 unos consejos:

    • Recuerda crear la tabla de la BBDD y sus datos.
    • Intenta re-aprovechar lo que hemos aprendido en los ejemplos.
    • Consulta la documentaci\u00f3n para utilizar un Reader apropiado para la lectura desde BBDD.
    • Date cuenta de que el Processor que necesitas es algo m\u00e1s complejo esta vez y necesitaras m\u00e1s de un modelo diferente.
    "},{"location":"appendix/springbatch/exercise/#ya-has-terminado","title":"\u00bfYa has terminado?","text":"

    Si has llegado a este punto es porque ya tienes terminado el tutorial. Por favor no te olvides de subir los proyectos a alg\u00fan repositorio Github propio (puedes revisar el anexo Tutorial b\u00e1sico de Git) y av\u00edsarnos para que podamos echarle un ojo y darte sugerencias y feedback .

    Si es una formaci\u00f3n ligada a proyecto, que tu responsable nos contacte para que podamos darle prioridad al feedback.

    "},{"location":"appendix/springbatch/filetodb/","title":"Categor\u00eda - Spring Batch","text":"

    Al igual que el tutorial b\u00e1sico de Spring Boot, debemos configurar el entorno y crear el proyecto.

    Para la configuraci\u00f3n del entorno nos remitimos a la gu\u00eda de instalaci\u00f3n donde se detalla el proceso de configuraci\u00f3n del Entorno de desarrollo

    Todos los pasos son exactamente iguales, lo \u00fanico que va a variar es la creaci\u00f3n del proyecto desde Spring Initializr:

    • Tipo de proyecto: Maven
    • Lenguage: Java
    • Versi\u00f3n Spring boot: 3.2.2 (o alguna similar)
    • Group: com.ccsw
    • ArtifactId: tutorial-batch
    • Versi\u00f3n Java: 17 (o similar)
    • Dependencias: Spring Batch, H2 Database

    Esto nos generar\u00e1 un proyecto que ya vendr\u00e1 configurado con Spring Batch y H2 para crear una BBDD en memoria de ejemplo con la que trabajaremos durante el tutorial.

    Esta parte de tutorial es una ampliaci\u00f3n de la parte de backend con Spring Boot, por tanto, no se ve a enfocar en las partes b\u00e1sicas aprendidas previamente, sino que se va a explicar el funcionamiento de los procesos batch.

    "},{"location":"appendix/springbatch/filetodb/#caso-de-uso","title":"Caso de Uso","text":"

    En este ejemplo no podemos seguir los mismos casos de uso que de los ejemplos del tutorial de Spring Boot, ya que sus requisitos no son v\u00e1lidos para implementarse como un proceso batch por lo que vamos a mantener las mismas entidades pero imaginar casos de uso diferentes.

    "},{"location":"appendix/springbatch/filetodb/#que-vamos-a-hacer","title":"\u00bfQu\u00e9 vamos a hacer?","text":"

    Vamos a implementar un batch para leer un fichero de Categorias e insertar los registros le\u00eddos en Base de Datos.

    "},{"location":"appendix/springbatch/filetodb/#como-lo-vamos-a-hacer","title":"\u00bfC\u00f3mo lo vamos a hacer?","text":"

    Seguiremos el esquema de funcionamiento habitual de un proceso batch que hemos visto en la parte de introducci\u00f3n:

    • ItemReader: Se va a leer de un fichero y convertir los registros le\u00eddos al modelo de Category.
    • ItemProcessor: Va a procesar todos los registros convirtiendo los textos a may\u00fasculas.
    • ItemWriter: Va a insertar los registros en la BBDD.
    • Step: El paso que contiene los elementos que van a realizar la funcionalidad.
    • Job: La tarea que contiene los pasos definidos.
    "},{"location":"appendix/springbatch/filetodb/#codigo","title":"C\u00f3digo","text":""},{"location":"appendix/springbatch/filetodb/#modelo","title":"Modelo","text":"

    En primer lugar, vamos a crear el modelo dentro del package com.ccsw.tutorialbatch.model. En este caso no trabajamos con entidades, ya que ahora son simples estructuras de datos.

    Category.java
    package com.ccsw.tutorialbatch.model;\n\npublic class Category {\n\n    private String name;\n    private String type;\n    private String characteristics;\n\n    public Category() {\n    }\n\n    public Category(String name, String type, String characteristics) {\n        this.name = name;\n        this.type = type;\n        this.characteristics = characteristics;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getType() {\n        return type;\n    }\n\n    public void setType(String type) {\n        this.type = type;\n    }\n\n    public String getCharacteristics() {\n        return characteristics;\n    }\n\n    public void setCharacteristics(String characteristics) {\n        this.characteristics = characteristics;\n    }\n\n    @Override\n    public String toString() {\n        return \"Category [name=\" + getName() + \", type=\" + getType() + \", characteristics=\" + getCharacteristics() + \"]\";\n    }\n\n}\n
    "},{"location":"appendix/springbatch/filetodb/#reader","title":"Reader","text":"

    Ahora, emplazamos \u00e9l Reader en la clase donde posteriormente a\u00f1adiremos la configuraci\u00f3n junto al resto de beans, dentro del package com.ccsw.tutorialbatch.config.

    CategoryBatchConfiguration.java
    package com.ccsw.tutorialbatch.config;\n\n...\n\n@Configuration\npublic class CategoryBatchConfiguration {\n\n    @Bean\n    public ItemReader<Category> readerCategory() {\n        return new FlatFileItemReaderBuilder<Category>().name(\"categoryItemReader\")\n                .resource(new ClassPathResource(\"category-list.csv\"))\n                .delimited()\n                .names(new String[] { \"name\", \"type\", \"characteristics\" })\n                .fieldSetMapper(new BeanWrapperFieldSetMapper<>() {{\n                    setTargetType(Category.class);\n                }})\n                .build();\n    }\n\n}\n

    Para la ingesta de datos vamos a hacer uso de FlatFileItemReader que nos proporciona Spring Batch. Como se puede observar se le proporciona el fichero a leer y el mapeo a la clase que deseamos. Aqu\u00ed el cat\u00e1logo de Readers que proporciona Spring Batch.

    "},{"location":"appendix/springbatch/filetodb/#processor","title":"Processor","text":"

    Posteriormente, emplazamos \u00e9l Processor dentro del package com.ccsw.tutorialbatch.processor.

    CategoryItemProcessor.java
    package com.ccsw.tutorialbatch.processor;\n\nimport com.ccsw.tutorialbatch.model.Category;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.batch.item.ItemProcessor;\n\npublic class CategoryItemProcessor implements ItemProcessor<Category, Category> {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(CategoryItemProcessor.class);\n\n    @Override\n    public Category process(final Category category) {\n        String name = category.getName().toUpperCase();\n        String type = category.getType().toUpperCase();\n        String characteristics = category.getCharacteristics().toUpperCase();\n\n        Category transformedCategory = new Category(name, type, characteristics);\n        LOGGER.info(\"Converting ( {} ) into ( {} )\", category, transformedCategory);\n\n        return transformedCategory;\n    }\n}\n

    Hemos implementado un Processor personalizado, esta clase implementa ItemProcessor donde especificamos de qu\u00e9 clase a qu\u00e9 clase se va a realizar la trasformaci\u00f3n.

    En nuestro caso, va a ser de Category a Category donde \u00fanicamente vamos a realizar una trasformaci\u00f3n de pasar los datos le\u00eddos a may\u00fasculas, ya que el Reader que veremos m\u00e1s adelante ya nos habr\u00e1 trasformado los datos del fichero al modelo deseado. Las trasformaciones en s\u00ed se especifican sobreescribiendo el m\u00e9todo process.

    "},{"location":"appendix/springbatch/filetodb/#writer","title":"Writer","text":"

    Posteriormente, a\u00f1adimos el writer a la clase de configuraci\u00f3n CategoryBatchConfiguration donde ya hab\u00edamos a\u00f1adido Reader.

    CategoryBatchConfiguration.java
    package com.ccsw.tutorialbatch.config;\n\n...\n\n@Configuration\npublic class CategoryBatchConfiguration {\n\n    ...\n\n    @Bean\n    public ItemWriter<Category> writerCategory(DataSource dataSource) {\n        return new JdbcBatchItemWriterBuilder<Category>()\n                .itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())\n                .sql(\"INSERT INTO category (name, type, characteristics) VALUES (:name, :type, :characteristics)\")\n                .dataSource(dataSource)\n                .build();\n    }\n\n}\n

    Para la parte de escritura usaremos JdbcBatchItemWriter que nos ayuda a lanzar inserciones en la base de datos de forma sencilla. \u00c9l DataSource se inicializa autom\u00e1ticamente con la instancia de H2 que se carga al arrancar el Batch. Aqu\u00ed el cat\u00e1logo de Writers que proporciona Spring Batch.

    "},{"location":"appendix/springbatch/filetodb/#step-y-job","title":"Step y Job","text":"

    Ahora ya podemos a\u00f1adir la configuraci\u00f3n del Step y del Job dentro de la clase de configuraci\u00f3n. La clase completa deber\u00eda quedar de esta forma:

    CategoryBatchConfiguration.java
    package com.ccsw.tutorialbatch.config;\n\n\nimport com.ccsw.tutorialbatch.model.Category;\nimport com.ccsw.tutorialbatch.processor.CategoryItemProcessor;\nimport com.ccsw.tutorialbatch.listener.JobCategoryCompletionNotificationListener;\nimport org.springframework.batch.core.Job;\nimport org.springframework.batch.core.Step;\nimport org.springframework.batch.core.job.builder.JobBuilder;\nimport org.springframework.batch.core.launch.support.RunIdIncrementer;\nimport org.springframework.batch.core.repository.JobRepository;\nimport org.springframework.batch.core.step.builder.StepBuilder;\nimport org.springframework.batch.item.ItemProcessor;\nimport org.springframework.batch.item.ItemReader;\nimport org.springframework.batch.item.ItemWriter;\nimport org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider;\nimport org.springframework.batch.item.database.builder.JdbcBatchItemWriterBuilder;\nimport org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;\nimport org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.io.ClassPathResource;\nimport org.springframework.transaction.PlatformTransactionManager;\n\nimport javax.sql.DataSource;\n\n@Configuration\npublic class CategoryBatchConfiguration {\n\n    @Bean\n    public ItemReader<Category> readerCategory() {\n        return new FlatFileItemReaderBuilder<Category>().name(\"categoryItemReader\")\n                .resource(new ClassPathResource(\"category-list.csv\"))\n                .delimited()\n                .names(new String[] { \"name\", \"type\", \"characteristics\" })\n                .fieldSetMapper(new BeanWrapperFieldSetMapper<>() {{\n                    setTargetType(Category.class);\n                }})\n                .build();\n    }\n\n    @Bean\n    public ItemProcessor<Category, Category> processorCategory() {\n\n        return new CategoryItemProcessor();\n    }\n\n    @Bean\n    public ItemWriter<Category> writerCategory(DataSource dataSource) {\n        return new JdbcBatchItemWriterBuilder<Category>()\n                .itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())\n                .sql(\"INSERT INTO category (name, type, characteristics) VALUES (:name, :type, :characteristics)\")\n                .dataSource(dataSource)\n                .build();\n    }\n\n    @Bean\n    public Step step1Category(JobRepository jobRepository, PlatformTransactionManager transactionManager, ItemReader<Category> readerCategory, ItemProcessor<Category, Category> processorCategory, ItemWriter<Category> writerCategory) {\n        return new StepBuilder(\"step1Category\", jobRepository)\n                .<Category, Category> chunk(10, transactionManager)\n                .reader(readerCategory)\n                .processor(processorCategory)\n                .writer(writerCategory)\n                .build();\n    }\n\n    @Bean\n    public Job jobCategory(JobRepository jobRepository, JobCategoryCompletionNotificationListener listener, Step step1Category) {\n        return new JobBuilder(\"jobCategory\", jobRepository)\n                .incrementer(new RunIdIncrementer())\n                .listener(listener)\n                .flow(step1Category)\n                .end()\n                .build();\n    }\n\n}\n
    • ItemReader: El bean del Reader que hemos creado anteriormente.
    • ItemProcessor: El bean del Processor que hemos creado anteriormente.
    • ItemWriter: El bean del Writer que hemos creado anteriormente.
    • Step: La creaci\u00f3n del Step se realiza mediante \u00e9l StepBuilder al que le definimos el tama\u00f1o del chunk que es el n\u00famero de elementos procesados por lote y le asignamos los tres beans creados previamente. En este caso solo vamos a tener un \u00fanico Step pero podr\u00edamos tener todos los que quisi\u00e9ramos.
    • Job: Finalmente, debemos definir \u00e9l Job que ser\u00e1 lo que se ejecute al lanzar nuestro proceso. La creaci\u00f3n se hace mediante el builder correspondiente como en el caso anterior. Se asigna el identificador de Job, el conjunto de steps, en este caso solo tenemos uno y finalmente el listener que es opcional y se crea en el siguiente paso.
    "},{"location":"appendix/springbatch/filetodb/#listener","title":"Listener","text":"

    Ahora, para verificar que nuestro proceso se ha ejecutado correctamente vamos a a\u00f1adir un Listener que al final de la ejecuci\u00f3n consultar\u00e1 que los datos se han insertado correctamente. Emplazamos \u00e9l Listener dentro del package com.ccsw.tutorialbatch.listener.

    JobCategoryCompletionNotificationListener.java
    package com.ccsw.tutorialbatch.listener;\n\n\nimport com.ccsw.tutorialbatch.model.Category;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.batch.core.BatchStatus;\nimport org.springframework.batch.core.JobExecution;\nimport org.springframework.batch.core.JobExecutionListener;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.jdbc.core.JdbcTemplate;\nimport org.springframework.stereotype.Component;\n\n@Component\npublic class JobCategoryCompletionNotificationListener implements JobExecutionListener {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(JobCategoryCompletionNotificationListener.class);\n\n    private final JdbcTemplate jdbcTemplate;\n\n    @Autowired\n    public JobCategoryCompletionNotificationListener(JdbcTemplate jdbcTemplate) {\n        this.jdbcTemplate = jdbcTemplate;\n    }\n\n    @Override\n    public void afterJob(JobExecution jobExecution) {\n        if (jobExecution.getStatus() == BatchStatus.COMPLETED) {\n            LOGGER.info(\"!!! JOB FINISHED! Time to verify the results\");\n\n            String query = \"SELECT name, type, characteristics FROM category\";\n            jdbcTemplate.query(query, (rs, row) -> new Category(rs.getString(1), rs.getString(2), rs.getString(3)))\n                .forEach(category -> LOGGER.info(\"Found < {} > in the database.\", category));\n        }\n    }\n}\n

    Para el listener implementamos la interface JobExecutionListener y sobreescribimos el m\u00e9todo afterJob que se ejecutara justo al terminar nuestro Job lanzando una consulta y mostrando el resultado.

    "},{"location":"appendix/springbatch/filetodb/#base-de-datos-y-fichero-carga","title":"Base de Datos y Fichero Carga","text":"

    Finalmente, debemos crear el fichero de inicializaci\u00f3n de base de datos con la tabla de categor\u00edas y crear el fichero que leeremos con los datos de las categor\u00edas que deseamos insertar.

    schema-all.sqlcategory-list.csv
    DROP TABLE category IF EXISTS;\n\nCREATE TABLE category  (\ncategory_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\nname VARCHAR(20),\ntype VARCHAR(20),\ncharacteristics VARCHAR(30)\n);\n
    Eurogames,Mechanics,Hard\nAmeritrash,Thematic,Mid\nFamiliar,Fillers,Easy\n
    "},{"location":"appendix/springbatch/filetodb/#pruebas","title":"Pruebas","text":"

    Ahora si arrancamos la aplicaci\u00f3n como cualquier aplicaci\u00f3n Spring Boot, podremos observar la traza de la ejecuci\u00f3n en nuestro log y comprobar que la ejecuci\u00f3n ha sido correcta y los registros se han insertado.

    Job: [FlowJob: [name=jobCategory]] launched with the following parameters: [{'run.id':'{value=1, type=class java.lang.Long, identifying=true}'}]\nExecuting step: [step1Category]\nConverting ( Category [name=Eurogames, type=Mechanics, characteristics=Hard] ) into ( Category [name=EUROGAMES, type=MECHANICS, characteristics=HARD] )\nConverting ( Category [name=Ameritrash, type=Thematic, characteristics=Mid] ) into ( Category [name=AMERITRASH, type=THEMATIC, characteristics=MID] )\nConverting ( Category [name=Familiar, type=Fillers, characteristics=Easy] ) into ( Category [name=FAMILIAR, type=FILLERS, characteristics=EASY] )\nStep: [step1Category] executed in 55ms\n!!! JOB FINISHED! Time to verify the results\nFound < Category [name=EUROGAMES, type=MECHANICS, characteristics=HARD] > in the database.\nFound < Category [name=AMERITRASH, type=THEMATIC, characteristics=MID] > in the database.\nFound < Category [name=FAMILIAR, type=FILLERS, characteristics=EASY] > in the database.\nJob: [FlowJob: [name=jobCategory]] completed with the following parameters: [{'run.id':'{value=1, type=class java.lang.Long, identifying=true}'}] and the following status: [COMPLETED] in 73ms\n
    "},{"location":"appendix/springbatch/filetofile/","title":"Autor - Spring Batch","text":"

    Ya tenemos todo configurado del paso anterior asi que proseguimos con el siguiente ejemplo.

    "},{"location":"appendix/springbatch/filetofile/#caso-de-uso","title":"Caso de Uso","text":"

    En este caso tambi\u00e9n debemos plantear requisitos diferentes para la parte de Autores.

    "},{"location":"appendix/springbatch/filetofile/#que-vamos-a-hacer","title":"\u00bfQu\u00e9 vamos a hacer?","text":"

    Vamos a implementar un batch para leer un fichero de Autores trasformar la nacionalidad del autor a c\u00f3digo de region y general un fichero con los datos trasformados.

    "},{"location":"appendix/springbatch/filetofile/#como-lo-vamos-a-hacer","title":"\u00bfC\u00f3mo lo vamos a hacer?","text":"

    Al igual que en el caso anterior seguiremos el esquema de funcionamiento habitual de un proceso batch que hemos visto en la parte de introducci\u00f3n:

    • ItemReader: Se va a leer de un fichero y convertir los registros le\u00eddos al modelo de Author.
    • ItemProcessor: Va a procesar todos los registros convirtiendo el c\u00f3digo de nacionalidad al formato xx_XX.
    • ItemWriter: Va a escribir los registros en un fichero.
    • Step: El paso que contiene los elementos que van a realizar la funcionalidad.
    • Job: La tarea que contiene los pasos definidos.
    "},{"location":"appendix/springbatch/filetofile/#codigo","title":"C\u00f3digo","text":""},{"location":"appendix/springbatch/filetofile/#modelo","title":"Modelo","text":"

    En primer lugar, vamos a crear el modelo dentro del package com.ccsw.tutorialbatch.model de la misma forma que en el ejemplo anterior.

    Author.java
    package com.ccsw.tutorialbatch.model;\n\npublic class Author {\n\n    private String name;\n    private String nationality;\n\n    public Author() {\n    }\n\n    public Author(String name, String nationality) {\n        this.name = name;\n        this.nationality = nationality;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getNationality() {\n        return nationality;\n    }\n\n    public void setNationality(String nationality) {\n        this.nationality = nationality;\n    }\n\n    @Override\n    public String toString() {\n        return \"Author [name=\" + getName() + \", nationality=\" + getNationality() + \"]\";\n    }\n\n}\n
    "},{"location":"appendix/springbatch/filetofile/#reader","title":"Reader","text":"

    Ahora, como en el caso anterior, emplazamos \u00e9l Reader en la clase donde posteriormente a\u00f1adiremos la configuraci\u00f3n junto al resto de beans, dentro del package com.ccsw.tutorialbatch.config.

    AuthorBatchConfiguration.java
    package com.ccsw.tutorialbatch.config;\n\n...\n\n@Configuration\npublic class AuthorBatchConfiguration {\n\n    @Bean\n    public ItemReader<Author> readerAuthor() {\n        return new FlatFileItemReaderBuilder<Author>().name(\"authorItemReader\")\n                .resource(new ClassPathResource(\"author-list.csv\"))\n                .delimited()\n                .names(new String[] { \"name\", \"nationality\" })\n                .fieldSetMapper(new BeanWrapperFieldSetMapper<>() {{\n                    setTargetType(Author.class);\n                }})\n                .build();\n    }\n

    Para la ingesta de datos vamos a hacer uso de este FlatFileItemReader que nos proporciona Spring Batch. Como se puede observar se le proporciona el fichero a leer y el mapeo a la clase que deseamos. Aqu\u00ed el cat\u00e1logo de Readers que proporciona Spring Batch.

    "},{"location":"appendix/springbatch/filetofile/#processor","title":"Processor","text":"

    Posteriormente, emplazamos \u00e9l Processor dentro del package com.ccsw.tutorialbatch.processor.

    AuthorItemProcessor.java
    package com.ccsw.tutorialbatch.processor;\n\n\nimport com.ccsw.tutorialbatch.model.Author;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.batch.item.ItemProcessor;\n\n\npublic class AuthorItemProcessor implements ItemProcessor<Author, Author> {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(AuthorItemProcessor.class);\n\n    @Override\n    public Author process(final Author author) {\n        String name = author.getName();\n        String nationality = author.getNationality().toLowerCase() + \"_\" + author.getNationality().toUpperCase();\n\n        Author transformedAuthor = new Author(name, nationality);\n        LOGGER.info(\"Converting ( {} ) into ( {} )\", author, transformedAuthor);\n\n        return transformedAuthor;\n    }\n}\n

    De la misma forma que en el caso anterior hemos implementado un Processor personalizado, esta clase implementa ItemProcessor donde especificamos de qu\u00e9 clase a qu\u00e9 clase se va a realizar la trasformaci\u00f3n.

    En nuestro caso, va a ser de Author a Author donde vamos a implementar la l\u00f3gica requerida para este caso de uso.

    "},{"location":"appendix/springbatch/filetofile/#writer","title":"Writer","text":"

    Posteriormente, a\u00f1adimos el writer a la clase de configuraci\u00f3n AuthorBatchConfiguration donde ya hab\u00edamos a\u00f1adido Reader.

    AuthorBatchConfiguration.java
    package com.ccsw.tutorialbatch.config;\n\n...\n\n@Configuration\npublic class AuthorBatchConfiguration {\n\n    ...\n\n    @Bean\n    public ItemWriter<Author> writerAuthor() {\n        return  new FlatFileItemWriterBuilder<Author>().name(\"writerAuthor\")\n                .resource(new FileSystemResource(\"target/test-outputs/author-output.txt\"))\n                .lineAggregator(new PassThroughLineAggregator<>())\n                .build();\n    }\n\n}\n

    A diferencia del ejemplo anterior utilizamos FlatFileItemWriter diferente que en este caso nos ayuda a crear un fichero con los datos deseados. Aqu\u00ed el cat\u00e1logo de Writers que proporciona Spring Batch.

    "},{"location":"appendix/springbatch/filetofile/#step-y-job","title":"Step y Job","text":"

    Ahora ya podemos a\u00f1adir la configuraci\u00f3n del Step y del Job dentro de la clase de configuraci\u00f3n. La clase completa deber\u00eda quedar de esta forma:

    AuthorBatchConfiguration.java
    package com.ccsw.tutorialbatch.config;\n\n\nimport com.ccsw.tutorialbatch.model.Author;\nimport com.ccsw.tutorialbatch.processor.AuthorItemProcessor;\nimport org.springframework.batch.core.Job;\nimport org.springframework.batch.core.Step;\nimport org.springframework.batch.core.job.builder.JobBuilder;\nimport org.springframework.batch.core.launch.support.RunIdIncrementer;\nimport org.springframework.batch.core.repository.JobRepository;\nimport org.springframework.batch.core.step.builder.StepBuilder;\nimport org.springframework.batch.item.ItemProcessor;\nimport org.springframework.batch.item.ItemReader;\nimport org.springframework.batch.item.ItemWriter;\nimport org.springframework.batch.item.file.FlatFileItemReader;\nimport org.springframework.batch.item.file.FlatFileItemWriter;\nimport org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;\nimport org.springframework.batch.item.file.builder.FlatFileItemWriterBuilder;\nimport org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;\nimport org.springframework.batch.item.file.transform.PassThroughLineAggregator;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.io.ClassPathResource;\nimport org.springframework.core.io.FileSystemResource;\nimport org.springframework.transaction.PlatformTransactionManager;\n\n@Configuration\npublic class AuthorBatchConfiguration {\n\n    @Bean\n    public ItemReader<Author> readerAuthor() {\n        return new FlatFileItemReaderBuilder<Author>().name(\"authorItemReader\")\n                .resource(new ClassPathResource(\"author-list.csv\"))\n                .delimited()\n                .names(new String[] { \"name\", \"nationality\" })\n                .fieldSetMapper(new BeanWrapperFieldSetMapper<>() {{\n                    setTargetType(Author.class);\n                }})\n                .build();\n    }\n\n    @Bean\n    public ItemProcessor<Author, Author> processorAuthor() {\n\n        return new AuthorItemProcessor();\n    }\n\n    @Bean\n    public ItemWriter<Author> writerAuthor() {\n        return  new FlatFileItemWriterBuilder<Author>().name(\"writerAuthor\")\n                .resource(new FileSystemResource(\"target/test-outputs/author-output.txt\"))\n                .lineAggregator(new PassThroughLineAggregator<>())\n                .build();\n    }\n\n    @Bean\n    public Step step1Author(JobRepository jobRepository, PlatformTransactionManager transactionManager, ItemReader<Author> readerAuthor, ItemProcessor<Author, Author> processorAuthor, ItemWriter<Author> writerAuthor) {\n        return new StepBuilder(\"step1Author\", jobRepository)\n                .<Author, Author> chunk(10, transactionManager)\n                .reader(readerAuthor)\n                .processor(processorAuthor)\n                .writer(writerAuthor)\n                .build();\n    }\n\n    @Bean\n    public Job jobAuthor(JobRepository jobRepository, Step step1Author) {\n        return new JobBuilder(\"jobAuthor\", jobRepository)\n                .incrementer(new RunIdIncrementer())\n                .flow(step1Author)\n                .end()\n                .build();\n    }\n\n}\n
    • ItemReader: El bean del Reader que hemos creado anteriormente.
    • ItemProcessor: El bean del Processor que hemos creado anteriormente.
    • ItemWriter: El bean del Writer que hemos creado anteriormente.
    • Step: La creaci\u00f3n del Step se realiza mediante \u00e9l StepBuilder al que le definimos el tama\u00f1o del chunk que es el n\u00famero de elementos procesados por lote y le asignamos los tres beans creados previamente. En este caso solo vamos a tener un \u00fanico Step pero podr\u00edamos tener todos los que quisi\u00e9ramos.
    • Job: Finalmente, debemos definir \u00e9l Job que ser\u00e1 lo que se ejecute al lanzar nuestro proceso. La creaci\u00f3n se hace mediante el builder correspondiente como en el caso anterior. Se asigna el identificador de Job, el conjunto de steps, en este caso solo tenemos uno. En este caso no necesitamos un listener, ya que para verificar el resultado podemos ver el archivo generado.
    "},{"location":"appendix/springbatch/filetofile/#fichero-carga","title":"Fichero Carga","text":"

    Finalmente, debemos crear el fichero que leeremos con los datos de los autores que deseamos procesar.

    author-list.csv
    Alan R. Moon,US\nVital Lacerda,PT\nSimone Luciani,IT\nPerepau Llistosella,ES\nMichael Kiesling,DE\nPhil Walker-Harding,US\n
    "},{"location":"appendix/springbatch/filetofile/#pruebas","title":"Pruebas","text":"

    Ahora ya tenemos dos Jobs en nuestro batch por lo que debemos especificar en el arranque cual queremos ejecutar.

    Esto se realiza pasando una VM option en el arranque de la aplicaci\u00f3n:

    -Dspring.batch.job.name=jobAuthor\n
    \u00f3
    -Dspring.batch.job.name=jobCtegory\n

    Hecho esto y ejecutado el batch, podremos ver la traza de la ejecuci\u00f3n en nuestro log y el fichero generado en el target del proyecto:

    Job: [FlowJob: [name=jobAuthor]] launched with the following parameters: [{'run.id':'{value=1, type=class java.lang.Long, identifying=true}'}]\nExecuting step: [step1Author]\nConverting ( Author [name=Alan R. Moon, nationality=US] ) into ( Author [name=Alan R. Moon, nationality=us_US] )\nConverting ( Author [name=Vital Lacerda, nationality=PT] ) into ( Author [name=Vital Lacerda, nationality=pt_PT] )\nConverting ( Author [name=Simone Luciani, nationality=IT] ) into ( Author [name=Simone Luciani, nationality=it_IT] )\nConverting ( Author [name=Perepau Llistosella, nationality=ES] ) into ( Author [name=Perepau Llistosella, nationality=es_ES] )\nConverting ( Author [name=Michael Kiesling, nationality=DE] ) into ( Author [name=Michael Kiesling, nationality=de_DE] )\nConverting ( Author [name=Phil Walker-Harding, nationality=US] ) into ( Author [name=Phil Walker-Harding, nationality=us_US] )\nStep: [step1Author] executed in 50ms\nJob: [FlowJob: [name=jobAuthor]] completed with the following parameters: [{'run.id':'{value=1, type=class java.lang.Long, identifying=true}'}] and the following status: [COMPLETED] in 67ms\n
    author-output.txt
    Author [name=Alan R. Moon, nationality=us_US]\nAuthor [name=Vital Lacerda, nationality=pt_PT]\nAuthor [name=Simone Luciani, nationality=it_IT]\nAuthor [name=Perepau Llistosella, nationality=es_ES]\nAuthor [name=Michael Kiesling, nationality=de_DE]\nAuthor [name=Phil Walker-Harding, nationality=us_US]\n
    "},{"location":"appendix/springbatch/intro/","title":"Introducci\u00f3n Batch - Spring Batch","text":""},{"location":"appendix/springbatch/intro/#que-son-los-procesos-batch","title":"Que son los procesos batch?","text":"

    El proceso batch o procesamiento por lotes es un proceso por el cual un sistema realiza procesos, muchas veces de forma simult\u00e1nea, de forma continuada y secuencial.

    Normalmente, este tipo de procesos se dividen en peque\u00f1as partes que se realizan de forma cont\u00ednua consiguiendo un mejor rendimiento.

    "},{"location":"appendix/springbatch/intro/#spring-batch","title":"Spring Batch","text":"

    Existente multiples soluciones para implementar procesos batch, en nuestro caso vamos a utilizar la soluci\u00f3n que nos ofrece Spring Framework y que est\u00e1 incluido dentro del m\u00f3dulo Spring Batch.

    Spring Batch es framework de procesos batch ligero y completo dise\u00f1ado para permitir el desarrollo de aplicaciones por lotes robustas, vitales para las operaciones diarias de los sistemas empresariales.

    Proporciona funciones reutilizables que son esenciales en el procesamiento de grandes vol\u00famenes de registros, incluyendo trazabilidad, gesti\u00f3n de transacciones, estad\u00edsticas de procesamiento de trabajos, reinicio de trabajos, omisi\u00f3n y gesti\u00f3n de recursos. Tambi\u00e9n proporciona servicios y funcionalidades m\u00e1s avanzadas que permitir\u00e1n realizar procesos batch de gran volumen y alto rendimiento mediante t\u00e9cnicas de optimizaci\u00f3n y partici\u00f3n.

    "},{"location":"appendix/springbatch/intro/#estructura","title":"Estructura","text":"
    • JobLauncher: Esta pieza es la encargada de la gesti\u00f3n de ejecuciones de los distintos Jobs que componen nuestro sistema. En nuestro ejemplo no vamos a utilizarla, ya que lanzaremos los procesos manualmente para simplificar el c\u00f3digo, pero pod\u00e9is consultar el detalle en la documentaci\u00f3n.
    • JobRepository: Se trata del repositorio que almacena informaci\u00f3n sobre cada Job y los datos de su ejecuci\u00f3n necesario para mantener la trazabilidad del sistema. Para m\u00e1s informaci\u00f3n consultar la documentaci\u00f3n.
    • Job: Se trata de la entidad principal de un proceso batch y es un bloque que contiene uno o varios steps que conforman el proceso a ejecutar.
    • Step: Un Step, como su nombre indica, es un paso en la ejecuci\u00f3n de un Job el cual contiene la l\u00f3gica de negocio de un determinado caso de uso. Un Step habitualmente est\u00e1 formado por un ItemReader, ItemProcessor y ItemWriter o por un Tasklet. La primera opci\u00f3n es relativa a la ejecuci\u00f3n normal de un batch donde asociamos el tama\u00f1o del lote y el procesado es en funci\u00f3n de esta configuraci\u00f3n. Esta es la opci\u00f3n que deber\u00edamos usar en la mayor\u00eda de los casos, mientras que la opci\u00f3n de Tasklet esta reservada para cuando necesitamos realizar operaciones de forma at\u00f3mica.
    • ItemReader: Se trata de la ingesta de datos para un determinado Step. Se puede realizar de forma manual o con los Readers que proporciona Spring Batch.
    • ItemProcessor: En esta pieza se realizan todas las trasformaciones de datos que contenga nuestra l\u00f3gica de negocio.
    • ItemWriter: Es la producci\u00f3n de datos por determinado Step. Se puede realizar de forma manual o con los Writers que proporciona Spring Batch.
    • Tasklet: En los casos que no deseemos realizar ingestas, trasformaci\u00f3n y producci\u00f3n de datos para realizar funcionalidades de forma at\u00f3mica tenemos disponibles los Tasklet.
    "},{"location":"appendix/springbatch/intro/#contexto-de-la-aplicacion","title":"Contexto de la aplicaci\u00f3n","text":"

    Llegados a este punto, \u00bfqu\u00e9 es lo que vamos a hacer en los siguientes pasos?. Bas\u00e1ndonos en el ejemplo del tutorial y en el Contexto de la aplicaci\u00f3n vamos a reinventar nuestros requisitos para poder resolver las problem\u00e1ticas con procesos batch.

    Ya deber\u00edamos tener claros los conceptos y los actores que compondr\u00e1n nuestro sistema, as\u00ed que, all\u00e1 vamos!!!

    "},{"location":"appendix/springbatch/summary/","title":"Resumen Batch - Spring Batch","text":""},{"location":"appendix/springbatch/summary/#que-hemos-hecho","title":"\u00bfQu\u00e9 hemos hecho?","text":"

    Llegados a este punto, ya has podido ver que los procesos batch tienen una filosof\u00eda muy diferente a una aplicaci\u00f3n Spring Boot corriente, ya que el objetivo de los procesos est\u00e1 enfocado en procesado de datos y realizaci\u00f3n de tareas recurrentes.

    En definitiva, lo que hemos implementado ha sido:

    • Lectura de fichero y persistencia en BBDD: Este ha sido el primer ejemplo donde hemos visto la estructura b\u00e1sica de un batch y hemos hecho uso de las herramientas que nos proporciona para realizar tareas complejas de forma sencilla.

    • Lectura de fichero y persistencia en fichero: Ejemplo similar al anterior para ilustrar la existencia de otro Writer y su utilizaci\u00f3n.

    • Limpieza: Puesta en escena de la utilizaci\u00f3n de Tasklet que nos permite realizar operaciones at\u00f3micas que no requieran lectura, procesado y escritura para abarcar todo el espectro de posibles requisitos para implementar un proceso.

    "},{"location":"appendix/springbatch/summary/#consideraciones","title":"Consideraciones","text":"

    En estos ejemplos hemos realizado la implementaci\u00f3n lo m\u00e1s sencilla posible de un proceso batch con Spring Batch y aunque no dista mucho de una implementaci\u00f3n para un proyecto real, aqu\u00ed un par de consideraciones a tener en cuenta:

    • Estructura: A diferencia de Spring Boot no existe un convenio unificado de organizaci\u00f3n de clases y paquetes por lo que se puede ver de muchas formas diferentes. Aqu\u00ed lo importante es que si se utiliza en un determinado proyecto, se debe respetar su estructura por homogeneidad y mantenibilidad del mismo.

    • Ejecuci\u00f3n: La ejecuci\u00f3n de los procesos normalmente se delega en herramientas externas para su programaci\u00f3n y ejecuci\u00f3n. Esto var\u00eda mucho en funci\u00f3n de la arquitectura que tenga implementada un determinado cliente.

    Y como siempre, para tener la informaci\u00f3n m\u00e1s actualizada, acude a la documentaci\u00f3n oficial de Spring Batch.

    "},{"location":"appendix/springbatch/summary/#siguientes-pasos","title":"Siguientes pasos","text":"

    Ahora te propongo hacer un peque\u00f1o ejercicio para poner aprueba si los conceptos se han consolidado. Puedes realizarlo en el punto Ahora hazlo t\u00fa!

    "},{"location":"appendix/springcloud/basic/","title":"Listado simple - Spring Boot","text":"

    A diferencia del tutorial b\u00e1sico de Spring Boot, donde constru\u00edamos una aplicaci\u00f3n monol\u00edtica, ahora vamos a construir multiples servicios por lo que necesitamos crear proyectos separados.

    Para la creaci\u00f3n de proyecto nos remitimos a la gu\u00eda de instalaci\u00f3n donde se detalla el proceso de creaci\u00f3n de nuevo proyecto Entorno de desarrollo

    Todos los pasos son exactamente iguales, lo \u00fanico que va a variar es el nombre de nuestro proyecto, que en este caso se va a llamar tutorial-category. El campo que debemos modificar es artifact en Spring Initilizr, el resto de campos se cambiaran autom\u00e1ticamente.

    "},{"location":"appendix/springcloud/basic/#estructurar-el-codigo-y-buenas-practicas","title":"Estructurar el c\u00f3digo y buenas pr\u00e1cticas","text":"

    Esta parte de tutorial es una ampliaci\u00f3n de la parte de backend con Spring Boot, por tanto, no se ve a enfocar en las partes b\u00e1sicas aprendidas previamente, sino que se va a explicar el funcionamiento de los micro servicios aplicados al mismo caso de uso.

    Para cualquier duda sobre la estructura del c\u00f3digo y buenas pr\u00e1cticas, consultar el apartado de Estructura y buenas pr\u00e1cticas, ya que aplican a este caso en el mismo modo.

    "},{"location":"appendix/springcloud/basic/#codigo","title":"C\u00f3digo","text":"

    Dado de vamos a implementar el micro servicio Spring Boot de Categor\u00edas, vamos a respetar la misma estructura del Listado simple de la version monol\u00edtica.

    "},{"location":"appendix/springcloud/basic/#entity-y-dto","title":"Entity y Dto","text":"

    En primer lugar, vamos a crear la entidad y el DTO dentro del package com.ccsw.tutorialcategory.category.model. Ojo al package que lo hemos renombrado con respecto al listado monol\u00edtico.

    Category.javaCategoryDto.java
    package com.ccsw.tutorialcategory.category.model;\n\nimport jakarta.persistence.*;\n\n/**\n * @author ccsw\n *\n */\n@Entity\n@Table(name = \"category\")\npublic class Category {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"id\", nullable = false)\n    private Long id;\n\n    @Column(name = \"name\", nullable = false)\n    private String name;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return name\n     */\n    public String getName() {\n\n        return this.name;\n    }\n\n    /**\n     * @param name new value of {@link #getName}.\n     */\n    public void setName(String name) {\n\n        this.name = name;\n    }\n\n}\n
    package com.ccsw.tutorialcategory.category.model;\n\n/**\n * @author ccsw\n *\n */\npublic class CategoryDto {\n\n    private Long id;\n\n    private String name;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return name\n     */\n    public String getName() {\n\n        return this.name;\n    }\n\n    /**\n     * @param name new value of {@link #getName}.\n     */\n    public void setName(String name) {\n\n        this.name = name;\n    }\n\n}\n
    "},{"location":"appendix/springcloud/basic/#repository-service-y-controller","title":"Repository, Service y Controller","text":"

    Posteriormente, emplazamos el resto de clases dentro del package com.ccsw.tutorialcategory.category.

    CategoryRepository.javaCategoryService.javaCategoryServiceImpl.javaCategoryController.java
    package com.ccsw.tutorialcategory.category;\n\nimport com.ccsw.tutorialcategory.category.model.Category;\nimport org.springframework.data.repository.CrudRepository;\n\n/**\n * @author ccsw\n *\n */\npublic interface CategoryRepository extends CrudRepository<Category, Long> {\n\n}\n
    package com.ccsw.tutorialcategory.category;\n\n\nimport com.ccsw.tutorialcategory.category.model.Category;\nimport com.ccsw.tutorialcategory.category.model.CategoryDto;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\npublic interface CategoryService {\n\n    /**\n     * Recupera una {@link Category} a partir de su ID\n     *\n     * @param id PK de la entidad\n     * @return {@link Category}\n     */\n    Category get(Long id);\n\n    /**\n     * M\u00e9todo para recuperar todas las {@link Category}\n     *\n     * @return {@link List} de {@link Category}\n     */\n    List<Category> findAll();\n\n    /**\n     * M\u00e9todo para crear o actualizar una {@link Category}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    void save(Long id, CategoryDto dto);\n\n    /**\n     * M\u00e9todo para borrar una {@link Category}\n     *\n     * @param id PK de la entidad\n     */\n    void delete(Long id) throws Exception;\n\n}\n
    package com.ccsw.tutorialcategory.category;\n\nimport com.ccsw.tutorialcategory.category.model.Category;\nimport com.ccsw.tutorialcategory.category.model.CategoryDto;\nimport jakarta.transaction.Transactional;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Service\n@Transactional\npublic class CategoryServiceImpl implements CategoryService {\n\n    @Autowired\n    CategoryRepository categoryRepository;\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public Category get(Long id) {\n\n        return this.categoryRepository.findById(id).orElse(null);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public List<Category> findAll() {\n\n        return (List<Category>) this.categoryRepository.findAll();\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void save(Long id, CategoryDto dto) {\n\n        Category category;\n\n        if (id == null) {\n            category = new Category();\n        } else {\n            category = this.get(id);\n        }\n\n        category.setName(dto.getName());\n\n        this.categoryRepository.save(category);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void delete(Long id) throws Exception {\n\n        if(this.get(id) == null){\n            throw new Exception(\"Not exists\");\n        }\n\n        this.categoryRepository.deleteById(id);\n    }\n\n}\n
    package com.ccsw.tutorialcategory.category;\n\nimport com.ccsw.tutorialcategory.category.model.Category;\nimport com.ccsw.tutorialcategory.category.model.CategoryDto;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.modelmapper.ModelMapper;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * @author ccsw\n *\n */\n@Tag(name = \"Category\", description = \"API of Category\")\n@RequestMapping(value = \"/category\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class CategoryController {\n\n    @Autowired\n    CategoryService categoryService;\n\n    @Autowired\n    ModelMapper mapper;\n\n    /**\n     * M\u00e9todo para recuperar todas las {@link Category}\n     *\n     * @return {@link List} de {@link CategoryDto}\n     */\n    @Operation(summary = \"Find\", description = \"Method that return a list of Categories\"\n    )\n    @RequestMapping(path = \"\", method = RequestMethod.GET)\n    public List<CategoryDto> findAll() {\n\n        List<Category> categories = this.categoryService.findAll();\n\n        return categories.stream().map(e -> mapper.map(e, CategoryDto.class)).collect(Collectors.toList());\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar una {@link Category}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Category\"\n    )\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody CategoryDto dto) {\n\n        this.categoryService.save(id, dto);\n    }\n\n    /**\n     * M\u00e9todo para borrar una {@link Category}\n     *\n     * @param id PK de la entidad\n     */\n    @Operation(summary = \"Delete\", description = \"Method that deletes a Category\")\n    @RequestMapping(path = \"/{id}\", method = RequestMethod.DELETE)\n    public void delete(@PathVariable(\"id\") Long id) throws Exception {\n\n        this.categoryService.delete(id);\n    }\n\n}\n
    "},{"location":"appendix/springcloud/basic/#sql-y-configuracion","title":"SQL y Configuraci\u00f3n","text":"

    Finalmente, debemos crear el mismo fichero de inicializaci\u00f3n de base de datos con solo los datos de categor\u00edas y modificar ligeramente la configuraci\u00f3n inicial para a\u00f1adir un puerto manualmente. Esto es necesario ya que vamos a levantar varios servicios simult\u00e1neamente y necesitaremos levantarlos en puertos diferentes para que no colisionen entre ellos.

    data.sqlapplication.properties
    INSERT INTO category(name) VALUES ('Eurogames');\nINSERT INTO category(name) VALUES ('Ameritrash');\nINSERT INTO category(name) VALUES ('Familiar');\n
    server.port=8091\n\n#Database\nspring.datasource.url=jdbc:h2:mem:testdb\nspring.datasource.username=sa\nspring.datasource.password=sa\nspring.datasource.driver-class-name=org.h2.Driver\n\nspring.jpa.database-platform=org.hibernate.dialect.H2Dialect\nspring.jpa.defer-datasource-initialization=true\nspring.jpa.show-sql=true\n\nspring.h2.console.enabled=true\n
    "},{"location":"appendix/springcloud/basic/#pruebas","title":"Pruebas","text":"

    Ahora si arrancamos la aplicaci\u00f3n server y abrimos el Postman podemos realizar las mismas pruebas del apartado de Listado simple pero esta vez apuntado al puerto 8091.

    "},{"location":"appendix/springcloud/basic/#siguientes-pasos","title":"Siguientes pasos","text":"

    Con esto ya tendr\u00edamos nuestro primer servicio separado. Podr\u00edamos conectar el frontend a este servicio, pero a medida que nuestra aplicaci\u00f3n creciera en n\u00famero de servicios ser\u00eda un poco engorroso todo, as\u00ed que todav\u00eda no lo vamos a conectar hasta que no tengamos toda la infraestructura.

    Vamos a convertir en micro servicio el siguiente listado.

    "},{"location":"appendix/springcloud/filtered/","title":"Listado filtrado - Spring Boot","text":"

    Al igual que en los caos anteriores vamos a crear un nuevo proyecto que contendr\u00e1 un nuevo micro servicio.

    Para la creaci\u00f3n de proyecto nos remitimos a la gu\u00eda de instalaci\u00f3n donde se detalla el proceso de creaci\u00f3n de nuevo proyecto Entorno de desarrollo

    Todos los pasos son exactamente iguales, lo \u00fanico que va a variar, es el nombre de nuestro proyecto, que en este caso se va a llamar tutorial-game. El campo que debemos modificar es artifact en Spring Initilizr, el resto de campos se cambiaran autom\u00e1ticamente.

    "},{"location":"appendix/springcloud/filtered/#codigo","title":"C\u00f3digo","text":"

    Dado de vamos a implementar el micro servicio Spring Boot de Juegos, vamos a respetar la misma estructura del Listado filtrado de la version monol\u00edtica.

    "},{"location":"appendix/springcloud/filtered/#criteria","title":"Criteria","text":"

    En primer lugar, vamos a a\u00f1adir la clase que necesitamos para realizar el filtrado y vimos en la version monol\u00edtica del tutorial en el package com.ccsw.tutorialgame.common.criteria.

    SearchCriteria.java
    package com.ccsw.tutorialgame.common.criteria;\n\npublic class SearchCriteria {\n\n    private String key;\n    private String operation;\n    private Object value;\n\n    public SearchCriteria(String key, String operation, Object value) {\n\n        this.key = key;\n        this.operation = operation;\n        this.value = value;\n    }\n\n    public String getKey() {\n        return key;\n    }\n\n    public void setKey(String key) {\n        this.key = key;\n    }\n\n    public String getOperation() {\n        return operation;\n    }\n\n    public void setOperation(String operation) {\n        this.operation = operation;\n    }\n\n    public Object getValue() {\n        return value;\n    }\n\n    public void setValue(Object value) {\n        this.value = value;\n    }\n\n}\n
    "},{"location":"appendix/springcloud/filtered/#entity-y-dto","title":"Entity y Dto","text":"

    Seguimos con la entidad y el DTO dentro del package com.ccsw.tutorialgame.game.model. En este punto, f\u00edjate que nuestro modelo de Entity no tiene relaci\u00f3n con la tabla Author ni Category ya que estos dos objetos no pertenecen a nuestro dominio y se gestionan desde otro micro servicio. Lo que tendremos ahora ser\u00e1 el identificador del registro que hace referencia a esos objetos. Ya no usaremos @JoinColumn porque en nuestro modelo no existen esas tablas relacionadas.

    Sin embargo el Dto si que utiliza relaciones, ya que son relaciones de negocio (en el Service) y no son relaciones de dominio (en BBDD o Repository)

    Game.javaGameDto.java
    package com.ccsw.tutorialgame.game.model;\n\nimport jakarta.persistence.*;\n\n\n/**\n * @author ccsw\n *\n */\n@Entity\n@Table(name = \"game\")\npublic class Game {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"id\", nullable = false)\n    private Long id;\n\n    @Column(name = \"title\", nullable = false)\n    private String title;\n\n    @Column(name = \"age\", nullable = false)\n    private String age;\n\n    @Column(name = \"category_id\", nullable = false)\n    private Long idCategory;\n\n    @Column(name = \"author_id\", nullable = false)\n    private Long idAuthor;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return title\n     */\n    public String getTitle() {\n\n        return this.title;\n    }\n\n    /**\n     * @param title new value of {@link #getTitle}.\n     */\n    public void setTitle(String title) {\n\n        this.title = title;\n    }\n\n    /**\n     * @return age\n     */\n    public String getAge() {\n\n        return this.age;\n    }\n\n    /**\n     * @param age new value of {@link #getAge}.\n     */\n    public void setAge(String age) {\n\n        this.age = age;\n    }\n\n    /**\n     * @return idCategory\n     */\n    public Long getIdCategory() {\n\n        return this.idCategory;\n    }\n\n    /**\n     * @param idCategory new value of {@link #getIdCategory}.\n     */\n    public void setIdCategory(Long idCategory) {\n\n        this.idCategory = idCategory;\n    }\n\n    /**\n     * @return idAuthor\n     */\n    public Long getIdAuthor() {\n\n        return this.idAuthor;\n    }\n\n    /**\n     * @param idAuthor new value of {@link #getIdAuthor}.\n     */\n    public void setIdAuthor(Long idAuthor) {\n\n        this.idAuthor = idAuthor;\n    }\n\n}\n
    package com.ccsw.tutorialgame.game.model;\n\n\nimport com.ccsw.tutorialgame.author.model.AuthorDto;\nimport com.ccsw.tutorialgame.category.model.CategoryDto;\n\n/**\n * @author ccsw\n *\n */\npublic class GameDto {\n\n    private Long id;\n\n    private String title;\n\n    private String age;\n\n    private Long idCategory;\n\n    private Long idAuthor;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return title\n     */\n    public String getTitle() {\n\n        return this.title;\n    }\n\n    /**\n     * @param title new value of {@link #getTitle}.\n     */\n    public void setTitle(String title) {\n\n        this.title = title;\n    }\n\n    /**\n     * @return age\n     */\n    public String getAge() {\n\n        return this.age;\n    }\n\n    /**\n     * @param age new value of {@link #getAge}.\n     */\n    public void setAge(String age) {\n\n        this.age = age;\n    }\n\n    /**\n     * @return idCategory\n     */\n    public Long getIdCategory() {\n\n        return this.idCategory;\n    }\n\n    /**\n     * @param idCategory new value of {@link #getIdCategory}.\n     */\n    public void setIdCategory(Long idCategory) {\n\n        this.idCategory = idCategory;\n    }\n\n    /**\n     * @return idAuthor\n     */\n    public Long getIdAuthor() {\n\n        return this.idAuthor;\n    }\n\n    /**\n     * @param idAuthor new value of {@link #getIdAuthor}.\n     */\n    public void setIdAuthor(Long idAuthor) {\n\n        this.idAuthor = idAuthor;\n    }\n\n}\n
    "},{"location":"appendix/springcloud/filtered/#repository-service-controller","title":"Repository, Service, Controller","text":"

    Posteriormente, emplazamos el resto de clases dentro del package com.ccsw.tutorialgame.game.

    GameRepository.javaGameService.javaGameSpecification.javaGameServiceImpl.javaGameController.java
    package com.ccsw.tutorialgame.game;\n\nimport com.ccsw.tutorialgame.game.model.Game;\nimport org.springframework.data.jpa.repository.JpaSpecificationExecutor;\nimport org.springframework.data.repository.CrudRepository;\n\n/**\n * @author ccsw\n *\n */\npublic interface GameRepository extends CrudRepository<Game, Long>, JpaSpecificationExecutor<Game> {\n\n}\n
    package com.ccsw.tutorialgame.game;\n\nimport com.ccsw.tutorialgame.game.model.Game;\nimport com.ccsw.tutorialgame.game.model.GameDto;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\npublic interface GameService {\n\n    /**\n     * Recupera los juegos filtrando opcionalmente por t\u00edtulo y/o categor\u00eda\n     *\n     * @param title t\u00edtulo del juego\n     * @param idCategory PK de la categor\u00eda\n     * @return {@link List} de {@link Game}\n     */\n    List<Game> find(String title, Long idCategory);\n\n    /**\n     * Guarda o modifica un juego, dependiendo de si el identificador est\u00e1 o no informado\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    void save(Long id, GameDto dto);\n\n}\n
    package com.ccsw.tutorialgame.game;\n\nimport com.ccsw.tutorialgame.common.criteria.SearchCriteria;\nimport com.ccsw.tutorialgame.game.model.Game;\nimport jakarta.persistence.criteria.*;\nimport org.springframework.data.jpa.domain.Specification;\n\n\npublic class GameSpecification implements Specification<Game> {\n\n    private static final long serialVersionUID = 1L;\n\n    private final SearchCriteria criteria;\n\n    public GameSpecification(SearchCriteria criteria) {\n\n        this.criteria = criteria;\n    }\n\n    @Override\n    public Predicate toPredicate(Root<Game> root, CriteriaQuery<?> query, CriteriaBuilder builder) {\n        if (criteria.getOperation().equalsIgnoreCase(\":\") && criteria.getValue() != null) {\n            Path<String> path = getPath(root);\n            if (path.getJavaType() == String.class) {\n                return builder.like(path, \"%\" + criteria.getValue() + \"%\");\n            } else {\n                return builder.equal(path, criteria.getValue());\n            }\n        }\n        return null;\n    }\n\n    private Path<String> getPath(Root<Game> root) {\n        String key = criteria.getKey();\n        String[] split = key.split(\"[.]\", 0);\n\n        Path<String> expression = root.get(split[0]);\n        for (int i = 1; i < split.length; i++) {\n            expression = expression.get(split[i]);\n        }\n\n        return expression;\n    }\n\n}\n
    package com.ccsw.tutorialgame.game;\n\nimport com.ccsw.tutorialgame.common.criteria.SearchCriteria;\nimport com.ccsw.tutorialgame.game.model.Game;\nimport com.ccsw.tutorialgame.game.model.GameDto;\nimport jakarta.transaction.Transactional;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.jpa.domain.Specification;\nimport org.springframework.stereotype.Service;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Service\n@Transactional\npublic class GameServiceImpl implements GameService {\n\n    @Autowired\n    GameRepository gameRepository;\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public List<Game> find(String title, Long idCategory) {\n\n        GameSpecification titleSpec = new GameSpecification(new SearchCriteria(\"title\", \":\", title));\n        GameSpecification categorySpec = new GameSpecification(new SearchCriteria(\"idCategory\", \":\", idCategory));\n\n        Specification<Game> spec = Specification.where(titleSpec).and(categorySpec);\n        // Desde la versi\u00f3n 3.5.0 de Spring Boot, la nueva manera es\n        Specification<Game> spec = titleSpec.and(categorySpec);\n\n        return this.gameRepository.findAll(spec);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void save(Long id, GameDto dto) {\n\n        Game game;\n\n        if (id == null) {\n            game = new Game();\n        } else {\n            game = this.gameRepository.findById(id).orElse(null);\n        }\n\n        BeanUtils.copyProperties(dto, game, \"id\");\n\n        game.setIdAuthor(dto.getIdAuthor());\n        game.setIdCategory(dto.getIdCategory());\n\n        this.gameRepository.save(game);\n    }\n\n}\n
    package com.ccsw.tutorialgame.game;\n\nimport com.ccsw.tutorialgame.game.model.Game;\nimport com.ccsw.tutorialgame.game.model.GameDto;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.modelmapper.ModelMapper;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * @author ccsw\n *\n */\n@Tag(name = \"Game\", description = \"API of Game\")\n@RequestMapping(value = \"/game\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class GameController {\n\n    @Autowired\n    GameService gameService;\n\n    @Autowired\n    ModelMapper mapper;\n\n    /**\n     * M\u00e9todo para recuperar una lista de {@link Game}\n     *\n     * @param title t\u00edtulo del juego\n     * @param idCategory PK de la categor\u00eda\n     * @return {@link List} de {@link GameDto}\n     */\n    @Operation(summary = \"Find\", description = \"Method that return a filtered list of Games\")\n    @RequestMapping(path = \"\", method = RequestMethod.GET)\n    public List<GameDto> find(@RequestParam(value = \"title\", required = false) String title,\n                              @RequestParam(value = \"idCategory\", required = false) Long idCategory) {\n\n        List<Game> game = this.gameService.find(title, idCategory);\n\n        return game.stream().map(e -> mapper.map(e, GameDto.class)).collect(Collectors.toList());\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Game}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Game\")\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody GameDto dto) {\n\n        gameService.save(id, dto);\n    }\n\n}\n
    "},{"location":"appendix/springcloud/filtered/#sql-y-configuracion","title":"SQL y Configuraci\u00f3n","text":"

    Finalmente, debemos crear el script de inicializaci\u00f3n de base de datos con solo los datos de juegos y modificar ligeramente la configuraci\u00f3n inicial para a\u00f1adir un puerto manualmente para poder tener multiples micro servicios funcionando simult\u00e1neamente.

    data.sqlapplication.properties
    INSERT INTO game(title, age, category_id, author_id) VALUES ('On Mars', '14', 1, 2);\nINSERT INTO game(title, age, category_id, author_id) VALUES ('Aventureros al tren', '8', 3, 1);\nINSERT INTO game(title, age, category_id, author_id) VALUES ('1920: Wall Street', '12', 1, 4);\nINSERT INTO game(title, age, category_id, author_id) VALUES ('Barrage', '14', 1, 3);\nINSERT INTO game(title, age, category_id, author_id) VALUES ('Los viajes de Marco Polo', '12', 1, 3);\nINSERT INTO game(title, age, category_id, author_id) VALUES ('Azul', '8', 3, 5);\n
    server.port=8093\n\n#Database\nspring.datasource.url=jdbc:h2:mem:testdb\nspring.datasource.username=sa\nspring.datasource.password=sa\nspring.datasource.driver-class-name=org.h2.Driver\n\nspring.jpa.database-platform=org.hibernate.dialect.H2Dialect\nspring.jpa.defer-datasource-initialization=true\nspring.jpa.show-sql=true\n\nspring.h2.console.enabled=true\n
    "},{"location":"appendix/springcloud/filtered/#pruebas","title":"Pruebas","text":"

    Ahora si arrancamos la aplicaci\u00f3n server y abrimos el Postman podemos realizar las mismas pruebas del apartado de Listado filtrado pero esta vez apuntado al puerto 8093.

    F\u00edjate que cuando probemos el listado de juegos, devolver\u00e1 identificadores en idAuthor y idCategory, y no objetos como funcionaba hasta ahora en la aplicaci\u00f3n monol\u00edtica. As\u00ed que las pruebas que realices para insertar tambi\u00e9n deben utilizar esas propiedades y NO objetos.

    "},{"location":"appendix/springcloud/filtered/#siguientes-pasos","title":"Siguientes pasos","text":"

    En este punto ya tenemos un micro servicio de categor\u00edas en el puerto 8091, un micro servicio de autores en el puerto 8092 y un \u00faltimo micro servicio de juegos en el puerto 8093.

    Si ahora fueramos a conectarlo con el frontend tendr\u00edamos dos problemas:

    • Por un lado, el frontend debe recordar la IP y el puerto en el que se encuentra cada servicio. Adem\u00e1s, este podr\u00eda cambiar si lo desplegamos en nube o lo movemos de servidor, y el frontend deber\u00eda ser capaz de refrescarse para actualizar la informaci\u00f3n.
    • Por otro lado, como hemos comentado, se ha cambiado el contrato del endpoint de juegos. Ahora ya no devuelve la informaci\u00f3n de author y category sino que devuelve su ID. Esto obliga al frontend a tener que hacer dos llamadas extra para completar la informaci\u00f3n. Estar\u00edamos llevando l\u00f3gica de negocio al frontend y esto no nos convence.

    Para poder solverntar ambos problemas, necesitamos conectar todos nuestros micro servicios con una infraestructura que nos ayudar\u00e1 a gestionar todo el ecosistema de micro servicios. Vamos all\u00e1 con el \u00faltimo punto.

    "},{"location":"appendix/springcloud/infra/","title":"Infraestructura - Spring Cloud","text":"

    Creados los tres micro servicios que compondr\u00e1n nuestro aplicativo, ya podemos empezar con la creaci\u00f3n de las piezas de infraestructura que ser\u00e1n las encargadas de realizar la orquestaci\u00f3n.

    "},{"location":"appendix/springcloud/infra/#service-discovery-eureka","title":"Service Discovery - Eureka","text":"

    Para esta pieza hay muchas aplicaciones de mercado, incluso los propios proveedores de cloud tiene la suya propia, pero en este caso, vamos a utilizar la que ofrece Spring Cloud, as\u00ed que vamos a crear un proyecto de una forma similar a la que estamos acostumbrados.

    "},{"location":"appendix/springcloud/infra/#crear-el-servicio","title":"Crear el servicio","text":"

    Volviendo una vez m\u00e1s a Spring Initializr seleccionaremos los siguientes datos:

    • Tipo de proyecto: Maven
    • Lenguage: Java
    • Versi\u00f3n Spring boot: 3.0.4 (o alguna similar)
    • Group: com.ccsw
    • ArtifactId: tutorial-eureka
    • Versi\u00f3n Java: 19
    • Dependencias: Eureka Server

    Es importante que a\u00f1adamos la dependencia de Eureka Server para que sea capaz de ejecutar el proyecto como si fuera un servidor Eureka.

    "},{"location":"appendix/springcloud/infra/#configurar-el-servicio","title":"Configurar el servicio","text":"

    Importamos el proyecto dentro del IDE y ya solo nos queda activar el servidor y configurarlo.

    En primer lugar, a\u00f1adimos la anotaci\u00f3n que habilita el servidor de Eureka.

    TutorialEurekaApplication.java
    package com.ccsw.tutorialeureka;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;\n\n@SpringBootApplication\n@EnableEurekaServer\npublic class TutorialEurekaApplication {\n\n    public static void main(String[] args) {\n        SpringApplication.run(TutorialEurekaApplication.class, args);\n    }\n\n}\n

    Ahora debemos a\u00f1adir las configuraciones necesarias. En primer lugar para facilitar la visualizaci\u00f3n de las propiedades vamos a renombrar nuestro fichero application.properties a application.yml. Hecho esto, a\u00f1adimos la configuraci\u00f3n de puerto que ya conocemos y a\u00f1adimos directivas sobre que Eureka no se registre a s\u00ed mismo dentro del cat\u00e1logo de servicios.

    application.yml
    server:\n  port: 8761\neureka:\n  client:\n    registerWithEureka: false\n    fetchRegistry: false\n
    "},{"location":"appendix/springcloud/infra/#probar-el-servicio","title":"Probar el servicio","text":"

    Hechas estas sencillas configuraciones y arrancando el proyecto, nos dirigimos a la http://localhost/8761 donde podemos ver la interfaz de Eureka y si miramos con detenimiento, vemos que el cat\u00e1logo de servicios aparece vac\u00edo, ya que a\u00fan no se ha registrado ninguno de ellos.

    "},{"location":"appendix/springcloud/infra/#micro-servicios","title":"Micro servicios","text":"

    Ahora que ya tenemos disponible Eureka, ya podemos proceder a registrar nuestros micro servicios dentro del cat\u00e1logo. Para ello vamos a realizar las mismas modificaciones sobre los tres micro servicios. Recuerda que hay que realizarlo sobre los tres para que se registren todos.

    "},{"location":"appendix/springcloud/infra/#configurar-micro-servicios","title":"Configurar micro servicios","text":"

    Para este fin debemos a\u00f1adir una nueva dependencia dentro del pom.xml y modificar la configuraci\u00f3n del proyecto.

    pom.xmlapplication.properties
    <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\nxsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>3.0.4</version>\n        <relativePath/> <!-- lookup parent from repository -->\n    </parent>\n    <groupId>com.ccsw</groupId>\n    <artifactId>tutorial-XXX</artifactId> <!-- Cada proyecto tiene su configaci\u00f3n propia, NO modificar -->\n    <version>0.0.1-SNAPSHOT</version>\n    <name>tutorial-XXX</name> <!-- Cada proyecto tiene su configaci\u00f3n propia, NO modificar -->\n    <description>Demo project for Spring Boot</description>\n    <properties>\n        <java.version>19</java.version>\n        <spring-cloud.version>2022.0.1</spring-cloud.version>\n    </properties>\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-jpa</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springdoc</groupId>\n            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>\n            <version>2.0.3</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.hibernate</groupId>\n            <artifactId>hibernate-validator</artifactId>\n            <version>8.0.0.Final</version>\n        </dependency>\n\n        <dependency>\n            <groupId>net.sf.dozer</groupId>\n            <artifactId>dozer</artifactId>\n            <version>5.5.1</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.cloud</groupId>\n            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.h2database</groupId>\n            <artifactId>h2</artifactId>\n            <scope>runtime</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n    <dependencyManagement>\n        <dependencies>\n            <dependency>\n                <groupId>org.springframework.cloud</groupId>\n                <artifactId>spring-cloud-dependencies</artifactId>\n                <version>${spring-cloud.version}</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n        </dependencies>\n    </dependencyManagement>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n
    spring.application.name=spring-cloud-eureka-client-XXX\nserver.port=809X\n\n#Database\nspring.datasource.url=jdbc:h2:mem:testdb\nspring.datasource.username=sa\nspring.datasource.password=sa\nspring.datasource.driver-class-name=org.h2.Driver\n\nspring.jpa.database-platform=org.hibernate.dialect.H2Dialect\nspring.jpa.defer-datasource-initialization=true\nspring.jpa.show-sql=true\n\nspring.h2.console.enabled=true\n\n#Eureka\neureka.client.serviceUrl.defaultZone=${EUREKA_URI:http://localhost:8761/eureka}\neureka.instance.preferIpAddress=true\n

    Como podemos observar, lo que hemos hecho, es a\u00f1adir la dependencia de Eureka Client y le hemos comunicado a cada micro servicio donde tenemos arrancado Eureka. De este modo al arrancar cada micro servicio, este se registrar\u00e1 autom\u00e1ticamente dentro de Eureka.

    Para poder diferenciar cada micro servicio, estos tienen su configuraci\u00f3n de nombre y puerto (mantenemos el puerto que hab\u00edamos configurado en pasos previos):

    • Categor\u00edas: spring.application.name=spring-cloud-eureka-client-category
    • Autores: spring.application.name=spring-cloud-eureka-client-author
    • Juegos: spring.application.name=spring-cloud-eureka-client-game

    Nombres en vez de rutas

    Estos nombres ser\u00e1n por los que vamos a identificar cada micro servicio dentro de Eureka que ser\u00e1 quien conozca las rutas de los mismos, asi cuando queramos realizar redirecciones a estos no necesitaremos conocerlas rutas ni los puertos de los mismos, con proporcionar los nombres tendremos la informaci\u00f3n completa de como llegar a ellos.

    "},{"location":"appendix/springcloud/infra/#probar-micro-servicios","title":"Probar micro servicios","text":"

    Hechas estas configuraciones y arrancados los micro servicios, volvemos a dirigirnos a Eureka en http://localhost/8761 donde podemos ver que estos aparecen en el listado de servicios registrados.

    "},{"location":"appendix/springcloud/infra/#gateway","title":"Gateway","text":"

    Para esta pieza, de nuevo, hay muchas implementaciones y aplicaciones de mercado, pero nosotros vamos a utilizar la de Spring Cloud, as\u00ed que vamos a crear un nuevo proyecto de una forma similar a la de Eureka.

    "},{"location":"appendix/springcloud/infra/#crear-el-servicio_1","title":"Crear el servicio","text":"

    Volviendo una vez m\u00e1s a Spring Initializr seleccionaremos los siguientes datos:

    • Tipo de proyecto: Maven
    • Lenguage: Java
    • Versi\u00f3n Spring boot: 3.0.4 (o alguna similar)
    • Group: com.ccsw
    • ArtifactId: tutorial-gateway
    • Versi\u00f3n Java: 19
    • Dependencias: Gateway, Eureka Client

    Ojo con las dependencias de Gateway y de Eureka Client que debemos a\u00f1adir.

    "},{"location":"appendix/springcloud/infra/#configurar-el-servicio_1","title":"Configurar el servicio","text":"

    De nuevo lo importamos en nuestro IDE y pasamos a a\u00f1adir las configuraciones pertinentes.

    Al igual que en el caso de Eureka vamos a renombrar nuestro fichero application.properties a application.yml.

    application.yml
    server:\n  port: 8080\neureka:\n  client:\n    serviceUrl:\n      defaultZone: http://localhost:8761/eureka\nspring:\n  application:\n    name: spring-cloud-eureka-client-gateway\n  cloud:\n    gateway:\n      default-filters:\n        - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin\n      globalcors:\n        corsConfigurations:\n          '[/**]':\n             allowedOrigins: \"*\"\n             allowedMethods: \"*\"\n             allowedHeaders: \"*\"\n      routes:\n        - id: category\n          uri: lb://SPRING-CLOUD-EUREKA-CLIENT-CATEGORY\n          predicates:\n            - Path=/category/**\n        - id: author\n          uri: lb://SPRING-CLOUD-EUREKA-CLIENT-AUTHOR\n          predicates:\n            - Path=/author/**\n        - id: game\n          uri: lb://SPRING-CLOUD-EUREKA-CLIENT-GAME\n          predicates:\n            - Path=/game/**\n

    Lo que hemos hecho aqu\u00ed es configurar el puerto como 8080 ya que el Gateway va a ser nuestro punto de acceso y el encargado de redirigir cada petici\u00f3n al micro servicio correcto.

    Posteriormente hemos configurado el cliente de Eureka para que el Gateway establezca comunicaci\u00f3n con Eureka que hemos configurado previamente para, en primer lugar, registrarse como un cliente y seguidamente obtener informaci\u00f3n del cat\u00e1logo de servicios existentes.

    El paso siguiente es darle un nombre a la aplicaci\u00f3n para que se registre en Eureka y a\u00f1adir configuraci\u00f3n de CORS para que cuando realicemos las llamadas desde navegador pueda realizar la redirecci\u00f3n correctamente.

    Finalmente a\u00f1adimos las directrices de redirecci\u00f3n al Gateway indic\u00e1ndole los nombres de los micro servicios con los que estos se han registrado en Eureka junto a los predicados que incluyen las rutas parciales que queremos que sean redirigidas a cada micro servicio.

    Con esto nos queda la siguiente configuraci\u00f3n:

    • Las rutas que incluyan en su path category redirigir\u00e1n al micro servicio de Categorias
    • Las rutas que incluyan en su path author redirigir\u00e1n al micro servicio de Autores
    • Las rutas que incluyan en su path game redirigir\u00e1n al micro servicio de Juegos
    "},{"location":"appendix/springcloud/infra/#probar-el-servicio_1","title":"Probar el servicio","text":"

    Hechas esto y arrancado el proyecto, volvemos a dirigirnos a Eureka en http://localhost/8761 donde podemos ver que el Gateway se ha registrado correctamente junto al resto de clientes.

    "},{"location":"appendix/springcloud/infra/#feign-client","title":"Feign Client","text":"

    El \u00faltimo paso es la implementaci\u00f3n de la comunicaci\u00f3n entre los micro servicios, en este caso necesitamos que nuestro micro servicio de Game obtenga datos de Category y Author para poder servir informaci\u00f3n completa de los Game ya que en su modelo solo posee los identificadores. Si record\u00e1is, est\u00e1bamos respondiendo solamente con los id.

    "},{"location":"appendix/springcloud/infra/#configurar-el-servicio_2","title":"Configurar el servicio","text":"

    Para la comunicaci\u00f3n entre los distintos servicios, Spring Cloud nos prove de Feign Clients que ofrecen una interfaz muy sencilla de comunicaci\u00f3n y que utiliza a la perfecci\u00f3n la infraestructura que ya hemos construido.

    En primer lugar debemos a\u00f1adir la dependencia necesaria dentro de nuestro pom.xml del micro servicio de Game.

    pom.xml
    ...\n        <dependency>\n            <groupId>org.springframework.cloud</groupId>\n            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.cloud</groupId>\n            <artifactId>spring-cloud-starter-openfeign</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.h2database</groupId>\n            <artifactId>h2</artifactId>\n            <scope>runtime</scope>\n        </dependency>\n...\n

    El siguiente paso es habilitar el uso de los Feign Clients mediante la anotaci\u00f3n de SpringCloud.

    TutorialGameApplication.java
    package com.ccsw.tutorialgame;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.cloud.openfeign.EnableFeignClients;\n\n@SpringBootApplication\n@EnableFeignClients\npublic class TutorialGameApplication {\n\n    public static void main(String[] args) {\n        SpringApplication.run(TutorialGameApplication.class, args);\n    }\n\n}\n
    "},{"location":"appendix/springcloud/infra/#configurar-los-clientes","title":"Configurar los clientes","text":"

    Realizadas las configuraciones ya podemos realizar los cambios necesarios en nuestro c\u00f3digo para implementar la comunicaci\u00f3n. En primer lugar vamos a crear los clientes de Categor\u00edas y Autores.

    CategoryClient.javaAuthorClient.java
    package com.ccsw.tutorialgame.category;\n\nimport com.ccsw.tutorialgame.category.model.CategoryDto;\nimport org.springframework.cloud.openfeign.FeignClient;\nimport org.springframework.web.bind.annotation.GetMapping;\n\nimport java.util.List;\n\n@FeignClient(value = \"SPRING-CLOUD-EUREKA-CLIENT-CATEGORY\", url = \"http://localhost:8080\")\npublic interface CategoryClient {\n\n    @GetMapping(value = \"/category\")\n    List<CategoryDto> findAll();\n}\n
    package com.ccsw.tutorialgame.author;\n\nimport com.ccsw.tutorialgame.author.model.AuthorDto;\nimport org.springframework.cloud.openfeign.FeignClient;\nimport org.springframework.web.bind.annotation.GetMapping;\n\nimport java.util.List;\n\n@FeignClient(value = \"SPRING-CLOUD-EUREKA-CLIENT-AUTHOR\", url = \"http://localhost:8080\")\npublic interface AuthorClient {\n\n    @GetMapping(value = \"/author\")\n    List<AuthorDto> findAll();\n}\n

    Lo que hacemos aqu\u00ed es crear una simple interfaz donde a\u00f1adimos la configuraci\u00f3n del Feign Client con la url del Gateway a trav\u00e9s del cual vamos a realizar todas las comunicaciones y creamos un m\u00e9todo abstracto con la anotaci\u00f3n pertinente para hacer referencia al endpoint de obtenci\u00f3n del listado.

    "},{"location":"appendix/springcloud/infra/#invocar-los-clientes","title":"Invocar los clientes","text":"

    Con esto ya podemos inyectar estas interfaces dentro de nuestro controlador para obtener todos los datos necesarios que completaran la informaci\u00f3n de la Category y Author de cada Game.

    Adem\u00e1s, vamos a cambiar el Dto de respuesta, para que en vez de devolver ids, devuelva los objetos correspondientes, que son los que est\u00e1 esperando nuestro frontend. Para ello, primero crearemos los Dtos que necesitamos. Los crearemos en:

    • com.ccsw.tutorialgame.category.model
    • com.ccsw.tutorialgame.author.model
    CategoryDto.javaAuthorDto.java
    package com.ccsw.tutorialgame.category.model;\n\n/**\n * @author ccsw\n *\n */\npublic class CategoryDto {\n\n    private Long id;\n\n    private String name;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return name\n     */\n    public String getName() {\n\n        return this.name;\n    }\n\n    /**\n     * @param name new value of {@link #getName}.\n     */\n    public void setName(String name) {\n\n        this.name = name;\n    }\n\n}\n
    package com.ccsw.tutorialgame.author.model;\n\n/**\n * @author ccsw\n *\n */\npublic class AuthorDto {\n\n    private Long id;\n\n    private String name;\n\n    private String nationality;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return name\n     */\n    public String getName() {\n\n        return this.name;\n    }\n\n    /**\n     * @param name new value of {@link #getName}.\n     */\n    public void setName(String name) {\n\n        this.name = name;\n    }\n\n    /**\n     * @return nationality\n     */\n    public String getNationality() {\n\n        return this.nationality;\n    }\n\n    /**\n     * @param nationality new value of {@link #getNationality}.\n     */\n    public void setNationality(String nationality) {\n\n        this.nationality = nationality;\n    }\n\n}\n

    Adem\u00e1s, modificaremos nuestro GameDto para hacer uso de esos objetos.

    GameDto.java
    package com.ccsw.tutorialgame.game.model;\n\n\nimport com.ccsw.tutorialgame.author.model.AuthorDto;\nimport com.ccsw.tutorialgame.category.model.CategoryDto;\n\n/**\n * @author ccsw\n *\n */\npublic class GameDto {\n\n    private Long id;\n\n    private String title;\n\n    private String age;\n\n    private CategoryDto category;\n\n    private AuthorDto author;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return title\n     */\n    public String getTitle() {\n\n        return this.title;\n    }\n\n    /**\n     * @param title new value of {@link #getTitle}.\n     */\n    public void setTitle(String title) {\n\n        this.title = title;\n    }\n\n    /**\n     * @return age\n     */\n    public String getAge() {\n\n        return this.age;\n    }\n\n    /**\n     * @param age new value of {@link #getAge}.\n     */\n    public void setAge(String age) {\n\n        this.age = age;\n    }\n\n    /**\n     * @return category\n     */\n    public CategoryDto getCategory() {\n\n        return this.category;\n    }\n\n    /**\n     * @param category new value of {@link #getCategory}.\n     */\n    public void setCategory(CategoryDto category) {\n\n        this.category = category;\n    }\n\n    /**\n     * @return author\n     */\n    public AuthorDto getAuthor() {\n\n        return this.author;\n    }\n\n    /**\n     * @param author new value of {@link #getAuthor}.\n     */\n    public void setAuthor(AuthorDto author) {\n\n        this.author = author;\n    }\n\n}\n

    Y por \u00faltimo implementaremos el c\u00f3digo necesario para transformar los ids en objetos dto. Aqu\u00ed lo que haremos ser\u00e1 recuperar todos los autores y categor\u00edas, haciendo uso de los Feign Client, y cuando ejecutemos el mapeo de los juegos, ir sustituyendo sus valores por los dtos correspondientes.

    GameController.java
    package com.ccsw.tutorialgame.game;\n\nimport com.ccsw.tutorialgame.author.AuthorClient;\nimport com.ccsw.tutorialgame.author.model.AuthorDto;\nimport com.ccsw.tutorialgame.category.CategoryClient;\nimport com.ccsw.tutorialgame.category.model.CategoryDto;\nimport com.ccsw.tutorialgame.game.model.Game;\nimport com.ccsw.tutorialgame.game.model.GameDto;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * @author ccsw\n *\n */\n@Tag(name = \"Game\", description = \"API of Game\")\n@RequestMapping(value = \"/game\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class GameController {\n\n    @Autowired\n    GameService gameService;\n\n    @Autowired\n    CategoryClient categoryClient;\n\n    @Autowired\n    AuthorClient authorClient;\n\n    /**\n     * M\u00e9todo para recuperar una lista de {@link Game}\n     *\n     * @param title t\u00edtulo del juego\n     * @param idCategory PK de la categor\u00eda\n     * @return {@link List} de {@link GameDto}\n     */\n    @Operation(summary = \"Find\", description = \"Method that return a filtered list of Games\")\n    @RequestMapping(path = \"\", method = RequestMethod.GET)\n    public List<GameDto> find(@RequestParam(value = \"title\", required = false) String title,\n                              @RequestParam(value = \"idCategory\", required = false) Long idCategory) {\n\n        List<CategoryDto> categories = categoryClient.findAll();\n        List<AuthorDto> authors = authorClient.findAll();\n\n        return gameService.find(title, idCategory).stream().map(game -> {\n            GameDto gameDto = new GameDto();\n\n            gameDto.setId(game.getId());\n            gameDto.setTitle(game.getTitle());\n            gameDto.setAge(game.getAge());\n            gameDto.setCategory(categories.stream().filter(category -> category.getId().equals(game.getIdCategory())).findFirst().orElse(null));\n            gameDto.setAuthor(authors.stream().filter(author -> author.getId().equals(game.getIdAuthor())).findFirst().orElse(null));\n\n            return gameDto;\n        }).collect(Collectors.toList());\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Game}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Game\")\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody GameDto dto) {\n\n        gameService.save(id, dto);\n    }\n\n}\n

    Con todo esto, ya tenemos construido nuestro aplicativo de micro servicios con la arquitectura Spring Cloud. Podemos proceder a realizar las mismas pruebas tanto manuales como a trav\u00e9s de los frontales.

    Escalado

    Una de las principales ventajas de las arquitecturas de micro servicios, es la posibilidad de escalar partes de los aplicativos sin tener que escalar el sistema completo. Para confirmar que esto es asi, podemos levantar multiples instancias de cada servicio en puertos diferentes y veremos que esto se refleja en Eureka y el Gateway balancear\u00e1 autom\u00e1ticamente entre las distintas instancias.

    "},{"location":"appendix/springcloud/intro/","title":"Introducci\u00f3n Micro Servicios - Spring Cloud","text":""},{"location":"appendix/springcloud/intro/#que-son-los-micro-servicios","title":"Que son los micro servicios?","text":"

    Pues como su nombre indica, son servicios peque\u00f1itos

    Aunque si nos vamos a una definici\u00f3n m\u00e1s t\u00e9cnica (seg\u00fan ChatGPT):

    Los micro servicios son una arquitectura de software en la que una aplicaci\u00f3n est\u00e1 compuesta por peque\u00f1os servicios independientes que se comunican entre s\u00ed a trav\u00e9s de interfaces bien definidas. Cada servicio se enfoca en realizar una tarea espec\u00edfica dentro de la aplicaci\u00f3n y se ejecuta de manera aut\u00f3noma.

    Cada micro servicio es responsable de un dominio del negocio y puede ser desarrollado, probado, implementado y escalado de manera independiente. Esto permite una mayor flexibilidad y agilidad en el desarrollo y la implementaci\u00f3n de aplicaciones, ya que los cambios en un servicio no afectan a otros servicios.

    Adem\u00e1s, los micro servicios son escalables y resistentes a fallos, ya que si un servicio falla, los dem\u00e1s servicios pueden seguir funcionando. Tambi\u00e9n permiten la utilizaci\u00f3n de diferentes tecnolog\u00edas para cada servicio, lo que ayuda a optimizar el rendimiento y la eficiencia en la aplicaci\u00f3n en general.

    "},{"location":"appendix/springcloud/intro/#spring-cloud","title":"Spring Cloud","text":"

    Existente multiples soluciones para implementar micro servicios, en nuestro caso vamos a utilizar la soluci\u00f3n que nos ofrece Spring Framework y que est\u00e1 incluido dentro del m\u00f3dulo Spring Cloud.

    Esta soluci\u00f3n nace hace ya varios a\u00f1os como parte de la infraestructura de Netflix para dar soluci\u00f3n a sus propias necesidades. Con el tiempo este c\u00f3digo opensource ha sido adquirido por Spring Framework y se ha incluido dentro de su ecosistema, evolucionandolo con nuevas funcionalidades. Todo ello ha sido publicado bajo el m\u00f3dulo de Spring Cloud.

    "},{"location":"appendix/springcloud/intro/#contexto-de-la-aplicacion","title":"Contexto de la aplicaci\u00f3n","text":"

    Llegados a este punto, \u00bfqu\u00e9 es lo que vamos a hacer en los siguientes puntos?. Pues vamos a coger nuestra aplicaci\u00f3n monol\u00edtica que ya tenemos implementada durante todo el tutorial, y vamos a proceder a trocearla e implementarla con una metodolog\u00eda de micro servicios.

    Pero, adem\u00e1s de trocear la aplicaci\u00f3n en peque\u00f1os servicios, nos va a hacer falta una serie de servicios / utilidades para conectar todo el ecosistema. Nos har\u00e1 falta una infraestructura.

    "},{"location":"appendix/springcloud/intro/#infraestructura","title":"Infraestructura","text":"

    A diferencia de una aplicaci\u00f3n monol\u00edtica, en un enfoque de micro servicios, ya no basta \u00fanicamente con la aplicaci\u00f3n desplegada en su servidor, sino que ser\u00e1n necesarios varios actores que se responsabilizar\u00e1n de darle consistencia al sistema, permitir la comunicaci\u00f3n entre ellos, y ayudar\u00e1n a solventar ciertos problemas que nos surgir\u00e1n al trocear nuestras aplicaciones.

    Las principales piezas que vamos a utilizar para la implementaci\u00f3n de nuestra infraestructura, ser\u00e1n:

    • Service Discovery / Eureka Server: Como vamos a tener varios servicios distribuidos por nuestra red, necesitaremos conocer donde est\u00e1 funcionando cada uno de ellos, su IP, su puerto e incluso sus m\u00e9tricas de acceso (localizaci\u00f3n, zona, estado de carga, etc.). Vamos a necesitar un Service Discovery que no es m\u00e1s que un cat\u00e1logo de todos los servicios que componen el ecosistema al cual cada servicio debe informar de forma proactiva, de su localizaci\u00f3n y disponibilidad.
    • Client-side Service Discovery / Eureka Client: Como hemos mencionado en el punto anterior, todos los servicios del ecosistema (incluidos nuestros micro servicios) deben conectarse con el Service Discovery e informar peri\u00f3dicamente a este cat\u00e1logo de su estado y sus m\u00e9tricas para que en caso de perdida de servicio, el resto de elementos lo sepan y puedan tomar decisiones al respecto. Tambi\u00e9n nos servir\u00e1 para que cada elemento pueda guardar en local una cach\u00e9 del cat\u00e1logo publicado, que se ir\u00e1 refrescando cada vez que lance un health check.
    • Edge Server / Gateway / Proxy: Se trata de un servicio que har\u00e1 de intermediario entre el mundo exterior y el mundo de microservicios. Adem\u00e1s permitir\u00e1 hacer redirecci\u00f3n y balanceo entre todos los elementos registrados en el Service Discovery. Es altamente configurable (rutas, redirecciones, carga, etc.) y es una pieza fundamental para unificar todas las llamadas en un \u00fanico punto del ecosistema.
    • Feign Client: Esta utilidad que provee directamente Spring Cloud nos permite comunicarnos entre los diferentes micro servicios de Spring, de una forma muy sencilla y sin tener que estar gestionando llamadas API Rest.
    "},{"location":"appendix/springcloud/intro/#diagrama-de-la-arquitectura","title":"Diagrama de la arquitectura","text":"

    Con las piezas identificadas anteriormente y con el Contexto de la aplicaci\u00f3n en mente, lo que vamos a hacer en los siguientes puntos es trocear el sistema y generar la siguiente arquitectura:

    Ya deber\u00edamos tener claros los conceptos y los actores que compondr\u00e1n nuestro sistema, as\u00ed que, all\u00e1 vamos!!!

    "},{"location":"appendix/springcloud/paginated/","title":"Listado paginado - Spring Boot","text":"

    Al igual que en el caso anterior vamos a crear un nuevo proyecto que contendr\u00e1 un nuevo micro servicio.

    Para la creaci\u00f3n de proyecto nos remitimos a la gu\u00eda de instalaci\u00f3n donde se detalla el proceso de creaci\u00f3n de nuevo proyecto Entorno de desarrollo

    Todos los pasos son exactamente iguales, lo \u00fanico que va a variar, es el nombre de nuestro proyecto, que en este caso se va a llamar tutorial-author. El campo que debemos modificar es artifact en Spring Initilizr, el resto de campos se cambiaran autom\u00e1ticamente.

    "},{"location":"appendix/springcloud/paginated/#codigo","title":"C\u00f3digo","text":"

    Dado de vamos a implementar el micro servicio Spring Boot de Autores, vamos a respetar la misma estructura del Listado paginado de la version monol\u00edtica.

    "},{"location":"appendix/springcloud/paginated/#paginacion","title":"Paginaci\u00f3n","text":"

    En primer lugar, vamos a a\u00f1adir la clase que necesitamos para realizar la paginaci\u00f3n y vimos en la version monol\u00edtica del tutorial en el package com.ccsw.tutorialauthor.common.pagination. Ojo al package que lo hemos renombrado con respecto al listado monol\u00edtico.

    PageableRequest.java
    package com.ccsw.tutorialauthor.common.pagination;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport org.springframework.data.domain.*;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\npublic class PageableRequest implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private int pageNumber;\n\n    private int pageSize;\n\n    private List<SortRequest> sort;\n\n    public PageableRequest() {\n\n        sort = new ArrayList<>();\n    }\n\n    public PageableRequest(int pageNumber, int pageSize) {\n\n        this();\n        this.pageNumber = pageNumber;\n        this.pageSize = pageSize;\n    }\n\n    public PageableRequest(int pageNumber, int pageSize, List<SortRequest> sort) {\n\n        this();\n        this.pageNumber = pageNumber;\n        this.pageSize = pageSize;\n        this.sort = sort;\n    }\n\n    public int getPageNumber() {\n        return pageNumber;\n    }\n\n    public void setPageNumber(int pageNumber) {\n        this.pageNumber = pageNumber;\n    }\n\n    public int getPageSize() {\n        return pageSize;\n    }\n\n    public void setPageSize(int pageSize) {\n        this.pageSize = pageSize;\n    }\n\n    public List<SortRequest> getSort() {\n        return sort;\n    }\n\n    public void setSort(List<SortRequest> sort) {\n        this.sort = sort;\n    }\n\n    @JsonIgnore\n    public Pageable getPageable() {\n\n        return PageRequest.of(this.pageNumber, this.pageSize, Sort.by(sort.stream().map(e -> new Sort.Order(e.getDirection(), e.getProperty())).collect(Collectors.toList())));\n    }\n\n    public static class SortRequest implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        private String property;\n\n        private Sort.Direction direction;\n\n        protected String getProperty() {\n            return property;\n        }\n\n        protected void setProperty(String property) {\n            this.property = property;\n        }\n\n        protected Sort.Direction getDirection() {\n            return direction;\n        }\n\n        protected void setDirection(Sort.Direction direction) {\n            this.direction = direction;\n        }\n    }\n\n}\n
    "},{"location":"appendix/springcloud/paginated/#entity-y-dto","title":"Entity y Dto","text":"

    Seguimos con la entidad y los DTOs dentro del package com.ccsw.tutorialauthor.author.model.

    Author.javaAuthorDto.javaAuthorSearchDto.java
    package com.ccsw.tutorialauthor.author.model;\n\nimport jakarta.persistence.*;\n\n/**\n * @author ccsw\n *\n */\n@Entity\n@Table(name = \"author\")\npublic class Author {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"id\", nullable = false)\n    private Long id;\n\n    @Column(name = \"name\", nullable = false)\n    private String name;\n\n    @Column(name = \"nationality\")\n    private String nationality;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return name\n     */\n    public String getName() {\n\n        return this.name;\n    }\n\n    /**\n     * @param name new value of {@link #getName}.\n     */\n    public void setName(String name) {\n\n        this.name = name;\n    }\n\n    /**\n     * @return nationality\n     */\n    public String getNationality() {\n\n        return this.nationality;\n    }\n\n    /**\n     * @param nationality new value of {@link #getNationality}.\n     */\n    public void setNationality(String nationality) {\n\n        this.nationality = nationality;\n    }\n\n}\n
    package com.ccsw.tutorialauthor.author.model;\n\n/**\n * @author ccsw\n *\n */\npublic class AuthorDto {\n\n    private Long id;\n\n    private String name;\n\n    private String nationality;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return name\n     */\n    public String getName() {\n\n        return this.name;\n    }\n\n    /**\n     * @param name new value of {@link #getName}.\n     */\n    public void setName(String name) {\n\n        this.name = name;\n    }\n\n    /**\n     * @return nationality\n     */\n    public String getNationality() {\n\n        return this.nationality;\n    }\n\n    /**\n     * @param nationality new value of {@link #getNationality}.\n     */\n    public void setNationality(String nationality) {\n\n        this.nationality = nationality;\n    }\n\n}\n
    package com.ccsw.tutorialauthor.author.model;\n\nimport com.ccsw.tutorialauthor.common.pagination.PageableRequest;\n\n/**\n * @author ccsw\n *\n */\npublic class AuthorSearchDto {\n\n    private PageableRequest pageable;\n\n    public PageableRequest getPageable() {\n        return pageable;\n    }\n\n    public void setPageable(PageableRequest pageable) {\n        this.pageable = pageable;\n    }\n}\n
    "},{"location":"appendix/springcloud/paginated/#repository-service-y-controller","title":"Repository, Service y Controller","text":"

    Posteriormente, emplazamos el resto de clases dentro del package com.ccsw.tutorialauthor.author.

    AuthorRepository.javaAuthorService.javaAuthorServiceImpl.javaAuthorController.java
    package com.ccsw.tutorialauthor.author;\n\nimport com.ccsw.tutorialauthor.author.model.Author;\nimport org.springframework.data.domain.Page;\nimport org.springframework.data.domain.Pageable;\nimport org.springframework.data.repository.CrudRepository;\n\n/**\n * @author ccsw\n *\n */\npublic interface AuthorRepository extends CrudRepository<Author, Long> {\n\n    /**\n     * M\u00e9todo para recuperar un listado paginado de {@link Author}\n     *\n     * @param pageable pageable\n     * @return {@link Page} de {@link Author}\n     */\n    Page<Author> findAll(Pageable pageable);\n\n}\n
    package com.ccsw.tutorialauthor.author;\n\nimport com.ccsw.tutorialauthor.author.model.Author;\nimport com.ccsw.tutorialauthor.author.model.AuthorDto;\nimport com.ccsw.tutorialauthor.author.model.AuthorSearchDto;\nimport org.springframework.data.domain.Page;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\npublic interface AuthorService {\n\n    /**\n     * Recupera un {@link Author} a trav\u00e9s de su ID\n     *\n     * @param id PK de la entidad\n     * @return {@link Author}\n     */\n    Author get(Long id);\n\n    /**\n     * M\u00e9todo para recuperar un listado paginado de {@link Author}\n     *\n     * @param dto dto de b\u00fasqueda\n     * @return {@link Page} de {@link Author}\n     */\n    Page<Author> findPage(AuthorSearchDto dto);\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    void save(Long id, AuthorDto dto);\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     */\n    void delete(Long id) throws Exception;\n\n    /**\n     * Recupera un listado de autores {@link Author}\n     *\n     * @return {@link List} de {@link Author}\n     */\n    List<Author> findAll();\n\n}\n
    package com.ccsw.tutorialauthor.author;\n\nimport com.ccsw.tutorialauthor.author.model.Author;\nimport com.ccsw.tutorialauthor.author.model.AuthorDto;\nimport com.ccsw.tutorialauthor.author.model.AuthorSearchDto;\nimport jakarta.transaction.Transactional;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.domain.Page;\nimport org.springframework.stereotype.Service;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Service\n@Transactional\npublic class AuthorServiceImpl implements AuthorService {\n\n    @Autowired\n    AuthorRepository authorRepository;\n\n    /**\n     * {@inheritDoc}\n     * @return\n     */\n    @Override\n    public Author get(Long id) {\n\n        return this.authorRepository.findById(id).orElse(null);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public Page<Author> findPage(AuthorSearchDto dto) {\n\n        return this.authorRepository.findAll(dto.getPageable().getPageable());\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void save(Long id, AuthorDto data) {\n\n        Author author;\n\n        if (id == null) {\n            author = new Author();\n        } else {\n            author = this.get(id);\n        }\n\n        BeanUtils.copyProperties(data, author, \"id\");\n\n        this.authorRepository.save(author);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void delete(Long id) throws Exception {\n\n        if(this.get(id) == null){\n            throw new Exception(\"Not exists\");\n        }\n\n        this.authorRepository.deleteById(id);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public List<Author> findAll() {\n\n        return (List<Author>) this.authorRepository.findAll();\n    }\n\n}\n
    package com.ccsw.tutorialauthor.author;\n\nimport com.ccsw.tutorialauthor.author.model.Author;\nimport com.ccsw.tutorialauthor.author.model.AuthorDto;\nimport com.ccsw.tutorialauthor.author.model.AuthorSearchDto;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.modelmapper.ModelMapper;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.domain.Page;\nimport org.springframework.data.domain.PageImpl;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * @author ccsw\n *\n */\n@Tag(name = \"Author\", description = \"API of Author\")\n@RequestMapping(value = \"/author\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class AuthorController {\n\n    @Autowired\n    AuthorService authorService;\n\n    @Autowired\n    ModelMapper mapper;\n\n    /**\n     * M\u00e9todo para recuperar un listado paginado de {@link Author}\n     *\n     * @param dto dto de b\u00fasqueda\n     * @return {@link Page} de {@link AuthorDto}\n     */\n    @Operation(summary = \"Find Page\", description = \"Method that return a page of Authors\")\n    @RequestMapping(path = \"\", method = RequestMethod.POST)\n    public Page<AuthorDto> findPage(@RequestBody AuthorSearchDto dto) {\n\n        Page<Author> page = this.authorService.findPage(dto);\n\n        return new PageImpl<>(page.getContent().stream().map(e -> mapper.map(e, AuthorDto.class)).collect(Collectors.toList()), page.getPageable(), page.getTotalElements());\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Author\")\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody AuthorDto dto) {\n\n        this.authorService.save(id, dto);\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     */\n    @Operation(summary = \"Delete\", description = \"Method that deletes a Author\")\n    @RequestMapping(path = \"/{id}\", method = RequestMethod.DELETE)\n    public void delete(@PathVariable(\"id\") Long id) throws Exception {\n\n        this.authorService.delete(id);\n    }\n\n    /**\n     * Recupera un listado de autores {@link Author}\n     *\n     * @return {@link List} de {@link AuthorDto}\n     */\n    @Operation(summary = \"Find\", description = \"Method that return a list of Authors\")\n    @RequestMapping(path = \"\", method = RequestMethod.GET)\n    public List<AuthorDto> findAll() {\n\n        List<Author> authors = this.authorService.findAll();\n\n        return authors.stream().map(e -> mapper.map(e, AuthorDto.class)).collect(Collectors.toList());\n    }\n\n}\n
    "},{"location":"appendix/springcloud/paginated/#sql-y-configuracion","title":"SQL y Configuraci\u00f3n","text":"

    Finalmente, debemos crear el script de inicializaci\u00f3n de base de datos con solo los datos de author y modificar ligeramente la configuraci\u00f3n inicial para a\u00f1adir un puerto manualmente para poder tener multiples micro servicios funcionando simult\u00e1neamente.

    data.sqlapplication.properties
    INSERT INTO author(name, nationality) VALUES ('Alan R. Moon', 'US');\nINSERT INTO author(name, nationality) VALUES ('Vital Lacerda', 'PT');\nINSERT INTO author(name, nationality) VALUES ('Simone Luciani', 'IT');\nINSERT INTO author(name, nationality) VALUES ('Perepau Llistosella', 'ES');\nINSERT INTO author(name, nationality) VALUES ('Michael Kiesling', 'DE');\nINSERT INTO author(name, nationality) VALUES ('Phil Walker-Harding', 'US');\n
    server.port=8092\n\n#Database\nspring.datasource.url=jdbc:h2:mem:testdb\nspring.datasource.username=sa\nspring.datasource.password=sa\nspring.datasource.driver-class-name=org.h2.Driver\n\nspring.jpa.database-platform=org.hibernate.dialect.H2Dialect\nspring.jpa.defer-datasource-initialization=true\nspring.jpa.show-sql=true\n\nspring.h2.console.enabled=true\n
    "},{"location":"appendix/springcloud/paginated/#pruebas","title":"Pruebas","text":"

    Ahora si arrancamos la aplicaci\u00f3n server y abrimos el Postman podemos realizar las mismas pruebas del apartado de Listado paginado pero esta vez apuntado al puerto 8092.

    "},{"location":"appendix/springcloud/paginated/#siguientes-pasos","title":"Siguientes pasos","text":"

    En este punto ya tenemos un micro servicio de categor\u00edas en el puerto 8091 y un micro servicio de autores en el puerto 8092. Al igual que antes, con estos datos ya podr\u00edamos conectar el frontend a estos servicios, pero vamos a esperar un poquito m\u00e1s a tener toda la infraestructura, para que sea m\u00e1s sencillo.

    Vamos a convertir en micro servicio el \u00faltimo listado.

    "},{"location":"appendix/springcloud/summary/","title":"Resumen Micro Servicios - Spring Cloud","text":""},{"location":"appendix/springcloud/summary/#que-hemos-hecho","title":"\u00bfQu\u00e9 hemos hecho?","text":"

    Llegados a este punto, ya has podido comprobar que implementar una aplicaci\u00f3n orientada a micro servicios es bastante similar a una aplicaci\u00f3n monol\u00edtica, con la salvedad de que tienes que tener en cuenta la distribuci\u00f3n de estos, y por tanto su gesti\u00f3n y coordinaci\u00f3n.

    En definitiva, lo que hemos implementado ha sido:

    • Service Discovery: Que ayudar\u00e1 a tener un cat\u00e1logo de todos las piezas de mi infraestructura, su IP, su puerto y ciertas m\u00e9tricas que ayuden luego en la elecci\u00f3n de servicio.

    • Gateway: Que centraliza las peticiones en un \u00fanico punto y permite hacer de balanceo de carga, seguridad, etc. Ser\u00e1 el punto de entrada a nuestro ecosistema.

    • Micro servicio Category: Contiene las operaciones sobre el \u00e1mbito funcional de categor\u00edas, guarda y recupera informaci\u00f3n de ese \u00e1mbito funcional.

    • Micro servicio Author: Contiene las operaciones sobre el \u00e1mbito funcional de autores, guarda y recupera informaci\u00f3n de ese \u00e1mbito funcional.

    • Micro servicio Game: Contiene las operaciones sobre el \u00e1mbito funcional de autores, guarda y recupera informaci\u00f3n de ese \u00e1mbito funcional. Adem\u00e1s, realiza llamadas entre los otros dos micro servicios para nutrir de m\u00e1s informaci\u00f3n sus endpoints.

    El diagrama de nuestra aplicaci\u00f3n ahora es as\u00ed:

    "},{"location":"appendix/springcloud/summary/#siguientes-pasos","title":"Siguientes pasos","text":"

    Bueno, el siguiente paso m\u00e1s evidente, ser\u00e1 ver que si conectas el frontend sigue funcionando exactamente igual que lo estaba haciendo antes.

    Ahora te propongo hacer el mismo ejercicio con los otros dos m\u00f3dulos Cliente y Pr\u00e9stamo que has tenido que implementar en el punto Ahora hazlo tu!.

    Ten en cuenta que Cliente no depende de nadie, pero Pr\u00e9stamo si que depende de Cliente y de Game. A ver como solucionas los cruces y sobre todo los filtros

    "},{"location":"appendix/springcloud/summary/#mas-formacion-mas-informacion","title":"M\u00e1s formaci\u00f3n, m\u00e1s informaci\u00f3n","text":"

    Pues ya estar\u00eda todo, ahora solo te puedo dar la enhorabuena y pasar algo de informaci\u00f3n extra / cursos / formaciones por si quieres seguir aprendiendo.

    Por un lado tienes el itinerario avanzado de Springboot donde se puede m\u00e1s detalle de micro servicios.

    Por otro lado tambi\u00e9n tienes los itinerarios de Cloud ya que no todo va a ser micro servicios con Spring Cloud, tambi\u00e9n existen micro servicios con otras tecnolog\u00edas, aunque el concepto es muy similar.

    "},{"location":"cleancode/angular/","title":"Estructura y Buenas pr\u00e1cticas - Angular","text":"

    Nota

    Antes de empezar y para puntualizar, Angular se considera un framework SPA Single-page application.

    En esta parte vamos a explicar los fundamentos de un proyecto en Angular y las recomendaciones existentes.

    "},{"location":"cleancode/angular/#estructura-y-funcionamiento","title":"Estructura y funcionamiento","text":""},{"location":"cleancode/angular/#ciclo-de-vida-de-angular","title":"Ciclo de vida de Angular","text":"

    El comportamiento de ciclo de vida de un componente Angular pasa por diferentes etapas que podemos ver en el esquema que mostramos a continuaci\u00f3n:

    Es importante tenerlo claro para saber que m\u00e9todos podemos utilizar para realizar operaciones con el componente.

    "},{"location":"cleancode/angular/#carpetas-creadas-por-angular","title":"Carpetas creadas por Angular","text":"

    Al crear una aplicaci\u00f3n Angular, tendremos los siguientes directorios:

    • node_modules: Todos los m\u00f3dulos de librer\u00edas usadas por el proyecto.
    • \\src\\app: Contiene todo el c\u00f3digo asociado al proyecto.
      • \\src\\assets: Normalmente la carpeta usada para los recursos.
      • \\src\\environments: Aqu\u00ed ir\u00e1n los ficheros relacionados con los entornos de desarrollos.

    Otros ficheros importantes de un proyecto de Angular

    Otros archivos que debemos tener en cuenta dentro del proyecto son:

    • angular.json: Configuraci\u00f3n del propio CLI. La madre de todos los configuradores
    • package.json: Dependencias de librer\u00edas y scripts
    "},{"location":"cleancode/angular/#estructura-de-modulos","title":"Estructura de m\u00f3dulos","text":"

    Existe m\u00faltiples consensos al respecto de como estructurar un proyecto en Angular, pero al final, depende de los requisitos del proyecto. Una sugerencia de como hacerlo es la siguiente:

    - src\\app\n    - core              /* Componentes y utilidades comunes */ \n        - header        /* Estructura del header */ \n        - footer        /* Estructura del footer */ \n  - domain1       /* M\u00f3dulo con los componentes del dominio1 */\n      - services        /* Servicios con operaciones del dominio1 */ \n      - models          /* Modelos de datos del dominio1 */ \n      - component1      /* Componente1 del dominio1 */ \n      - componentX      /* ComponenteX del dominio1 */ \n  - domainX       /* As\u00ed para el resto de dominios de la aplicaci\u00f3n */\n

    Recordar, que esto es una sugerencia para una estructura de carpetas y componentes. No existe un estandar.

    ATENCI\u00d3N: Componentes gen\u00e9ricos

    Debemos tener en cuenta que a la hora de programar un componente core, lo ideal es pensar que sea un componente plug & play, es decir que si lo copias y lo llevas a otro proyecto funcione sin la necesidad de adaptarlo.

    "},{"location":"cleancode/angular/#buenas-practicas","title":"Buenas pr\u00e1cticas","text":"

    A continuaci\u00f3n veremos un listado de buenas pr\u00e1cticas de Angular y de c\u00f3digo limpio que deber\u00edamos intentar seguir en nuestro desarrollo.

    "},{"location":"cleancode/angular/#estructura-de-archivos","title":"Estructura de archivos","text":"

    Antes de empezar con un proyecto lo ideal, es pararse y pensar en los requerimientos de una buena estructura, en un futuro lo agradecer\u00e1s.

    "},{"location":"cleancode/angular/#nombres-claros","title":"Nombres claros","text":"

    Utilizar la S de los principios S.O.L.I.D para los nombres de variables, m\u00e9todos y dem\u00e1s c\u00f3digo.

    El efecto que produce este principio son clases con nombres muy descriptivos y por tanto largos.

    Tambi\u00e9n se recomienta utilizar kebab-case para los nombres de ficheros. Ej. hero-button.component.ts

    "},{"location":"cleancode/angular/#organiza-tu-codigo","title":"Organiza tu c\u00f3digo","text":"

    Intenta organizar tu c\u00f3digo fuente:

    • Lo m\u00e1s importante debe ir arriba.
    • Primero propiedades, despu\u00e9s m\u00e9todos.
    • Un Item para un archivo: cada archivo deber\u00eda contener solamente un componente, al igual que los servicios.
    • Solo una responsabilidad: Cada clase o modulo deber\u00eda tener solamente una responsabilidad.
    • El nombre correcto: las propiedades y m\u00e9todos deber\u00edan usar el sistema de camel case (ej: getUserByName), al contrario, las clases (componentes, servicios, etc) deben usar upper camel case (ej: UserComponent).
    • Los componentes y servicios deben tener su respectivo sufijo: UserComponent, UserService.
    • Imports: los archivos externos van primero.
    "},{"location":"cleancode/angular/#usar-linters-prettier-eslint","title":"Usar linters Prettier & ESLint","text":"

    Un linter es una herramienta que nos ayuda a seguir las buenas pr\u00e1cticas o gu\u00edas de estilo de nuestro c\u00f3digo fuente. En este caso, para JavaScript, proveeremos de unos muy famosos. Una de las m\u00e1s famosas es la combinaci\u00f3n de Angular app to ESLint with Prettier, AirBnB Styleguide Recordar que a\u00f1adir este tipo de configuraci\u00f3n es opcional, pero necesaria para tener un buen c\u00f3digo de calidad.

    "},{"location":"cleancode/angular/#git-hooks","title":"Git Hooks","text":"

    Los Git Hooks son scripts de shell que se ejecutan autom\u00e1ticamente antes o despu\u00e9s de que Git ejecute un comando importante como Commit o Push. Para hacer uso de el es tan sencillo como:

    npm install husky --save-dev

    Y a\u00f1adir en el fichero lo siguiente:

    // package.json\n{\n  \"husky\": {\n    \"hooks\": {\n      \"pre-commit\": \"npm test\",\n      \"pre-push\": \"npm test\",\n      \"...\": \"...\"\n    }\n  }\n}\n

    Usar husky para el preformateo de c\u00f3digo antes de subirlo

    Es una buena pr\u00e1ctica que todo el equipo use el mismo est\u00e1ndar de formateo de codigo, con husky se puede solucionar.

    "},{"location":"cleancode/angular/#utilizar-banana-in-the-box","title":"Utilizar Banana in the Box","text":"

    Como el nombre sugiere banana in the box se debe a la forma que tiene lo siguiente: [{}] Esto es una forma muy sencilla de trabajar los cambios en la forma de Two ways binding. Es decir, el padre informa de un valor u objeto y el hijo lo manipula y actualiza el estado/valor al padre inmediatamente. La forma de implementarlo es sencillo

    Padre: HTML:

    <my-input [(text)]=\"text\"></my-input>

    Hijo

    @Input() value: string;\n@Output() valueChange = new EventEmitter<string>();\nupdateValue(value){\n    this.value = value;\n    this.valueChange.emit(value);\n}\n

    Prefijo Change

    Destacar que el prefijo 'Change' es necesario incluirlo en el Hijo para que funcione

    "},{"location":"cleancode/angular/#correcto-uso-de-los-servicios","title":"Correcto uso de los servicios","text":"

    Una buena practica es aconsejable no declarar los servicios en el provides, sino usar un decorador que forma parte de las ultimas versiones de Angular

    @Injectable({\n  providedIn: 'root',\n})\nexport class HeroService {\n  constructor() { }\n}\n
    "},{"location":"cleancode/angular/#lazy-load","title":"Lazy Load","text":"

    Lazy Load es un patr\u00f3n de dise\u00f1o que consiste en retrasar la carga o inicializaci\u00f3n

    desde el app-routing.module.ts o desde app.routes.ts si estamos en Angular 17+

    A\u00f1adiremos un codigo parecido a este

      // Para cargar modulos\n  {\n    path: 'customers',\n    loadChildren: () => import('./customers/customers.module').then(m => m.CustomersModule)\n  },\n\n  // Para cargar standalone components\n  {\n    path: 'customers',\n    loadComponent: () => import('./customers/customers.component').then(m => m.CustomersComponent)\n  },\n

    Con esto veremos que el m\u00f3dulo o componente se cargar\u00e1 seg\u00fan se necesite.

    "},{"location":"cleancode/nodejs/","title":"Estructura y Buenas pr\u00e1cticas - Nodejs","text":""},{"location":"cleancode/nodejs/#estructura-y-funcionamiento","title":"Estructura y funcionamiento","text":"

    En los proyectos Nodejs no existe nada estandarizado y oficial que hable sobre estructura de proyectos y nomenclatura de Nodejs. Tan solo existen algunas sugerencias y buenas pr\u00e1cticas a la hora de desarrollar que te recomiendo que utilices en la medida de lo posible.

    Tip

    Piensa que el c\u00f3digo fuente que escribes hoy, es como un libro que se leer\u00e1 durante a\u00f1os. Alguien tendr\u00e1 que coger tu c\u00f3digo y leerlo en unos meses o a\u00f1os para hacer alguna modificaci\u00f3n y, como buenos desarrolladores que somos, tenemos la obligaci\u00f3n de facilitarle en todo lo posible la comprensi\u00f3n de ese c\u00f3digo fuente. Quiz\u00e1 esa persona futura podr\u00edas ser tu en unos meses y quedar\u00eda muy mal que no entendieras ni tu propio c\u00f3digo

    "},{"location":"cleancode/nodejs/#estructura-en-capas","title":"Estructura en capas","text":"

    Todos los proyectos para crear una Rest API con node y express est\u00e1n divididos en capas. Como m\u00ednimo estar\u00e1 la capa de rutas, controlador y modelo. En nuestro caso vamos a a\u00f1adir una capa mas de servicios para quitarle trabajo al controlador y desacoplarlo de la capa de datos. As\u00ed si en el futuro queremos cambiar nuestra base de datos no romperemos tanto \ud83d\ude0a

    Rutas

    En nuestro proyecto una ruta ser\u00e1 una secci\u00f3n de c\u00f3digo express que asociar\u00e1 un verbo http, una ruta o patr\u00f3n de url y una funci\u00f3n perteneciente al controlador para manejar esa petici\u00f3n.

    Controladores

    En nuestros controladores tendremos los m\u00e9todos que obtendr\u00e1n las solicitudes de las rutas, se comunicar\u00e1n con la capa de servicio y convertir\u00e1n estas solicitudes en respuestas http.

    Servicio

    Nuestra capa de servicio incluir\u00e1 toda la l\u00f3gica de negocio de nuestra aplicaci\u00f3n. Para realizar sus operaciones puede realizar llamadas tanto a otras clases dentro de esta capa, como a clases de la capa inferior.

    Modelo

    Como su nombre indica esta capa representa los modelos de datos de nuestra aplicaci\u00f3n. En nuestro caso, al usar un ODM, solo tendremos modelos de datos definidos seg\u00fan sus requisitos.

    "},{"location":"cleancode/nodejs/#buenas-practicas","title":"Buenas pr\u00e1cticas","text":""},{"location":"cleancode/nodejs/#accesos-entre-capas","title":"Accesos entre capas","text":"

    En base a la divisi\u00f3n por capas que hemos comentado arriba, y el resto de entidades implicadas, hay una serie de reglas important\u00edsimas que debes seguir muy de cerca:

    • Un Controlador

      • NO debe contener l\u00f3gica en su clase. Solo est\u00e1 permitido que ejecute l\u00f3gica a trav\u00e9s de una llamada al objeto de la capa L\u00f3gica.
      • NO puede ejecutar directamente operaciones de la capa Acceso a Datos, siempre debe pasar por la capa de servicios.
      • Debemos seguir una coherencia entre todas las URL de las operaciones. Por ejemplo, si elegimos save para guardar, usemos esa palabra en todas las operaciones que sean de ese tipo. Evitad utilizar diferentes palabras save, guardar, persistir, actualizar para la misma acci\u00f3n.
    • Un Servicio

      • NO puede llamar a objetos de la capa Controlador.
      • NO debe llamar a Acceso a Datos que NO sean de su \u00e1mbito / competencia.
      • Si es necesario puede llamar a otros Servicios para recuperar cierta informaci\u00f3n que no sea de su \u00e1mbito / competencia.
      • Es un buen lugar para implementar la l\u00f3gica de negocio.
    "},{"location":"cleancode/nodejs/#usar-linters-prettier-eslint-se-recomienda-encarecidamente","title":"Usar linters Prettier & ESLint (Se recomienda encarecidamente)","text":"

    Un linter es una herramienta que nos ayuda a seguir las buenas pr\u00e1cticas o gu\u00edas de estilo de nuestro c\u00f3digo fuente. En este caso, para JavaScript, proveeremos de unos muy famosos. Una de las m\u00e1s famosas es la combinaci\u00f3n de Angular app to ESLint with Prettier, AirBnB Styleguide Recordar que a\u00f1adir este tipo de configuraci\u00f3n es opcional, pero necesaria para tener un buen c\u00f3digo de calidad.

    "},{"location":"cleancode/react/","title":"Estructura y Buenas pr\u00e1cticas - React","text":"

    Nota

    Antes de empezar y para puntualizar, React se considera un framework SPA Single-page application.

    Aqu\u00ed tenemos que puntualizar que React por s\u00ed mismo es una librer\u00eda y no un framework, puesto que se ocupa de las interfaces de usuario. Sin embargo, diversos a\u00f1adidos pueden convertir a React en un producto equiparable en caracter\u00edsticas a un framework.

    En esta parte vamos a explicar los fundamentos de un proyecto en React y las recomendaciones existentes.

    "},{"location":"cleancode/react/#estructura-y-funcionamiento","title":"Estructura y funcionamiento","text":""},{"location":"cleancode/react/#como-funciona-react","title":"Como funciona React","text":"

    React es una herramienta para crear interfaces de usuario de una manera \u00e1gil y vers\u00e1til, en lugar de manipular el DOM del navegador directamente, React crea un DOM virtual en la memoria, d\u00f3nde realiza toda la manipulaci\u00f3n necesaria antes de realizar los cambios en el DOM del navegador. Estas interfaces de usuario denominadas componentes pueden definirse como clases o funciones independiente y reutilizables con unos par\u00e1metros de entrada que devuelven elementos de react. En ese tutorial solo utilizaremos componentes de tipo funci\u00f3n.

    Por si no te suena, un componente web es una forma de crear un bloque de c\u00f3digo encapsulado y de responsabilidad \u00fanica que puede reutilizarse en cualquier pagina mediante nuevas etiquetas html.

    Nota

    Desde la versi\u00f3n 16.8 se introdujo en React el concepto de hooks. Esto permiti\u00f3 usar el estado y otras caracter\u00edsticas de React sin necesidad de escribir una clase.

    "},{"location":"cleancode/react/#ciclo-de-vida-de-un-componente-en-react","title":"Ciclo de vida de un componente en React","text":"

    El comportamiento de ciclo de vida de un componente React pasa por diferentes etapas que podemos ver en el esquema que mostramos a continuaci\u00f3n:

    Es importante tenerlo claro para saber que m\u00e9todos podemos utilizar para realizar operaciones con el componente.

    "},{"location":"cleancode/react/#carpetas-creadas-por-react","title":"Carpetas creadas por React","text":"

    Al crear una aplicaci\u00f3n React, tendremos los siguientes directorios:

    • node_modules: Todos los m\u00f3dulos de librer\u00edas usadas por el proyecto.
    • \\src\\app: Contiene todo el c\u00f3digo asociado al proyecto.
      • \\src\\assets: Normalmente la carpeta usada para los recursos.

    Otros ficheros importantes de un proyecto de React

    Otros archivos que debemos tener en cuenta dentro del proyecto son:

    • package.json: Dependencias de librer\u00edas y scripts
    "},{"location":"cleancode/react/#estructura-de-nuestro-proyecto","title":"Estructura de nuestro proyecto","text":"

    Existe m\u00faltiples consensos al respecto de c\u00f3mo estructurar un proyecto en React, pero al final, depende de los requisitos del proyecto. Una sugerencia de c\u00f3mo hacerlo es la siguiente:

    - src\\\n    - components         /* Componentes comunes */ \n  - context            /* Carpeta para almacenar el contexto de la aplicaci\u00f3n */ \n  - pages              /* Carpeta para componentes asociados a rutas del navegador */\n      - components     /* Componentes propios de cada p\u00e1gina */ \n  - redux              /* Para todo aquello relacionado con el estado de nuestra aplicaci\u00f3n */\n  - types              /* Carpeta para los tipos de datos de typescript */\n

    Recordad, que \u00e9sto es una sugerencia para una estructura de carpetas y componentes. No existe un est\u00e1ndar.

    "},{"location":"cleancode/react/#buenas-practicas","title":"Buenas pr\u00e1cticas","text":"

    A continuaci\u00f3n, veremos un listado de buenas pr\u00e1cticas de React y de c\u00f3digo limpio que deber\u00edamos intentar seguir en nuestro desarrollo.

    "},{"location":"cleancode/react/#estructura-de-archivos","title":"Estructura de archivos","text":"

    Antes de empezar con un proyecto lo ideal, es pararse y pensar en los requerimientos de una buena estructura, en un futuro lo agradecer\u00e1s.

    "},{"location":"cleancode/react/#nombres-claros","title":"Nombres claros","text":"

    Utilizar la S de los principios S.O.L.I.D para los nombres de variables, m\u00e9todos y dem\u00e1s c\u00f3digo.

    El efecto que produce este principio son clases con nombres muy descriptivos y por tanto largos.

    "},{"location":"cleancode/react/#organiza-tu-codigo","title":"Organiza tu c\u00f3digo","text":"

    Intenta organizar tu c\u00f3digo fuente:

    • Lo m\u00e1s importante debe ir arriba.
    • Primero propiedades, despu\u00e9s m\u00e9todos.
    • Un Item para un archivo: cada archivo deber\u00eda contener solamente un componente, al igual que los servicios.
    • Solo una responsabilidad: Cada clase o modulo deber\u00eda tener solamente una responsabilidad.
    • El nombre correcto: las propiedades y m\u00e9todos deber\u00edan usar el sistema de camel case (ej: getUserByName), al contrario, las clases (componentes, servicios, etc) deben usar upper camel case (ej: UserComponent).
    • Los componentes y servicios deben tener su respectivo sufijo: UserComponent, UserService.
    • Imports: los archivos externos van primero.
    "},{"location":"cleancode/react/#usar-linters-prettier-eslint","title":"Usar linters Prettier & ESLint","text":"

    Un linter es una herramienta que nos ayuda a seguir las buenas pr\u00e1cticas o gu\u00edas de estilo de nuestro c\u00f3digo fuente. En este caso, para JavaScript, proveeremos de unos muy famosos. Recordar que a\u00f1adir este tipo de configuraci\u00f3n es opcional, pero necesaria para tener un buen c\u00f3digo de calidad.

    "},{"location":"cleancode/react/#usa-el-estado-correctamente","title":"Usa el estado correctamente","text":"

    La primera regla del hook useState es usarlo solo localmente. El estado global de nuestra aplicaci\u00f3n debe de entrar a nuestro componente a trav\u00e9s de las props as\u00ed como las mutaciones de este solo deben realizarse mediante alguna herramienta de gesti\u00f3n de estados como redux. Por otro lado, es preferible no abusar de los hooks y solo usarlos cuando sea realmente necesario ya que pueden reducir el rendimiento de nuestra aplicaci\u00f3n.

    "},{"location":"cleancode/react/#reutiliza-codigo-y-componentes","title":"Reutiliza c\u00f3digo y componentes","text":"

    Siempre que sea posible deberemos de reutilizar c\u00f3digo mediante funciones compartidas o bien si este c\u00f3digo implica almacenamiento de estado u otras caracter\u00edsticas similares mediante custom Hooks.

    "},{"location":"cleancode/react/#usa-ts-en-lugar-de-js","title":"Usa TS en lugar de JS","text":"

    Ya hemos creado nuestro proyecto incluyendo typescript pero esto no viene por defecto en un proyecto React como si pasa con Angular. Nuestra recomendaci\u00f3n es que siempre que puedas a\u00f1adas typescript a tus proyectos React, no solo se gana calidad en el c\u00f3digo, sino que eliminamos la probabilidad de usar un componente incorrectamente y ganamos tiempo de desarrollo.

    "},{"location":"cleancode/springboot/","title":"Estructura y Buenas pr\u00e1cticas - Spring Boot","text":""},{"location":"cleancode/springboot/#estructura-y-funcionamiento","title":"Estructura y funcionamiento","text":"

    En Springboot no existe nada estandarizado y oficial que hable sobre estructura de proyectos y nomenclatura. Tan solo existen algunas sugerencias y buenas pr\u00e1cticas a la hora de desarrollar que te recomiendo que utilices en la medida de lo posible.

    Tip

    Piensa que el c\u00f3digo fuente que escribes hoy, es como un libro que se leer\u00e1 durante a\u00f1os. Alguien tendr\u00e1 que coger tu c\u00f3digo y leerlo en unos meses o a\u00f1os para hacer alguna modificaci\u00f3n y, como buenos desarrolladores que somos, tenemos la obligaci\u00f3n de facilitarle en todo lo posible la comprensi\u00f3n de ese c\u00f3digo fuente. Quiz\u00e1 esa persona futura podr\u00edas ser tu en unos meses y quedar\u00eda muy mal que no entendieras ni tu propio c\u00f3digo

    "},{"location":"cleancode/springboot/#estructura-en-capas","title":"Estructura en capas","text":"

    Todos los proyectos web que construimos basados en Springboot se caracterizan por estar divididos en tres capas (a menos que utilicemos DDD para desarrollar que entonces existen infinitas capas ).

    • Controlador. Es la capa m\u00e1s alta, la que tiene acceso directo con el cliente. En esta capa es donde se exponen las operaciones que queremos publicar y que el cliente puede consumir. Para realizar sus operaciones lo m\u00e1s normal es que realice llamadas a las clases de la capa inmediatamente inferior.
    • L\u00f3gica. Tambi\u00e9n llamada capa de Servicios. Es la capa intermedia que da soporte a las operaciones que est\u00e1n expuestas y ejecutan toda la l\u00f3gica de negocio de la aplicaci\u00f3n. Para realizar sus operaciones puede realizar llamadas tanto a otras clases dentro de esta capa, como a clases de la capa inferior.
    • Acceso a Datos. Como su nombre indica, es la capa que accede a datos. T\u00edpicamente es la capa que ejecuta las consultas contra BBDD, pero esto no tiene por qu\u00e9 ser obligadamente as\u00ed. Tambi\u00e9n entrar\u00edan en esa capa aquellas clases que consumen datos externos, por ejemplo de un servidor externo. Las clases de esta capa deben ser nodos finales, no pueden llamar a ninguna otra clase para ejecutar sus operaciones, ni siquiera de su misma capa.
    "},{"location":"cleancode/springboot/#estructura-de-proyecto","title":"Estructura de proyecto","text":"

    En proyectos medianos o grandes, estructurar los directorios del proyecto en base a la estructura anteriormente descrita ser\u00eda muy complejo, ya que en cada uno de los niveles tendr\u00edamos muchas clases. As\u00ed que lo normal es diferenciar por \u00e1mbito funcional y dentro de cada package realizar la separaci\u00f3n en Controlador, L\u00f3gica y Acceso a datos.

    Tened en cuenta en un mismo \u00e1mbito funcional puede tener varios controladores o varios servicios de l\u00f3gica uno por cada entidad que estemos tratando. Siempre que se pueda, agruparemos entidades que intervengan dentro de una misma funcionalidad.

    En nuestro caso del tutorial, tendremos tres \u00e1mbitos funcionales Categor\u00eda, Autor, y Juego que diferenciaremos cada uno con su propia estructura.

    "},{"location":"cleancode/springboot/#buenas-practicas","title":"Buenas pr\u00e1cticas","text":""},{"location":"cleancode/springboot/#nomenclatura-de-las-clases","title":"Nomenclatura de las clases","text":"

    @TODO: En construcci\u00f3n

    "},{"location":"cleancode/springboot/#accesos-entre-capas","title":"Accesos entre capas","text":"

    En base a la divisi\u00f3n por capas que hemos comentado arriba, y el resto de entidades implicadas, hay una serie de reglas important\u00edsimas que debes seguir muy de cerca:

    • Un Controlador
      • NO debe contener l\u00f3gica en su clase. Solo est\u00e1 permitido que ejecute l\u00f3gica a trav\u00e9s de una llamada al objeto de la capa L\u00f3gica.
      • NO puede ejecutar directamente operaciones de la capa Acceso a Datos, siempre debe pasar por la capa L\u00f3gica.
      • NO debe enviar ni recibir del cliente objetos de tipo Entity.
      • Es un buen lugar para realizar las conversiones de datos entre Entity y Dto.
      • En teor\u00eda cada operaci\u00f3n deber\u00eda tener su propio Dto, aunque los podemos reutilizar entre operaciones similares.
      • Debemos seguir una coherencia entre todas las URL de las operaciones. Por ejemplo si elegimos save para guardar, usemos esa palabra en todas las operaciones que sean de ese tipo. Evitad utilizar diferentes palabras save, guardar, persistir, actualizar para la misma acci\u00f3n.
    • Un Servicio
      • NO puede llamar a objetos de la la capa Controlador.
      • NO puede ejecutar directamente queries contra la BBDD, siempre debe pasar por la capa Acceso a Datos.
      • NO debe llamar a Acceso a Datos que NO sean de su \u00e1mbito / competencia.
      • Si es necesario puede llamar a otros Servicios para recuperar cierta informaci\u00f3n que no sea de su \u00e1mbito / competencia.
      • Debe trabajar en la medida de lo posible con objetos de tipo Entity.
      • Es un buen lugar para implementar la l\u00f3gica de negocio.
    • Un Acceso a Datos
      • NO puede llamar a ninguna otra capa. Ni Controlador, ni Servicios, ni Acceso a Datos.
      • NO debe contener l\u00f3gica en su clase.
      • Esta capa solo debe resolver el dato que se le ha solicitado y devolverlo a la capa de Servicios.
    "},{"location":"cleancode/vuejs/","title":"Estructura y Buenas pr\u00e1cticas - Vue.js","text":"

    Nota

    Antes de empezar y para puntualizar, Vue.js es un framework progresivo para construir interfaces de usuario. A diferencia de otros frameworks monol\u00edticos, Vue.js est\u00e1 dise\u00f1ado desde cero para ser utilizado incrementalmente. La librer\u00eda central est\u00e1 enfocada solo en la capa de visualizaci\u00f3n, y es f\u00e1cil de utilizar e integrar con otras librer\u00edas o proyectos existentes. Por otro lado, Vue.js tambi\u00e9n es perfectamente capaz de impulsar sofisticadas Single-Page Applications cuando se utiliza en combinaci\u00f3n con herramientas modernas y librer\u00edas de apoyo.

    En esta parte vamos a explicar los fundamentos de un proyecto en Vue.js y las recomendaciones existentes.

    "},{"location":"cleancode/vuejs/#estructura-y-funcionamiento","title":"Estructura y funcionamiento","text":""},{"location":"cleancode/vuejs/#ciclos-de-vida-de-un-componente","title":"Ciclos de vida de un componente","text":"

    Vue.js cuenta con un conjunto de ciclos de vida que permiten a los desarrolladores controlar y personalizar el comportamiento de sus componentes en diferentes momentos. Estos ciclos de vida se pueden agrupar en tres fases principales: creaci\u00f3n, actualizaci\u00f3n y eliminaci\u00f3n.

    A continuaci\u00f3n, te explicar\u00e9 cada uno de los ciclos de vida disponibles en Vue.js junto con la Options API:

    1. beforeCreate: Este ciclo de vida se ejecuta inmediatamente despu\u00e9s de que se haya creado una instancia de componente, pero antes de que se haya creado su DOM. En este punto, a\u00fan no es posible acceder a las propiedades del componente y a\u00fan no se han establecido las observaciones reactivas.

    2. created: Este ciclo de vida se ejecuta despu\u00e9s de que se haya creado una instancia de componente y se hayan establecido las observaciones reactivas. En este punto, el componente ya puede acceder a sus propiedades y m\u00e9todos.

    3. beforeMount: Este ciclo de vida se ejecuta justo antes de que el componente se monte en el DOM. En este punto, el componente ya est\u00e1 preparado para ser renderizado, pero a\u00fan no se ha agregado al \u00e1rbol de elementos del DOM.

    4. mounted: Este ciclo de vida se ejecuta despu\u00e9s de que el componente se ha montado en el DOM. En este punto, el componente ya est\u00e1 en el \u00e1rbol de elementos del DOM y se puede acceder a sus elementos hijos y a los elementos del DOM que lo rodean.

    5. beforeUpdate: Este ciclo de vida se ejecuta justo antes de que el componente se actualice en respuesta a un cambio en sus propiedades o estado. En este punto, el componente a\u00fan no se ha actualizado en el DOM.

    6. updated: Este ciclo de vida se ejecuta despu\u00e9s de que el componente se haya actualizado en el DOM en respuesta a un cambio en sus propiedades o estado. En este punto, el componente ya se ha actualizado en el DOM y se puede acceder a sus elementos hijos y a los elementos del DOM que lo rodean.

    7. beforeUnmount: Este ciclo de vida se ejecuta justo antes de que el componente se elimine del DOM. En este punto, el componente a\u00fan est\u00e1 en el \u00e1rbol de elementos del DOM.

    8. unmounted: Este ciclo de vida se ejecuta despu\u00e9s de que el componente se haya eliminado del DOM. En este punto, el componente ya no est\u00e1 en el \u00e1rbol de elementos del DOM y no se puede acceder a sus elementos hijos.

    9. errorCaptured: Este ciclo de vida se ejecuta cuando se produce un error en cualquier descendiente del componente y se captura en el componente actual. Esto permite que el componente maneje el error de forma personalizada en lugar de propagarse hacia arriba en la cadena de componentes.

    10. activated: Este ciclo de vida se ejecuta cuando un componente que se encuentra en un \u00e1rbol de componentes inactivo (por ejemplo, un componente en una pesta\u00f1a inactiva) se activa.

    11. deactivated: Este ciclo de vida se ejecuta cuando un componente que se encuentra en un \u00e1rbol de componentes activo (por ejemplo, un componente en una pesta\u00f1a activa) se desactiva y se vuelve inactivo.

    12. renderTracked: Este ciclo de vida se ejecuta cuando se observa una dependencia en el proceso de renderizado del componente. Esto se utiliza principalmente para fines de depuraci\u00f3n.

    13. renderTriggered: Este ciclo de vida se ejecuta cuando se desencadena un nuevo renderizado del componente. Esto se utiliza principalmente para fines de depuraci\u00f3n.

    14. serverPrefetch: Este ciclo de vida se utiliza en el contexto de renderizado del lado del servidor (SSR). Se ejecuta cuando el componente se preprocesa en el servidor antes de enviarse al cliente. En este punto, el componente a\u00fan no se ha montado en el DOM y no se pueden realizar operaciones que dependan del DOM. Esto se utiliza principalmente para cargar datos de forma as\u00edncrona antes de que se renderice el componente en el servidor.

    Os dejo un peque\u00f1o esquema de los ciclos de vida mas importantes y en que momento se ejecutan:

    Es importante tenerlo claro para saber que m\u00e9todos podemos utilizar para realizar operaciones con el componente.

    "},{"location":"cleancode/vuejs/#carpetas-creadas-por-vuejs","title":"Carpetas creadas por Vue.js","text":"
    • node_modules: Todos los m\u00f3dulos de librer\u00edas usadas por el proyecto.
    • public: Contiene iconos y archivos accesibles por todos los usuarios.
    • .quasar: Contiene configuraci\u00f3n propia de Quasar.
    • \\src: Contiene todo el c\u00f3digo asociado al proyecto.
      • \\src\\assets: Normalmente la carpeta usada para los recursos.
      • \\src\\components: Aqu\u00ed ir\u00e1n los diferentes componentes que iremos creando para la aplicaci\u00f3n.
      • \\src\\router: Es la carpeta donde el scafolding nos mete el router con sus diferentes rutas.
      • \\src\\layouts: Aqu\u00ed iran las diferentes vistas de la aplicaci\u00f3n.

    Otros ficheros importantes de un proyecto de Vue.js

    Otros archivos que debemos tener en cuenta dentro del proyecto son:

    • quasar.d.ts: Configurador de la conexi\u00f3n entre la librer\u00eda y Vue
    • package.json: Dependencias de librer\u00edas y scripts
    • quasar.config.js: Configurador del CLI de Quasar
    • \\src\\App.vue: Punto de entrada a nuestra aplicaci\u00f3n
    "},{"location":"cleancode/vuejs/#buenas-practicas","title":"Buenas pr\u00e1cticas","text":"

    A continuaci\u00f3n veremos un listado de buenas pr\u00e1cticas de Vue.js y de c\u00f3digo limpio que deber\u00edamos intentar seguir en nuestro desarrollo.

    "},{"location":"cleancode/vuejs/#estructura-de-archivos","title":"Estructura de archivos","text":"

    Antes de empezar con un proyecto lo ideal, es pararse y pensar en los requerimientos de una buena estructura, en un futuro lo agradecer\u00e1s.

    "},{"location":"cleancode/vuejs/#nombres-claros","title":"Nombres claros","text":"

    Determinar una manera de nombrar a los componentes (UpperCamelCase, lowerCamelCase, kebab-case, snake_case, ...) y continuarla para todos los archivos, nombres descriptivos de los componentes y en una ruta acorde (si es un componente que forma parte de una pantalla, se ubicar\u00e1 dentro de la carpeta de esa pantalla pero si se usa en m\u00e1s de una pantalla, se ubicar\u00e1 en una carpeta externa a cualquier pantalla llamada common), componentes de m\u00e1ximo 350 l\u00edneas y componentes con finalidad \u00fanica (recibe los datos necesarios para realizar las tareas b\u00e1sicas de ese componente).

    "},{"location":"cleancode/vuejs/#organiza-tu-codigo","title":"Organiza tu c\u00f3digo","text":"

    El c\u00f3digo debe estar ordenado dentro de los componente siguiendo un orden de importancia similar a este:

    1. Importaciones de las diferentes librer\u00edas o componentes usados.
    2. Importaciones de funciones de otros archivos (como utils).
    3. Variables o constantes usadas para almacenar la informaci\u00f3n necesaria en este componente.
    4. Funciones necesarias para el resto del c\u00f3digo.
    5. Variables computadas, watchers, etc.
    6. C\u00f3digo HTML del componente.
    "},{"location":"cleancode/vuejs/#consejos-varios","title":"Consejos varios","text":"

    Modificaciones entre componentes

    A la hora de crear un componente b\u00e1sico (como un input propio) que necesite modificar su propio valor (algo que un componente hijo no debe hacer, ya que la variable estar\u00e1 en el padre), saber diferenciar entre v-model y modelValue (esta \u00faltima s\u00ed que permite modificar el valor en el padre mediante el evento update:modelValue sin tener que hacer nada m\u00e1s en el padre que pasarle el valor).

    Utiliza formateo y correcci\u00f3n de c\u00f3digo

    Si has seguido nuestro tutorial se habr\u00e1 instalado ESLint y Prettier. Si no, deber\u00edas instalarlo para generar c\u00f3digo de buena calidad. Adem\u00e1s de instalar alguna extensi\u00f3n en Visual Studio Code que te ayude a gestionar esas herramientas.

    Nomenclatura de funciones y variables

    El nombre de las funciones, al igual que los path de una API, deber\u00edan ser autoexplicativos y no tener que seguir la traza del c\u00f3digo para saber qu\u00e9 hace. Con un buen nombre para cada funci\u00f3n o variables de estado, evitas tener que a\u00f1adir comentarios para explicar qu\u00e9 hace o qu\u00e9 almacena cada una de ellas.

    "},{"location":"develop/basic/angular/","title":"Listado simple - Angular","text":"

    Ahora que ya tenemos listo el proyecto frontend de Angular (en el puerto 4200), ya podemos empezar a codificar la soluci\u00f3n.

    "},{"location":"develop/basic/angular/#primeros-pasos","title":"Primeros pasos","text":"

    Antes de empezar

    Quiero hacer hincapi\u00e9 que Angular tiene una documentaci\u00f3n muy extensa y completa, as\u00ed que te recomiendo que hagas uso de ella cuando tengas cualquier duda. Tanto en la propia web de documentaci\u00f3n de Angular como en la web de componentes Angular Material puedes buscar casi cualquier ejemplo que necesites.

    Si abrimos el proyecto con el IDE que tengamos (Visual Studio Code en el caso del tutorial) podemos ver que en la carpeta src/app existen unos ficheros ya creados por defecto. Estos ficheros son:

    • app.component.ts \u2192 contiene el c\u00f3digo inicial del proyecto escrito en TypeScript.
    • app.component.html \u2192 contiene la plantilla inicial del proyecto escrita en HTML.
    • app.component.scss \u2192 contiene los estilos CSS privados de la plantilla inicial.

    Vamos a modificar este c\u00f3digo inicial para ver como funciona. Abrimos el fichero app.component.ts y modificamos la l\u00ednea donde se asigna un valor a la variable title.

    app.component.ts
    ...\ntitle = 'Tutorial de Angular';\n...\n

    Ahora abrimos el fichero app.component.html, borramos todo el c\u00f3digo de la plantilla y a\u00f1adimos el siguiente c\u00f3digo:

    app.component.html
    <h1>{{title}}</h1>\n

    Las llaves dobles permiten hacen un binding entre el c\u00f3digo del componente y la plantilla. Es decir, en este caso ir\u00e1 al c\u00f3digo TypeScript y buscar\u00e1 el valor de la variable title.

    Consejo

    El binding tambi\u00e9n nos sirve para ejecutar los m\u00e9todos de TypeScript desde el c\u00f3digo HTML. Adem\u00e1s si el valor que contiene la variable se modificara durante la ejecuci\u00f3n de alg\u00fan m\u00e9todo, autom\u00e1ticamente el c\u00f3digo HTML refrescar\u00eda el nuevo valor de la variable title

    Si abrimos el navegador y accedemos a http://localhost:4200/ podremos ver el resultado del c\u00f3digo.

    "},{"location":"develop/basic/angular/#layout-general","title":"Layout general","text":""},{"location":"develop/basic/angular/#crear-componente","title":"Crear componente","text":"

    Lo primero que vamos a hacer es escoger un tema y una paleta de componentes para trabajar. Lo m\u00e1s c\u00f3modo es trabajar con Material que ya viene perfectamente integrado en Angular. Ejecutamos el comando y elegimos la paleta de colores que m\u00e1s nos guste o bien creamos una custom:

    ng add @angular/material\n

    Recuerda

    Al a\u00f1adir una nueva librer\u00eda tenemos que parar el servidor y volver a arrancarlo para que compile y precargue las nuevas dependencias.

    Una vez a\u00f1adida la dependencia, lo que queremos es crear una primera estructura inicial a la p\u00e1gina. Si te acuerdas cual era la estructura (y si no te acuerdas, vuelve a la secci\u00f3n Contexto de la aplicaci\u00f3n y lo revisas), ten\u00edamos una cabecera superior con un logo y t\u00edtulo y unas opciones de men\u00fa.

    Pues vamos a ello, crearemos esa estructura com\u00fan para toda la aplicaci\u00f3n. Este componente al ser algo core para toda la aplicaci\u00f3n deber\u00edamos crearlo dentro del m\u00f3dulo core como ya vimos anteriormente.

    Pero antes de todo, vamos a crear los m\u00f3dulos generales de la aplicaci\u00f3n, as\u00ed que ejecutamos en consola el comando que nos permite crear un m\u00f3dulo nuevo:

    ng generate module core\n

    Y a\u00f1adimos esos m\u00f3dulos al m\u00f3dulo padre de la aplicaci\u00f3n:

    app.module.ts
    import { BrowserModule } from '@angular/platform-browser';\nimport { NgModule } from '@angular/core';\n\nimport { AppRoutingModule } from './app-routing.module';\nimport { AppComponent } from './app.component';\nimport { BrowserAnimationsModule } from '@angular/platform-browser/animations';\nimport { CoreModule } from './core/core.module';\n\n@NgModule({\n  declarations: [\n    AppComponent\n  ],\n  imports: [\n    BrowserModule,\n    AppRoutingModule,\n    CoreModule,\n    BrowserAnimationsModule,\n  ],\n  providers: [],\n  bootstrap: [AppComponent]\n})\nexport class AppModule { }\n

    Y despu\u00e9s crearemos el componente header, dentro del m\u00f3dulo core. Para eso ejecutaremos el comando:

    ng generate component core/header\n
    "},{"location":"develop/basic/angular/#codigo-de-la-pantalla","title":"C\u00f3digo de la pantalla","text":"

    Esto nos crear\u00e1 una carpeta con los ficheros del componente, donde tendremos que copiar el siguiente contenido:

    header.component.htmlheader.component.scss
    <mat-toolbar>\n    <mat-toolbar-row>\n        <div class=\"header_container\">\n            <div class=\"header_title\">              \n                <mat-icon>storefront</mat-icon> Ludoteca Tan\n            </div>\n\n            <div class=\"header_separator\"> | </div>\n\n            <div class=\"header_menu\">\n                <div class=\"header_button\">\n                    <a routerLink=\"/games\" routerLinkActive=\"active\">Cat\u00e1logo</a>\n                </div>\n                <div class=\"header_button\">\n                    <a routerLink=\"/categories\" routerLinkActive=\"active\">Categor\u00edas</a>\n                </div>\n                <div class=\"header_button\">\n                    <a routerLink=\"/authors\" routerLinkActive=\"active\">Autores</a>\n                </div>\n            </div>\n\n            <div class=\"header_login\">\n                <mat-icon>account_circle</mat-icon> Sign in\n            </div>\n        </div>\n    </mat-toolbar-row>\n</mat-toolbar>\n
    .mat-toolbar {\n  background-color: blue;\n  color: white;\n}\n\n.header_container {\n    display: flex;\n    width: 100%;\n    .header_title {\n        .mat-icon {\n            vertical-align: sub;\n        }\n    }\n\n    .header_separator {\n        margin-left: 30px;\n        margin-right: 30px;\n    }\n\n    .header_menu {\n        flex-grow: 4;\n        display: flex;\n        flex-direction: row;\n\n        .header_button {\n            margin-left: 1em;\n            margin-right: 1em;\n            font-size: 16px;\n\n            a {\n              font-weight: lighter;\n              text-decoration: none;\n              cursor: pointer;\n              color: white;\n            }\n\n            a:hover {\n              color: grey;\n            }\n\n            a.active {\n              font-weight: normal;\n              text-decoration: underline;\n              color: lightyellow;\n            }\n\n        }\n    }\n\n    .header_login {\n      font-size: 16px;\n      cursor: pointer;\n      .mat-icon {\n          vertical-align: sub;\n      }\n  }\n}\n

    Al utilizar etiquetas de material como mat-toolbar o mat-icon y routerLink necesitaremos importar las dependencias. Esto lo podemos hacer directamente en el m\u00f3dulo del que depende, es decir en el fichero core.module.ts

    core.module.ts
    import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatToolbarModule } from '@angular/material/toolbar';\nimport { HeaderComponent } from './header/header.component';\nimport { RouterModule } from '@angular/router';\n\n\n@NgModule({\n  declarations: [HeaderComponent],\n  imports: [\n    CommonModule,\n    RouterModule,\n    MatIconModule, \n    MatToolbarModule,\n  ],\n  exports: [\n    HeaderComponent\n  ]\n})\nexport class CoreModule { }\n

    Adem\u00e1s de a\u00f1adir las dependencias, diremos que este m\u00f3dulo va a exportar el componente HeaderComponent para poder utilizarlo desde otras p\u00e1ginas.

    Ya por \u00faltimo solo nos queda modificar la p\u00e1gina general de la aplicaci\u00f3n app.component.html para a\u00f1adirle el componente HeaderComponent.

    app.component.html
    <div>\n  <app-header></app-header>\n  <div>\n    <router-outlet></router-outlet>\n  </div>\n</div>\n

    Vamos al navegador y refrescamos la p\u00e1gina, deber\u00eda aparecer una barra superior (Header) con las opciones de men\u00fa. Algo similar a esto:

    Recuerda

    Cuando se a\u00f1aden componentes a los ficheros html, siempre se deben utilizar los selectores definidos para el componente. En el caso anterior hemos a\u00f1adido app-header que es el mismo nombre selector que tiene el componente en el fichero header.component.ts. Adem\u00e1s, recuerda que para poder utilizar componentes de otros m\u00f3dulos, los debes exportar ya que de lo contrario tan solo podr\u00e1n utilizarse dentro del m\u00f3dulo donde se declaran.

    "},{"location":"develop/basic/angular/#creando-un-listado-basico","title":"Creando un listado b\u00e1sico","text":""},{"location":"develop/basic/angular/#crear-componente_1","title":"Crear componente","text":"

    Ya tenemos la estructura principal, ahora vamos a crear nuestra primera pantalla. Vamos a empezar por la de Categor\u00edas que es la m\u00e1s sencilla, ya que se trata de un listado, que muestra datos sin filtrar ni paginar.

    Como categor\u00edas es un dominio funcional de la aplicaci\u00f3n, vamos a crear un m\u00f3dulo que contenga toda la funcionalidad de ese dominio. Ejecutamos en consola:

    ng generate module category\n

    Y por tanto, al igual que hicimos anteriormente, hay que a\u00f1adir el m\u00f3dulo al fichero app.module.ts

    app.module.ts
    import { BrowserModule } from '@angular/platform-browser';\nimport { NgModule } from '@angular/core';\n\nimport { AppRoutingModule } from './app-routing.module';\nimport { AppComponent } from './app.component';\nimport { BrowserAnimationsModule } from '@angular/platform-browser/animations';\nimport { CoreModule } from './core/core.module';\nimport { CategoryModule } from './category/category.module';\n\n\n@NgModule({\n  declarations: [\n    AppComponent\n  ],\n  imports: [\n    BrowserModule,\n    AppRoutingModule,\n    CoreModule,\n    CategoryModule,\n    BrowserAnimationsModule,\n  ],\n  providers: [],\n  bootstrap: [AppComponent]\n})\nexport class AppModule { }\n

    Ahora todas las pantallas, componentes y servicios que creemos, referidos a este dominio funcional, deber\u00e1n ir dentro del modulo cagegory.

    Vamos a crear un primer componente que ser\u00e1 un listado de categor\u00edas. Para ello vamos a ejecutar el siguiente comando:

    ng generate component category/category-list\n

    Para terminar de configurar la aplicaci\u00f3n, vamos a a\u00f1adir la ruta del componente dentro del componente routing de Angular, para poder acceder a \u00e9l, para ello modificamos el fichero app-routing.module.ts

    app-routing.module.ts
    import { NgModule } from '@angular/core';\nimport { Routes, RouterModule } from '@angular/router';\nimport { CategoryListComponent } from './category/category-list/category-list.component';\n\nconst routes: Routes = [\n  { path: 'categories', component: CategoryListComponent },\n];\n\n@NgModule({\n  imports: [RouterModule.forRoot(routes)],\n  exports: [RouterModule]\n})\nexport class AppRoutingModule { }\n

    Si abrimos el navegador y accedemos a http://localhost:4200/ podremos navegar mediante el men\u00fa Categor\u00edas el cual abrir\u00e1 el componente que acabamos de crear.

    "},{"location":"develop/basic/angular/#codigo-de-la-pantalla_1","title":"C\u00f3digo de la pantalla","text":"

    Ahora vamos a construir la pantalla. Para manejar la informaci\u00f3n del listado, necesitamos almacenar los datos en un objeto de tipo model. Para ello crearemos un fichero en category\\model\\category.ts donde implementaremos la clase necesaria. Esta clase ser\u00e1 la que utilizaremos en el c\u00f3digo html y ts de nuestro componente.

    category.ts
    export class Category {\n    id: number;\n    name: string;\n}\n

    Tambi\u00e9n, escribiremos el c\u00f3digo de la pantalla de listado.

    category-list.component.htmlcategory-list.component.scsscategory-list.component.ts
    <div class=\"container\">\n    <h1>Listado de Categor\u00edas</h1>\n\n    <mat-table [dataSource]=\"dataSource\">\n        <ng-container matColumnDef=\"id\">\n            <mat-header-cell *matHeaderCellDef> Identificador </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.id}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"name\">\n            <mat-header-cell *matHeaderCellDef> Nombre categor\u00eda  </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.name}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"action\">\n            <mat-header-cell *matHeaderCellDef></mat-header-cell>\n            <mat-cell *matCellDef=\"let element\">\n                <button mat-icon-button color=\"primary\"><mat-icon>edit</mat-icon></button>\n                <button mat-icon-button color=\"accent\"><mat-icon>clear</mat-icon></button>\n            </mat-cell>\n        </ng-container>\n\n        <mat-header-row *matHeaderRowDef=\"displayedColumns; sticky: true\"></mat-header-row>\n        <mat-row *matRowDef=\"let row; columns: displayedColumns;\"></mat-row>\n    </mat-table>\n\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\">Nueva categor\u00eda</button>\n    </div>   \n</div>\n
    .container {\n  margin: 20px;\n\n  mat-table {\n    margin-top: 10px;\n    margin-bottom: 20px;\n\n    .mat-header-row {\n      background-color:#f5f5f5;\n\n      .mat-header-cell {\n        text-transform: uppercase;\n        font-weight: bold;\n        color: #838383;\n      }      \n    }\n\n    .mat-column-id {\n      flex: 0 0 20%;\n      justify-content: center;\n    }\n\n    .mat-column-action {\n      flex: 0 0 10%;\n      justify-content: center;\n    }\n  }\n\n  .buttons {\n    text-align: right;\n  }\n}\n
    import { Component, OnInit } from '@angular/core';\nimport { MatTableDataSource } from '@angular/material/table';\nimport { Category } from '../model/category';\n\n@Component({\n  selector: 'app-category-list',\n  templateUrl: './category-list.component.html',\n  styleUrls: ['./category-list.component.scss']\n})\nexport class CategoryListComponent implements OnInit {\n\n  dataSource = new MatTableDataSource<Category>();\n  displayedColumns: string[] = ['id', 'name', 'action'];\n\n  constructor() { }\n\n  ngOnInit(): void {\n  }\n\n}\n

    El c\u00f3digo HTML es f\u00e1cil de seguir pero por si acaso:

    • L\u00ednea 4: Creamos la tabla con la variable dataSource definida en el fichero .ts
    • L\u00ednea 5: Definici\u00f3n de la primera columna, su cabecera y el dato que va a contener
    • L\u00ednea 10: Definici\u00f3n de la segunda columna, su cabecera y el dato que va a contener
    • L\u00ednea 15: Definici\u00f3n de la tercera columna, su cabecera vac\u00eda y los dos botones de acci\u00f3n
    • L\u00ednea 23 y 24: Construcci\u00f3n de la cabecera y las filas

    Y ya por \u00faltimo, a\u00f1adimos los componentes que se han utilizado de Angular Material a las dependencias del m\u00f3dulo donde est\u00e1 definido el componente en este caso category\\category.module.ts:

    category.module.ts
    import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { MatTableModule } from '@angular/material/table';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatButtonModule } from '@angular/material/button';\nimport { CategoryListComponent } from './category-list/category-list.component';\n\n@NgModule({\n  declarations: [CategoryListComponent],\n  imports: [\n    CommonModule,\n    MatTableModule,\n    MatIconModule, \n    MatButtonModule\n  ],\n})\nexport class CategoryModule { }\n

    Si abrimos el navegador y accedemos a http://localhost:4200/ y pulsamos en el men\u00fa de Categor\u00edas obtendremos una pantalla con un listado vac\u00edo (solo con cabeceras) y un bot\u00f3n de crear Nueva Categor\u00eda que aun no hace nada.

    "},{"location":"develop/basic/angular/#anadiendo-datos","title":"A\u00f1adiendo datos","text":"

    En este punto y para ver como responde el listado, vamos a a\u00f1adir datos. Si tuvieramos el backend implementado podr\u00edamos consultar los datos directamente de una operaci\u00f3n de negocio de backend, pero ahora mismo no lo tenemos implementado as\u00ed que para no bloquear el desarrollo vamos a mockear los datos.

    "},{"location":"develop/basic/angular/#creando-un-servicio","title":"Creando un servicio","text":"

    En angular, cualquier acceso a datos debe pasar por un service, as\u00ed que vamos a crearnos uno para todas las operaciones de categor\u00edas. Vamos a la consola y ejecutamos:

    ng generate service category/category\n

    Esto nos crear\u00e1 un servicio, que adem\u00e1s podemos utilizarlo inyect\u00e1ndolo en cualquier componente que lo necesite.

    "},{"location":"develop/basic/angular/#implementando-un-servicio","title":"Implementando un servicio","text":"

    Vamos a implementar una operaci\u00f3n de negocio que recupere el listado de categor\u00edas y lo vamos a hacer de forma reactiva (as\u00edncrona) para simular una petici\u00f3n a backend. Modificamos los siguientes ficheros:

    category.service.tscategory-list.component.ts
    import { Injectable } from '@angular/core';\nimport { Observable } from 'rxjs';\nimport { Category } from './model/category';\n\n@Injectable({\n  providedIn: 'root'\n})\nexport class CategoryService {\n\n  constructor() { }\n\n  getCategories(): Observable<Category[]> {\n    return new Observable();\n  }\n}\n
    import { Component, OnInit } from '@angular/core';\nimport { MatTableDataSource } from '@angular/material/table';\nimport { Category } from '../model/category';\nimport { CategoryService } from '../category.service';\n\n@Component({\n  selector: 'app-category-list',\n  templateUrl: './category-list.component.html',\n  styleUrls: ['./category-list.component.scss']\n})\nexport class CategoryListComponent implements OnInit {\n\n  dataSource = new MatTableDataSource<Category>();\n  displayedColumns: string[] = ['id', 'name', 'action'];\n\n  constructor(\n    private categoryService: CategoryService,\n  ) { }\n\n  ngOnInit(): void {\n    this.categoryService.getCategories().subscribe(\n      categories => this.dataSource.data = categories\n    );\n  }\n}\n
    "},{"location":"develop/basic/angular/#mockeando-datos","title":"Mockeando datos","text":"

    Como hemos comentado anteriormente, el backend todav\u00eda no est\u00e1 implementado as\u00ed que vamos a mockear datos. Nos crearemos un fichero mock-categories.ts dentro de model, con datos ficticios y modificaremos el servicio para que devuelva esos datos. De esta forma, cuando tengamos implementada la operaci\u00f3n de negocio en backend, tan solo tenemos que sustuir el c\u00f3digo que devuelve datos est\u00e1ticos por una llamada http.

    mock-categories.tscategory.service.ts
    import { Category } from \"./category\";\n\nexport const CATEGORY_DATA: Category[] = [\n    { id: 1, name: 'Dados' },\n    { id: 2, name: 'Fichas' },\n    { id: 3, name: 'Cartas' },\n    { id: 4, name: 'Rol' },\n    { id: 5, name: 'Tableros' },\n    { id: 6, name: 'Tem\u00e1ticos' },\n    { id: 7, name: 'Europeos' },\n    { id: 8, name: 'Guerra' },\n    { id: 9, name: 'Abstractos' },\n]    \n
    import { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Category } from './model/category';\nimport { CATEGORY_DATA } from './model/mock-categories';\n\n@Injectable({\n  providedIn: 'root'\n})\nexport class CategoryService {\n\n  constructor() { }\n\n  getCategories(): Observable<Category[]> {\n    return of(CATEGORY_DATA);\n  }\n}\n

    Si ahora refrescamos la p\u00e1gina web, veremos que el listado ya tiene datos con los que vamos a interactuar.

    "},{"location":"develop/basic/angular/#simulando-las-otras-peticiones","title":"Simulando las otras peticiones","text":"

    Para terminar, vamos a simular las otras dos peticiones, la de editar y la de borrar para cuando tengamos que utilizarlas. El servicio debe quedar m\u00e1s o menos as\u00ed:

    category.service.ts
    import { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Category } from './model/category';\nimport { CATEGORY_DATA } from './model/mock-categories';\n\n@Injectable({\n  providedIn: 'root'\n})\nexport class CategoryService {\n\n  constructor() { }\n\n  getCategories(): Observable<Category[]> {\n    return of(CATEGORY_DATA);\n  }\n\n  saveCategory(category: Category): Observable<Category> {\n    return of(null);\n  }\n\n  deleteCategory(idCategory : number): Observable<any> {\n    return of(null);\n  }  \n}\n
    "},{"location":"develop/basic/angular/#anadiendo-acciones-al-listado","title":"A\u00f1adiendo acciones al listado","text":""},{"location":"develop/basic/angular/#crear-componente_2","title":"Crear componente","text":"

    Ahora nos queda a\u00f1adir las acciones al listado: crear, editar y eliminar. Empezaremos primero por las acciones de crear y editar, que ambas deber\u00edan abrir una ventana modal con un formulario para poder modificar datos de la entidad Categor\u00eda. Como siempre, para crear un componente usamos el asistente de Angular, esta vez al tratarse de una pantalla que solo vamos a utilizar dentro del dominio de categor\u00edas, tiene sentido que lo creemos dentro de ese m\u00f3dulo:

    ng generate component category/category-edit\n

    Ahora vamos a hacer que se abra al pulsar el bot\u00f3n Nueva categor\u00eda. Para eso, vamos al fichero category-list.component.ts y a\u00f1adimos un nuevo m\u00e9todo:

    category-list.component.ts
    ...\nimport { MatDialog } from '@angular/material/dialog';\nimport { CategoryEditComponent } from '../category-edit/category-edit.component';\n...\n  constructor(\n    private categoryService: CategoryService,\n    public dialog: MatDialog,\n  ) { }\n...\n  createCategory() {    \n    const dialogRef = this.dialog.open(CategoryEditComponent, {\n      data: {}\n    });\n\n    dialogRef.afterClosed().subscribe(result => {\n      this.ngOnInit();\n    });    \n  }  \n...\n

    Para poder abrir un componente dentro de un dialogo necesitamos obtener en el constructor un MatDialog. De ah\u00ed que hayamos tenido que a\u00f1adirlo como import y en el constructor.

    Dentro del m\u00e9todo createCategory lo que hacemos es crear un dialogo con el componente CategoryEditComponent en su interior, pasarle unos datos de creaci\u00f3n, donde podemos poner estilos del dialog y un objeto data donde pondremos los datos que queremos pasar entre los componentes. Por \u00faltimo, nos suscribimos al evento afterClosed para ejecutar las acciones que creamos oportunas, en nuestro caso volveremos a cargar el listado inicial.

    Como hemos utilizado un MatDialog en el componente, necesitamos a\u00f1adirlo tambi\u00e9n al m\u00f3dulo, as\u00ed que abrimos el fichero category.module.ts y a\u00f1adimos:

    category.module.ts
    ...\nimport { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';\n\n@NgModule({\n  declarations: [CategoryListComponent, CategoryEditComponent],\n  imports: [\n    ...\n    MatDialogModule\n  ],\n  providers: [\n    {\n      provide: MAT_DIALOG_DATA,\n      useValue: {},\n    },\n  ]\n})\nexport class CategoryModule { }\n

    Y ya por \u00faltimo enlazamos el click en el bot\u00f3n con el m\u00e9todo que acabamos de crear para abrir el dialogo. Modificamos el fichero category-list.component.html y a\u00f1adimos el evento click:

    category-list.component.html
    ...\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\" (click)=\"createCategory()\">Nueva categor\u00eda</button> \n    </div>   \n</div>\n

    Si refrescamos el navegador y pulsamos el bot\u00f3n Nueva categor\u00eda veremos como se abre una ventana modal de tipo Dialog con el componente nuevo que hemos creado, aunque solo se leer\u00e1 category-edit works! que es el contenido por defecto del componente.

    "},{"location":"develop/basic/angular/#codigo-del-dialogo","title":"C\u00f3digo del dialogo","text":"

    Ahora vamos a darle forma al formulario de editar y crear. Para ello vamos al html, ts y css del componente y pegamos el siguiente contenido:

    category-edit.component.htmlcategory-edit.component.scsscategory-edit.component.ts
    <div class=\"container\">\n    <h1>Crear categor\u00eda</h1>\n\n    <form>\n        <mat-form-field>\n            <mat-label>Identificador</mat-label>\n            <input type=\"text\" matInput placeholder=\"Identificador\" [(ngModel)]=\"category.id\" name=\"id\" disabled>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Nombre</mat-label>\n            <input type=\"text\" matInput placeholder=\"Nombre de categor\u00eda\" [(ngModel)]=\"category.name\" name=\"name\" required>\n            <mat-error>El nombre no puede estar vac\u00edo</mat-error>\n        </mat-form-field>\n    </form>\n\n    <div class=\"buttons\">\n        <button mat-stroked-button (click)=\"onClose()\">Cerrar</button>\n        <button mat-flat-button color=\"primary\" (click)=\"onSave()\">Guardar</button>\n    </div>\n</div>\n
    .container {\n    min-width: 350px;\n    max-width: 500px;\n    padding: 20px;\n\n    form {\n        display: flex;\n        flex-direction: column;\n        margin-bottom:20px;\n    }\n\n    .buttons {\n      text-align: right;\n\n      button {\n          margin-left: 10px;\n      }\n    }\n}\n
    import { Component, OnInit } from '@angular/core';\nimport { MatDialogRef } from '@angular/material/dialog';\nimport { CategoryService } from '../category.service';\nimport { Category } from '../model/category';\n\n@Component({\n  selector: 'app-category-edit',\n  templateUrl: './category-edit.component.html',\n  styleUrls: ['./category-edit.component.scss']\n})\nexport class CategoryEditComponent implements OnInit {\n\n  category : Category;\n\n  constructor(\n    public dialogRef: MatDialogRef<CategoryEditComponent>,\n    private categoryService: CategoryService\n  ) { }\n\n  ngOnInit(): void {\n    this.category = new Category();\n  }\n\n  onSave() {\n    this.categoryService.saveCategory(this.category).subscribe(result => {\n      this.dialogRef.close();\n    });    \n  }  \n\n  onClose() {\n    this.dialogRef.close();\n  }\n\n}\n

    Si te fijas en el c\u00f3digo TypeScript, hemos a\u00f1adido en el m\u00e9todo onSave una llamada al servicio de CategoryService que aunque no realice ninguna operaci\u00f3n de momento, por lo menos lo dejamos preparado para conectar con el servidor.

    Adem\u00e1s, como siempre, al utilizar componentes matInput, matForm, matError hay que a\u00f1adirlos como dependencias en el m\u00f3dulo category.module.ts:

    category.module.ts
    ...\nimport { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\n\n@NgModule({\n  declarations: [CategoryListComponent, CategoryEditComponent],\n  imports: [\n    ...\n    MatDialogModule,\n    MatFormFieldModule,\n    MatInputModule,\n    FormsModule,\n    ReactiveFormsModule,\n  ],\n  providers: [\n    {\n      provide: MAT_DIALOG_DATA,\n      useValue: {},\n    },\n  ]\n})\nexport class CategoryModule { }\n

    Ahora podemos navegar y abrir el cuadro de dialogo mediante el bot\u00f3n Nueva categor\u00eda para ver como queda nuestro formulario.

    "},{"location":"develop/basic/angular/#utilizar-el-dialogo-para-editar","title":"Utilizar el dialogo para editar","text":"

    El mismo componente que hemos utilizado para crear una nueva categor\u00eda, nos sirve tambi\u00e9n para editar una categor\u00eda existente. Tan solo tenemos que utilizar la funcionalidad que Angular nos proporciona y pasarle los datos a editar en la llamada de apertura del Dialog. Vamos a implementar funcionalidad sobre el icono editar, tendremos que modificar unos cuantos ficheros:

    category-list.component.htmlcategory-list.component.ts
    <div class=\"container\">\n    <h1>Listado de Categor\u00edas</h1>\n\n    <mat-table [dataSource]=\"dataSource\"> \n        <ng-container matColumnDef=\"id\">\n            <mat-header-cell *matHeaderCellDef> Identificador </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.id}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"name\">\n            <mat-header-cell *matHeaderCellDef> Nombre categor\u00eda  </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.name}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"action\">\n            <mat-header-cell *matHeaderCellDef></mat-header-cell>\n            <mat-cell *matCellDef=\"let element\">\n                <button mat-icon-button color=\"primary\" (click)=\"editCategory(element)\">\n                    <mat-icon>edit</mat-icon>\n                </button>\n                <button mat-icon-button color=\"accent\"><mat-icon>clear</mat-icon></button>\n            </mat-cell>\n        </ng-container>\n\n        <mat-header-row *matHeaderRowDef=\"displayedColumns; sticky: true\"></mat-header-row>\n        <mat-row *matRowDef=\"let row; columns: displayedColumns;\"></mat-row>\n    </mat-table>\n\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\" (click)=\"createCategory()\">Nueva categor\u00eda</button> \n    </div>   \n</div>\n
    export class CategoryListComponent implements OnInit {\n\n  dataSource = new MatTableDataSource<Category>();\n  displayedColumns: string[] = ['id', 'name', 'action'];\n\n  constructor(\n    private categoryService: CategoryService,\n    public dialog: MatDialog,\n  ) { }\n\n  ngOnInit(): void {\n    this.categoryService.getCategories().subscribe(\n      categories => this.dataSource.data = categories\n    );\n  }\n\n  createCategory() {    \n    const dialogRef = this.dialog.open(CategoryEditComponent, {\n      data: {}\n    });\n\n    dialogRef.afterClosed().subscribe(result => {\n      this.ngOnInit();\n    });    \n  }  \n\n  editCategory(category: Category) {\n    const dialogRef = this.dialog.open(CategoryEditComponent, {\n      data: { category: category }\n    });\n\n    dialogRef.afterClosed().subscribe(result => {\n      this.ngOnInit();\n    });\n  }\n}\n

    Y los Dialog:

    category-edit.component.htmlcategory-edit.component.ts
    <div class=\"container\">\n    <h1 *ngIf=\"category.id == null\">Crear categor\u00eda</h1>\n    <h1 *ngIf=\"category.id != null\">Modificar categor\u00eda</h1>\n\n    <form>\n        <mat-form-field>\n...\n
    import { Component, OnInit, Inject } from '@angular/core';\nimport { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';\nimport { CategoryService } from '../category.service';\nimport { Category } from '../model/category';\n\n@Component({\n  selector: 'app-category-edit',\n  templateUrl: './category-edit.component.html',\n  styleUrls: ['./category-edit.component.scss']\n})\nexport class CategoryEditComponent implements OnInit {\n\n  category : Category;\n\n  constructor(\n    public dialogRef: MatDialogRef<CategoryEditComponent>,\n    @Inject(MAT_DIALOG_DATA) public data: any,\n    private categoryService: CategoryService\n  ) { }\n\n  ngOnInit(): void {\n    if (this.data.category != null) {\n      this.category = this.data.category;\n    }\n    else {\n      this.category = new Category();\n    }\n  }\n\n  onSave() {\n    this.categoryService.saveCategory(this.category).subscribe(result => {\n      this.dialogRef.close();\n    });    \n  }  \n\n  onClose() {\n    this.dialogRef.close();\n  }\n\n}\n

    Navegando ahora por la p\u00e1gina y pulsando en el icono de editar, se deber\u00eda abrir una ventana con los datos que hemos seleccionado, similar a esta imagen:

    Si te fijas, al modificar los datos dentro de la ventana de di\u00e1logo se modifica tambi\u00e9n en el listado. Esto es porque estamos pasando el mismo objeto desde el listado a la ventana dialogo y al ser el listado y el formulario reactivos los dos, cualquier cambio sobre los datos se refresca directamente en la pantalla.

    Hay veces en la que este comportamiento nos interesa, pero en este caso no queremos que se modifique el listado. Para solucionarlo debemos hacer una copia del objeto, para que ambos modelos (formulario y listado) utilicen objetos diferentes. Es tan sencillo como modificar category-edit.component.ts y a\u00f1adirle una copia del dato

    category-edit.component.ts
        ...\n    ngOnInit(): void {\n      if (this.data.category != null) {\n        this.category = Object.assign({}, this.data.category);\n      }\n      else {\n        this.category = new Category();\n      }\n    }\n    ...\n

    Cuidado

    Hay que tener mucho cuidado con el binding de los objetos. Hay veces que al modificar un objeto NO queremos que se modifique en todas sus instancias y tenemos que poner especial cuidado en esos aspectos.

    "},{"location":"develop/basic/angular/#accion-de-borrado","title":"Acci\u00f3n de borrado","text":"

    Por norma general, toda acci\u00f3n de borrado de un dato de pantalla requiere una confirmaci\u00f3n previa por parte del usuario. Es decir, para evitar que el dato se borre accidentalmente el usuario tendr\u00e1 que confirmar su acci\u00f3n. Por tanto vamos a crear un componente que nos permita pedir una confirmaci\u00f3n al usuario.

    Como esta pantalla de confirmaci\u00f3n va a ser algo com\u00fan a muchas acciones de borrado de nuestra aplicaci\u00f3n, vamos a crearla dentro del m\u00f3dulo core. Como siempre, ejecutamos el comando en consola:

    ng generate component core/dialog-confirmation\n

    E implementamos el c\u00f3digo que queremos que tenga el componente. Al ser un componente gen\u00e9rico vamos a aprovechar y leeremos las variables que le pasemos en data.

    dialog-confirmation.component.htmldialog-confirmation.component.scssdialog-confirmation.component.ts
    <div class=\"container\">\n    <h1>{{title}}</h1>\n    <div [innerHTML]=\"description\" class=\"description\"></div>\n\n    <div class=\"buttons\">\n        <button mat-stroked-button (click)=\"onNo()\">No</button>\n        <button mat-flat-button color=\"primary\" (click)=\"onYes()\">S\u00ed</button>\n    </div>\n</div>    \n
    .container {\n    min-width: 350px;\n    max-width: 500px;\n    padding: 20px;\n\n    .description {\n      margin-bottom: 20px;\n    }\n\n    .buttons {\n      text-align: right;\n\n      button {\n          margin-left: 10px;\n      }\n    }\n}    \n
    import { Component, OnInit, Inject } from '@angular/core';\nimport { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';\n\n@Component({\n  selector: 'app-dialog-confirmation',\n  templateUrl: './dialog-confirmation.component.html',\n  styleUrls: ['./dialog-confirmation.component.scss']\n})\nexport class DialogConfirmationComponent implements OnInit {\n\n  title : string;\n  description : string;\n\n  constructor(\n    public dialogRef: MatDialogRef<DialogConfirmationComponent>,\n    @Inject(MAT_DIALOG_DATA) public data: any\n  ) { }\n\n  ngOnInit(): void {\n    this.title = this.data.title;\n    this.description = this.data.description;\n  }\n\n  onYes() {\n    this.dialogRef.close(true);\n  }\n\n  onNo() {\n    this.dialogRef.close(false);\n  }\n}\n

    Recuerda

    Recuerda que los componentes utilizados en el di\u00e1logo de confirmaci\u00f3n se deben a\u00f1adir al m\u00f3dulo padre al que pertenecen, en este caso a core.module.ts

    imports: [\n  CommonModule,\n  RouterModule,\n  MatIconModule, \n  MatToolbarModule,\n  MatDialogModule,\n  MatButtonModule,\n],\nproviders: [\n  {\n    provide: MAT_DIALOG_DATA,\n    useValue: {},\n  },\n],\n

    Ya por \u00faltimo, una vez tenemos el componente gen\u00e9rico de dialogo, vamos a utilizarlo en nuestro listado al pulsar el bot\u00f3n eliminar:

    category-list.component.htmlcategory-list.component.ts
        ...\n    <ng-container matColumnDef=\"action\">\n        <mat-header-cell *matHeaderCellDef></mat-header-cell>\n        <mat-cell *matCellDef=\"let element\">\n            <button mat-icon-button color=\"primary\" (click)=\"editCategory(element)\">\n                <mat-icon>edit</mat-icon>\n            </button>\n            <button mat-icon-button color=\"accent\" (click)=\"deleteCategory(element)\">\n                <mat-icon>clear</mat-icon>\n            </button>\n        </mat-cell>\n    </ng-container>\n    ...\n
      ...\n  deleteCategory(category: Category) {    \n    const dialogRef = this.dialog.open(DialogConfirmationComponent, {\n      data: { title: \"Eliminar categor\u00eda\", description: \"Atenci\u00f3n si borra la categor\u00eda se perder\u00e1n sus datos.<br> \u00bfDesea eliminar la categor\u00eda?\" }\n    });\n\n    dialogRef.afterClosed().subscribe(result => {\n      if (result) {\n        this.categoryService.deleteCategory(category.id).subscribe(result => {\n          this.ngOnInit();\n        }); \n      }\n    });\n  }  \n  ...    \n

    Aqu\u00ed tambi\u00e9n hemos realizado la llamada a categoryService, aunque no se realice ninguna acci\u00f3n, pero as\u00ed lo dejamos listo para enlazarlo.

    Llegados a este punto, ya solo nos queda enlazar las acciones de la pantalla con las operaciones de negocio del backend.

    "},{"location":"develop/basic/angular/#conectar-con-backend","title":"Conectar con Backend","text":"

    Antes de seguir

    Antes de seguir con este punto, debes implementar el c\u00f3digo de backend en la tecnolog\u00eda que quieras (Springboot o Nodejs). Si has empezado este cap\u00edtulo implementando el frontend, por favor accede a la secci\u00f3n correspondiente de backend para poder continuar con el tutorial. Una vez tengas implementadas todas las operaciones para este listado, puedes volver a este punto y continuar con Angular.

    El siguiente paso, como es obvio ser\u00e1 hacer que Angular llame directamente al servidor backend para leer y escribir datos y eliminar los datos mockeados en Angular.

    Manos a la obra!

    "},{"location":"develop/basic/angular/#llamada-del-listado","title":"Llamada del listado","text":"

    La idea es que el m\u00e9todo getCategories() de category.service.ts en lugar de devolver datos est\u00e1ticos, realice una llamada al servidor a la ruta http://localhost:8080/category.

    Abrimos el fichero y susituimos la l\u00ednea que antes devolv\u00eda los datos est\u00e1ticos por esto:

    category.service.ts
    import { HttpClient } from '@angular/common/http';\nimport { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Category } from './model/category';\n\n@Injectable({\nprovidedIn: 'root'\n})\nexport class CategoryService { \n\n    constructor(\n        private http: HttpClient\n    ) { }\n\n    getCategories(): Observable<Category[]> {\n        return this.http.get<Category[]>('http://localhost:8080/category');\n    }\n\n    saveCategory(category: Category): Observable<Category> {\n        return of(null);\n    }\n\n    deleteCategory(idCategory : number): Observable<any> {\n        return of(null);\n    }  \n}\n

    Como hemos a\u00f1adido un componente nuevo HttpClient tenemos que a\u00f1adir la dependencia al m\u00f3dulo padre.

    category.module.ts
    import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { MatTableModule } from '@angular/material/table';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatButtonModule } from '@angular/material/button';\nimport { CategoryListComponent } from './category-list/category-list.component';\nimport { CategoryEditComponent } from './category-edit/category-edit.component';\nimport { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\nimport { HttpClientModule } from '@angular/common/http';\n\n@NgModule({\n  declarations: [CategoryListComponent, CategoryEditComponent],\n  imports: [\n    CommonModule,\n    MatTableModule,\n    MatIconModule, \n    MatButtonModule,\n    MatDialogModule,\n    MatFormFieldModule,\n    MatInputModule,\n    FormsModule,\n    ReactiveFormsModule,\n    HttpClientModule,\n  ],\n  providers: [\n    {\n      provide: MAT_DIALOG_DATA,\n      useValue: {},\n    },\n  ]\n})\nexport class CategoryModule { }\n

    Si ahora refrescas el navegador (recuerda tener arrancado tambi\u00e9n el servidor) y accedes a la pantalla de Categor\u00edas deber\u00eda aparecer el listado con los datos que vienen del servidor.

    "},{"location":"develop/basic/angular/#llamada-de-guardado-edicion","title":"Llamada de guardado / edici\u00f3n","text":"

    Para la llamada de guardado har\u00edamos lo mismo, pero invocando la operaci\u00f3n de negocio put.

    category.service.ts
    import { HttpClient } from '@angular/common/http';\nimport { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Category } from './model/category';\n\n@Injectable({\nprovidedIn: 'root'\n})\nexport class CategoryService { \n\n    constructor(\n        private http: HttpClient\n    ) { }\n\n    getCategories(): Observable<Category[]> {\n        return this.http.get<Category[]>('http://localhost:8080/category');\n    }\n\n    saveCategory(category: Category): Observable<Category> {\n\n        let url = 'http://localhost:8080/category';\n        if (category.id != null) url += '/'+category.id;\n\n        return this.http.put<Category>(url, category);\n    }\n\n    deleteCategory(idCategory : number): Observable<any> {\n        return of(null);\n    }  \n\n} \n

    Ahora podemos probar a modificar o a\u00f1adir una nueva categor\u00eda desde la pantalla y deber\u00eda aparecer los nuevos datos en el listado.

    "},{"location":"develop/basic/angular/#llamada-de-borrado","title":"Llamada de borrado","text":"

    Y ya por \u00faltimo, la llamada de borrado, deber\u00edamos cambiarla e invocar a la operaci\u00f3n de negocio delete.

    category.service.ts
    import { HttpClient } from '@angular/common/http';\nimport { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Category } from './model/category';\n\n@Injectable({\nprovidedIn: 'root'\n})\nexport class CategoryService { \n\n    constructor(\n        private http: HttpClient\n    ) { }\n\n    getCategories(): Observable<Category[]> {\n        return this.http.get<Category[]>('http://localhost:8080/category');\n    }\n\n    saveCategory(category: Category): Observable<Category> {\n\n        let url = 'http://localhost:8080/category';\n        if (category.id != null) url += '/'+category.id;\n\n        return this.http.put<Category>(url, category);\n    }\n\n    deleteCategory(idCategory : number): Observable<any> {\n        return this.http.delete('http://localhost:8080/category/'+idCategory);\n    }  \n\n} \n

    Ahora podemos probar a modificar o a\u00f1adir una nueva categor\u00eda desde la pantalla y deber\u00eda aparecer los nuevos datos en el listado.

    Como ves, es bastante sencillo conectar server y client.

    "},{"location":"develop/basic/angular/#depuracion","title":"Depuraci\u00f3n","text":"

    Una parte muy importante del desarrollo es tener la capacidad de depurar nuestro c\u00f3digo, en este apartado vamos a explicar como se realiza debug en Front.

    Esta parte se puede realizar con nuestro navegador favorito, en este caso vamos a utilizar Chrome.

    El primer paso es abrir las herramientas del desarrollador del navegador presionando F12.

    En esta herramienta tenemos varias partes importantes:

    • Elements: Inspector de los elementos del DOM de nuestra aplicaci\u00f3n que nos ayuda identificar el c\u00f3digo generado.
    • Console: Consola donde podemos ver mensajes importantes que nos ayudan a identificar posibles problemas.
    • Source: El navegador de ficheros que componen nuestra aplicaci\u00f3n.
    • Network: El registro de peticiones que realiza nuestra aplicaci\u00f3n.

    Identificados los elementos importantes, vamos a depurar la operaci\u00f3n de crear categor\u00eda.

    Para ello nos dirigimos a la pesta\u00f1a de Source, en el \u00e1rbol de carpetas nos dirigimos a la ruta donde est\u00e1 localizado el c\u00f3digo de nuestra aplicaci\u00f3n webpack://src/app.

    Dentro de esta carpeta est\u00e9 localizado todo el c\u00f3digo fuente de la aplicaci\u00f3n, en nuestro caso vamos a localizar componente category-edit.component que crea una nueva categor\u00eda.

    Dentro del fichero ya podemos a\u00f1adir puntos de ruptura (breakpoint), en nuestro caso queremos comprobar que el nombre introducido se captura bien y se env\u00eda al service correctamente.

    Colocamos el breakpoint en la l\u00ednea de invocaci\u00f3n del service (click sobre el n\u00famero de la l\u00ednea) y desde la interfaz creamos una nueva categor\u00eda.

    Hecho esto, podemos observar que a nivel de interfaz, la aplicaci\u00f3n se detiene y aparece un panel de manejo de los puntos de interrupci\u00f3n:

    En cuanto a la herramienta del desarrollador nos lleva al punto exacto donde hemos a\u00f1adido el breakpoint y se para en este punto ofreci\u00e9ndonos la posibilidad de explorar el contenido de las variables del c\u00f3digo:

    Aqu\u00ed podemos comprobar que efectivamente la variable category tiene el valor que hemos introducido por pantalla y se propaga correctamente hacia el service.

    Para continuar con la ejecuci\u00f3n basta con darle al bot\u00f3n de play del panel de manejo de interrupci\u00f3n o al que aparece dentro de la herramienta de desarrollo (parte superior derecha).

    Por \u00faltimo, vamos a revisar que la petici\u00f3n REST se ha realizado correctamente al backend, para ello nos dirigimos a la pesta\u00f1a Network y comprobamos las peticiones realizadas:

    Aqu\u00ed podemos observar el registro de todas las peticiones y haciendo click sobre una de ellas, obtenemos el detalle de esta.

    • Header: Informaci\u00f3n de las cabeceras enviadas (aqu\u00ed podemos ver que se ha hecho un PUT a la ruta correcta).
    • Payload: El cuerpo de la petici\u00f3n (vemos el cuerpo del mensaje con el nombre enviado).
    • Preview: Respuesta de la petici\u00f3n normalizada (vemos la respuesta con el identificador creado para la nueva categor\u00eda).
    "},{"location":"develop/basic/angular17/","title":"Listado simple - Angular","text":"

    Ahora que ya tenemos listo el proyecto frontend de Angular (en el puerto 4200), ya podemos empezar a codificar la soluci\u00f3n.

    "},{"location":"develop/basic/angular17/#primeros-pasos","title":"Primeros pasos","text":"

    Antes de empezar

    Quiero hacer hincapi\u00e9 que Angular tiene una documentaci\u00f3n muy extensa y completa, as\u00ed que te recomiendo que hagas uso de ella cuando tengas cualquier duda. Tanto en la propia web de documentaci\u00f3n de Angular como en la web de componentes Angular Material puedes buscar casi cualquier ejemplo que necesites.

    Si abrimos el proyecto con el IDE que tengamos (Visual Studio Code en el caso del tutorial) podemos ver que en la carpeta src/app existen unos ficheros ya creados por defecto. Estos ficheros son:

    • app.ts \u2192 contiene el c\u00f3digo inicial del proyecto escrito en TypeScript.
    • app.html \u2192 contiene la plantilla inicial del proyecto escrita en HTML.
    • app.scss \u2192 contiene los estilos CSS privados de la plantilla inicial.

    Info

    Comprueba el fichero tsconfig.json del proyecto, puede que est\u00e9s usando el modo strict o tengas strictNullChecks a true. Con estas propiedades activadas se obliga al compilador a tratar los tipos null y undefined de manera m\u00e1s estricta.

    Comportamiento sin strictNullChecks (false): - null y undefined se consideran valores v\u00e1lidos para cualquier tipo - Puedes asignar null o undefined a variables de cualquier tipo sin errores - Mayor flexibilidad pero menos seguridad de tipos

    Comportamiento con strictNullChecks (true): - Solo los tipos null y undefined pueden contener esos valores expl\u00edcitamente - Debes usar tipos union para permitir nulidad: string | null, number | undefined - El compilador previene acceso a propiedades/m\u00e9todos en valores potencialmente nulos - Mayor seguridad de tipos y detecci\u00f3n de errores en tiempo de compilaci\u00f3n

    Vamos a modificar el c\u00f3digo inicial para ver como funciona. Abrimos el fichero app.component.ts y modificamos la l\u00ednea donde se asigna un valor a la variable title.

    app.component.ts
    ...\n   protected readonly title = signal('Tutorial de Angular');\n...\n

    Ahora abrimos el fichero app.component.html, borramos todo el c\u00f3digo de la plantilla y a\u00f1adimos el siguiente c\u00f3digo:

    app.component.html
    <h1>{{ title() }}</h1>\n

    Las llaves dobles permiten hacen un binding entre el c\u00f3digo del componente y la plantilla. Es decir, en este caso ir\u00e1 al c\u00f3digo TypeScript y buscar\u00e1 el valor de la variable title.

    Consejo

    El binding tambi\u00e9n nos sirve para ejecutar los m\u00e9todos de TypeScript desde el c\u00f3digo HTML. Adem\u00e1s, si el valor que contiene la variable se modificar\u00e1 durante la ejecuci\u00f3n de alg\u00fan m\u00e9todo, autom\u00e1ticamente el c\u00f3digo HTML refrescar\u00eda el nuevo valor de la variable title

    Si abrimos el navegador y accedemos a http://localhost:4200/ podremos ver el resultado del c\u00f3digo.

    "},{"location":"develop/basic/angular17/#layout-general","title":"Layout general","text":""},{"location":"develop/basic/angular17/#crear-componente","title":"Crear componente","text":"

    Lo primero que vamos a hacer es escoger un tema y una paleta de componentes para trabajar. Lo m\u00e1s c\u00f3modo es trabajar con Material que ya viene perfectamente integrado en Angular. Ejecutamos el comando y elegimos la paleta de colores que m\u00e1s nos guste o bien creamos una custom:

    ng add @angular/material\n

    Recuerda

    Al a\u00f1adir una nueva librer\u00eda tenemos que parar el servidor y volver a arrancarlo para que compile y recargue las nuevas dependencias.

    Una vez a\u00f1adida la dependencia, lo que queremos es crear una primera estructura inicial a la p\u00e1gina. Si te acuerdas cu\u00e1l era la estructura (y si no te acuerdas, vuelve a la secci\u00f3n Contexto de la aplicaci\u00f3n y lo revisas), ten\u00edamos una cabecera superior con un logo y t\u00edtulo y unas opciones de men\u00fa.

    Para agrupar los componentes comunes de nuestra aplicaci\u00f3n vamos a crear una carpeta llamada \"core\" dentro de la carpeta \"src\", e iremos creando los componentes que necesitemos. Empecemos con el header. Desde la ra\u00edz de nuestro proyecto introducimos el siguiente comando:

    ng generate component core/header\n

    Angular 17+

    A partir de Angular 17, el CLI apuesta por una arquitectura standalone y una estructura inicial m\u00e1s simple al crear un proyecto con ng new.

    Sin embargo, Angular no impone una topolog\u00eda concreta para componentes o servicios. La generaci\u00f3n de archivos como *.component.ts, *.service.ts y sus carpetas asociadas sigue siendo el comportamiento por defecto del CLI y solo cambia si se utilizan flags como --flat, opciones de inline o schematics personalizadas.

    "},{"location":"develop/basic/angular17/#codigo-de-la-pantalla","title":"C\u00f3digo de la pantalla","text":"

    Esto nos crear\u00e1 una carpeta con los ficheros del componente, donde tendremos que copiar el siguiente contenido:

    header.component.htmlheader.component.scss
    <mat-toolbar>\n    <mat-toolbar-row>\n        <div class=\"header_container\">\n            <div class=\"header_title\">              \n                <mat-icon>storefront</mat-icon> Ludoteca Tan\n            </div>\n\n            <div class=\"header_separator\"> | </div>\n\n            <div class=\"header_menu\">\n                <div class=\"header_button\">\n                    <a routerLink=\"/games\" routerLinkActive=\"active\">Cat\u00e1logo</a>\n                </div>\n                <div class=\"header_button\">\n                    <a routerLink=\"/categories\" routerLinkActive=\"active\">Categor\u00edas</a>\n                </div>\n                <div class=\"header_button\">\n                    <a routerLink=\"/authors\" routerLinkActive=\"active\">Autores</a>\n                </div>\n            </div>\n\n            <div class=\"header_login\">\n                <mat-icon>account_circle</mat-icon> Sign in\n            </div>\n        </div>\n    </mat-toolbar-row>\n</mat-toolbar>\n
    .mat-toolbar {\n  background-color: blue;\n  color: white;\n}\n\n.header_container {\n    display: flex;\n    width: 100%;\n    .header_title {\n        .mat-icon {\n            vertical-align: sub;\n        }\n    }\n\n    .header_separator {\n        margin-left: 30px;\n        margin-right: 30px;\n    }\n\n    .header_menu {\n        flex-grow: 4;\n        display: flex;\n        flex-direction: row;\n\n        .header_button {\n            margin-left: 1em;\n            margin-right: 1em;\n            font-size: 16px;\n\n            a {\n              font-weight: lighter;\n              text-decoration: none;\n              cursor: pointer;\n              color: white;\n            }\n\n            a:hover {\n              color: grey;\n            }\n\n            a.active {\n              font-weight: normal;\n              text-decoration: underline;\n              color: lightyellow;\n            }\n\n        }\n    }\n\n    .header_login {\n      font-size: 16px;\n      cursor: pointer;\n      .mat-icon {\n          vertical-align: sub;\n      }\n  }\n}\n

    Al utilizar etiquetas de material como mat-toolbar o mat-icon y routerLink necesitaremos importar las dependencias. Al tratarse de un standalone component lo tendremos que hacer directamente en el atributo \"imports\" de nuestro component en el fichero header.component.ts.

    Angular 17+

    A partir de Angular 17, los componentes se crean por defecto como standalone, para que este no sea el caso debemos indic\u00e1rselo con --standalone false.

    header.component.ts
        import { CommonModule } from '@angular/common';\n    import { Component } from '@angular/core';\n    import { RouterModule } from '@angular/router';\n    import { MatIconModule } from '@angular/material/icon';\n    import { MatToolbarModule } from '@angular/material/toolbar';\n\n    @Component({\n        selector: 'app-header',\n        standalone: true,\n        imports: [\n            CommonModule,\n            RouterModule,\n            MatIconModule, \n            MatToolbarModule,\n        ],\n        templateUrl: './header.component.html',\n        styleUrl: './header.component.scss'\n    })\n    export class HeaderComponent {\n\n    }\n

    Ahora, para poder usar nuestro componente en las p\u00e1ginas del componente AppComponent tendremos tambi\u00e9n que a\u00f1adir en el atributo \"imports\" de app.component.ts nuestro nuevo componente:

    app.component.html
        import { Component, signal } from '@angular/core';\n    import { RouterOutlet } from '@angular/router';\n    import { HeaderComponent } from '../core/header/header.component';\n\n    @Component({\n        selector: 'app-root',\n        standalone: true,\n        imports: [RouterOutlet, HeaderComponent],\n        templateUrl: './app.component.html',\n        styleUrl: './app.component.scss'\n    })\n    export class AppComponent {\n        protected readonly title = signal('Tutorial de Angular');\n    }\n

    Ya por \u00faltimo solo nos queda modificar la p\u00e1gina general de la aplicaci\u00f3n app.component.html para a\u00f1adirle el componente HeaderComponent.

    app.component.html
    <div>\n  <app-header></app-header>\n  <div>\n    <router-outlet></router-outlet>\n  </div>\n</div>\n

    Vamos al navegador y refrescamos la p\u00e1gina, deber\u00eda aparecer una barra superior (Header) con las opciones de men\u00fa. Algo similar a esto:

    Recuerda

    Cuando se a\u00f1aden componentes a los ficheros html, siempre se deben utilizar los selectores definidos para el componente. En el caso anterior hemos a\u00f1adido app-header que es el mismo nombre selector que tiene el componente en el fichero header.component.ts. Adem\u00e1s, recuerda que para poder utilizar componentes, los debes importar en el componente donde vayan a ser utilizados.

    "},{"location":"develop/basic/angular17/#creando-un-listado-basico","title":"Creando un listado b\u00e1sico","text":""},{"location":"develop/basic/angular17/#crear-componente_1","title":"Crear componente","text":"

    Ya tenemos la estructura principal, ahora vamos a crear nuestra primera pantalla. Vamos a empezar por la de Categor\u00edas que es la m\u00e1s sencilla, ya que se trata de un listado, que muestra datos sin filtrar ni paginar.

    Como categor\u00edas es un dominio funcional de la aplicaci\u00f3n, vamos a crear una carpeta que contenga toda la funcionalidad de ese dominio y todas las pantallas, componentes y servicios que creemos referidos a este dominio funcional, deber\u00e1n ir dentro de esta nueva carpeta.

    Vamos a crear un primer componente que ser\u00e1 un listado de categor\u00edas. Para ello vamos a ejecutar el siguiente comando desde src:

    ng generate component category/category-list --type=page\n

    Como en este caso crearemos una p\u00e1gina, hemos a\u00f1adido el par\u00e1metro --type=page al comando. Esto hace que los ficheros generados tengan la extensi\u00f3n .page.ts y .page.html.

    Para terminar de configurar la aplicaci\u00f3n, vamos a a\u00f1adir la ruta del componente dentro del componente routing de Angular, para poder acceder a \u00e9l, para ello modificamos el fichero app.routes.ts

    app.routes.ts
    import { Routes } from '@angular/router';\n\nexport const routes: Routes = [\n    { path: 'categories', loadComponent: () => import('../category/category-list/category-list.page').then(m => m.CategoryListPage)},\n];\n

    Si abrimos el navegador y accedemos a http://localhost:4200/ podremos navegar mediante el men\u00fa Categor\u00edas el cual abrir\u00e1 el componente que acabamos de crear.

    "},{"location":"develop/basic/angular17/#codigo-de-la-pantalla_1","title":"C\u00f3digo de la pantalla","text":"

    Ahora vamos a construir la pantalla. Para manejar la informaci\u00f3n del listado, necesitamos almacenar los datos en un objeto de tipo model. Para ello crearemos un fichero en category\\model\\category.ts donde implementaremos la clase necesaria. Esta clase ser\u00e1 la que utilizaremos en el c\u00f3digo html y ts de nuestro componente.

    category.ts
    export interface Category {\n    id: number;\n    name: string;\n}\n

    Tambi\u00e9n, escribiremos el c\u00f3digo de la pantalla de listado.

    category-list.component.htmlcategory-list.component.scsscategory-list.component.ts
    <div class=\"container\">\n    <h1>Listado de Categor\u00edas</h1>\n\n    <mat-table [dataSource]=\"dataSource\">\n        <ng-container matColumnDef=\"id\">\n            <mat-header-cell *matHeaderCellDef> Identificador </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.id}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"name\">\n            <mat-header-cell *matHeaderCellDef> Nombre categor\u00eda  </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.name}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"action\">\n            <mat-header-cell *matHeaderCellDef></mat-header-cell>\n            <mat-cell *matCellDef=\"let element\">\n                <button mat-icon-button color=\"primary\"><mat-icon>edit</mat-icon></button>\n                <button mat-icon-button color=\"accent\"><mat-icon>clear</mat-icon></button>\n            </mat-cell>\n        </ng-container>\n\n        <mat-header-row *matHeaderRowDef=\"displayedColumns; sticky: true\"></mat-header-row>\n        <mat-row *matRowDef=\"let row; columns: displayedColumns;\"></mat-row>\n    </mat-table>\n\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\">Nueva categor\u00eda</button>\n    </div>   \n</div>\n
    .container {\n  margin: 20px;\n\n  mat-table {\n    margin-top: 10px;\n    margin-bottom: 20px;\n\n    .mat-header-row {\n      background-color:#f5f5f5;\n\n      .mat-header-cell {\n        text-transform: uppercase;\n        font-weight: bold;\n        color: #838383;\n      }      \n    }\n\n    .mat-column-id {\n      flex: 0 0 20%;\n      justify-content: center;\n    }\n\n    .mat-column-action {\n      flex: 0 0 10%;\n      justify-content: center;\n    }\n  }\n\n  .buttons {\n    text-align: right;\n  }\n}\n
    import { Component, OnInit } from '@angular/core';\nimport { MatTableDataSource } from '@angular/material/table';\nimport { Category } from '../model/category';\nimport { CommonModule } from '@angular/common';\nimport { MatTableModule } from '@angular/material/table';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatButtonModule } from '@angular/material/button';\n\n@Component({\n    selector: 'app-category-list',\n    standalone: true,\n    imports: [\n        MatButtonModule,\n        MatIconModule,\n        MatTableModule,\n        CommonModule\n    ],\n    templateUrl: './category-list.component.html',\n    styleUrl: './category-list.component.scss'\n})\nexport class CategoryListComponent implements OnInit{\n\n    dataSource = new MatTableDataSource<Category>();\n    displayedColumns: string[] = ['id', 'name', 'action'];\n\n    constructor() { }\n\n    ngOnInit(): void {\n    }\n}\n

    El c\u00f3digo HTML es f\u00e1cil de seguir, pero por si acaso:

    • L\u00ednea 4: Creamos la tabla con la variable dataSource definida en el fichero .ts
    • L\u00ednea 5: Definici\u00f3n de la primera columna, su cabecera y el dato que va a contener
    • L\u00ednea 10: Definici\u00f3n de la segunda columna, su cabecera y el dato que va a contener
    • L\u00ednea 15: Definici\u00f3n de la tercera columna, su cabecera vac\u00eda y los dos botones de acci\u00f3n
    • L\u00ednea 23 y 24: Construcci\u00f3n de la cabecera y las filas

    Si abrimos el navegador y accedemos a http://localhost:4200/ y pulsamos en el men\u00fa de Categor\u00edas obtendremos una pantalla con un listado vac\u00edo (solo con cabeceras) y un bot\u00f3n de crear Nueva Categor\u00eda que a\u00fan no hace nada.

    "},{"location":"develop/basic/angular17/#anadiendo-datos","title":"A\u00f1adiendo datos","text":"

    En este punto y para ver como responde el listado, vamos a a\u00f1adir datos. Si tuvi\u00e9ramos el backend implementado podr\u00edamos consultar los datos directamente de una operaci\u00f3n de negocio de backend, pero ahora mismo no lo tenemos implementado, as\u00ed que para no bloquear el desarrollo vamos a mockear los datos.

    "},{"location":"develop/basic/angular17/#creando-un-servicio","title":"Creando un servicio","text":"

    En angular, cualquier acceso a datos debe pasar por un service, as\u00ed que vamos a crearnos uno para todas las operaciones de categor\u00edas. Vamos a la consola y ejecutamos:

    ng generate service category/category\n

    Esto nos crear\u00e1 un servicio, que adem\u00e1s podemos utilizarlo inyect\u00e1ndolo en cualquier componente que lo necesite.

    "},{"location":"develop/basic/angular17/#implementando-un-servicio","title":"Implementando un servicio","text":"

    Vamos a implementar una operaci\u00f3n de negocio que recupere el listado de categor\u00edas y lo vamos a hacer de forma reactiva (as\u00edncrona) para simular una petici\u00f3n a backend. Modificamos los siguientes ficheros:

    category.service.tscategory-list.component.ts
    import { Injectable } from '@angular/core';\nimport { Observable } from 'rxjs';\nimport { Category } from './model/category';\n\n@Injectable({\n  providedIn: 'root'\n})\nexport class CategoryService {\n\n  constructor() { }\n\n  getCategories(): Observable<Category[]> {\n    return new Observable();\n  }\n}\n
    import { Component, OnInit } from '@angular/core';\nimport { MatTableDataSource } from '@angular/material/table';\nimport { Category } from '../model/category';\nimport { CommonModule } from '@angular/common';\nimport { MatTableModule } from '@angular/material/table';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatButtonModule } from '@angular/material/button';\nimport { CategoryService } from '../category.service';\n\n@Component({\n    selector: 'app-category-list',\n    standalone: true,\n    imports: [\n        MatButtonModule,\n        MatIconModule,\n        MatTableModule,\n        CommonModule\n    ],\n    templateUrl: './category-list.component.html',\n    styleUrl: './category-list.component.scss'\n})\nexport class CategoryListComponent implements OnInit{\n    dataSource = new MatTableDataSource<Category>();\n    displayedColumns: string[] = ['id', 'name', 'action'];\n\n    constructor(\n        private categoryService: CategoryService,\n    ) { }\n\n    ngOnInit(): void {\n        this.categoryService.getCategories().subscribe(\n            categories => this.dataSource.data = categories\n        );\n    }\n}\n
    "},{"location":"develop/basic/angular17/#mockeando-datos","title":"Mockeando datos","text":"

    Como hemos comentado anteriormente, el backend todav\u00eda no est\u00e1 implementado, as\u00ed que vamos a mockear datos. Nos crearemos un fichero mock-categories.ts dentro de model, con datos ficticios y modificaremos el servicio para que devuelva esos datos. De esta forma, cuando tengamos implementada la operaci\u00f3n de negocio en backend, tan solo tenemos que sustituir el c\u00f3digo que devuelve datos est\u00e1ticos por una llamada http.

    mock-categories.tscategory.service.ts
    import { Category } from \"./category\";\n\nexport const CATEGORY_DATA: Category[] = [\n    { id: 1, name: 'Dados' },\n    { id: 2, name: 'Fichas' },\n    { id: 3, name: 'Cartas' },\n    { id: 4, name: 'Rol' },\n    { id: 5, name: 'Tableros' },\n    { id: 6, name: 'Tem\u00e1ticos' },\n    { id: 7, name: 'Europeos' },\n    { id: 8, name: 'Guerra' },\n    { id: 9, name: 'Abstractos' },\n]    \n
    import { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Category } from './model/category';\nimport { CATEGORY_DATA } from './model/mock-categories';\n\n@Injectable({\n  providedIn: 'root'\n})\nexport class CategoryService {\n\n  constructor() { }\n\n  getCategories(): Observable<Category[]> {\n    return of(CATEGORY_DATA);\n  }\n}\n

    Si ahora refrescamos la p\u00e1gina web, veremos que el listado ya tiene datos con los que vamos a interactuar.

    "},{"location":"develop/basic/angular17/#simulando-las-otras-peticiones","title":"Simulando las otras peticiones","text":"

    Para terminar, vamos a simular las otras dos peticiones, la de editar y la de borrar para cuando tengamos que utilizarlas. El servicio debe quedar m\u00e1s o menos as\u00ed:

    category.service.ts
    import { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Category } from './model/category';\nimport { CATEGORY_DATA } from './model/mock-categories';\n\n@Injectable({\n  providedIn: 'root'\n})\nexport class CategoryService {\n\n  constructor() { }\n\n  getCategories(): Observable<Category[]> {\n    return of(CATEGORY_DATA);\n  }\n\n  saveCategory(category: Category): Observable<Category> {\n    return of(null);\n  }\n\n  deleteCategory(idCategory : number): Observable<any> {\n    return of(null);\n  }  \n}\n
    "},{"location":"develop/basic/angular17/#anadiendo-acciones-al-listado","title":"A\u00f1adiendo acciones al listado","text":""},{"location":"develop/basic/angular17/#crear-componente_2","title":"Crear componente","text":"

    Ahora nos queda a\u00f1adir las acciones al listado: crear, editar y eliminar. Empezaremos primero por las acciones de crear y editar, que ambas deber\u00edan abrir una ventana modal con un formulario para poder modificar datos de la entidad Categor\u00eda. Como siempre, para crear un componente usamos el asistente de Angular, esta vez al tratarse de una pantalla que solo vamos a utilizar dentro del dominio de categor\u00edas, tiene sentido que lo creemos dentro de ese m\u00f3dulo:

    ng generate component category/category-edit\n

    Ahora vamos a hacer que se abra al pulsar el bot\u00f3n Nueva categor\u00eda. Para eso, vamos al fichero category-list.component.ts y a\u00f1adimos un nuevo m\u00e9todo:

    category-list.component.ts
    ...\nimport { MatDialog } from '@angular/material/dialog';\nimport { CategoryEditComponent } from '../category-edit/category-edit.component';\n...\n  imports: [\n    MatButtonModule,\n    MatIconModule,\n    MatTableModule,\n    CommonModule,\n],\n...\n  protected readonly categoryService = inject(CategoryService);\n  protected readonly dialog = inject(MatDialog);\n...\n  createCategory() {    \n    const dialogRef = this.dialog.open(CategoryEditComponent, {\n      data: {}\n    });\n\n    dialogRef.afterClosed().subscribe(result => {\n      if(!result) return;\n      this.loadData();\n    });    \n  }  \n...\n

    Para poder abrir un componente dentro de un di\u00e1logo necesitamos inyectar un MatDialog. De ah\u00ed que hayamos tenido que a\u00f1adirlo como import y usarlo con un inject.

    Dentro del m\u00e9todo createCategory lo que hacemos es crear un di\u00e1logo con el componente CategoryEditComponent en su interior, pasarle unos datos de creaci\u00f3n, donde podemos poner estilos del dialog y un objeto data donde pondremos los datos que queremos pasar entre los componentes. Por \u00faltimo, nos suscribimos al evento afterClosed para ejecutar las acciones que creamos oportunas, solo en el caso de que result sea true, en nuestro caso volveremos a cargar el listado inicial.

    Y ya por \u00faltimo enlazamos el click en el bot\u00f3n con el m\u00e9todo que acabamos de crear para abrir el di\u00e1logo. Modificamos el fichero category-list.component.html y a\u00f1adimos el evento click:

    category-list.component.html
    ...\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\" (click)=\"createCategory()\">Nueva categor\u00eda</button> \n    </div>   \n</div>\n

    Si refrescamos el navegador y pulsamos el bot\u00f3n Nueva categor\u00eda, veremos como se abre una ventana modal de tipo Dialog con el componente nuevo que hemos creado, aunque solo se leer\u00e1 category-edit works! que es el contenido por defecto del componente.

    "},{"location":"develop/basic/angular17/#codigo-del-dialogo","title":"C\u00f3digo del di\u00e1logo","text":"

    Ahora vamos a darle forma al formulario de editar y crear. Para ello vamos al html, ts y css del componente y pegamos el siguiente contenido:

    Errores de validaci\u00f3n

    Os recomendamos seguir el siguiente formato para los errores de validaci\u00f3n en formularios (ngModel, mat-error)

    category-edit.component.htmlcategory-edit.component.scsscategory-edit.component.ts
    <div class=\"container\">\n    <h1>Crear categor\u00eda</h1>\n\n    <form>\n        <mat-form-field>\n            <mat-label>Identificador</mat-label>\n            <input type=\"text\" matInput placeholder=\"Identificador\" [(ngModel)]=\"id\" name=\"id\" disabled>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Nombre</mat-label>\n            <input type=\"text\" matInput placeholder=\"Nombre de categor\u00eda\" [(ngModel)]=\"name\" name=\"name\" required>\n            <mat-error>El nombre no puede estar vac\u00edo</mat-error>\n        </mat-form-field>\n    </form>\n\n    <div class=\"buttons\">\n        <button mat-stroked-button (click)=\"onClose()\">Cerrar</button>\n        <button mat-flat-button color=\"primary\" (click)=\"onSave()\">Guardar</button>\n    </div>\n</div>\n
    .container {\n    min-width: 350px;\n    max-width: 500px;\n    padding: 20px;\n\n    form {\n        display: flex;\n        flex-direction: column;\n        margin-bottom:20px;\n    }\n\n    .buttons {\n      text-align: right;\n\n      button {\n          margin-left: 10px;\n      }\n    }\n}\n
    import { Component, OnInit, inject, signal } from '@angular/core';\nimport { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';\nimport { CategoryService } from '../category.service';\nimport { Category } from '../model/category';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatButtonModule } from '@angular/material/button';\n\n@Component({\n    selector: 'app-category-edit',\n    standalone: true,\n    imports: [FormsModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatButtonModule ],\n    templateUrl: './category-edit.component.html',\n    styleUrl: './category-edit.component.scss'\n})\nexport class CategoryEditComponent implements OnInit {\n    protected readonly dialogRef = inject(MatDialogRef<CategoryEditComponent>);\n    protected readonly categoryService = inject(CategoryService);\n\n    protected readonly id = signal<number | null>(null);\n    protected readonly name = signal<string | null>(null);\n\n    ngOnInit(): void {\n        this.loadFormData();\n    }\n\n    loadFormData(): void {\n        this.id.set(null);\n        this.name.set(null);\n    }\n\n    onSave() {\n        const id = this.id();\n        const name = this.name();\n\n        if(!name) {\n            return;\n        }\n\n        const category = { id, name } as Category;\n        this.categoryService.saveCategory(category).subscribe(() => {\n            this.dialogRef.close(true);\n        });\n    }\n\n    onClose() {\n        this.dialogRef.close();\n    }\n}\n

    Si te fijas en el c\u00f3digo TypeScript, hemos a\u00f1adido en el m\u00e9todo onSave una llamada al servicio de CategoryService que aunque no realice ninguna operaci\u00f3n de momento, por lo menos lo dejamos preparado para conectar con el servidor.

    Cast

    Se utiliza \"as Category\" porque el tipo obtenido del servidor siempre llegar\u00e1 con id, sin embargo al crear una nueva categor\u00eda no dispondremos de valor hasta que lo genere el backend

    \u26a0\ufe0f Nota: Usar con cuidado, ya que bypasea la validaci\u00f3n de tipos. Considera validar los datos en tiempo de ejecuci\u00f3n si es cr\u00edtico (c\u00f3mo se hace con la propiedad name)

    Adem\u00e1s, como siempre, al utilizar componentes matInput, matForm, matError hay que a\u00f1adir los m\u00f3dulos correspondientes como dependencias en el atributo imports.

    Ahora podemos navegar y abrir el cuadro de di\u00e1logo mediante el bot\u00f3n Nueva categor\u00eda para ver como queda nuestro formulario.

    "},{"location":"develop/basic/angular17/#utilizar-el-dialogo-para-editar","title":"Utilizar el di\u00e1logo para editar","text":"

    El mismo componente que hemos utilizado para crear una nueva categor\u00eda, nos sirve tambi\u00e9n para editar una categor\u00eda existente. Tan solo tenemos que utilizar la funcionalidad que Angular nos proporciona y pasarle los datos a editar en la llamada de apertura del Dialog. Vamos a implementar funcionalidad sobre el icono editar, tendremos que modificar unos cuantos ficheros:

    category-list.component.htmlcategory-list.component.ts
    <div class=\"container\">\n    <h1>Listado de Categor\u00edas</h1>\n\n    <mat-table [dataSource]=\"dataSource\"> \n        <ng-container matColumnDef=\"id\">\n            <mat-header-cell *matHeaderCellDef> Identificador </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.id}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"name\">\n            <mat-header-cell *matHeaderCellDef> Nombre categor\u00eda  </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.name}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"action\">\n            <mat-header-cell *matHeaderCellDef></mat-header-cell>\n            <mat-cell *matCellDef=\"let element\">\n                <button mat-icon-button color=\"primary\" (click)=\"editCategory(element)\">\n                    <mat-icon>edit</mat-icon>\n                </button>\n                <button mat-icon-button color=\"accent\"><mat-icon>clear</mat-icon></button>\n            </mat-cell>\n        </ng-container>\n\n        <mat-header-row *matHeaderRowDef=\"displayedColumns; sticky: true\"></mat-header-row>\n        <mat-row *matRowDef=\"let row; columns: displayedColumns;\"></mat-row>\n    </mat-table>\n\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\" (click)=\"createCategory()\">Nueva categor\u00eda</button> \n    </div>   \n</div>\n
    export class CategoryListComponent implements OnInit {\n\n  dataSource = new MatTableDataSource<Category>();\n  displayedColumns: string[] = ['id', 'name', 'action'];\n\n  protected readonly categoryService = inject(CategoryService);\n  protected readonly dialog = inject(MatDialog);\n\n  loadData(): void {\n    this.categoryService.getCategories().subscribe(\n      categories => this.dataSource.data = categories\n    );\n  }\n\n  ngOnInit(): void {\n    this.loadData();\n  }\n\n  createCategory() {    \n    const dialogRef = this.dialog.open(CategoryEditComponent, {\n      data: {}\n    });\n\n    dialogRef.afterClosed().subscribe(result => {\n      if(!result) return;\n      this.loadData();\n    });    \n  }  \n\n  editCategory(category: Category) {\n    const dialogRef = this.dialog.open(CategoryEditComponent, {\n      data: { category }\n    });\n\n    dialogRef.afterClosed().subscribe(result => {\n      if(!result) return;\n      this.loadData();\n    });\n  }\n}\n

    Y los Dialog:

    category-edit.component.htmlcategory-edit.component.ts
    <div class=\"container\">\n    @if (id()) {\n        <h1>Modificar categor\u00eda</h1>\n    } @else {\n        <h1>Crear categor\u00eda</h1>\n    }\n\n    <form>\n        <mat-form-field>\n...\n
    import { Component, OnInit, inject, signal } from '@angular/core';\nimport { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';\nimport { CategoryService } from '../category.service';\nimport { Category } from '../model/category';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatButtonModule } from '@angular/material/button';\n\n@Component({\n    selector: 'app-category-edit',\n    standalone: true,\n    imports: [FormsModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatButtonModule ],\n    templateUrl: './category-edit.component.html',\n    styleUrl: './category-edit.component.scss'\n})\nexport class CategoryEditComponent implements OnInit {\n    protected readonly dialogRef = inject(MatDialogRef<CategoryEditComponent>);\n    protected readonly data = inject(MAT_DIALOG_DATA) as { category: Category };\n    protected readonly categoryService = inject(CategoryService);\n\n    protected readonly id = signal<number | null>(null);\n    protected readonly name = signal<string | null>(null);\n\n    ngOnInit(): void {\n        this.loadFormData(this.data.category ?? null);\n    }\n\n    loadFormData(initialData: Category | null): void {\n        this.id.set(initialData?.id ?? null);\n        this.name.set(initialData?.name ?? null);\n    }\n\n    onSave() {\n        const id = this.id();\n        const name = this.name();\n\n        if(!name) {\n            return;\n        }\n\n        const category = { id, name } as Category;\n        this.categoryService.saveCategory(category).subscribe(() => {\n            this.dialogRef.close(true);\n        });\n    }\n\n    onClose() {\n        this.dialogRef.close();\n    }\n}\n

    Navegando ahora por la p\u00e1gina y pulsando en el icono de editar, se deber\u00eda abrir una ventana con los datos que hemos seleccionado, similar a esta imagen:

    En el tutorial de Angular antiguo se aprovechaba que, al modificar los datos dentro de la ventana de di\u00e1logo, el listado se actualizaba autom\u00e1ticamente. Esto suced\u00eda porque se pasaba la misma referencia del objeto desde el listado al formulario y, al trabajar con objetos mutables, cualquier cambio se reflejaba en ambos.

    Con la llegada de Signals, este enfoque deja de ser recomendable. Los signals est\u00e1n dise\u00f1ados para modelar el estado de forma expl\u00edcita, predecible y controlada, evitando efectos colaterales derivados de compartir y mutar referencias entre distintas partes de la aplicaci\u00f3n.

    En lugar de modificar directamente un objeto que tambi\u00e9n utiliza el listado, trabajamos con estados independientes (por ejemplo, inicializando un signal con una copia del valor original) y aplicamos los cambios solo cuando el usuario los confirma. De esta forma, el listado no se ve afectado durante la edici\u00f3n.

    Esta filosof\u00eda \u2014estado local, sin mutaciones impl\u00edcitas y flujos de datos claros\u2014 hace que la interfaz sea m\u00e1s f\u00e1cil de razonar, mantener y depurar, y encaja con el modelo reactivo que propone Angular moderno.

    Recomendaci\u00f3n

    En Angular moderno se recomienda evitar el binding directo de objetos mutables. Compartir referencias puede provocar cambios no deseados en distintas partes de la aplicaci\u00f3n.

    Con el nuevo enfoque basado en Signals, es preferible modelar el estado de forma expl\u00edcita, trabajando con copias o estados independientes y aplicando los cambios de manera controlada. Esto reduce efectos colaterales y hace el flujo de datos m\u00e1s claro y predecible.

    "},{"location":"develop/basic/angular17/#accion-de-borrado","title":"Acci\u00f3n de borrado","text":"

    Por norma general, toda acci\u00f3n de borrado de un dato de pantalla requiere una confirmaci\u00f3n previa por parte del usuario. Es decir, para evitar que el dato se borre accidentalmente, el usuario tendr\u00e1 que confirmar su acci\u00f3n. Por tanto, vamos a crear un componente que nos permita pedir una confirmaci\u00f3n al usuario.

    Como esta pantalla de confirmaci\u00f3n va a ser algo com\u00fan a muchas acciones de borrado de nuestra aplicaci\u00f3n, vamos a crearla dentro del m\u00f3dulo core. Como siempre, ejecutamos el comando en consola:

    ng generate component core/dialog-confirmation\n

    E implementamos el c\u00f3digo que queremos que tenga el componente. Al ser un componente gen\u00e9rico vamos a aprovechar y leeremos las variables que le pasemos en data.

    dialog-confirmation.component.htmldialog-confirmation.component.scssdialog-confirmation.component.ts
    <div class=\"container\">\n    <h1>{{title}}</h1>\n    <div [innerHTML]=\"description\" class=\"description\"></div>\n\n    <div class=\"buttons\">\n        <button mat-stroked-button (click)=\"onClose()\">No</button>\n        <button mat-flat-button color=\"primary\" (click)=\"onClose(true)\">S\u00ed</button>\n    </div>\n</div>    \n
    .container {\n    min-width: 350px;\n    max-width: 500px;\n    padding: 20px;\n\n    .description {\n      margin-bottom: 20px;\n    }\n\n    .buttons {\n      text-align: right;\n\n      button {\n          margin-left: 10px;\n      }\n    }\n}    \n
    import { Component, inject, signal } from '@angular/core';\nimport { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';\nimport { MatButtonModule } from '@angular/material/button';\n\n@Component({\n    selector: 'app-dialog-confirmation',\n    standalone: true,\n    imports: [MatButtonModule],\n    templateUrl: './dialog-confirmation.component.html',\n    styleUrl: './dialog-confirmation.component.scss',\n})\nexport class DialogConfirmationComponent {\n    protected readonly title = signal<string | null>(null);\n    protected readonly description = signal<string | null>(null);\n\n    protected readonly dialogRef = inject(MatDialogRef<DialogConfirmationComponent>);\n    protected readonly data = inject(MAT_DIALOG_DATA);\n\n    ngOnInit(): void {\n        this.title.set(this.data.title);\n        this.description.set(this.data.description);\n    }\n\n    onClose(value = false) {\n        this.dialogRef.close(value);\n    }\n}\n

    Ya por \u00faltimo, una vez tenemos el componente gen\u00e9rico de di\u00e1logo, vamos a utilizarlo en nuestro listado al pulsar el bot\u00f3n eliminar:

    category-list.component.htmlcategory-list.component.ts
        ...\n    <ng-container matColumnDef=\"action\">\n        <mat-header-cell *matHeaderCellDef></mat-header-cell>\n        <mat-cell *matCellDef=\"let element\">\n            <button mat-icon-button color=\"primary\" (click)=\"editCategory(element)\">\n                <mat-icon>edit</mat-icon>\n            </button>\n            <button mat-icon-button color=\"accent\" (click)=\"deleteCategory(element)\">\n                <mat-icon>clear</mat-icon>\n            </button>\n        </mat-cell>\n    </ng-container>\n    ...\n
      ...\n  import { DialogConfirmationComponent } from '../../core/dialog-confirmation/dialog-confirmation.component';\n  ...\n  deleteCategory(category: Category) {    \n    const dialogRef = this.dialog.open(DialogConfirmationComponent, {\n      data: { title: \"Eliminar categor\u00eda\", description: \"Atenci\u00f3n si borra la categor\u00eda se perder\u00e1n sus datos.<br> \u00bfDesea eliminar la categor\u00eda?\" }\n    });\n\n    dialogRef.afterClosed().subscribe(result => {\n      if (result) {\n        this.categoryService.deleteCategory(category.id).subscribe(result => {\n          this.loadData();\n        }); \n      }\n    });\n  }  \n  ...    \n

    Aqu\u00ed tambi\u00e9n hemos realizado la llamada a categoryService, aunque no se realice ninguna acci\u00f3n, pero as\u00ed lo dejamos listo para enlazarlo.

    Llegados a este punto, ya solo nos queda enlazar las acciones de la pantalla con las operaciones de negocio del backend.

    "},{"location":"develop/basic/angular17/#conectar-con-backend","title":"Conectar con Backend","text":"

    Antes de seguir

    Antes de seguir con este punto, debes implementar el c\u00f3digo de backend en la tecnolog\u00eda que quieras (Springboot o Nodejs). Si has empezado este cap\u00edtulo implementando el frontend, por favor accede a la secci\u00f3n correspondiente de backend para poder continuar con el tutorial. Una vez tengas implementadas todas las operaciones para este listado, puedes volver a este punto y continuar con Angular.

    El siguiente paso, como es obvio, ser\u00e1 hacer que Angular llame directamente al servidor backend para leer y escribir datos y eliminar los datos mockeados en Angular.

    \u00a1Manos a la obra!

    "},{"location":"develop/basic/angular17/#llamada-del-listado","title":"Llamada del listado","text":"

    La idea es que el m\u00e9todo getCategories() de category.service.ts en lugar de devolver datos est\u00e1ticos, realice una llamada al servidor a la ruta http://localhost:8080/category.

    Abrimos el fichero y sustituimos la l\u00ednea que antes devolv\u00eda los datos est\u00e1ticos, por esto:

    category.service.ts
    import { Injectable, inject } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { Observable, of } from 'rxjs';\nimport { Category } from './model/category';\n\n@Injectable({\nprovidedIn: 'root'\n})\nexport class CategoryService { \n\n    protected readonly http = inject(HttpClient);\n\n    private baseUrl = 'http://localhost:8080/category';\n\n    getCategories(): Observable<Category[]> {\n        return this.http.get<Category[]>(this.baseUrl);\n    }\n\n    saveCategory(category: Category): Observable<Category> {\n        return of(null);\n    }\n\n    deleteCategory(idCategory : number): Observable<any> {\n        return of(null);\n    }  \n}\n

    Como hemos a\u00f1adido un componente nuevo HttpClient tenemos que configurar nuestro proyecto para poder realizar llamadas, para eso modificamos el fichero app.config.ts.

    app.config.ts
    import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';\nimport { provideRouter } from '@angular/router';\n\nimport { routes } from './app.routes';\nimport { provideAnimationsAsync } from '@angular/platform-browser/animations/async';\nimport { provideHttpClient, withFetch } from '@angular/common/http';\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideZoneChangeDetection({ eventCoalescing: true }),\n    provideRouter(routes),\n    provideAnimationsAsync(),\n    provideHttpClient(withFetch())\n  ]\n};\n

    Si ahora refrescas el navegador (recuerda tener arrancado tambi\u00e9n el servidor) y accedes a la pantalla de Categor\u00edas deber\u00eda aparecer el listado con los datos que vienen del servidor.

    "},{"location":"develop/basic/angular17/#llamada-de-guardado-edicion","title":"Llamada de guardado / edici\u00f3n","text":"

    Para la llamada de guardado har\u00edamos lo mismo, pero invocando la operaci\u00f3n de negocio put.

    category.service.ts
    import { Injectable, inject } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { Observable, of } from 'rxjs';\nimport { Category } from './model/category';\n\n@Injectable({\nprovidedIn: 'root'\n})\nexport class CategoryService { \n\n    protected readonly http = inject(HttpClient);\n\n    private baseUrl = 'http://localhost:8080/category';\n\n    getCategories(): Observable<Category[]> {\n        return this.http.get<Category[]>(this.baseUrl);\n    }\n\n    saveCategory(category: Category): Observable<Category> {\n        const { id } = category;\n        const url = id ? `${this.baseUrl}/${id}` : this.baseUrl;\n        return this.http.put<Category>(url, category);\n    }\n\n    deleteCategory(idCategory : number): Observable<any> {\n        return of(null);\n    }  \n\n} \n

    Ahora podemos probar a modificar o a\u00f1adir una nueva categor\u00eda desde la pantalla y deber\u00eda aparecer los nuevos datos en el listado.

    "},{"location":"develop/basic/angular17/#llamada-de-borrado","title":"Llamada de borrado","text":"

    Y ya por \u00faltimo, la llamada de borrado, deber\u00edamos cambiarla e invocar a la operaci\u00f3n de negocio delete.

    category.service.ts
    import { Injectable, inject } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { Observable, of } from 'rxjs';\nimport { Category } from './model/category';\n\n@Injectable({\nprovidedIn: 'root'\n})\nexport class CategoryService {\n    protected readonly http = inject(HttpClient);\n\n    private baseUrl = 'http://localhost:8080/category';\n\n    getCategories(): Observable<Category[]> {\n        return this.http.get<Category[]>(this.baseUrl);\n    }\n\n    saveCategory(category: Category): Observable<Category> {\n        const { id } = category;\n        const url = id ? `${this.baseUrl}/${id}` : this.baseUrl;\n        return this.http.put<Category>(url, category);\n    }\n\n    deleteCategory(idCategory : number): Observable<any> {\n        return this.http.delete(`${this.baseUrl}/${idCategory}`);\n    }  \n}\n

    Ahora podemos probar a modificar o a\u00f1adir una nueva categor\u00eda desde la pantalla y deber\u00eda aparecer los nuevos datos en el listado.

    Como ves, es bastante sencillo conectar server y client.

    "},{"location":"develop/basic/angular17/#depuracion","title":"Depuraci\u00f3n","text":"

    Una parte muy importante del desarrollo es tener la capacidad de depurar nuestro c\u00f3digo, en este apartado vamos a explicar como se realiza debug en Front.

    Esta parte se puede realizar con nuestro navegador favorito, en este caso vamos a utilizar Chrome.

    El primer paso es abrir las herramientas del desarrollador del navegador presionando F12.

    En esta herramienta tenemos varias partes importantes:

    • Elements: Inspector de los elementos del DOM de nuestra aplicaci\u00f3n que nos ayuda identificar el c\u00f3digo generado.
    • Console: Consola donde podemos ver mensajes importantes que nos ayudan a identificar posibles problemas.
    • Source: El navegador de ficheros que componen nuestra aplicaci\u00f3n.
    • Network: El registro de peticiones que realiza nuestra aplicaci\u00f3n.

    Identificados los elementos importantes, vamos a depurar la operaci\u00f3n de crear categor\u00eda.

    Para ello nos dirigimos a la pesta\u00f1a de Source, en el \u00e1rbol de carpetas nos dirigimos a la ruta donde est\u00e1 localizado el c\u00f3digo de nuestra aplicaci\u00f3n webpack://src/app.

    Dentro de esta carpeta est\u00e9 localizado todo el c\u00f3digo fuente de la aplicaci\u00f3n, en nuestro caso vamos a localizar componente category-edit.component que crea una nueva categor\u00eda.

    Dentro del fichero ya podemos a\u00f1adir puntos de ruptura (breakpoint), en nuestro caso queremos comprobar que el nombre introducido se captura bien y se env\u00eda al service correctamente.

    Colocamos el breakpoint en la l\u00ednea de invocaci\u00f3n del service (click sobre el n\u00famero de la l\u00ednea) y desde la interfaz creamos una nueva categor\u00eda.

    Hecho esto, podemos observar que a nivel de interfaz, la aplicaci\u00f3n se detiene y aparece un panel de manejo de los puntos de interrupci\u00f3n:

    En cuanto a la herramienta del desarrollador, nos lleva al punto exacto donde hemos a\u00f1adido el breakpoint y se para en este punto ofreci\u00e9ndonos la posibilidad de explorar el contenido de las variables del c\u00f3digo:

    Aqu\u00ed podemos comprobar que efectivamente la variable category tiene el valor que hemos introducido por pantalla y se propaga correctamente hacia el service.

    Para continuar con la ejecuci\u00f3n basta con darle al bot\u00f3n de play del panel de manejo de interrupci\u00f3n o al que aparece dentro de la herramienta de desarrollo (parte superior derecha).

    Por \u00faltimo, vamos a revisar que la petici\u00f3n REST se ha realizado correctamente al backend, para ello nos dirigimos a la pesta\u00f1a Network y comprobamos las peticiones realizadas:

    Aqu\u00ed podemos observar el registro de todas las peticiones y haciendo click sobre una de ellas, obtenemos el detalle de esta.

    • Header: Informaci\u00f3n de las cabeceras enviadas (aqu\u00ed podemos ver que se ha hecho un PUT a la ruta correcta).
    • Payload: El cuerpo de la petici\u00f3n (vemos el cuerpo del mensaje con el nombre enviado).
    • Preview: Respuesta de la petici\u00f3n normalizada (vemos la respuesta con el identificador creado para la nueva categor\u00eda).
    "},{"location":"develop/basic/nodejs/","title":"Listado simple - Nodejs","text":"

    Ahora que ya tenemos listo el proyecto backend de nodejs (en el puerto 8080) ya podemos empezar a codificar la soluci\u00f3n.

    "},{"location":"develop/basic/nodejs/#primeros-pasos","title":"Primeros pasos","text":"

    Antes de empezar

    Quiero hacer hincapi\u00e9 en Node tiene una documentaci\u00f3n muy extensa y completa, as\u00ed que te recomiendo que hagas uso de ella cuando tengas cualquier duda. Tanto en la web de node como de express encontrar\u00e1s informaci\u00f3n detallada del proceso que vamos a seguir.

    "},{"location":"develop/basic/nodejs/#estructurar-el-codigo","title":"Estructurar el c\u00f3digo","text":"

    La estructura de nuestro proyecto ser\u00e1 la siguiente:

    Vamos a aplicar una separaci\u00f3n por capas. En primer lugar, tendremos una capa de rutas para reenviar las solicitudes admitidas y cualquier informaci\u00f3n codificada en las urls de solicitud a la siguiente capa de controladores. La capa de control procesar\u00e1 las peticiones de las rutas y se comunicar\u00e1 con la capa de servicios devolviendo la respuesta de esta mediante respuestas http. En la capa de servicio se ejecutar\u00e1 toda la l\u00f3gica de la petici\u00f3n y se comunicar\u00e1 con los modelos de base de datos

    En nuestro caso una ruta es una secci\u00f3n de c\u00f3digo Express que asocia un verbo HTTP (GET, POST, PUT, DELETE, etc.), una ruta/patr\u00f3n de URL y una funci\u00f3n que se llama para manejar ese patr\u00f3n.

    \u00a1Ahora s\u00ed, vamos a programar!

    "},{"location":"develop/basic/nodejs/#capa-de-routes","title":"Capa de Routes","text":"

    Lo primero de vamos a crear es la carpeta principal de nuestra aplicaci\u00f3n donde estar\u00e1n contenidos los distintos elementos de la misma. Para ello creamos una carpeta llamada src en la ra\u00edz de nuestra aplicaci\u00f3n.

    El primero elemento que vamos a crear va a ser el fichero de rutas para la categor\u00eda. Para ello creamos una carpeta llamada routes en la carpeta src y dentro de esta carpeta crearemos un archivo llamado category.routes.js:

    category.routes.js
    import { Router } from 'express';\nimport { createCategory } from '../controllers/category.controller.js';\n\nconst categoryRouter = Router();\ncategoryRouter.put('/', createCategory);\n\nexport default categoryRouter;\n

    En este archivo estamos creando una ruta de tipo PUT que llamara al m\u00e9todo createCategory de nuestro futuro controlador de categor\u00edas (aunque todav\u00eda no lo hemos creado y por tanto fallar\u00e1).

    Ahora en nuestro archivo index.js vamos a a\u00f1adir lo siguiente justo despu\u00e9s de declarar la constante app:

    index.js
    ...\nimport categoryRouter from './src/routes/category.routes.js';\n...\n\n...\napp.use(cors({\n    origin: '*'\n}));\n\napp.use(express.json());\napp.use('/category', categoryRouter);\n\n...\n

    De este modo estamos asociando la url http://localhost:8080/category a nuestro router. Tambi\u00e9n usaremos express.json() para parsear las peticiones entrantes a formato json.

    "},{"location":"develop/basic/nodejs/#capa-de-controller","title":"Capa de Controller","text":"

    Lo siguiente ser\u00e1 crear el m\u00e9todo createCategory en nuestro controller. Para ello lo primero ser\u00e1 crear una carpeta controllers en la carpeta src de nuestro proyecto y dentro de esta un archivo llamado category.controller.js:

    category.controller.js
    export const createCategory = async (req, res) => {\n    console.log(req.body);\n    res.status(200).json(1);\n}\n

    Hemos creado la funci\u00f3n createCategory que recibir\u00e1 una request y una response. Estos par\u00e1metros vienen de la ruta de express y son la request y response de la petici\u00f3n HTTP. De momento simplemente vamos a hacer un console.log de req.body para ver el body de la petici\u00f3n y vamos a hacer una response 200 para indicar que todo ha ido correctamente.

    Si arrancamos el servidor y hacemos una petici\u00f3n PUT con Postman a http://localhost:8080/category con un body que pongamos formado correctamente podremos ver la salida que hemos programado en nuestro controller y en la consola de node podemos ver el contenido de req.body.

    "},{"location":"develop/basic/nodejs/#capa-de-modelo","title":"Capa de Modelo","text":"

    Ahora para que los datos que pasemos en el body los podamos guardar en BBDD necesitaremos un modelo y un esquema para la entidad Category. Vamos a crear una carpeta llamada schemas en la carpeta src de nuestro proyecto. Un schema no es m\u00e1s que un modelo de BBDD que especifica que campos estar\u00e1n presentes y cu\u00e1les ser\u00e1n sus tipos. Dentro de la carpeta de schemas creamos un archivo con el nombre category.schema.js:

    category.schema.js
    import mongoose from \"mongoose\";\nconst { Schema, model } = mongoose;\nimport normalize from 'normalize-mongoose';\n\nconst categorySchema = new Schema({\n    name: {\n        type: String,\n        require: true\n    }\n});\ncategorySchema.plugin(normalize);\nconst CategoryModel = model('Category', categorySchema);\n\nexport default CategoryModel;\n

    En este archivo estamos definiendo nuestro schema indicando sus propiedades y tipos, en nuestro caso \u00fanicamente name. Adem\u00e1s del tipo tambi\u00e9n indicaremos que el campo es obligatorio con la validation require para indicar que ese campo es obligatorio. Si quieres conocer otras validaciones aqu\u00ed tienes m\u00e1s info. Aparte de definir nuestro schema tambi\u00e9n lo estamos transformado en un modelo para poder trabajar con \u00e9l. En el constructor de model le pasamos el nombre del modelo y el schema que vamos a utilizar.

    "},{"location":"develop/basic/nodejs/#capa-de-servicio","title":"Capa de Servicio","text":"

    Como hemos visto en nuestra estructura la capa controller no puede comunicarse con la capa modelo, debe de haber una capa intermedia, para ello vamos a crear una carpeta services en la carpeta src de nuestro proyecto y dentro un archivo category.service.js:

    category.service.js
    import CategoryModel from '../schemas/category.schema.js';\n\nexport const createCategory = async function(name) {\n    try {\n        const category = new CategoryModel({ name });\n        return await category.save();\n    } catch (e) {\n        throw Error('Error creating category');\n    }\n}\n

    Hemos importado el modelo de categor\u00eda para poder realizar acciones sobre la BBDD y hemos creado una funci\u00f3n que recoger\u00e1 el nombre de la categor\u00eda y crear\u00e1 una nueva categor\u00eda con \u00e9l. Llamamos al m\u00e9todo save para guardar nuestra categor\u00eda y devolvemos el resultado. Ahora en nuestro m\u00e9todo del controller solo tenemos que llamar al servicio pas\u00e1ndole los par\u00e1metros que nos llegan en la petici\u00f3n:

    category.controller.js
    import * as CategoryService from '../services/category.service.js';\n\nexport const createCategory = async (req, res) => {\n    const { name } = req.body;\n    try {\n        const category = await CategoryService.createCategory(name);\n        res.status(200).json({\n            category\n        });\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n

    Si todo ha ido correctamente llamaremos al m\u00e9todo de respuesta con el c\u00f3digo 200 y la categor\u00eda creada. En caso contrario mandaremos un c\u00f3digo de error. Si ahora de nuevo vamos a postman y volvemos a lanzar nuestra petici\u00f3n podemos ver como nos devuelve una nueva categor\u00eda:

    "},{"location":"develop/basic/nodejs/#resto-de-operaciones","title":"Resto de Operaciones","text":""},{"location":"develop/basic/nodejs/#recuperacion-categorias","title":"Recuperaci\u00f3n categor\u00edas","text":"

    Ahora que ya podemos crear categor\u00edas lo siguiente ser\u00e1 crear un endpoint para recuperar las categor\u00edas creadas en nuestra base de datos. Podemos empezar a\u00f1adiendo un nuevo m\u00e9todo en nuestro servicio:

    category.service.js
    export const getCategories = async function () {\n    try {\n        return await CategoryModel.find().sort('name');\n    } catch (e) {\n        throw Error('Error fetching categories');\n    }\n}\n

    Al igual que en el anterior m\u00e9todo haremos uso del modelo, pero esta vez para hacer un find y ordenando los resultados por el campo name. Al m\u00e9todo find se le pueden pasar queries, projections y options. Te dejo por aqu\u00ed m\u00e1s info. En nuestro caso simplemente queremos que nos devuelva todas las categor\u00edas por lo que no le pasaremos nada.

    Creamos tambi\u00e9n un m\u00e9todo en el controlador para recuperar las categor\u00edas y que har\u00e1 uso del servicio:

    category.controller.js
    export const getCategories = async (req, res) => {\n    try {\n        const categories = await CategoryService.getCategories();\n        res.status(200).json(\n            categories\n        );\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n

    Y ahora que ya tenemos el m\u00e9todo creado en el controlador lo siguiente ser\u00e1 relacionar este m\u00e9todo con una ruta. Para ello en nuestro archivo category.routes.js tendremos que a\u00f1adir una nueva l\u00ednea:

    category.routes.js
    import { Router } from 'express';\nimport { createCategory, getCategories } from '../controllers/category.controller.js';\n\nconst categoryRouter = Router();\ncategoryRouter.put('/', createCategory);\ncategoryRouter.get('/', getCategories);\n\nexport default categoryRouter;\n

    De este modo cuando hagamos una petici\u00f3n GET a http://localhost:8080/category nos devolver\u00e1 el listado de categor\u00edas existentes:

    "},{"location":"develop/basic/nodejs/#actualizar-categoria","title":"Actualizar categor\u00eda","text":"

    Ahora vamos a por el m\u00e9todo para actualizar nuestras categor\u00edas. En el servicio creamos el siguiente m\u00e9todo:

    category.service.js
    export const updateCategory = async (id, name) => {\n    try {\n        const category = await CategoryModel.findById(id);\n        if (!category) {\n            throw Error('There is no category with that Id');\n        }    \n        return await CategoryModel.findByIdAndUpdate(id, {name});\n    } catch (e) {\n        throw Error(e);\n    }\n}\n

    A este m\u00e9todo le pasaremos de entrada el id y el nombre. Con ese id realizaremos una b\u00fasqueda para asegurarnos que esa categor\u00eda existe en nuestra base de datos. Si existe la categor\u00eda haremos una petici\u00f3n con findByIdAndUpdate donde el primer par\u00e1metro es el id y el segundo es el resto de los campos de nuestra entidad.

    En el controlador creamos el m\u00e9todo correspondiente:

    category.controller.js
    export const updateCategory = async (req, res) => {\n    const categoryId = req.params.id;\n    const { name } = req.body;\n    try {\n        await CategoryService.updateCategory(categoryId, name);\n        res.status(200).json(1);\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n

    Aqu\u00ed recogeremos el par\u00e1metro id que nos vendr\u00e1 en la url, por ejemplo: http://localhost:8080/category/1. Esto lo hacemos con req.params.id. El id es el nombre de la variable que le daremos en el router como veremos m\u00e1s adelante. Y una vez creado el m\u00e9todo en el controlador tendremos que a\u00f1adir la ruta en nuestro fichero de rutas correspondiente, pero como ya hemos dicho tendremos que indicar que nuestra ruta espera un par\u00e1metro id, lo haremos de la siguiente forma:

    category.routes.js
    import { Router } from 'express';\nimport { createCategory, getCategories, updateCategory } from '../controllers/category.controller.js';\n\nconst categoryRouter = Router();\ncategoryRouter.put('/', createCategory);\ncategoryRouter.get('/', getCategories);\ncategoryRouter.put('/:id', updateCategory);\n\nexport default categoryRouter;\n

    Y volvemos a probar en Postman:

    Y si hacemos de nuevo un GET vemos como la categor\u00eda se ha modificado correctamente:

    "},{"location":"develop/basic/nodejs/#borrado-categoria","title":"Borrado categor\u00eda","text":"

    Ya solo nos faltar\u00eda la operaci\u00f3n de delete para completar nuestro CRUD, en el servicio a\u00f1adimos un nuevo m\u00e9todo:

    category.service.js
    export const deleteCategory = async (id) => {\n    try {\n        const category = await CategoryModel.findById(id);\n        if (!category) {\n            throw Error('There is no category with that Id');\n        }\n        return await CategoryModel.findByIdAndDelete(id);\n    } catch (e) {\n        throw Error('Error deleting category');\n    }\n}\n

    Como vemos es muy parecido al update, recuperamos el id de los par\u00e1metros de la ruta y en este caso llamaremos al m\u00e9todo findByIdAndDelete. En nuestro controlador creamos el m\u00e9todo correspondiente:

    category.controller.js
    export const deleteCategory = async (req, res) => {\n    const categoryId = req.params.id;\n    try {\n        const deletedCategory = await CategoryService.deleteCategory(categoryId);\n        res.status(200).json({\n            category: deletedCategory\n        });\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n

    Y de nuevo a\u00f1adimos la ruta correspondiente al archivo de rutas:

    category.routes.js
    import { Router } from 'express';\nimport { createCategory, getCategories, updateCategory, deleteCategory } from '../controllers/category.controller.js';\n\nconst categoryRouter = Router();\ncategoryRouter.put('/', createCategory);\ncategoryRouter.get('/', getCategories);\ncategoryRouter.put('/:id', updateCategory);\ncategoryRouter.delete('/:id', deleteCategory);\n\nexport default categoryRouter;\n

    Y de nuevo, probamos en postman:

    Hacemos un get para comprobar que se ha borrado de nuestra base de datos:

    "},{"location":"develop/basic/nodejs/#capa-de-middleware-validaciones","title":"Capa de Middleware (Validaciones)","text":"

    Antes de pasar a nuestro siguiente CRUD vamos a ver en que consiste la Capa de Middleware. Un middleware es un c\u00f3digo que se ejecuta antes de que una petici\u00f3n http llegue a nuestro manejador de rutas o antes de que el cliente reciba una respuesta.

    En nuestro caso vamos a crear un middleware para asegurarnos que todos los campos que necesitamos en nuestras entidades vienen en el body de la petici\u00f3n. Vamos a crear una carpeta middlewares en la carpeta src de nuestro proyecto y dentro crearemos el fichero validateFields.js:

    validateFields.js
    import { response } from 'express';\nimport { validationResult } from 'express-validator';\n\nconst validateFields = (req, res = response, next) => {\n    const errors = validationResult(req);\n    if (!errors.isEmpty()) {\n        return res.status(400).json({\n            errors: errors.mapped()\n        });\n    }\n    next();\n}\n\nexport default validateFields;\n

    En este m\u00e9todo nos ayudaremos de la librer\u00eda express-validator para ver los errores que tenemos en nuestras rutas. Para ello llamaremos a la funci\u00f3n validationResult que nos devolver\u00e1 un array de errores que m\u00e1s tarde definiremos. Si el array no va vac\u00edo es porque se ha producido alg\u00fan error en las validaciones y ejecutara la response con un c\u00f3digo de error.

    Ahora definiremos las validaciones en nuestro archivo de rutas, deber\u00eda quedar de la siguiente manera:

    category.routes.js
    import { Router } from 'express';\nimport { check } from 'express-validator';\nimport validateFields from '../middlewares/validateFields.js';\nimport { getCategories, createCategory, deleteCategory, updateCategory } from '../controllers/category.controller.js';\nconst categoryRouter = Router();\n\ncategoryRouter.put('/:id', [\n    check('name').not().isEmpty(),\n    validateFields\n], updateCategory);\n\ncategoryRouter.put('/', [\n    check('name').not().isEmpty(),\n    validateFields\n], createCategory);\n\ncategoryRouter.get('/', getCategories);\ncategoryRouter.delete('/:id', deleteCategory);\n\nexport default categoryRouter;\n

    Aqu\u00ed nos ayudamos de nuevo de express-validator y de su m\u00e9todo check. Para las rutas en las que necesitemos validaciones, a\u00f1adimos un array como segundo par\u00e1metro. En este array vamos a\u00f1adiendo todas las validaciones que necesitemos. En nuestro caso solo queremos que el campo name no sea vac\u00edo, pero existen muchas m\u00e1s validaciones que puedes encontrar en la documentaci\u00f3n de express-validator. Importamos nuestro middleware y lo a\u00f1adimos en la \u00faltima posici\u00f3n de este array.

    De este modo no se realizar\u00e1n las peticiones que no pasen las validaciones:

    Y con esto habremos terminado nuestro primer CRUD.

    "},{"location":"develop/basic/nodejs/#depuracion","title":"Depuraci\u00f3n","text":"

    Una parte muy importante del desarrollo es tener la capacidad de depurar nuestro c\u00f3digo, en este apartado vamos a explicar como se realiza debug en Backend.

    Esta parte se realiza con las herramientas incluidas dentro de nuestro IDE favorito, en este caso vamos a utilizar el Visual Estudio.

    Lo primero que debemos hacer es configurar el modo Debug de nuestro proyecto.

    Para ello nos dirigimos a la opci\u00f3n Run and Debug y creamos el fichero de launch necesario:

    Esto nos crear\u00e1 el fichero necesario y ya podremos arrancar la aplicaci\u00f3n mediante esta herramienta presionando el bot\u00f3n Launch Program (seleccionamos tipo de aplicaci\u00f3n Node y el script de arranque que ser\u00e1 el que hemos utilizado en el desarrollo):

    Arrancada la aplicaci\u00f3n de este modo, vamos a depurar la operaci\u00f3n de crear categor\u00eda.

    Para ello vamos a abrir nuestro fichero donde tenemos la implementaci\u00f3n del servicio de creaci\u00f3n de la capa de la l\u00f3gica de negocio category.service.js.

    Dentro del fichero ya podemos a\u00f1adir puntos de ruptura (breakpoint), en nuestro caso queremos comprobar que el nombre introducido se recibe correctamente.

    Colocamos el breakpoint en la primera l\u00ednea del m\u00e9todo (click sobre el n\u00famero de la l\u00ednea) y desde la interfaz/postman creamos una nueva categor\u00eda.

    Hecho esto, podemos observar que a nivel de interfaz/postman, la petici\u00f3n se queda esperando y el IDE mostrar\u00e1 un panel de manejo de los puntos de interrupci\u00f3n:

    El IDE nos lleva al punto exacto donde hemos a\u00f1adido el breakpoint y se para en este punto ofreci\u00e9ndonos la posibilidad de explorar el contenido de las variables del c\u00f3digo:

    Aqu\u00ed podemos comprobar que efectivamente la variable name tiene el valor que hemos introducido por pantalla/postman.

    Para continuar con la ejecuci\u00f3n basta con darle al bot\u00f3n de play del panel de manejo de los puntos de interrupci\u00f3n.

    "},{"location":"develop/basic/react/","title":"Listado simple - React","text":"

    Ahora que ya tenemos listo el proyecto frontend de React (en el puerto 5173), ya podemos empezar a codificar la soluci\u00f3n.

    "},{"location":"develop/basic/react/#primeros-pasos","title":"Primeros pasos","text":"

    Antes de empezar

    Quiero hacer hincapi\u00e9 que React tiene una documentaci\u00f3n muy extensa y completa, as\u00ed que te recomiendo que hagas uso de ella cuando tengas cualquier duda. Tanto en la propia web de documentaci\u00f3n de React como en la web de componentes Mui puedes buscar casi cualquier ejemplo que necesites.

    Si abrimos el proyecto con el IDE que tengamos (Visual Studio Code en el caso del tutorial) podemos ver que en la carpeta src existen unos ficheros ya creados por defecto. Estos ficheros son:

    • main.tsx \u2192 contiene el componente principal del proyecto.
    • index.css \u2192 contiene los estilos CSS globales de la aplicaci\u00f3n.
    • APP.tsx \u2192 contiene el componente inicial del proyecto
    • APP.css \u2192 contiene los estilos para el componente APP.

    Aunque main.tsx y App.tsx puedan parecer lo mismo main.tsx se suele dejar tal y como esta ya que lo \u00fanico que hace es asociar el div con id \u201croot\u201d del archivo index.html de la ra\u00edz de nuestro proyecto para que sea el nodo principal de React. En el archivo App.tsx es donde realmente empezamos a desarrollar c\u00f3digo.

    Si abrimos main.tsx podemos ver que se esta usando <App /> como una etiqueta html. El nombre con que exportemos nuestros componentes ser\u00e1 el nombre de la etiqueta html utilizado para renderizar los componentes.

    Vamos a modificar este c\u00f3digo inicial para ver c\u00f3mo funciona. Abrimos el fichero App.tsx y vamos a dejarlo de esta manera:

    import { useState } from 'react'\nimport './App.css'\n\nfunction App() {\n  const [count, setCount] = useState(0)\n  const probando = \"probando 123\";\n\n  return (\n    <>\n      <p>{probando}</p>\n      <div className=\"card\">\n        <button onClick={() => setCount((count) => count + 1)}>\n          count is {count}\n        </button>\n      </div>\n    </>\n  )\n}\n\nexport default App\n
    En los componentes React siempre se suele seguir el mismo orden, primero introduciremos los imports necesarios, luego podemos declarar variables y funciones que no se vayan a modificar, despu\u00e9s creamos nuestra funci\u00f3n principal con el nombre del componente y dentro de esta lo primero que se suelen declarar son todas las variables, despu\u00e9s a\u00f1adiremos m\u00e9todos del componente y por \u00faltimo tenemos que llamar a return para devolver lo que queramos renderizar.

    Si ahora abrimos nuestro navegador veremos en pantalla el valor de la variable \"probando\" que hemos introducido mediante una expresi\u00f3n en un tag p de html y un bot\u00f3n que si pulsamos incrementar\u00e1 el valor de la cuenta en uno. Si refrescamos la pantalla el valor de la cuenta volver\u00e1 autom\u00e1ticamente a 0. Es hora de explicar como funciona un componente React y el hook useState.

    "},{"location":"develop/basic/react/#jsx","title":"JSX","text":"

    JSX significa Javascript XML. JSX nos permite escribir elementos HTML en JavaScript y colocarlos en el DOM. Con JSX puedes escribir expresiones dentro de llaves \u201c{}\u201d. Estas expresiones pueden ser variables, propiedades o cualquier expresi\u00f3n Javascript valida. JSX ejecutar\u00e1 esta expresi\u00f3n y devolver\u00e1 el resultado.

    Por ejemplo, si queremos mostrar un elemento de forma condicional lo podemos hacer de la siguiente manera:

            {\n          variableBooleana  && <p>El valor es true</p>\n        }\n

    Tambi\u00e9n podemos usar el operador ternario para condiciones:

            {\n          variableBooleana  ? <p>El valor es true</p> : <p>El valor es false</p>\n        }\n

    Y si lo que queremos es recorrer un array e ir representando los elementos lo podemos hacer de la siguiente manera:

            {\n          arrayNumerico.map(numero => <p>Mi valor es {numero}</p>)\n        }\n

    React solo puede devolver un elemento en su bloque return, es por eso por lo que algunas veces se rodea todo el c\u00f3digo con un elemento llamado Fragment \u201c<></>\u201d. Estos fragment no soportan ni propiedades ni atributos y no tendr\u00e1n visibilidad en el dom.

    Dentro de una expresi\u00f3n podemos ver dos formas de llamar a una funci\u00f3n:

    <Button onClick={callToCancelar}>Cancelar</Button>\n<Button onClick={() => callToCancelar('param1')}>Cancelar</Button>\n

    En la primera se pasa una funci\u00f3n por referencia y Button es el responsable de los par\u00e1metros del evento. En la segunda tras hacer onClick se ejecuta la funci\u00f3n callToCancelar con los par\u00e1metros que nosotros queramos quitando esa responsabilidad a Button. En t\u00e9rminos de rendimiento es mejor la primera manera ya que en la segunda se vuelve a crear la funci\u00f3n en cada renderizado, pero hay veces que es necesario hacerlo as\u00ed para tomar control de los par\u00e1metros.

    "},{"location":"develop/basic/react/#usestate-hook","title":"useState hook","text":"

    Todo componente en React tiene una serie de variables. Algunas de estas son propiedades de entrada como podr\u00edan serlo disabled en un bot\u00f3n y que se trasmiten de componentes padres a hijos.

    Luego tenemos variables y constantes declaradas dentro del componente como por ejemplo la constante probando de nuestro ejemplo. Y finalmente tenemos unas variables especiales dentro de nuestro componente que corresponden al estado de este.

    Si modificamos el estado de un componente este autom\u00e1ticamente se volver\u00e1 a renderizar y producir\u00e1 una nueva representaci\u00f3n en pantalla.

    Como ya hemos comentado previamente los hooks aparecieron en la versi\u00f3n 16.8 de React. Antes de esto si quer\u00edamos acceder al estado de un componente solo pod\u00edamos acceder a este mediante componentes de clase, pero desde esta versi\u00f3n podemos hacer uso de estas funciones especiales para utilizar estas caracter\u00edsticas de React.

    M\u00e1s tarde veremos otras, pero de momento vamos a ver useState.

    const [count, setCount] = useState(0)\n

    En nuestro ejemplo tenemos una variable count que va mostrando su valor en el interior de un bot\u00f3n. Si pulsamos el bot\u00f3n ejecutara la funci\u00f3n setCount que actualiza el valor de nuestro contador. A esta funci\u00f3n se le puede pasar o bien el nuevo valor que tomar\u00e1 esta variable de estado o bien una funci\u00f3n cuyo primer par\u00e1metro es el valor actual de la variable. Siempre que se actualice la variable del estado de producir\u00e1 un nuevo renderizado del componente, eso lo pod\u00e9is comprobar escribiendo un console.log antes del return. En nuestro caso hemos inicializado nuestra variable de estado con el valor 0, pero puede inicializarse con un valor de cualquier tipo javascript. No existe limite en el n\u00famero de variables de estado por componente.

    Debemos tener en cuenta que si modificamos el estado de un componente que renderiza otros componentes, estos tambi\u00e9n se volver\u00e1n a renderizar al cambiar el estado del componente padre. Es por esto por lo que debemos tener cuidado a la hora de modificar estados y renderizar los hijos correctamente.

    Nota

    Para evitar el re-renderizado de los componentes hijos existe una funci\u00f3n especial en React llamada memo que evita este comportamiento si las props de los hijos no se ven modificadas. En este curso no cubriremos esta funcionalidad.

    Nota

    Por convenci\u00f3n todos los hooks empiezan con use. Si en alg\u00fan proyecto tienes que crear un custom hook es importante seguir esta nomenclatura.

    "},{"location":"develop/basic/react/#libreria-de-componentes-y-resto-de-dependencias","title":"Librer\u00eda de componentes y resto de dependencias","text":"

    Antes de continuar con nuestro curso vamos a instalar las dependencias necesarias para empezar a construir la base de nuestra aplicaci\u00f3n. Para ello ejecutamos lo siguiente en la consola en la ra\u00edz de nuestro proyecto:

    npm i @mui/material @mui/icons-material react-router-dom react-redux @reduxjs/toolkit @emotion/react @emotion/styled\n

    Como librer\u00eda de componentes vamos a utilizar Mui, anteriormente conocido como Material ui, es una librer\u00eda muy utilizada en los proyectos de React con una gran documentaci\u00f3n. Tambi\u00e9n necesitaremos las librer\u00edas de emotion necesarias para trabajar con Mui.

    Vamos a utilizar la librer\u00eda react router dom que nos permitir\u00e1 definir y usar rutas de navegaci\u00f3n en nuestra aplicaci\u00f3n.

    Vamos a instalar tambi\u00e9n react redux y redux toolkit para gestionar el estado global de nuestra aplicaci\u00f3n.

    "},{"location":"develop/basic/react/#layout-general","title":"Layout general","text":""},{"location":"develop/basic/react/#crear-componente","title":"Crear componente","text":"

    Lo primero que haremos ser\u00e1 borrar el contenido del archivo App.css y vamos a modificar index.css con el siguiente contenido:

    :root {\n  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;\n}\n\nbody {\n  margin: 0;\n}\n\n@media (prefers-color-scheme: light) {\n  :root {\n    color: #213547;\n    background-color: #ffffff;\n  }\n}\n\n.container {\n  margin: 20px;\n}\n\n.newButton {\n  display: flex;\n  justify-content: flex-end;\n  margin-top: 20px;\n}\n

    Ahora vamos a crear los distintos componentes que compondr\u00e1n nuestra aplicaci\u00f3n. Para ello dentro de la carpeta src vamos a crear una nueva carpeta llamada pages y dentro de esta crearemos tres carpetas relativas a nuestras paginas navegables: \u201cAuthor\u201d, \u201cCategory\u201d y \u201cGame\u201d. Dentro de estas a su vez crearemos un fichero llamado Author.tsx, Category.tsx y Game.tsx respectivamente, cuyo contenido ser\u00e1 una funci\u00f3n que tendr\u00e1 por nombre el mismo nombre que el fichero y que devolver\u00e1 un div cuyo contenido ser\u00e1 tambi\u00e9n el nombre del fichero:

    export const Game = () => {\n  return (\n    <div>Game</div>\n  )\n}\n

    Ahora vamos a crear en la carpeta src otra carpeta cuyo nombre ser\u00e1 \u201ccomponents\u201d y dentro de esta un fichero llamado Layout.tsx cuyo contenido ser\u00e1 el siguiente:

    import { useState } from \"react\";\nimport AppBar from \"@mui/material/AppBar\";\nimport Box from \"@mui/material/Box\";\nimport Toolbar from \"@mui/material/Toolbar\";\nimport IconButton from \"@mui/material/IconButton\";\nimport Typography from \"@mui/material/Typography\";\nimport Menu from \"@mui/material/Menu\";\nimport MenuIcon from \"@mui/icons-material/Menu\";\nimport Container from \"@mui/material/Container\";\nimport Button from \"@mui/material/Button\";\nimport MenuItem from \"@mui/material/MenuItem\";\nimport CasinoIcon from \"@mui/icons-material/Casino\";\nimport { useNavigate, Outlet } from \"react-router-dom\";\n\nconst pages = [\n  { name: \"Catalogo\", link: \"games\" },\n  { name: \"Categor\u00edas\", link: \"categories\" },\n  { name: \"Autores\", link: \"authors\" },\n];\n\nexport const Layout = () => {\n  const navigate = useNavigate();\n\n  const [anchorElNav, setAnchorElNav] = useState<null | HTMLElement>(\n    null\n  );\n\n  const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => {\n    setAnchorElNav(event.currentTarget);\n  };\n\n  const handleCloseNavMenu = (link: string) => {\n    navigate(`/${link}`);\n    setAnchorElNav(null);\n  };\n\n  return (\n    <>\n      <AppBar position=\"static\">\n        <Container maxWidth=\"xl\">\n          <Toolbar disableGutters>\n            <CasinoIcon sx={{ display: { xs: \"none\", md: \"flex\" }, mr: 1 }} />\n            <Typography\n              variant=\"h6\"\n              noWrap\n              component=\"a\"\n              href=\"/\"\n              sx={{\n                mr: 2,\n                display: { xs: \"none\", md: \"flex\" },\n                fontFamily: \"monospace\",\n                fontWeight: 700,\n                letterSpacing: \".3rem\",\n                color: \"inherit\",\n                textDecoration: \"none\",\n              }}\n            >\n              Ludoteca Tan\n            </Typography>\n\n            <Box sx={{ flexGrow: 1, display: { xs: \"flex\", md: \"none\" } }}>\n              <IconButton\n                size=\"large\"\n                aria-label=\"account of current user\"\n                aria-controls=\"menu-appbar\"\n                aria-haspopup=\"true\"\n                onClick={handleOpenNavMenu}\n                color=\"inherit\"\n              >\n                <MenuIcon />\n              </IconButton>\n              <Menu\n                id=\"menu-appbar\"\n                anchorEl={anchorElNav}\n                anchorOrigin={{\n                  vertical: \"bottom\",\n                  horizontal: \"left\",\n                }}\n                keepMounted\n                transformOrigin={{\n                  vertical: \"top\",\n                  horizontal: \"left\",\n                }}\n                open={Boolean(anchorElNav)}\n                onClose={handleCloseNavMenu}\n                sx={{\n                  display: { xs: \"block\", md: \"none\" },\n                }}\n              >\n                {pages.map((page) => (\n                  <MenuItem\n                    key={page.name}\n                    onClick={() => handleCloseNavMenu(page.link)}\n                  >\n                        <Typography textAlign=\"center\">\n                        {page.name}\n                      </Typography>\n                  </MenuItem>\n                ))}\n              </Menu>\n            </Box>\n            <CasinoIcon sx={{ display: { xs: \"flex\", md: \"none\" }, mr: 1 }} />\n            <Typography\n              variant=\"h5\"\n              noWrap\n              component=\"a\"\n              href=\"\"\n              sx={{\n                mr: 2,\n                display: { xs: \"flex\", md: \"none\" },\n                flexGrow: 1,\n                fontFamily: \"monospace\",\n                fontWeight: 700,\n                letterSpacing: \".3rem\",\n                color: \"inherit\",\n                textDecoration: \"none\",\n              }}\n            >\n              Ludoteca Tan\n            </Typography>\n            <Box sx={{ flexGrow: 1, display: { xs: \"none\", md: \"flex\" } }}>\n              {pages.map((page) => (\n                <Button\n                  key={page.name}\n                  onClick={() => handleCloseNavMenu(page.link)}\n                  sx={{ my: 2, color: \"white\", display: \"block\" }}\n                >\n                  {page.name}\n                </Button>\n              ))}\n            </Box>\n          </Toolbar>\n        </Container>\n      </AppBar>\n      <Outlet />\n    </>\n  );\n};\n

    Aunque puede parecer complejo por su tama\u00f1o en realidad no es tanto, casi todo es c\u00f3digo cogido directamente de un ejemplo de layout de navegaci\u00f3n de un componente de MUI.

    Lo m\u00e1s destacable es un nuevo hook (en realidad es un custom hook de react router dom) llamado useNavigate que como su propio nombre indica navegara a la ruta correspondiente seg\u00fan el valor pulsado.

    Las etiquetas sx son para dar estilo a los componentes de MUI. Tambi\u00e9n se puede sobrescribir el estilo mediante hojas css pero es m\u00e1s complejo y requiere una configuraci\u00f3n inicial que no cubriremos en este tutorial.

    Si nos fijamos en la l\u00ednea 90 se introduce una expresi\u00f3n javascript en la cual se recorre el array de pages declarado al inicio del componente y para cada uno de los valores se llama a MenuItem que es otro componente React al que se le pasan las props key, onClick y aunque no lo veamos tambi\u00e9n la prop \u201cchildren\u201d.

    La prop children estar\u00e1 presente cuando pasemos elementos entre los tags de un elemento:

    <MenuItem >     \n<Typography>I\u2019m a child</Typography>\n </MenuItem>\n
    El uso de la prop children no es muy recomendado y se prefiere que se pasen los elementos como una prop m\u00e1s.

    Siempre que rendericemos un array en react es recomendable usar una prop especial llamada \u201ckey\u201d, de hecho, si no la usamos la consola de desarrollo se nos llenar\u00e1 de warnings por no usarla.

    Esta key lo que permite a React es identificar cada elemento de formar m\u00e1s eficiente, as\u00ed si modificamos, a\u00f1adimos o eliminamos un elemento de un array no ser\u00e1 necesario volver a renderizar todo el array, solo se eliminar\u00e1 el elemento necesario.

    En la parte final del archivo tenemos una llamada al elemento Outlet. Este elemento es el que albergara el componente asociado a la ruta seleccionada.

    Por \u00faltimo, el archivo App.tsx se tiene que quedar de esta manera:

    import { BrowserRouter, Routes, Route, Navigate } from \"react-router-dom\";\nimport { Game } from \"./pages/Game/Game\";\nimport { Author } from \"./pages/Author/Author\";\nimport { Category } from \"./pages/Category/Category\";\nimport { Layout } from \"./components/Layout\";\n\nfunction App() {\n  return (\n        <BrowserRouter>\n          <Routes>\n            <Route element={<Layout />}>\n              <Route index path=\"games\" element={<Game />} />\n              <Route path=\"categories\" element={<Category />} />\n              <Route path=\"authors\" element={<Author />} />\n              <Route path=\"*\" element={<Navigate to=\"/games\" />} />\n            </Route>\n          </Routes>\n        </BrowserRouter>\n  );\n}\n\nexport default App;\n
    De esta manera definimos cada una de nuestras rutas y las asociamos a una p\u00e1gina.

    Vamos a arrancar el proyecto de nuevo con npm run dev y navegamos a http://localhost:5173/.

    Ahora podemos ver como autom\u00e1ticamente nos lleva a http://localhost:5173/games debido al \u00faltimo route en el que redirigimos cualquier path que no coincida con los anteriores a /games. Si pulsamos sobre las distintas opciones del men\u00fa podemos ver c\u00f3mo va cambiando el outlet de nuestra aplicaci\u00f3n con los distintos div creados para cada uno de los componentes p\u00e1gina.

    "},{"location":"develop/basic/react/#creando-un-listado-simple","title":"Creando un listado simple","text":""},{"location":"develop/basic/react/#pagina-categorias","title":"P\u00e1gina categor\u00edas","text":"

    Ya tenemos la estructura principal, ahora vamos a crear nuestra primera pantalla. Vamos a empezar por la de categor\u00edas.

    Lo primero que vamos a hacer es crear una carpeta llamada types dentro de src/. Aqu\u00ed crearemos los tipos de typescript. Creamos un nuevo fichero llamado Category.ts cuyo contenido ser\u00e1 el siguiente:

    export interface Category {\n  id: string;\n  name: string;\n}\n

    Ahora vamos a crear un archivo de estilos que ser\u00e1 solo utilizado por el componente Category. Para ello dentro de la carpeta src/pages/Category vamos a crear un archivo llamado Category.module.css. Al llamar al archivo de esta manera React reconoce este archivo como un archivo \u00fanico para un componente y hace que sus reglas css sean m\u00e1s prioritarias, aunque por ejemplo exista una clase con el mismo nombre en el archivo index.css.

    El contenido de nuestro archivo css ser\u00e1 el siguiente:

    .tableActions {\n    margin-right: 20px;\n    display: flex;\n    justify-content: flex-end;\n    align-content: flex-start;\n    gap: 19px;\n}\n

    Y por \u00faltimo el contenido de nuestro fichero src/pages/Category.tsx quedar\u00eda as\u00ed:

    import { useState } from \"react\";\nimport Table from \"@mui/material/Table\";\nimport TableBody from \"@mui/material/TableBody\";\nimport TableCell from \"@mui/material/TableCell\";\nimport TableContainer from \"@mui/material/TableContainer\";\nimport TableHead from \"@mui/material/TableHead\";\nimport TableRow from \"@mui/material/TableRow\";\nimport Paper from \"@mui/material/Paper\";\nimport Button from \"@mui/material/Button\";\nimport EditIcon from \"@mui/icons-material/Edit\";\nimport ClearIcon from \"@mui/icons-material/Clear\";\nimport IconButton from \"@mui/material/IconButton\";\nimport styles from \"./Category.module.css\";\nimport { Category as CategoryModel } from \"../../types/Category\";\n\nexport const Category = () => {\n  const data = [\n    {\n      id: \"1\",\n      name: \"Test 1\",\n    },\n    {\n      id: \"2\",\n      name: \"Test 2\",\n    },\n  ];\n\n  return (\n    <div className=\"container\">\n      <h1>Listado de Categor\u00edas</h1>\n      <TableContainer component={Paper}>\n        <Table sx={{ minWidth: 650 }} aria-label=\"simple table\">\n          <TableHead\n            sx={{\n              \"& th\": {\n                backgroundColor: \"lightgrey\",\n              },\n            }}\n          >\n            <TableRow>\n              <TableCell>Identificador</TableCell>\n              <TableCell>Nombre categor\u00eda</TableCell>\n              <TableCell></TableCell>\n            </TableRow>\n          </TableHead>\n          <TableBody>\n            {data.map((category: CategoryModel) => (\n              <TableRow\n                key={category.id}\n                sx={{ \"&:last-child td, &:last-child th\": { border: 0 } }}\n              >\n                <TableCell component=\"th\" scope=\"row\">\n                  {category.id}\n                </TableCell>\n                <TableCell component=\"th\" scope=\"row\">\n                  {category.name}\n                </TableCell>\n                <TableCell>\n                  <div className={styles.tableActions}>\n                    <IconButton aria-label=\"update\" color=\"primary\">\n                      <EditIcon />\n                    </IconButton>\n                    <IconButton aria-label=\"delete\" color=\"error\">\n                      <ClearIcon />\n                    </IconButton>\n                  </div>\n                </TableCell>\n              </TableRow>\n            ))}\n          </TableBody>\n        </Table>\n      </TableContainer>\n      <div className=\"newButton\">\n        <Button variant=\"contained\">Nueva categor\u00eda</Button>\n      </div>\n    </div>\n  );\n};\n
    De momento vamos a usar un listado mockeado para mostrar nuestras categorias. El c\u00f3digo JSX esta sacado pr\u00e1cticamente en su totalidad del ejemplo de una tabla de Mui y solo hemos modificado el nombre del array que tenemos que recorrer, sus atributos y hemos a\u00f1adido unos botones de acci\u00f3n para editar y borrar autores que de momento no hacen nada.

    Si abrimos un navegador (con el servidor arrancado npm run dev) y vamos a http://localhost:5173/categories podremos ver nuestro listado con los datos mockeados.

    Ahora vamos a crear un componente que se mostrar\u00e1 cuando pulsemos el bot\u00f3n de nueva categor\u00eda. En la carpeta src/pages/category vamos a crear una nueva carpeta llamada components y dentro de esta crearemos un nuevo fichero llamado CreateCategory.tsx que tendr\u00e1 el siguiente contenido:

    import { useState } from \"react\";\nimport Button from \"@mui/material/Button\";\nimport TextField from \"@mui/material/TextField\";\nimport Dialog from \"@mui/material/Dialog\";\nimport DialogActions from \"@mui/material/DialogActions\";\nimport DialogContent from \"@mui/material/DialogContent\";\nimport DialogTitle from \"@mui/material/DialogTitle\";\nimport { Category } from \"../../../types/Category\";\n\ninterface Props {\n  category: Category | null;\n  closeModal: () => void;\n  create: (name: string) => void;\n}\n\nexport default function CreateCategory(props: Props) {\n  const [name, setName] = useState(props?.category?.name || \"\");\n\n  return (\n    <div>\n      <Dialog open={true} onClose={props.closeModal}>\n        <DialogTitle>\n          {props.category ? \"Actualizar Categor\u00eda\" : \"Crear Categor\u00eda\"}\n        </DialogTitle>\n        <DialogContent>\n          {props.category && (\n            <TextField\n              margin=\"dense\"\n              disabled\n              id=\"id\"\n              label=\"Id\"\n              fullWidth\n              value={props.category.id}\n              variant=\"standard\"\n            />\n          )}\n          <TextField\n            margin=\"dense\"\n            id=\"name\"\n            label=\"Nombre\"\n            fullWidth\n            variant=\"standard\"\n            onChange={(event) => setName(event.target.value)}\n            value={name}\n          />\n        </DialogContent>\n        <DialogActions>\n          <Button onClick={props.closeModal}>Cancelar</Button>\n          <Button onClick={() => props.create(name)} disabled={!name}>\n            {props.category ? \"Actualizar\" : \"Crear\"}\n          </Button>\n        </DialogActions>\n      </Dialog>\n    </div>\n  );\n}\n

    Para este componente hemos definido una categor\u00eda como par\u00e1metro de entrada para poder reutilizar el componente en el caso de una edici\u00f3n y poder pasar la categor\u00eda a editar, en nuestro caso inicial al ser un alta esta categor\u00eda tendr\u00e1 el valor null. Tambi\u00e9n hemos definido dos funciones en los par\u00e1metros de entrada para controlar o bien el cerrado del modal o bien la creaci\u00f3n de un autor.

    Esta es la forma directa que tienen de comunicaci\u00f3n los componentes padre/hijo en React, el padre puede pasar datos de lectura o funciones a sus componentes hijos a las que estos llamaran para comunicarse con \u00e9l.

    As\u00ed en nuestro ejemplo el componente CreateCategory llamar\u00e1 a la funci\u00f3n create a la que pasar\u00e1 un nuevo objecto Category y ser\u00e1 el padre (nuestra p\u00e1gina Category) el que decidir\u00e1 qu\u00e9 hacer con esos datos al igual que ocurre con los eventos en los tags de html.

    En el estado de nuestro componente solo vamos a almacenar los datos introducidos en el formulario, en el caso de una edici\u00f3n el valor inicial del nombre de la categor\u00eda ser\u00e1 el que venga de entrada.

    Adem\u00e1s introducido unas expresiones que modificar\u00e1n la visualizaci\u00f3n del componente (titulo, id, texto de los botones, \u2026) dependiendo de si tenemos un autor de entrada o no.

    Ahora tenemos que a\u00f1adir nuestro nuevo componente en nuestra p\u00e1gina Category:

    Importamos el componente:

    import CreateCategory from \"./components/CreateCategory\";\n

    Creamos las funciones que le pasaremos al componente, dej\u00e1ndolas de momento vac\u00edas:

    const createCategory = () => {\n\n  }\n\n  const handleCloseCreate = () => {\n\n  }\n

    Y a\u00f1adimos en el c\u00f3digo JSX lo siguiente tras nuestro button:

          <CreateCategory\n          create={createCategory}\n          category={null}\n          closeModal={handleCloseCreate}\n        />\n

    Si ahora vamos a nuestro navegador, a la p\u00e1gina de categor\u00edas, podremos ver el formulario para crear una categor\u00eda, pero \u00e9sta fijo y no hay manera de cerrarlo. Vamos a cambiar este comportamiento mediante una variable booleana en el estado del componente que decidir\u00e1 cuando se muestra este. Adem\u00e1s, a\u00f1adiremos a nuestro bot\u00f3n el c\u00f3digo necesario para mostrar el componente y a\u00f1adiremos a la funci\u00f3n handleCloseCreate el c\u00f3digo para ocultarlo.

    A\u00f1adimos un nuevo estado:

    const [openCreate, setOpenCreate] = useState(false);\n

    Modificamos la function handleCloseCreate:

    const handleCloseCreate = () => {\n    setOpenCreate(false);\n  };\n

    Y por \u00faltimo modificamos el c\u00f3digo del return de la siguiente manera:

          <div className=\"newButton\">\n        <Button variant=\"contained\" onClick={() => setOpenCreate(true)}>\n          Nueva categor\u00eda\n        </Button>\n      </div>\n      {openCreate && (\n        <CreateCategory\n          create={createCategory}\n          category={null}\n          closeModal={handleCloseCreate}\n        />\n      )}\n

    Si probamos ahora vemos que ya se realiza la funcionalidad de abrir y cerrar nuestro formulario de manera correcta.

    Ahora vamos a a\u00f1adir la funcionalidad para que al pulsar el bot\u00f3n de edici\u00f3n pasemos la categor\u00eda a editar a nuestro formulario. Para esto vamos a necesitar una nueva variable en nuestro estado donde almacenaremos la categor\u00eda a editar:

    const [categoryToUpdate, setCategoryToUpdate] =\n    useState<CategoryModel | null>(null);\n

    Modificamos el c\u00f3digo de nuestro bot\u00f3n:

                        <IconButton\n                      aria-label=\"update\"\n                      color=\"primary\"\n                      onClick={() => {\n                        setCategoryToUpdate(category);\n                        setOpenCreate(true);\n                      }}\n                    >\n                      <EditIcon />\n                    </IconButton>\n

    Y la entrada a nuestro componente:

          {openCreate && (\n        <CreateCategory\n          create={createCategory}\n          category={categoryToUpdate}\n          closeModal={handleCloseCreate}\n        />\n      )}\n

    Si ahora hacemos una prueba en nuestro navegador y pulsamos el bot\u00f3n de editar vemos como nuestro formulario ya se carga correctamente pero hay un problema, si pulsamos el bot\u00f3n de editar, cerramos el formulario y le damos al boton de nueva categor\u00eda vemos que el formulario mantiene los datos anteriores. Vamos a solucionar este problema volviendo a dejar vacia la variable categoryToUpdate cuando se cierre el componente:

    Modificamos la funci\u00f3n handleCloseCreate:

    const handleCloseCreate = () => {\n    setOpenCreate(false);\n    setCategoryToUpdate(null);\n  };\n

    Y vemos que el funcionamiento ya es el correcto.

    Ahora vamos a darle funcionalidad al bot\u00f3n de borrado. Cuando pulsemos sobre este bot\u00f3n se nos debe mostrar un mensaje de alerta para confirmar nuestra decisi\u00f3n. Como este es un mensaje que vamos a reutilizar en el resto de la aplicaci\u00f3n vamos a crear un componente en la carpeta src/components llamado ConfirmDialog.tsx con el siguiente contenido:

    import Button from \"@mui/material/Button\";\nimport DialogContentText from \"@mui/material/DialogContentText\";\nimport Dialog from \"@mui/material/Dialog\";\nimport DialogActions from \"@mui/material/DialogActions\";\nimport DialogContent from \"@mui/material/DialogContent\";\nimport DialogTitle from \"@mui/material/DialogTitle\";\n\ninterface Props {\n  closeModal: () => void;\n  confirm: () => void;\n  title: string;\n  text: string;\n}\n\nexport const ConfirmDialog = (props: Props) => {\n  return (\n    <div>\n      <Dialog open={true} onClose={props.closeModal}>\n        <DialogTitle>{props.title}</DialogTitle>\n        <DialogContent>\n          <DialogContentText>{props.text}</DialogContentText>\n        </DialogContent>\n        <DialogActions>\n          <Button onClick={props.closeModal}>Cancelar</Button>\n          <Button onClick={() => props.confirm()}>Confirmar</Button>\n        </DialogActions>\n      </Dialog>\n    </div>\n  );\n};\n

    Y vamos a a\u00f1adirlo a nuestra p\u00e1gina de categorias, pero al igual que paso con nuestro formulario de altas no queremos que este componente se muestre siempre, sino que estar\u00e1 condicionado al valor de una nueva variable en nuestro estado. En este caso vamos a almacenar el id de la categor\u00eda a borrar.

    Importamos nuestro nuevo componente:

    import { ConfirmDialog } from \"../../components/ConfirmDialog\";\n

    Creamos una nueva variable en el estado:

    const [idToDelete, setIdToDelete] = useState(\"\");\n

    Creamos una nueva funci\u00f3n:

    const deleteCategory = () => {};\n

    Modificamos el bot\u00f3n de borrado:

                          <IconButton\n                        aria-label=\"delete\"\n                        color=\"error\"\n                        onClick={() => {\n                          setIdToDelete(category.id);\n                        }}\n                      >\n                        <ClearIcon />\n                      </IconButton>\n

    Y a\u00f1adimos el c\u00f3digo necesario en nuestro return para incluir el nuevo componente:

          {!!idToDelete && (\n        <ConfirmDialog\n          title=\"Eliminar categor\u00eda\"\n          text=\"Atenci\u00f3n si borra la categor\u00eda se perder\u00e1n sus datos. \u00bfDesea eliminar la categor\u00eda?\"\n          confirm={deleteCategory}\n          closeModal={() => setIdToDelete('')}\n        />\n      )}\n
    "},{"location":"develop/basic/react/#recuperando-datos","title":"Recuperando datos","text":"

    Ya estamos preparados para llamar a nuestro back. Hay muchas maneras de recuperar datos del back en React. Si no queremos usar ninguna librer\u00eda externa podemos hacer uso del m\u00e9todo fetch, pero tendr\u00edamos que repetir mucho c\u00f3digo o bien construir interceptores, para el manejo de errores, construcci\u00f3n de middlewares,\u2026 adem\u00e1s, no es lo mas utilizado. Hoy en d\u00eda se opta por librer\u00edas como Axios o Redux Toolkit query que facilitan el uso de este m\u00e9todo.

    Nosotros vamos a utilizar una herramienta de redux llamada Redux Toolkit Query, pero primero vamos a explicar que es redux.

    "},{"location":"develop/basic/react/#redux","title":"Redux","text":"

    Redux es una librer\u00eda que implementa el patr\u00f3n de dise\u00f1o Flux y que nos permite crear un estado global.

    Nuestros componentes pueden realizar acciones asociadas a un reducer que modificar\u00e1n este estado global llamado generalmente store y a su vez estar\u00e1n subscritos a variables de este estado para estar atentos a posibles cambios.

    Antes se sol\u00edan construir ficheros de actions, de reducers y un fichero de store, pero con redux toolkit se ha simplificado todo. Por un lado, podemos tener slices, que son ficheros que agrupan acciones, reducers y parte del estado y por otro lado podemos tener servicios donde declaramos llamadas a nuestra api y redux guarda las llamadas en nuestro estado global para que sean accesibles desde cualquier parte de nuestra aplicaci\u00f3n.

    Vamos a crear una carpeta llamada redux dentro de la carpeta src y a su vez dentro de src/redux vamos a crear dos carpetas: features donde crearemos nuestros slices y services donde crearemos las llamadas al api.

    Dentro de la carpeta services vamos a crear un fichero llamado ludotecaApi.ts con el siguiente contenido:

    import { createApi, fetchBaseQuery } from \"@reduxjs/toolkit/query/react\";\nimport { Category } from \"../../types/Category\";\n\nexport const ludotecaAPI = createApi({\n  reducerPath: \"ludotecaApi\",\n  baseQuery: fetchBaseQuery({\n    baseUrl: \"http://localhost:8080\",\n  }),\n  tagTypes: [\"Category\"],\n  endpoints: (builder) => ({\n    getCategories: builder.query<Category[], null>({\n      query: () => \"category\",\n      providesTags: [\"Category\"],\n    }),\n    createCategory: builder.mutation({\n      query: (payload) => ({\n        url: \"/category\",\n        method: \"PUT\",\n        body: payload,\n        headers: {\n          \"Content-type\": \"application/json; charset=UTF-8\",\n        },\n      }),\n      invalidatesTags: [\"Category\"],\n    }),\n    deleteCategory: builder.mutation({\n      query: (id: string) => ({\n        url: `/category/${id}`,\n        method: \"DELETE\",\n      }),\n      invalidatesTags: [\"Category\"],\n    }),\n    updateCategory: builder.mutation({\n      query: (payload: Category) => ({\n        url: `category/${payload.id}`,\n        method: \"PUT\",\n        body: payload,\n      }),\n      invalidatesTags: [\"Category\"],\n    }),\n  }),\n});\n\nexport const {\n    useGetCategoriesQuery,\n    useCreateCategoryMutation,\n    useDeleteCategoryMutation,\n    useUpdateCategoryMutation\n} = ludotecaAPI;\n

    Con esto ya habr\u00edamos creado las acciones que llaman al back y almacenan el resultado en nuestro estado. Para configurar nuestra api le tenemos que dar un nombre, una url base, una series de tags y nuestros endpoints que pueden ser de tipo query para realizar consultas o mutation. Tambi\u00e9n exportamos los hooks que nos van a permitir hacer uso de estos endpoints. Si los endpoints los creamos de tipo query, cuando hacemos uso de estos hooks se realizar\u00e1 una consulta al back y recibiremos los datos de la consulta en nuestros par\u00e1metros del hook entre otras cosas. Si los creamos de tipo mutation lo que nos devolver\u00e1 el hook ser\u00e1 la acci\u00f3n que tenemos que llamar para realizar esta llamada.

    Los tags sirven para cachear el resultado, pero cuando llamamos a una mutation y pasamos informaci\u00f3n en invalidateTags, esto va a hacer que se vuelva a lanzar la query afectada por estos tags para actualizar su resultado, por eso hemos a\u00f1adido el providesTags en la query, para que desde nuestras p\u00e1ginas usemos los hooks exportados.

    Ahora vamos a crear dentro de la carpeta src/redux un fichero llamado store.ts con el siguiente contenido:

    import { configureStore } from \"@reduxjs/toolkit\";\nimport { setupListeners } from \"@reduxjs/toolkit/dist/query\";\nimport { ludotecaAPI } from \"./services/ludotecaApi\";\n\nexport const store = configureStore({\n  reducer: {\n    [ludotecaAPI.reducerPath]: ludotecaAPI.reducer,\n  },\n  middleware: (getDefaultMiddleware) =>\n    getDefaultMiddleware().concat([ludotecaAPI.middleware]),\n});\n\nsetupListeners(store.dispatch);\n\n// Infer the `RootState` and `AppDispatch` types from the store itself\nexport type RootState = ReturnType<typeof store.getState>;\n// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}\nexport type AppDispatch = typeof store.dispatch;\n

    Aqu\u00ed b\u00e1sicamente creamos el store con nuestro reducer. Cabe destacar que podemos crear tantos reducers como queramos siempre que les demos distintos nombres.

    Ahora ya podr\u00edamos hacer uso de los hooks que vienen con redux llamados useDispatch para llamar a nuestras actions y useSelect para suscribirnos a los cambios en el estado, pero como estamos usando typescript tendr\u00edamos que tipar todos estos m\u00e9todos y variables que usamos en todos nuestros componentes resultando un c\u00f3digo un tanto sucio y repetitivo. Tambi\u00e9n podemos simplemente ignorar a typescript y deshabilitar las reglas para estos ficheros, pero vamos a hacerlo bien.

    Vamos a crear un fichero llamado hooks.ts dentro de la carpeta de redux y su contenido ser\u00e1 el siguiente:

    import {  useDispatch, useSelector } from 'react-redux'\nimport type { TypedUseSelectorHook } from 'react-redux'\nimport type { RootState, AppDispatch } from './store'\n\n// Use throughout your app instead of plain `useDispatch` and `useSelector`\ntype DispatchFunc = () => AppDispatch\nexport const useAppDispatch: DispatchFunc = useDispatch\nexport const useAppSelector: TypedUseSelectorHook<RootState> = useSelector\n

    Estos ser\u00e1n los m\u00e9todos que usaremos en lugar de useDispatch y useSelector.

    Ahora vamos a modificar nuestro fichero App.tsx a\u00f1adiendo los imports necesarios y rodeando nuestro c\u00f3digo con el tag provider:

    import { Provider } from \"react-redux\";\n\nimport { store } from \"./redux/store\";\n\n    <Provider store={store}>\n      <BrowserRouter>\n        <Routes>\n          <Route element={<Layout />}>\n            <Route index path=\"games\" element={<Game />} />\n            <Route path=\"categories\" element={<Category />} />\n            <Route path=\"authors\" element={<Author />} />\n            <Route path=\"*\" element={<Navigate to=\"/games\" />} />\n          </Route>\n        </Routes>\n      </BrowserRouter>\n    </Provider>\n

    Ahora ya podemos hacer uso de los m\u00e9todos de redux para modificar y leer el estado global de nuestra aplicaci\u00f3n.

    Volvemos a nuestro componente Category y vamos a importar los hooks de nuestra api para hacer uso de ellos:

    import { useAppDispatch } from \"../../redux/hooks\";\nimport {\n  useCreateCategoryMutation,\n  useDeleteCategoryMutation,\n  useGetCategoriesQuery,\n  useUpdateCategoryMutation,\n} from \"../../redux/services/ludotecaApi\";\n

    Eliminamos la variable mockeada data y a\u00f1adimos en su lugar lo siguiente:

    const dispatch = useAppDispatch();\n  const { data, error, isLoading } = useGetCategoriesQuery(null);\n\n  const [\n    deleteCategoryApi,\n    { isLoading: isLoadingDelete, error: errorDelete },\n  ] = useDeleteCategoryMutation();\n  const [createCategoryApi, { isLoading: isLoadingCreate }] =\n    useCreateCategoryMutation();\n\n  const [updateCategoryApi, { isLoading: isLoadingUpdate }] =\n    useUpdateCategoryMutation();\n

    Como ya hemos dicho anteriormente, los hooks de la api de tipo query nos devolver\u00e1n datos mientras que los hooks de tipo mutation nos devuelven acciones que podemos lanzar con el m\u00e9todo dispatch. El resto de los par\u00e1metros nos dan informaci\u00f3n para saber el estado de la llamada, por ejemplo, para saber si esta cargando, si se ha producido un error, etc\u2026

    Tenemos que modificar el c\u00f3digo que recorre data ya que este valor ahora puede estar sin definir:

            <TableBody>\n            {data &&\n              data.map((category: CategoryModel) => (\n

    Y ahora si tenemos datos en la base de datos y vamos a nuestro navegador podemos ver que ya se est\u00e1n representando estos datos en la tabla de categor\u00edas.

    Modificamos el m\u00e9todo createCategory:

    const createCategory = (category: string) => {\n    setOpenCreate(false);\n    if (categoryToUpdate) {\n      updateCategoryApi({ id: categoryToUpdate.id, name: category })\n        .then(() => {\n          setCategoryToUpdate(null);\n        })\n        .catch((err) => console.log(err));\n    } else {\n      createCategoryApi({ name: category })\n        .then(() => {\n          setCategoryToUpdate(null);\n        })\n        .catch((err) => console.log(err));\n    }\n  };\n

    Si tenemos almacenada alguna categor\u00eda para actualizar llamaremos a la acci\u00f3n para actualizar la categor\u00eda que recuperamos del hook y si no tenemos categor\u00eda almacenada llamaremos al m\u00e9todo para crear una categor\u00eda nueva. Estos m\u00e9todos nos devuelven una promesa que cuando resolvemos volvemos a poner el valor de la categor\u00eda a actualizar a null.

    Implementamos el m\u00e9todo para borrar categor\u00edas:

    const deleteCategory = () => {\n    deleteCategoryApi(idToDelete)\n      .then(() => \n      setIdToDelete(''))\n      .catch((err) => console.log(err));\n  };\n

    Ahora si probamos en nuestro navegador ya podremos realizar todas las funciones de la p\u00e1gina: listar, crear, actualizar y borrar, pero aun vamos a darle m\u00e1s funcionalidad.

    Vamos a crear una variable en el estado global de nuestra aplicaci\u00f3n para mostrar alertas de informaci\u00f3n o de error. Para ello creamos un nuevo fichero en la carpeta src/redux/features llamado messageSlice.ts cuyo contenido ser\u00e1 el siguiente:

    import { createSlice } from '@reduxjs/toolkit'\nimport type {PayloadAction} from \"@reduxjs/toolkit\"\n\nexport const messageSlice = createSlice({\n  name: 'message',\n  initialState: {\n    text: '',\n    type: ''\n  },\n  reducers: {\n    deleteMessage: (state) => {\n        state.text = ''\n        state.type = ''\n    },\n    setMessage: (state, action : PayloadAction<{text: string; type: string}>) => {\n        state.text = action.payload.text;\n        state.type = action.payload.type;\n    },\n  },\n})\n\nexport const { deleteMessage, setMessage } = messageSlice.actions;\nexport default messageSlice.reducer;\n

    Como ya hemos dicho anteriormente los slices son un concepto introducido en Redux Toolkit y no es ni m\u00e1s ni menos que un fichero que agrupa reducers, actions y selectors.

    En este fichero declaramos el nombre del selector (message) para despu\u00e9s poder recuperar los datos en un componente, declaramos el estado inicial de nuestro slice, creamos las funciones de los reducers y declaramos dos acciones.

    Los reducers son funciones puras que modifican el estado, en nuestro caso utilizamos un reducer para resetear el estado y otro para setear el texto y el tipo de mensaje. Con Redux Toolkit podemos acceder directamente al estado dentro de nuestros reducers. En los reducers que no usan esta herramienta lo que se hace es devolver un objeto que ser\u00e1 el nuevo estado.

    Las acciones son las que invocan a los reducers. Estas solo dicen que hacer, pero no como hacerlo. Con Redux Toolkit las acciones se generan autom\u00e1ticamente y solo tenemos que hacer un destructuring del objecto actions de nuestro slice para recuperarlas y exportarlas.

    Ahora vamos a modificar el fichero src/redux/store.ts para a\u00f1adir el nuevo reducer:

    import { configureStore } from \"@reduxjs/toolkit\";\nimport { setupListeners } from \"@reduxjs/toolkit/dist/query\";\nimport { ludotecaAPI } from \"./services/ludotecaApi\";\nimport messageReducer from \"./features/messageSlice\";\n\nexport const store = configureStore({\n  reducer: {\n    messageReducer,\n    [ludotecaAPI.reducerPath]: ludotecaAPI.reducer,\n  },\n  middleware: (getDefaultMiddleware) =>\n    getDefaultMiddleware().concat([ludotecaAPI.middleware]),\n});\n\nsetupListeners(store.dispatch);\n\n// Infer the `RootState` and `AppDispatch` types from the store itself\nexport type RootState = ReturnType<typeof store.getState>;\n// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}\nexport type AppDispatch = typeof store.dispatch;\n

    Con esto ya podemos hacer uso de esta funcionalidad. Vamos a modificar el componente Layout para que pueda recibir mensajes y mostrarlos por pantalla.

    import Alert from \"@mui/material/Alert\";\nimport { useAppDispatch, useAppSelector } from \"../redux/hooks\";\nimport { deleteMessage } from \"../redux/features/messageSlice\";\n\n  const dispatch = useAppDispatch();\n  const { text, type } = useAppSelector((state) => state.messageReducer);\n\n  useEffect(() => {\n    setTimeout(() => {\n      dispatch(deleteMessage());\n    }, 3000);\n  }, [text, type]);\n\n      {text && (\n        <Alert severity={type === \"error\" ? \"error\" : \"success\"}>{text}</Alert>\n      )}\n

    Hemos a\u00f1adido c\u00f3digo para que el componente layout este subscrito a las variables text y type de nuestro contexto global. Si tenemos text se mostrar\u00e1 la alerta y adem\u00e1s hemos incluido un nuevo hook useEffect gracias al cual cuando el componente reciba un text llamar\u00e1 a una funci\u00f3n que pasados 3 segundos borrar\u00e1 el mensaje de nuestro estado ocultando as\u00ed el Alert.

    Pero antes de seguir adelante vamos a explicar que hace useEffect exactamente ya que es un hook de React muy utilizado.

    "},{"location":"develop/basic/react/#useeffect","title":"useEffect","text":"

    El ciclo de vida de los componentes en React permit\u00eda en los componentes de tipo clase poder ejecutar c\u00f3digo en diferentes fases de montaje, actualizaci\u00f3n y desmontaje. De esta forma, pod\u00edamos a\u00f1adir cierta funcionalidad en las distintas etapas de nuestro componente.

    Con los hooks tambi\u00e9n podremos acceder a ese ciclo de vida en nuestros componentes funcionales, aunque de una forma m\u00e1s clara y sencilla. Para ello usaremos useEffect, un hook que recibe como par\u00e1metro una funci\u00f3n que se ejecutar\u00e1 cada vez que se modifique el valor de las las dependencias que pasemos como segundo par\u00e1metro.

    Hay otros casos especiales de useEffect, por ejemplo, si hubi\u00e9semos dejado el array de dependencias de useEffect vac\u00edo, solo se llamar\u00eda a la funci\u00f3n la primera vez que se renderiza el componente.

    useEffect(() => {\nconsole.log(\u2018Solo me muestro en el primer render\u2019);\n  }, []);\n

    Y si queremos que solo se llame a la funci\u00f3n cuando se desmonta el componente lo que tenemos que hacer es devolver de useEffect una funci\u00f3n con el c\u00f3digo que queremos que se ejecute una vez que se desmonte:

    useEffect(() => {\nreturn () => {\n        console.log(\u2018Me desmonto!!\u2019)\n}\n  }, []);\n

    Dentro de la carpeta src/types vamos a crear un fichero llamado appTypes.ts que contendr\u00e1 todos aquellos tipos o interfaces auxiliares para construir nuestra aplicaci\u00f3n:

    export interface BackError {\n  msg: string;\n}\n

    Ahora ya podemos incluir en nuestra p\u00e1gina de categor\u00edas el c\u00f3digo para guardar los mensajes de informaci\u00f3n y error en el estado global, importamos lo necesario:

    import { setMessage } from \"../../redux/features/messageSlice\";\nimport { BackError } from \"../../types/appTypes\";\n

    A\u00f1adimos:

    useEffect(() => {\n    if (errorDelete) {\n      if (\"status\" in errorDelete) {\n        dispatch(\n          setMessage({\n            text: (errorDelete?.data as BackError).msg,\n            type: \"error\",\n          })\n        );\n      }\n    }\n  }, [errorDelete, dispatch]);\n\n  useEffect(() => {\n    if (error) {\n      dispatch(setMessage({ text: \"Se ha producido un error\", type: \"error\" }));\n    }\n  }, [error]);\n

    Y modificamos:

    const createCategory = (category: string) => {\n    setOpenCreate(false);\n    if (categoryToUpdate) {\n      updateCategoryApi({ id: categoryToUpdate.id, name: category })\n        .then(() => {\n          dispatch(\n            setMessage({\n              text: \"Categor\u00eda actualizada correctamente\",\n              type: \"ok\",\n            })\n          );\n          setCategoryToUpdate(null);\n        })\n        .catch((err) => console.log(err));\n    } else {\n      createCategoryApi({ name: category })\n        .then(() => {\n          dispatch(\n            setMessage({ text: \"Categor\u00eda creada correctamente\", type: \"ok\" })\n          );\n          setCategoryToUpdate(null);\n        })\n        .catch((err) => console.log(err));\n    }\n  };\n\n  const deleteCategory = () => {\n    deleteCategoryApi(idToDelete)\n      .then(() => {\n        dispatch(\n          setMessage({\n            text: \"Categor\u00eda borrada correctamente\",\n            type: \"ok\",\n          })\n        );\n        setIdToDelete(\"\");\n      })\n      .catch((err) => console.log(err));\n  };\n

    Si ahora probamos nuestra aplicaci\u00f3n al borrar, actualizar o crear una categor\u00eda nos deber\u00eda de mostrar un mensaje de informaci\u00f3n.

    Ya casi estamos terminando con nuestra p\u00e1gina de categor\u00edas, pero vamos a a\u00f1adir tambi\u00e9n un loader para cuando nuestra acciones est\u00e9n en estado de loading. Para esto vamos a hacer uso de otra de las maneras que tiene React de almacenar informaci\u00f3n global, el contexto.

    "},{"location":"develop/basic/react/#context-api","title":"Context API","text":"

    Una de las caracter\u00edsticas que llegaron en las \u00faltimas versiones de React fue el contexto, una forma de pasar datos que pueden considerarse globales a un \u00e1rbol de componentes sin la necesidad de utilizar Redux. El uso de contextos mediante la Context API es una soluci\u00f3n m\u00e1s ligera y sencilla que redux y que no est\u00e1 mal para aplicaciones que no son excesivamente grandes.

    En general cuando queramos usar estados globales que no sean demasiado grandes y no se haga demasiada escritura sobre ellos ser\u00e1 preferible usar Context API en lugar de redux.

    Vamos a crear un contexto para utilizar un loader en nuestra aplicaci\u00f3n.

    Lo primero ser\u00e1 crear una carpeta llamada context dentro de la carpeta src de nuestro proyecto y dentro de esta crearemos un nuevo fichero llamado LoaderProvider.tsx con el siguiente contenido:

    import { createContext, useState } from \"react\";\nimport Backdrop from \"@mui/material/Backdrop\";\nimport CircularProgress from \"@mui/material/CircularProgress\";\n\nexport const LoaderContext = createContext({\n  loading: false,\n  showLoading: (_show: boolean) => {},\n});\n\ntype Props = {\n  children: JSX.Element;\n};\n\nexport const LoaderProvider = ({ children }: Props) => {\n  const showLoading = (show: boolean) => {\n    setState((prev) => ({\n      ...prev,\n      loading: show,\n    }));\n  };\n\n  const [state, setState] = useState({\n    loading: false,\n    showLoading,\n  });\n\n  return (\n    <LoaderContext.Provider value={state}>\n      <Backdrop\n        sx={{ color: \"#fff\", zIndex: (theme) => theme.zIndex.drawer + 1 }}\n        open={state.loading}\n      >\n        <CircularProgress color=\"inherit\" />\n      </Backdrop>\n\n      {children}\n    </LoaderContext.Provider>\n  );\n};\n

    Y ahora modificamos nuestro fichero App.tsx de la siguiente manera:

    import { LoaderProvider } from \"./context/LoaderProvider\";\n    <LoaderProvider>\n      <Provider store={store}>\n        <BrowserRouter>\n          <Routes>\n            <Route element={<Layout />}>\n              <Route index path=\"games\" element={<Game />} />\n              <Route path=\"categories\" element={<Category />} />\n              <Route path=\"authors\" element={<Author />} />\n              <Route path=\"*\" element={<Navigate to=\"/games\" />} />\n            </Route>\n          </Routes>\n        </BrowserRouter>\n      </Provider>\n    </LoaderProvider>\n

    Lo que hemos hecho ha sido envolver toda nuestra aplicaci\u00f3n dentro de nuestro provider de tal modo que esta el children en el fichero LoaderProvider, pero ahora y gracias a la funcionalidad de createContext la variable loading y el m\u00e9todo showLoading estar\u00e1n disponibles en todos los sitios de nuestra aplicaci\u00f3n.

    Ahora para hacer uso de esta funcionalidad nos vamos a nuestra pagina de Categorias e importamos lo siguiente:

    import { useState, useEffect, useContext  } from \"react\";\n\nimport { LoaderContext } from \"../../context/LoaderProvider\";\n

    Declaramos una nueva constante:

    const loader = useContext(LoaderContext);\n

    Podemos hace uso del m\u00e9todo showLoading donde queramos, en nuestro caso vamos a crear otro useEffect que estar\u00e1 pendiente de los cambios en cualquiera de los loadings:

    useEffect(() => {\n    loader.showLoading(\n      isLoadingCreate || isLoading || isLoadingDelete || isLoadingUpdate\n    );\n  }, [isLoadingCreate, isLoading, isLoadingDelete, isLoadingUpdate]);\n

    Probamos la aplicaci\u00f3n y vemos que cuando se carga el listado o realizamos cualquier llamada al back se muestra brevemente nuestro loader.

    "},{"location":"develop/basic/springboot/","title":"Listado simple - Spring Boot","text":"

    Ahora que ya tenemos listo el proyecto backend de Spring Boot (en el puerto 8080) ya podemos empezar a codificar la soluci\u00f3n.

    "},{"location":"develop/basic/springboot/#primeros-pasos","title":"Primeros pasos","text":"

    Antes de empezar

    Quiero hacer hincapi\u00e9 en Spring Boot tiene una documentaci\u00f3n muy extensa y completa, as\u00ed que te recomiendo que hagas uso de ella cuando tengas cualquier duda. Tanto la propia web de Spring como en el portal de tutoriales de Baeldung puedes buscar casi cualquier ejemplo que necesites.

    "},{"location":"develop/basic/springboot/#estructurar-el-codigo","title":"Estructurar el c\u00f3digo","text":"

    Vamos a hacer un breve refresco de la estructura del c\u00f3digo que ya se ha visto en puntos anteriores.

    Las clases deben estar agrupadas por \u00e1mbito funcional, en nuestro caso como vamos a hacer la funcionalidad de Categor\u00edas pues deber\u00eda estar todo dentro de un package del tipo com.ccsw.tutorial.category.

    Adem\u00e1s, deber\u00edamos aplicar la separaci\u00f3n por capas como ya se vi\u00f3 en el esquema:

    La primera capa, la de Controlador, se encargar\u00e1 de procesar las peticiones y transformar datos. Esta capa llamar\u00e1 a la capa de L\u00f3gica de negocio que ejecutar\u00e1 las operaciones, ayud\u00e1ndose de otros objetos de esa misma capa de L\u00f3gica o bien de llamadas a datos a trav\u00e9s de la capa de Acceso a Datos

    Ahora s\u00ed, vamos a programar!.

    "},{"location":"develop/basic/springboot/#capa-de-operaciones-controller","title":"Capa de operaciones: Controller","text":"

    En esta capa es donde se definen las operaciones que pueden ser consumidas por los clientes. Se caracterizan por estar anotadas con las anotaciones @Controller o @RestController y por las anotaciones @RequestMapping que nos permiten definir las rutas de acceso.

    Recomendaci\u00f3n: Breve detalle REST

    Antes de continuar te recomiendo encarecidamente que leas el Anexo: Detalle REST donde se explica brevemente como estructurar los servicios REST que veremos a continuaci\u00f3n.

    "},{"location":"develop/basic/springboot/#controller-de-ejemplo","title":"Controller de ejemplo","text":"

    Vamos a crear una clase CategoryController.java dentro del package com.ccsw.tutorial.category para definir las rutas de las operaciones.

    CategoryController.java
    package com.ccsw.tutorial.category;\n\nimport java.util.List;\n\nimport org.springframework.web.bind.annotation.CrossOrigin;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestMethod;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\n/**\n * @author ccsw\n * \n */\n@Tag(name = \"Category\", description = \"API of Category\")\n@RequestMapping(value = \"/category\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class CategoryController {\n\n    /**\n     * M\u00e9todo para probar el servicio\n     * \n     */\n    @RequestMapping(path = \"\", method = RequestMethod.GET)\n    public String prueba() {\n\n        return \"Probando el Controller\";\n    }\n\n}\n

    Ahora si arrancamos la aplicaci\u00f3n server, abrimos el Postman y creamos una petici\u00f3n GET a la url http://localhost:8080/category nos responder\u00e1 con el mensaje que hemos programado.

    "},{"location":"develop/basic/springboot/#implementar-operaciones","title":"Implementar operaciones","text":"

    Ahora que ya tenemos un controlador y una operaci\u00f3n de negocio ficticia, vamos a borrarla y a\u00f1adir las operaciones reales que consumir\u00e1 nuestra pantalla. Deberemos a\u00f1adir una operaci\u00f3n para listar, una para actualizar, una para guardar y una para borrar. Aunque para hacerlo m\u00e1s c\u00f3modo, utilizaremos la misma operaci\u00f3n para guardar y para actualizar. Adem\u00e1s, no vamos a trabajar directamente con datos simples, sino que usaremos objetos para recibir informaci\u00f3n y para enviar informaci\u00f3n.

    Estos objetos t\u00edpicamente se denominan DTO (Data Transfer Object) y nos sirven justamente para encapsular informaci\u00f3n que queremos transportar. En realidad no son m\u00e1s que clases pojo sencillas con propiedades, getters y setters.

    Para nuestro ejemplo crearemos una clase CategoryDto dentro del package com.ccsw.tutorial.category.model con el siguiente contenido:

    CategoryDto.java
    package com.ccsw.tutorial.category.model;\n\n/**\n * @author ccsw\n * \n */\npublic class CategoryDto {\n\n    private Long id;\n\n    private String name;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return name\n     */\n    public String getName() {\n\n        return this.name;\n    }\n\n    /**\n     * @param name new value of {@link #getName}.\n     */\n    public void setName(String name) {\n\n        this.name = name;\n    }\n\n}\n

    A continuaci\u00f3n utilizaremos esta clase en nuestro Controller para implementar las tres operaciones de negocio.

    CategoryController.java
    package com.ccsw.tutorial.category;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport org.springframework.web.bind.annotation.CrossOrigin;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestMethod;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport com.ccsw.tutorial.category.model.CategoryDto;\n\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\n/**\n * @author ccsw\n * \n */\n@Tag(name = \"Category\", description = \"API of Category\")\n@RequestMapping(value = \"/category\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class CategoryController {\n\n    private long SEQUENCE = 1;\n    private Map<Long, CategoryDto> categories = new HashMap<Long, CategoryDto>();\n\n    /**\n     * M\u00e9todo para recuperar todas las categorias\n     *\n     * @return {@link List} de {@link CategoryDto}\n     */\n    @Operation(summary = \"Find\", description = \"Method that return a list of Categories\")\n    @RequestMapping(path = \"\", method = RequestMethod.GET)\n    public List<CategoryDto> findAll() {\n\n        return new ArrayList<CategoryDto>(this.categories.values());\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar una categoria\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Category\")\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody CategoryDto dto) {\n\n        CategoryDto category;\n\n        if (id == null) {\n            category = new CategoryDto();\n            category.setId(this.SEQUENCE++);\n            this.categories.put(category.getId(), category);\n        } else {\n            category = this.categories.get(id);\n        }\n\n        category.setName(dto.getName());\n    }\n\n    /**\n     * M\u00e9todo para borrar una categoria\n     *\n     * @param id PK de la entidad\n     */\n    @Operation(summary = \"Delete\", description = \"Method that deletes a Category\")\n    @RequestMapping(path = \"/{id}\", method = RequestMethod.DELETE)\n    public void delete(@PathVariable(\"id\") Long id) {\n\n        this.categories.remove(id);\n    }\n}\n

    Como todav\u00eda no tenemos acceso a BD, hemos creado una variable tipo HashMap y una variable Long, que simular\u00e1n una BD y una secuencia. Tambi\u00e9n hemos implementado tres operaciones GET, PUT y DELETE que realizan las acciones necesarias por nuestra pantalla. Ahora podr\u00edamos probarlo desde el Postman con cuatro ejemplo sencillos.

    F\u00edjate que el m\u00e9todo save tiene dos rutas. La ruta normal category/ y la ruta informada category/1. Esto es porque hemos juntado la acci\u00f3n create y update en un mismo m\u00e9todo para facilitar el desarrollo. Es totalmente v\u00e1lido y funcional.

    Atenci\u00f3n

    Los datos que se reciben pueden venir informados como un par\u00e1metro en la URL Get, como una variable en el propio path o dentro del body de la petici\u00f3n. Cada uno de ellos se recupera con una anotaci\u00f3n especial: @RequestParam, @PathVariable y @RequestBody respectivamente.

    Como no tenemos ning\u00fan dato dado de alta, podemos probar en primer lugar a realizar una inserci\u00f3n de datos con el m\u00e9todo PUT.

    PUT /category nos sirve para insertar Categor\u00edas nuevas (si no tienen el id informado) o para actualizar Categor\u00edas (si tienen el id informado). F\u00edjate que los datos que se env\u00edan est\u00e1n en el body como formato JSON (parte izquierda de la imagen). Si no env\u00edas datos, te dar\u00e1 un error.

    GET /category nos devuelve un listado de Categor\u00edas, siempre que hayamos insertado algo antes.

    DELETE /category nos sirve eliminar Categor\u00edas. F\u00edjate que el dato del ID que se env\u00eda est\u00e1 en el path.

    Prueba a jugar borrando categor\u00edas que no existen o modificando categor\u00edas que no existen. Tal y como est\u00e1 programado, el borrado no dar\u00e1 error, pero la modificaci\u00f3n deber\u00eda dar un NullPointerException al no existir el dato a modificar.

    "},{"location":"develop/basic/springboot/#documentacion-openapi","title":"Documentaci\u00f3n (OpenAPI)","text":"

    Si te acuerdas, en el punto de Entorno de desarrollo, a\u00f1adimos el m\u00f3dulo de OpenAPI a nuestro proyecto, y en el desarrollo de nuestro Controller hemos anotado tanto la clase como los m\u00e9todos con sus correspondientes etiquetas @Tag y @Operation.

    Esto nos va a ayudar a generar documentaci\u00f3n autom\u00e1tica de nuestras APIs haciendo que nuestro c\u00f3digo sea m\u00e1s mantenible y nuestra documentaci\u00f3n mucho m\u00e1s fiable.

    Para ver el resultado, con el proyecto arrancado nos dirigimos a la ruta por defecto de OpenAPI: http://localhost:8080/swagger-ui/index.html

    Aqu\u00ed podemos observar el cat\u00e1logo de endpoints generados, ver los tipos de entrada y salida e incluso realizar peticiones a los mismos. Este ser\u00e1 el contrato de nuestros endpoints, que nos ayudar\u00e1 a integrarnos con el equipo frontend (en el caso del tutorial seguramente seremos nosotros mismos).

    "},{"location":"develop/basic/springboot/#aspectos-importantes","title":"Aspectos importantes","text":"

    Los aspectos importantes de la capa Controller son:

    • La clase debe estar anotada con @Controller o @RestController. Mejor usar la \u00faltima anotaci\u00f3n, ya que est\u00e1s diciendo que las operaciones son de tipo Rest y no har\u00e1 falta configurar nada
    • La ruta general al controlador se define con el @RequestMapping global de la clase, aunque tambi\u00e9n se puede obviar esta anotaci\u00f3n y a\u00f1adir a cada una de las operaciones la ruta ra\u00edz.
    • Los m\u00e9todos que queramos exponer como operaciones deben ir anotados tambi\u00e9n con @RequestMapping con la info:
      • path \u2192 Que nos permite definir un path para la operaci\u00f3n, siempre sum\u00e1ndole el path de la clase (si es que tuviera)
      • method \u2192 Que nos permite definir el verbo de http que vamos a atender. Podemos tener el mismo path con diferente method, sin problema. Por lo general utilizaremos:
        • GET \u2192 Generalmente se usa para recuperar informaci\u00f3n
        • POST \u2192 Se utiliza para hacer update y filtrados complejos de informaci\u00f3n
        • PUT \u2192 Se utiliza para hacer save de informaci\u00f3n
        • DELETE \u2192 Se utiliza para hacer borrados de informaci\u00f3n
    "},{"location":"develop/basic/springboot/#capa-de-servicio-service","title":"Capa de Servicio: Service","text":"

    Pero en realidad la cosa no funciona as\u00ed. Hemos implementado parte de la l\u00f3gica de negocio (las operaciones/acciones de guardado, borrado y listado) dentro de lo que ser\u00eda la capa de operaciones o servicios al cliente. Esta capa no debe ejecutar l\u00f3gica de negocio, tan solo debe hacer transformaciones de datos y enrutar peticiones, toda la l\u00f3gica deber\u00eda ir en la capa de servicio.

    "},{"location":"develop/basic/springboot/#implementar-servicios","title":"Implementar servicios","text":"

    Pues vamos a arreglarlo. Vamos a crear un servicio y vamos a mover la l\u00f3gica de negocio al servicio.

    CategoryService.javaCategoryServiceImpl.javaCategoryController.java
    package com.ccsw.tutorial.category;\n\nimport com.ccsw.tutorial.category.model.CategoryDto;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n * \n */\npublic interface CategoryService {\n\n    /**\n     * M\u00e9todo para recuperar todas las categor\u00edas\n     *\n     * @return {@link List} de {@link Category}\n     */\n    List<CategoryDto> findAll();\n\n    /**\n     * M\u00e9todo para crear o actualizar una categor\u00eda\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    void save(Long id, CategoryDto dto);\n\n    /**\n     * M\u00e9todo para borrar una categor\u00eda\n     *\n     * @param id PK de la entidad\n     */\n    void delete(Long id);\n\n}\n
    package com.ccsw.tutorial.category;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport org.springframework.stereotype.Service;\n\nimport com.ccsw.tutorial.category.model.CategoryDto;\n\n/**\n * @author ccsw\n *\n */\n@Service\npublic class CategoryServiceImpl implements CategoryService {\n\n    private long SEQUENCE = 1;\n    private Map<Long, CategoryDto> categories = new HashMap<Long, CategoryDto>();\n\n    /**\n     * {@inheritDoc}\n     */\n    public List<CategoryDto> findAll() {\n\n        return new ArrayList<CategoryDto>(this.categories.values());\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    public void save(Long id, CategoryDto dto) {\n\n        CategoryDto category;\n\n        if (id == null) {\n            category = new CategoryDto();\n            category.setId(this.SEQUENCE++);\n            this.categories.put(category.getId(), category);\n        } else {\n            category = this.categories.get(id);\n        }\n\n        category.setName(dto.getName());\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    public void delete(Long id) {\n\n        this.categories.remove(id);\n    }\n\n}\n
    package com.ccsw.tutorial.category;\n\nimport java.util.List;\n\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.CrossOrigin;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestMethod;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport com.ccsw.tutorial.category.model.CategoryDto;\n\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\n/**\n * @author ccsw\n * \n */\n@Tag(name = \"Category\", description = \"API of Category\")\n@RequestMapping(value = \"/category\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class CategoryController {\n\n    @Autowired\n    private CategoryService categoryService;\n\n    /**\n     * M\u00e9todo para recuperar todas las categorias\n     *\n     * @return {@link List} de {@link CategoryDto}\n     */\n    @Operation(summary = \"Find\", description = \"Method that return a list of Categories\")\n    @RequestMapping(path = \"\", method = RequestMethod.GET)\n    public List<CategoryDto> findAll() {\n\n        return this.categoryService.findAll();\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar una categoria\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Category\")\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody CategoryDto dto) {\n\n        this.categoryService.save(id, dto);\n    }\n\n    /**\n     * M\u00e9todo para borrar una categoria\n     *\n     * @param id PK de la entidad\n     */\n    @Operation(summary = \"Delete\", description = \"Method that deletes a Category\")\n    @RequestMapping(path = \"/{id}\", method = RequestMethod.DELETE)\n    public void delete(@PathVariable(\"id\") Long id) {\n\n        this.categoryService.delete(id);\n    }\n}\n

    Ahora ya tenemos bien estructurado nuestro proyecto. Ya tenemos las dos capas necesarias Controladores y Servicios y cada uno se encarga de llevar a cabo su cometido de forma correcta.

    "},{"location":"develop/basic/springboot/#aspectos-importantes_1","title":"Aspectos importantes","text":"

    Los aspectos importantes de la capa Service son:

    • Toda la l\u00f3gica de negocio, operaciones y dem\u00e1s debe estar implementada en los servicios. Los controladores simplemente invocan servicios y transforman ciertos datos.
    • Es buena pr\u00e1ctica que la capa de servicios se implemente usando el patr\u00f3n fachada, esto quiere decir que necesitamos tener una Interface y al menos una implementaci\u00f3n de esa Interface. Y siempre debemos interactuar con la Interface. Esto nos permitir\u00e1 a futuro poder sustituir la implementaci\u00f3n por otra diferente sin que el resto del c\u00f3digo se vea afectado. Especialmente \u00fatil cuando queremos mockear comportamientos en tests.
    • La capa de servicio puede invocar a otros servicios en sus operaciones, pero nunca debe invocar a un controlador.
    • Para crear un servicio se debe anotar mediante @Service y adem\u00e1s debe implementar la Interface del servicio. Un error muy com\u00fan al arrancar un proyecto y ver que no funcionan las llamadas, es porqu\u00e9 no existe la anotaci\u00f3n @Service o porqu\u00e9 no se ha implementado la Interface.
    • La forma de inyectar y utilizar componentes manejados por Spring Boot es mediante la anotaci\u00f3n @Autowired. NO intentes crear un objeto de CategoryServiceImpl, ni hacer un new, ya que no estar\u00e1 manejado por Springboot y dar\u00e1 fallos de NullPointer. Lo mejor es dejar que Spring Boot lo gestione y utilizar las inyecciones de dependencias.
    "},{"location":"develop/basic/springboot/#capa-de-datos-repository","title":"Capa de Datos: Repository","text":"

    Pero no siempre vamos a acceder a los datos mediante un HasMap en memoria. En algunas ocasiones queremos que nuestro proyecto acceda a un servicio de datos como puede ser una BBDD, un servicio externo, un acceso a disco, etc. Estos accesos se deben hacer desde la capa de acceso a datos, y en concreto para nuestro ejemplo, lo haremos a trav\u00e9s de un Repository para que acceda a una BBDD.

    Para el tutorial no necesitamos configurar una BBDD externa ni complicarnos demasiado. Vamos a utilizar una librer\u00eda muy \u00fatil llamada H2 que nos permite levantar una BBDD en memoria persistiendo los datos en memoria o en disco, de hecho ya la configuramos en el apartado de Entorno de desarrollo.

    "},{"location":"develop/basic/springboot/#implementar-entity","title":"Implementar Entity","text":"

    Lo primero que haremos ser\u00e1 crear nuestra entity con la que vamos a persistir y recuperar informaci\u00f3n. Las entidades igual que los DTOs deber\u00edan estar agrupados dentro del package model de cada funcionalidad, as\u00ed que vamos a crear una nueva clase java.

    Category.java
    package com.ccsw.tutorial.category.model;\n\nimport jakarta.persistence.*;\n\n/**\n * @author ccsw\n * \n */\n@Entity\n@Table(name = \"category\")\npublic class Category {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"id\", nullable = false)\n    private Long id;\n\n    @Column(name = \"name\", nullable = false)\n    private String name;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return name\n     */\n    public String getName() {\n\n        return this.name;\n    }\n\n    /**\n     * @param name new value of {@link #getName}.\n     */\n    public void setName(String name) {\n\n        this.name = name;\n    }\n\n}\n

    Si te fijas, la Entity suele ser muy similar a un DTO, tiene unas propiedades y sus getters y setters. Pero a diferencia de los DTOs, esta clase tiene una serie de anotaciones que permiten a JPA hacer su magia y generar consultas SQL a la BBDD. En este ejemplo vemos 4 anotaciones importantes:

    • @Entity \u2192 Le indica a Springboot que se trata de una clase que implementa una Entidad de BBDD. Sin esta anotaci\u00f3n no es posible hacer queries.
    • @Table \u2192 Le indica a JPA el nombre y el schema de la tabla que representa esta clase. Por claridad se deber\u00eda poner siempre, aunque si el nombre de la tabla es igual al nombre de la clase no es necesaria la anotaci\u00f3n.
    • @Id y @GeneratedValue \u2192 Le indica a JPA que esta propiedad es la que mapea una Primary Key y adem\u00e1s que esta PK se genera con la estrategia que se le indique en la anotaci\u00f3n @GeneratedValue, que puede ser:
      • Generaci\u00f3n de PK por Secuence, la que utiliza Oracle, en este caso habr\u00e1 que indicarle un nombre de secuencia.
      • Generaci\u00f3n de PK por Indentity, la que utiliza MySql o SQLServer, el auto-incremental.
      • Generaci\u00f3n de PK por Table, en algunas BBDD se permite tener una tabla donde se almacenan como registros todas las secuencias.
      • Generaci\u00f3n de PK Auto, elige la mejor estrategia en funci\u00f3n de la BBDD que hemos seleccionado.
    • @Column \u2192 Le indica a JPA que esta propiedad mapea una columna de la tabla y le especifica el nombre de la columna. Al igual que la anotaci\u00f3nd de Table, esta anotaci\u00f3n no es necesaria aunque si es muy recomendable. Por claridad se deber\u00eda poner siempre, aunque si el nombre de la columna es igual al nombre de la propiedad no es necesaria la anotaci\u00f3n.

    Hay muchas otras anotaciones, pero estas son las b\u00e1sicas, ya ir\u00e1s aprendiendo otras.

    Consejo

    Para definir las PK de las tablas, intenta evitar una PK compuesta de m\u00e1s de una columna. La programaci\u00f3n se hace muy compleja y las magias que hace JPA en la oscuridad se complican mucho. Mi recomendaci\u00f3n es que siempre utilices una PK n\u00famerica, en la medida de lo posible, y si es necesario, crees \u00edndices compuestos de b\u00fasqueda o checks compuestos para evitar duplicidades.

    "},{"location":"develop/basic/springboot/#juego-de-datos-de-bbdd","title":"Juego de datos de BBDD","text":"

    Spring Boot autom\u00e1ticamente cuando arranque el proyecto escaner\u00e1 todas las @Entity y crear\u00e1 las estructuras de las tablas en la BBDD en memoria, gracias a las anotaciones que hemos puesto. Adem\u00e1s de esto, lanzar\u00e1 los scripts de construcci\u00f3n de BBDD que tenemos en la carpeta src/main/resources/. As\u00ed que, teniendo clara la estructura de la Entity podemos configurar los ficheros con los juegos de datos que queramos, y para ello vamos a utilizar el fichero data.sql que creamos en su momento.

    Sabemos que la tabla se llamar\u00e1 category y que tendr\u00e1 dos columnas, una columna id, que ser\u00e1 la PK autom\u00e1tica, y una columna name. Podemos escribir el siguiente script para rellenar datos:

    data.sql
    INSERT INTO category(name) VALUES ('Eurogames');\nINSERT INTO category(name) VALUES ('Ameritrash');\nINSERT INTO category(name) VALUES ('Familiar');\n
    "},{"location":"develop/basic/springboot/#implementar-repository","title":"Implementar Repository","text":"

    Ahora que ya tenemos el juego de datos y la entidad implementada, vamos a ver como acceder a BBDD desde Java. Esto lo haremos con un Repository. Existen varias formas de utilizar los repositories, desde el todo autom\u00e1tico y magia de JPA hasta el repositorio manual en el que hay que codificar todo. En el tutorial voy a explicar varias formas de implementarlo para este CRUD y los siguientes CRUDs.

    Como ya se dijo en puntos anteriores, el acceso a datos se debe hacer siempre a trav\u00e9s de un Repository, as\u00ed que vamos a implementar uno. En esta capa, al igual que pasaba con los services, es recomendable utilizar el patr\u00f3n fachada, para poder sustituir implementaciones sin afectar al c\u00f3digo.

    CategoryRepository.java
    package com.ccsw.tutorial.category;\n\nimport com.ccsw.tutorial.category.model.Category;\nimport org.springframework.data.repository.CrudRepository;\n\n/**\n * @author ccsw\n *\n */\npublic interface CategoryRepository extends CrudRepository<Category, Long> {\n\n}\n

    \u00bfQu\u00e9 te parece?, sencillo, \u00bfno?. Spring ya tiene una implementaci\u00f3n por defecto de un CrudRepository, tan solo tenemos que crear una interface que extienda de la interface CrudRepository pas\u00e1ndole como tipos la Entity y el tipo de la Primary Key. Con eso Spring construye el resto y nos provee de los m\u00e9todos t\u00edpicos y necesarios para un CRUD.

    Ahora vamos a utilizarla en \u00e9l Service, pero hay un problema. \u00c9l Repository devuelve un objeto tipo Category y \u00e9l Service y Controller devuelven un objeto tipo CategoryDto. Esto es porque en cada capa se debe con un \u00e1mbito de modelos diferente. Podr\u00edamos hacer que todo el back trabajara con Category que son entidades de persistencia, pero no es lo correcto y nos podr\u00eda llevar a cometer errores, o modificar el objeto y que sin que nosotros lo orden\u00e1semos se persistiera ese cambio en BBDD.

    El \u00e1mbito de trabajo de las capas con el que solemos trabajar y est\u00e1 m\u00e1s extendido es el siguiente:

    • Los datos que vienen y van al cliente, deber\u00edan ser en la mayor\u00eda de los casos datos en formato json
    • Al entrar en un Controller esos datos json se transforman en un DTO. Al salir del Controller hacia el cliente, esos DTOs se transforman en formato json. Estas conversiones son autom\u00e1ticas, las hace Springboot (en realidad las hace la librer\u00eda de jackson codehaus).
    • Cuando un Controller ejecuta una llamada a un Service, generalmente le pasa sus datos en DTO, y el Service se encarga de transformar esto a una Entity. A la inversa, cuando un Service responde a un Controller, \u00e9l responde con una Entity y el Controller ya se encargar\u00e1 de transformarlo a DTO.
    • Por \u00faltimo, para los Repository, siempre se trabaja de entrada y salida con objetos tipo Entity

    Parece un l\u00edo, pero ya ver\u00e1s como es muy sencillo ahora que veremos el ejemplo. Una \u00faltima cosa, para hacer esas transformaciones, las podemos hacer a mano usando getters y setters o bien utilizar el objeto DozerBeanMapper que hemos configurado al principio.

    El c\u00f3digo deber\u00eda quedar as\u00ed:

    CategoryServiceImpl.javaCategoryService.javaCategoryController.java
    package com.ccsw.tutorial.category;\n\nimport com.ccsw.tutorial.category.model.Category;\nimport com.ccsw.tutorial.category.model.CategoryDto;\nimport jakarta.transaction.Transactional;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Service\n@Transactional\npublic class CategoryServiceImpl implements CategoryService {\n\n    @Autowired\n    CategoryRepository categoryRepository;\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public List<Category> findAll() {\n\n          return (List<Category>) this.categoryRepository.findAll();\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void save(Long id, CategoryDto dto) {\n\n          Category category;\n\n          if (id == null) {\n             category = new Category();\n          } else {\n             category = this.categoryRepository.findById(id).orElse(null);\n          }\n\n          category.setName(dto.getName());\n\n          this.categoryRepository.save(category);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void delete(Long id) throws Exception {\n\n          if(this.categoryRepository.findById(id).orElse(null) == null){\n             throw new Exception(\"Not exists\");\n          }\n\n          this.categoryRepository.deleteById(id);\n    }\n\n}\n
    package com.ccsw.tutorial.category;\n\nimport com.ccsw.tutorial.category.model.Category;\nimport com.ccsw.tutorial.category.model.CategoryDto;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n * \n */\npublic interface CategoryService {\n\n    /**\n     * M\u00e9todo para recuperar todas las {@link Category}\n     *\n     * @return {@link List} de {@link Category}\n     */\n    List<Category> findAll();\n\n    /**\n     * M\u00e9todo para crear o actualizar una {@link Category}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    void save(Long id, CategoryDto dto);\n\n    /**\n     * M\u00e9todo para borrar una {@link Category}\n     *\n     * @param id PK de la entidad\n     */\n    void delete(Long id) throws Exception;\n\n}\n
    package com.ccsw.tutorial.category;\n\nimport com.ccsw.tutorial.category.model.Category;\nimport com.ccsw.tutorial.category.model.CategoryDto;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.modelmapper.ModelMapper;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * @author ccsw\n *\n */\n@Tag(name = \"Category\", description = \"API of Category\")\n@RequestMapping(value = \"/category\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class CategoryController {\n\n    @Autowired\n    CategoryService categoryService;\n\n    @Autowired\n    ModelMapper mapper;\n\n    /**\n     * M\u00e9todo para recuperar todas las {@link Category}\n     *\n     * @return {@link List} de {@link CategoryDto}\n     */\n    @Operation(summary = \"Find\", description = \"Method that return a list of Categories\"\n    )\n    @RequestMapping(path = \"\", method = RequestMethod.GET)\n    public List<CategoryDto> findAll() {\n\n        List<Category> categories = this.categoryService.findAll();\n\n        return categories.stream().map(e -> mapper.map(e, CategoryDto.class)).collect(Collectors.toList());\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar una {@link Category}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Category\"\n    )\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody CategoryDto dto) {\n\n        this.categoryService.save(id, dto);\n    }\n\n    /**\n     * M\u00e9todo para borrar una {@link Category}\n     *\n     * @param id PK de la entidad\n     */\n    @Operation(summary = \"Delete\", description = \"Method that deletes a Category\")\n    @RequestMapping(path = \"/{id}\", method = RequestMethod.DELETE)\n    public void delete(@PathVariable(\"id\") Long id) throws Exception {\n\n        this.categoryService.delete(id);\n    }\n\n}\n

    El Service no tiene nada raro, simplemente accede al Repository (siempre anotado como @Autowired) y recupera datos o los guarda. F\u00edjate en el caso especial del save que la \u00fanica diferencia es que en un caso tendr\u00e1 id != null y por tanto internamente har\u00e1 un update, y en otro caso tendr\u00e1 id == null y por tanto internamente har\u00e1 un save.

    En cuanto a la interface, lo \u00fanico que cambiamos fue los objetos de respuesta, que ahora pasan a ser de tipo Category. Los de entrada se mantienen como CategoryDto.

    Por \u00faltimo, en \u00e9l Controller se puede ver como se utiliza el conversor de DozerBeanMapper (siempre anotado como @Autowired), que permite traducir una lista a un tipo concreto, o un objeto \u00fanico a un tipo concreto. La forma de hacer estas conversiones siempre es por nombre de propiedad. Las propiedades del objeto destino se deben llamar igual que las propiedades del objeto origen. En caso contrario se quedar\u00e1n a null.

    Ojo con el mapeo

    Ojo a esta \u00faltima frase, debe quedar meridianamente claro. La forma de mapear de un objeto origen a un objeto destino siempre es a trav\u00e9s del nombre. Los getters del origen deben ser iguales a los getters del destino. Si hay una letra diferente o unas may\u00fasculas o min\u00fasculas diferentes NO realizar\u00e1 el mapeo y se quedar\u00e1 la propiedad a null.

    Para terminar, cuando queramos realizar un mapeo masivo de los diferentes registros, tenemos que itulizar la API Stream de Java, que nos proporciona una forma sencilla de realizar estas operativas, sobre colecciones de elementos, mediante el uso del m\u00e9todo intermedio map y el reductor por defecto para listas. Te recomiendo echarle un ojo a la teor\u00eda de Introducci\u00f3n a API Java Streams.

    BBDD

    Si quires ver el contenido de la base de datos puedes acceder a un IDE web autopublicado por H2 en la ruta http://localhost:8080/h2-console

    "},{"location":"develop/basic/springboot/#aspectos-importantes_2","title":"Aspectos importantes","text":"

    Los aspectos importantes de la capa Repository son:

    • Al igual que los Service, se debe utilizar el patr\u00f3n fachada, por lo que tendremos una Interface y posiblemente una implementaci\u00f3n.
    • A menudo la implementaci\u00f3n la hace internamente Spring Boot, pero hay veces que podemos hacer una implementaci\u00f3n manual. Lo veremos en siguientes puntos.
    • Los Repository trabajan siempre con Entity que no son m\u00e1s que mapeos de una tabla o de una vista que existe en BBDD.
    • Los Repository no contienen l\u00f3gica de negocio, ni transformaciones, simplemente acceder a datos, persisten o devuelven informaci\u00f3n.
    • Los Repository JAM\u00c1S deben llamar a otros Repository ni Service.
    • Intenta que tus clases Entity sean lo m\u00e1s sencillas posible, sobre todo en cuanto a Primary Keys, se simplificar\u00e1 mucho el desarrollo.
    "},{"location":"develop/basic/springboot/#capa-de-testing-tdd","title":"Capa de Testing: TDD","text":"

    Por \u00faltimo y aunque no deber\u00eda ser lo \u00faltimo que se desarrolla sino todo lo contrario, deber\u00eda ser lo primero en desarrollar, tenemos la bater\u00eda de pruebas. Con fines did\u00e1cticos, he querido ense\u00f1arte un ciclo de desarrollo para ir recorriendo las diferentes capas de una aplicaci\u00f3n, pero en realidad, para realizar el desarrollo deber\u00eda aplicar TDD (Test Driven Development). Si quieres aprender las reglas b\u00e1sicas de como aplicar TDD al desarrollo diario, te recomiendo que leas el Anexo. TDD.

    En este caso, y sin que sirva de precedente, ya tenemos implementados los m\u00e9todos de la aplicaci\u00f3n, y ahora vamos a testearlos. Existen muchas formas de testing en funci\u00f3n del componente o la capa que se quiera testear. En realidad, a medida que vayas programando ir\u00e1s aprendiendo todas ellas, de momento realizaremos dos tipos de test simples que prueben las casu\u00edsticas de los m\u00e9todos.

    El enfoque que seguiremos en este tutorial ser\u00e1 realizar las pruebas mediante test unitarios y test de integraci\u00f3n.

    • Test unitarios: Se trata de pruebas estrictamente relativas a la calidad est\u00e1tica del c\u00f3digo de una determinada operaci\u00f3n de la capa de la l\u00f3gica de negocio (Service). Estas pruebas no inicializan el contexto de Spring y deben simular todas las piezas ajenas a la funcionalidad testeada.
    • Test de integraci\u00f3n: Se tratan de pruebas completas de un determinado endpoint que conlleva inicializar el contexto de Spring (base de datos incluida) y realizar una llama REST para comprobar el flujo completo de la API.

    Lo primero ser\u00e1 conocer que queremos probar y para ello nos vamos a hacer una lista:

    Test unitarios:

    • Prueba de listado, debe probar la l\u00f3gica dentro de la operaci\u00f3n de negocio de consulta de Categor\u00eda
    • Prueba de creaci\u00f3n, debe probar la l\u00f3gica dentro de la operaci\u00f3n de negocio de creaci\u00f3n una nueva Categor\u00eda
    • Prueba de modificaci\u00f3n, debe probar la l\u00f3gica dentro de la operaci\u00f3n de negocio de modificaci\u00f3n una Categor\u00eda existente
    • Prueba de borrado, debe probar la l\u00f3gica dentro de la operaci\u00f3n de negocio de borrado de una Categor\u00eda existente

    Test de integraci\u00f3n:

    • Prueba de listado, debe recuperar los elementos de la tabla Categor\u00eda
    • Prueba de creaci\u00f3n, debe crear una nueva Categor\u00eda
    • Prueba de modificaci\u00f3n correcta, debe modificar una Categor\u00eda existente
    • Prueba de modificaci\u00f3n incorrecta, debe dar error al modificar una Categor\u00eda que no existe
    • Prueba de borrado correcta, debe borrar una Categor\u00eda existente
    • Prueba de borrado incorrecta, debe dar error al borrar una Categor\u00eda que no existe

    Se podr\u00edan hacer muchos m\u00e1s tests, pero creo que con esos son suficientes para que entiendas como se comporta esta capa. Si te fijas, hay que probar tanto los resultados correctos como los resultados incorrectos. El usuario no siempre se va a comportar como nosotros pensamos.

    Pues vamos a ello.

    "},{"location":"develop/basic/springboot/#pruebas-de-listado","title":"Pruebas de listado","text":"

    Vamos a empezar haciendo una clase de test dentro de la carpeta src/test/java. No queremos que los test formen parte del c\u00f3digo productivo de la aplicaci\u00f3n, por eso utilizamos esa ruta que queda fuera del scope general de la aplicaci\u00f3n (main).

    Crearemos las clases (en la package category):

    • Test unitarios: com.ccsw.tutorial.category.CategoryTest
    • Test de integraci\u00f3n: com.ccsw.tutorial.category.CategoryIT
    CategoryTest.javaCategoryIT.java
    package com.ccsw.tutorial.category;\n\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\npublic class CategoryTest {\n\n}\n
    package com.ccsw.tutorial.category;\n\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.test.web.client.TestRestTemplate;\nimport org.springframework.boot.test.web.server.LocalServerPort;\nimport org.springframework.test.annotation.DirtiesContext;\n\n@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)\n@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)\npublic class CategoryIT {\n\n    @LocalServerPort\n    private int port;\n\n    @Autowired\n    private TestRestTemplate restTemplate;\n\n}\n

    Estas clases son sencillas y tan solo tienen anotaciones espec\u00edficas de Spring Boot para cada tipo de test:

    • @SpringBootTest. Esta anotaci\u00f3n lo que hace es inicializar el contexto de Spring cada vez que se inician los test de jUnit. Aunque el contexto tarda unos segundos en arrancar, lo bueno que tiene es que solo se inicializa una vez y se lanzan todos los jUnits en bater\u00eda con el mismo contexto.
    • @DirtiesContext. Esta anotaci\u00f3n le indica a Spring que los test van a ser transaccionales, y por tanto cuando termine la ejecuci\u00f3n de cada uno de los test, autom\u00e1ticamente por la anotaci\u00f3n de arriba, Spring har\u00e1 un rearranque parcial del contexto y dejar\u00e1 el estado de la BBDD como estaba inicialmente.
    • @ExtendWith. Esta anotaci\u00f3n le indica a Spring que no debe inicializar el contexto, ya que se trata de test est\u00e1ticos que no lo requieren.

    Para las pruebas de integraci\u00f3n nos faltar\u00e1 configurar la aplicaci\u00f3n de test, al igual que hicimos con la aplicaci\u00f3n 'productiva'. Deberemos abrir el fichero src/test/resources/application.properties y a\u00f1adir la configuraci\u00f3n de la BBDD. Para este tutorial vamos a utilizar la misma BBDD que la aplicaci\u00f3n productiva, pero de normal la aplicaci\u00f3n se conectar\u00e1 a una BBDD, generalmente f\u00edsica, mientras que los test jUnit se conectar\u00e1n a otra BBDD, generalmente en memoria.

    application.properties
    #Database\nspring.datasource.url=jdbc:h2:mem:testdb\nspring.datasource.username=sa\nspring.datasource.password=sa\nspring.datasource.driver-class-name=org.h2.Driver\n\nspring.jpa.database-platform=org.hibernate.dialect.H2Dialect\nspring.jpa.defer-datasource-initialization=true\n

    Con todo esto ya podemos crear nuestro primer test. Iremos a las clases CategoryIT y CategoryTest donde a\u00f1adiremos un m\u00e9todo p\u00fablico. Los test siempre tienen que ser m\u00e9todos p\u00fablicos que devuelvan el tipo void.

    CategoryTest.javaCategoryIT.java
    package com.ccsw.tutorial.category;\n\nimport com.ccsw.tutorial.category.model.Category;\nimport com.ccsw.tutorial.category.model.CategoryDto;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.Mockito.*;\n\n@ExtendWith(MockitoExtension.class)\npublic class CategoryTest {\n\n    @Mock\n    private CategoryRepository categoryRepository;\n\n    @InjectMocks\n    private CategoryServiceImpl categoryService;\n\n    @Test\n    public void findAllShouldReturnAllCategories() {\n\n          List<Category> list = new ArrayList<>();\n          list.add(mock(Category.class));\n\n          when(categoryRepository.findAll()).thenReturn(list);\n\n          List<Category> categories = categoryService.findAll();\n\n          assertNotNull(categories);\n          assertEquals(1, categories.size());\n    }\n}\n
    package com.ccsw.tutorial.category;\n\nimport com.ccsw.tutorial.category.model.CategoryDto;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;    \nimport org.springframework.boot.test.web.client.TestRestTemplate;\nimport org.springframework.boot.test.web.server.LocalServerPort;\nimport org.springframework.core.ParameterizedTypeReference;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.test.annotation.DirtiesContext;\n\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)\n@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)\npublic class CategoryIT {\n\n    public static final String LOCALHOST = \"http://localhost:\";\n    public static final String SERVICE_PATH = \"/category\";\n\n    @LocalServerPort\n    private int port;\n\n    @Autowired\n    private TestRestTemplate restTemplate;\n\n    ParameterizedTypeReference<List<CategoryDto>> responseType = new ParameterizedTypeReference<List<CategoryDto>>(){};\n\n    @Test\n    public void findAllShouldReturnAllCategories() {\n\n          ResponseEntity<List<CategoryDto>> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.GET, null, responseType);\n\n          assertNotNull(response);\n          assertEquals(3, response.getBody().size());\n    }\n}\n

    Es muy importante marcar cada m\u00e9todo de prueba con la anotaci\u00f3n @Test, en caso contrario no se ejecutar\u00e1. Lo que se debe hacer en cada m\u00e9todo que implementemos es probar una y solo una acci\u00f3n.

    En los ejemplos anteriores (CategoryTest), por un lado hemos comprobado el m\u00e9todo findAll() el cual por debajo invoca una llamada al repository de categor\u00eda, la cual hemos simulado con una respuesta ficticia limit\u00e1ndonos \u00fanicamente a la l\u00f3gica contenida en la operaci\u00f3n de negocio.

    Mientras que por otro lado (CategoryIT), hemos probado la llamando al m\u00e9todo GET del endpoint http://localhost:XXXX/category comprobando que realmente nos devuelve 3 resultados, que son los que hay en BBDD inicialmente.

    Muy importante: Nomenclatura de los tests

    La nomenclatura de los m\u00e9todos de test debe sigue una estructura determinada. Hay muchas formas de nombrar a los m\u00e9todos, pero nosotros solemos utilizar la estructura 'should', para indicar lo que va a hacer. En el ejemplo anterior el m\u00e9todo 'findAll' debe devolver 'AllCategories'. De esta forma sabemos cu\u00e1l es la intenci\u00f3n del test, y si por cualquier motivo diera un fallo, sabemos que es lo que NO est\u00e1 funcionando de nuestra aplicaci\u00f3n.

    Para comprobar que el test funciona, podemos darle bot\u00f3n derecho sobre la clase de CategoryIT y pulsar en Run as -> JUnit Test. Si todo funciona correctamente, deber\u00e1 aparecer una pantalla de ejecuci\u00f3n y todos nuestros tests (en este caso solo uno) corriendo correctamente (en verde). El proceso es el mismo para la clase CategoryTest.

    "},{"location":"develop/basic/springboot/#pruebas-de-creacion","title":"Pruebas de creaci\u00f3n","text":"

    Vamos con los siguientes test, los que probar\u00e1n una creaci\u00f3n de una nueva Categor\u00eda. A\u00f1adimos el siguiente m\u00e9todo a la clase de test:

    CategoryTest.javaCategoryIT.java
    public static final String CATEGORY_NAME = \"CAT1\";\n\n@Test\npublic void saveNotExistsCategoryIdShouldInsert() {\n\n      CategoryDto categoryDto = new CategoryDto();\n      categoryDto.setName(CATEGORY_NAME);\n\n      ArgumentCaptor<Category> category = ArgumentCaptor.forClass(Category.class);\n\n      categoryService.save(null, categoryDto);\n\n      verify(categoryRepository).save(category.capture());\n\n      assertEquals(CATEGORY_NAME, category.getValue().getName());\n}\n
    public static final Long NEW_CATEGORY_ID = 4L;\npublic static final String NEW_CATEGORY_NAME = \"CAT4\";\n\n@Test\npublic void saveWithoutIdShouldCreateNewCategory() {\n\n      CategoryDto dto = new CategoryDto();\n      dto.setName(NEW_CATEGORY_NAME);\n\n      restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.PUT, new HttpEntity<>(dto), Void.class);\n\n      ResponseEntity<List<CategoryDto>> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.GET, null, responseType);\n      assertNotNull(response);\n      assertEquals(4, response.getBody().size());\n\n      CategoryDto categorySearch = response.getBody().stream().filter(item -> item.getId().equals(NEW_CATEGORY_ID)).findFirst().orElse(null);\n      assertNotNull(categorySearch);\n      assertEquals(NEW_CATEGORY_NAME, categorySearch.getName());\n}\n

    En el primer caso, estamos construyendo un objeto CategoryDto para darle un nombre a la Categor\u00eda e invocamos a la operaci\u00f3n save pasandole un ID a nulo. Para identificar que el funcionamiento es el esperado, capturamos la categor\u00eda que se proporciona al repository al intentar realizar la acci\u00f3n ficticia de guardado y comprobamos que el valor es el que se proporciona en la invocaci\u00f3n.

    De forma similar en el segundo caso, estamos construyendo un objeto CategoryDto para darle un nombre a la Categor\u00eda e invocamos al m\u00e9todo PUT sin a\u00f1adir en la ruta referencia al identificador. Seguidamente, recuperamos de nuevo la lista de categor\u00edas y en este caso deber\u00eda tener 4 resultados. Hacemos un filtrado buscando la nueva Categor\u00eda que deber\u00eda tener un ID = 4 y deber\u00eda ser la que acabamos de crear.

    Si ejecutamos, veremos que ambos test ahora aparecen en verde.

    "},{"location":"develop/basic/springboot/#pruebas-de-modificacion","title":"Pruebas de modificaci\u00f3n","text":"

    Para este caso de prueba, vamos a realizar varios test, como hemos comentado anteriormente. Tenemos que probar que es lo que pasa cuando intentamos modificar un elemento que existe, pero tambi\u00e9n debemos probar que es lo que pasa cuando intentamos modificar un elemento que no existe.

    Empezamos con el sencillo, un test que pruebe una modificaci\u00f3n existente.

    CategoryTest.javaCategoryIT.java
    public static final Long EXISTS_CATEGORY_ID = 1L;\n\n@Test\npublic void saveExistsCategoryIdShouldUpdate() {\n\n  CategoryDto categoryDto = new CategoryDto();\n  categoryDto.setName(CATEGORY_NAME);\n\n  Category category = mock(Category.class);\n  when(categoryRepository.findById(EXISTS_CATEGORY_ID)).thenReturn(Optional.of(category));\n\n  categoryService.save(EXISTS_CATEGORY_ID, categoryDto);\n\n  verify(categoryRepository).save(category);\n}\n
    public static final Long MODIFY_CATEGORY_ID = 3L;\n\n@Test\npublic void modifyWithExistIdShouldModifyCategory() {\n\n      CategoryDto dto = new CategoryDto();\n      dto.setName(NEW_CATEGORY_NAME);\n\n      restTemplate.exchange(LOCALHOST + port + SERVICE_PATH + \"/\" + MODIFY_CATEGORY_ID, HttpMethod.PUT, new HttpEntity<>(dto), Void.class);\n\n      ResponseEntity<List<CategoryDto>> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.GET, null, responseType);\n      assertNotNull(response);\n      assertEquals(3, response.getBody().size());\n\n      CategoryDto categorySearch = response.getBody().stream().filter(item -> item.getId().equals(MODIFY_CATEGORY_ID)).findFirst().orElse(null);\n      assertNotNull(categorySearch);\n      assertEquals(NEW_CATEGORY_NAME, categorySearch.getName());\n}\n

    En el caso de los test unitarios, comprobamos la l\u00f3gica de la modificaci\u00f3n simulando que el repository nos devuelve una categor\u00eda que modificar y verificado que se invoca el guardado sobre la misma.

    En el caso de los test de integraci\u00f3n, la misma filosof\u00eda que en el test anterior, pero esta vez modificamos la Categor\u00eda de ID = 3. Luego la filtramos y vemos que realmente se ha modificado. Adem\u00e1s comprobamos que el listado de todas los registros sigue siendo 3 y no se ha creado un nuevo registro.

    En el siguiente test, probaremos un resultado err\u00f3neo.

    CategoryIT.java
    @Test\npublic void modifyWithNotExistIdShouldInternalError() {\n\n      CategoryDto dto = new CategoryDto();\n      dto.setName(NEW_CATEGORY_NAME);\n\n      ResponseEntity<?> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH + \"/\" + NEW_CATEGORY_ID, HttpMethod.PUT, new HttpEntity<>(dto), Void.class);\n\n      assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());\n}\n

    Intentamos modificar el ID = 4, que no deber\u00eda existir en BBDD y por tanto lo que se espera en el test es que lance un 500 Internal Server Error al llamar al m\u00e9todo PUT.

    "},{"location":"develop/basic/springboot/#pruebas-de-borrado","title":"Pruebas de borrado","text":"

    Ya por \u00faltimo implementamos las pruebas de borrado.

    CategoryTest.javaCategoryIT.java
    @Test\npublic void deleteExistsCategoryIdShouldDelete() throws Exception {\n\n      Category category = mock(Category.class);\n      when(categoryRepository.findById(EXISTS_CATEGORY_ID)).thenReturn(Optional.of(category));\n\n      categoryService.delete(EXISTS_CATEGORY_ID);\n\n      verify(categoryRepository).deleteById(EXISTS_CATEGORY_ID);\n}\n
    public static final Long DELETE_CATEGORY_ID = 2L;\n\n@Test\npublic void deleteWithExistsIdShouldDeleteCategory() {\n\n      restTemplate.exchange(LOCALHOST + port + SERVICE_PATH + \"/\" + DELETE_CATEGORY_ID, HttpMethod.DELETE, null, Void.class);\n\n      ResponseEntity<List<CategoryDto>> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.GET, null, responseType);\n      assertNotNull(response);\n      assertEquals(2, response.getBody().size());\n}\n\n@Test\npublic void deleteWithNotExistsIdShouldInternalError() {\n\n      ResponseEntity<?> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH + \"/\" + NEW_CATEGORY_ID, HttpMethod.DELETE, null, Void.class);\n\n      assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());\n}\n

    En cuanto al test unitario, se invoca a la operaci\u00f3n delete y se verifica que la operaci\u00f3n requerida del repository es invocado con el atributo correcto.

    En lo relativo a las pruebas de integraci\u00f3n, en el primer test, se invoca el m\u00e9todo DELETE y posteriormente se comprueba que el listado tiene un tama\u00f1o de 2 (uno menos que el original). Mientras que en el segundo test, se comprueba que con ID no v\u00e1lido, devuelve un 500 Internal Server Error.

    Con esto tendr\u00edamos m\u00e1s o menos probados los casos b\u00e1sicos de nuestra aplicaci\u00f3n y tendr\u00edamos una peque\u00f1a red de seguridad que nos ayudar\u00eda por si a futuro necesitamos hacer alg\u00fan cambio o evolutivo.

    "},{"location":"develop/basic/springboot/#que-hemos-aprendido","title":"\u00bfQ\u00fae hemos aprendido?","text":"

    Resumiendo un poco los pasos que hemos seguido:

    • Hay que definir y agrupar por \u00e1mbito funcional, hemos creado el package com.ccsw.tutorial.category para aglutinar todas las clases.
    • Lo primero que debemos empezar a construir siempre son los test, aunque en este cap\u00edtulo del tutorial lo hemos hecho al rev\u00e9s solo con fines did\u00e1cticos. En los siguientes cap\u00edtulos lo haremos de forma correcta, y esto nos ayudar\u00e1 a pensar y dise\u00f1ar que es lo que queremos implementar realmente.
    • La implementaci\u00f3n de la aplicaci\u00f3n se deber\u00eda separar por capas:
      • Controller \u2192 Maneja las peticiones de entrada del cliente y realiza transformaciones. No ejecuta directamente l\u00f3gica de negocio, para eso utiliza llamadas a la siguiente capa.
      • Service \u2192 Ejecuta la l\u00f3gica de negocio en sus m\u00e9todos o llamando a otros objetos de la misma capa. No ejecuta directamente accesos a datos, para eso utiliza la siguiente capa.
      • Repository \u2192 Realiza los accesos a datos de lectura y escritura. NUNCA debe llamar a otros objetos de la misma capa ni de capas anteriores.
    • Hay que tener en cuenta los objetos modelo que se mueven en cada capa. Generalmente son:
      • Json \u2192 Los datos que vienen y van del cliente al Controller.
      • DTO \u2192 Los datos se mueven dentro del Controller y sirven para invocar llamadas. Tambi\u00e9n son los datos que devuelve un Controller.
      • Entity \u2192 Los datos que sirven para persistir y leer datos de una BBDD y que NUNCA deber\u00edan ir m\u00e1s all\u00e1 del Controller.
    "},{"location":"develop/basic/springboot/#depuracion","title":"Depuraci\u00f3n","text":"

    Una parte muy importante del desarrollo es tener la capacidad de depurar nuestro c\u00f3digo, en este apartado vamos a explicar como se realiza debug en Backend.

    Esta parte se realiza con las herramientas incluidas dentro de nuestro IDE favorito, depender\u00e1 del que tengas instalado, se hace de una forma u otra.

    "},{"location":"develop/basic/springboot/#intellij","title":"IntelliJ","text":"

    En caso de que hayas elegido instalar IntelliJ, lo primero que debemos hacer es arrancar la aplicaci\u00f3n en modo Debug:

    o bien

    Arrancada la aplicaci\u00f3n en este modo, vamos a depurar la operaci\u00f3n de crear categor\u00eda.

    Para ello vamos a abrir nuestro fichero donde tenemos la implementaci\u00f3n del servicio de creaci\u00f3n de la capa de la l\u00f3gica de negocio CategoryServiceImpl.

    Dentro del fichero ya podemos a\u00f1adir puntos de ruptura (breakpoint), en nuestro caso queremos comprobar en el m\u00e9todo save que el nombre introducido se recibe correctamente.

    Colocamos el breakpoint en la primera l\u00ednea del m\u00e9todo save (click al lado del n\u00famero de la l\u00ednea):

    y desde la interfaz/postman creamos una nueva categor\u00eda para lanzar la petici\u00f3n y que se detenga la ejecuci\u00f3n en debug.

    Hecho esto, podemos observar que a nivel de interfaz/postman, la petici\u00f3n se queda esperando y el IDE pasa modo Debug. En la parte inferior del IDE podemos ver la pila de llamadas y las variables actuales en memoria:

    El IDE nos lleva al punto exacto donde hemos a\u00f1adido el breakpoint y se para en este punto ofreci\u00e9ndonos la posibilidad de explorar el contenido de las variables del c\u00f3digo:

    Aqu\u00ed podemos comprobar que efectivamente el atributo name de la variable dto tiene el valor que hemos introducido por pantalla/postman.

    Para continuar con la ejecuci\u00f3n basta con darle al bot\u00f3n de play de la barra de herramientas inferior-izquierda, o incluso navegar por las siguientes l\u00edneas de c\u00f3digo.

    "},{"location":"develop/basic/springboot/#eclipse","title":"Eclipse","text":"

    En caso de que hayas elegido instalar Eclipse, lo primero que debemos hacer es arrancar la aplicaci\u00f3n en modo Debug:

    Arrancada la aplicaci\u00f3n en este modo, vamos a depurar la operaci\u00f3n de crear categor\u00eda.

    Para ello vamos a abrir nuestro fichero donde tenemos la implementaci\u00f3n del servicio de creaci\u00f3n de la capa de la l\u00f3gica de negocio CategoryServiceImpl.

    Dentro del fichero ya podemos a\u00f1adir puntos de ruptura (breakpoint), en nuestro caso queremos comprobar que el nombre introducido se recibe correctamente.

    Colocamos el breakpoint en la primera l\u00ednea del m\u00e9todo (click sobre el n\u00famero de la l\u00ednea) y desde la interfaz/postman creamos una nueva categor\u00eda.

    Hecho esto, podemos observar que a nivel de interfaz/postman, la petici\u00f3n se queda esperando y el IDE pasa modo Debug (la primera vez nos preguntar\u00e1 si queremos hacerlo, le decimos que si):

    El IDE nos lleva al punto exacto donde hemos a\u00f1adido el breakpoint y se para en este punto ofreci\u00e9ndonos la posibilidad de explorar el contenido de las variables del c\u00f3digo:

    Aqu\u00ed podemos comprobar que efectivamente el atributo name de la variable dto tiene el valor que hemos introducido por pantalla/postman.

    Para continuar con la ejecuci\u00f3n basta con darle al bot\u00f3n de play de la barra de herramientas superior.

    Nota: para volver al modo Java de Eclipse, presionamos el bot\u00f3n que se sit\u00faa a la izquierda del modo Debug en el que ha entrado el IDE autom\u00e1ticamente.

    "},{"location":"develop/basic/vuejs/","title":"Listado simple - VUE","text":"

    Ahora que ya tenemos listo el proyecto frontend de VUE, ya podemos empezar a codificar la soluci\u00f3n.

    "},{"location":"develop/basic/vuejs/#primeros-pasos","title":"Primeros pasos","text":"

    Antes de empezar

    Quiero hacer hincapi\u00e9 que VUE tiene una documentaci\u00f3n muy extensa y completa, as\u00ed que te recomiendo que hagas uso de ella cuando tengas cualquier duda. En la propia web de documentaci\u00f3n de VUE puedes buscar casi cualquier ejemplo que necesites.

    Vamos a realizar una pantalla lo m\u00e1s parecida a la siguiente captura para empezar:

    Lo primero que vamos a hacer es crear los componentes de las tres pr\u00f3ximas pantallas mediante el siguiente comando:

    npx quasar new page CatalogPage CategoriesPage AuthorsPage\n

    Y ahora vamos a crear las rutas que nos van a hacer llegar hasta ellos:

    import { RouteRecordRaw } from 'vue-router';\nimport MainLayout from 'layouts/MainLayout.vue';\nimport IndexPage from 'pages/IndexPage.vue';\nimport CatalogPage from 'pages/CatalogPage.vue';\nimport CategoriesPage from 'pages/CategoriesPage.vue';\nimport AuthorsPage from 'pages/AuthorsPage.vue';\n\nconst routes: RouteRecordRaw[] = [\n  {\n    path: '/',\n    component: MainLayout,\n    children: [\n      { path: '', component: IndexPage },\n      { path: 'games', component: CatalogPage },\n      { path: 'categories', component: CategoriesPage },\n      { path: 'authors', component: AuthorsPage },\n    ],\n  },\n\n  // Always leave this as last one,\n  // but you can also remove it\n  // {\n  //   path: '/:catchAll(.*)*',\n  //   component: () => import('pages/ErrorNotFound.vue'),\n  // },\n];\n\nexport default routes;\n

    Una vez realizado esto, vamos a ponerle dentro de cada uno de los archivos creados el nombre del archivo donde est\u00e1 el comentario para saber que lleva al lugar correcto:

    <template>\n  <q-page padding> CatalogPage </q-page>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nexport default defineComponent({\n  name: 'CatalogPage',\n});\n</script>\n

    Por \u00faltimo, modificaremos el men\u00fa lateral para que lleve las opciones correctas y nos enlace a dichas pantallas (para esto, iremos al archivo MainLayout.vue):

    const linksList = [\n  {\n    title: 'Cat\u00e1logo',\n    icon: 'list',\n    link: 'games',\n  },\n  {\n    title: 'Categor\u00edas',\n    icon: 'dashboard',\n    link: 'categories',\n  },\n  {\n    title: 'Autores',\n    icon: 'face',\n    link: 'authors',\n  },\n];\n

    En caso de que no funcione correctamente, deber\u00eda solucionarse cambiando en el archivo EssentialLink.vue el prop \u201chref\u201d por el prop \u201cto\u201d:

    <template>\n  <q-item clickable tag=\"a\" :to=\"link\">\n    <q-item-section v-if=\"icon\" avatar>\n      <q-icon :name=\"icon\" />\n    </q-item-section>\n\n    <q-item-section>\n      <q-item-label>{{ title }}</q-item-label>\n    </q-item-section>\n  </q-item>\n</template>\n
    "},{"location":"develop/basic/vuejs/#codigo-de-la-pantalla","title":"C\u00f3digo de la pantalla","text":"

    Para empezar, usaremos un componente de tabla de la librer\u00eda de Quasar. Este componente nos ayudar\u00e1 a mostrar los datos de los juegos en un futuro.

    <template>\n  <q-page padding>\n    <q-table\n      :rows=\"catalogData\"\n      :columns=\"columns\"\n      title=\"Cat\u00e1logo\"\n      row-key=\"id\"\n    />\n  </q-page>\n</template>\n

    As\u00ed es como deber\u00eda quedar nuestro componente de tabla con todas las supuestas variables que m\u00e1s adelante le settearemos:

    <template>\n  <q-page padding>\n    <q-table\n      hide-bottom\n      :rows=\"catalogData\"\n      :columns=\"columns\"\n      v-model:pagination=\"pagination\"\n      title=\"Cat\u00e1logo\"\n      class=\"my-sticky-header-table\"\n      no-data-label=\"No hay resultados\"\n      row-key=\"id\"\n    />\n  </q-page>\n</template>\n

    Y as\u00ed es como vamos a necesitar que est\u00e9, ya que no va a tener paginado. \u00bfPor qu\u00e9?

    • hide-bottom \u2192 hace que no se muestre la zona baja de la tabla que es donde est\u00e1 el paginado.
    • v-model:pagination \u2192 har\u00e1 que vengan los datos que vengan, se muestren todos de la misma manera.
    • class \u2192 esta clase har\u00e1 que, si haciendo scroll pierdes los header, estos te acompa\u00f1en y siempre sepas qu\u00e9 columna es la que est\u00e1s mirando.
    • no-data-label \u2192 un mensaje por si alg\u00fan d\u00eda no hay datos o tiene un fallo el back.

    Todo esto no hace falta aprend\u00e9rselo, est\u00e1 en la documentaci\u00f3n de este componente. Pero vamos a ir usando algunos props como estos para configurar correctamente la tabla.

    "},{"location":"develop/basic/vuejs/#mockeando-datos","title":"Mockeando datos","text":"

    Y esto va a hacer que podamos mostrar datos:

    <script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nconst columns = [\n  { name: 'id', align: 'left', label: 'ID', field: 'id', sortable: true },\n  {\n    name: 'name',\n    align: 'left',\n    label: 'Nombre',\n    field: 'name',\n    sortable: true,\n  },\n  { name: 'options', align: 'left', label: 'Options', field: 'options' },\n];\n\nconst data = [\n  { id: 1, name: 'Dados' },\n  { id: 2, name: 'Fichas' },\n  { id: 3, name: 'Cartas' },\n  { id: 4, name: 'Rol' },\n  { id: 5, name: 'Tableros' },\n  { id: 6, name: 'Tem\u00e1ticos' },\n  { id: 7, name: 'Europeos' },\n  { id: 8, name: 'Guerra' },\n  { id: 9, name: 'Abstractos' },\n];\n\nexport default defineComponent({\n  name: 'CatalogPage',\n\n  setup() {\n    const catalogData = ref(data);\n\n    return {\n      catalogData,\n      columns: columns,\n      pagination: {\n        page: 1,\n        rowsPerPage: 0, // 0 means all rows\n      },\n    };\n  },\n});\n</script>\n
    Lo que estamos haciendo es settear unos datos, los nombres y estilos de las columnas, y los ajustes de la paginaci\u00f3n.

    "},{"location":"develop/basic/vuejs/#anadir-editar-y-eliminar-filas","title":"A\u00f1adir, editar y eliminar filas","text":"

    El c\u00f3digo final para esto, que m\u00e1s adelante explicaremos, es el siguiente:

    <template>\n  <q-page padding>\n    <q-table\n      hide-bottom\n      :rows=\"catalogData\"\n      :columns=\"columns\"\n      v-model:pagination=\"pagination\"\n      title=\"Cat\u00e1logo\"\n      class=\"my-sticky-header-table\"\n      no-data-label=\"No hay resultados\"\n      row-key=\"id\"\n    >\n      <template v-slot:top>\n        <q-btn flat round color=\"primary\" icon=\"add\" @click=\"showAdd = true\" />\n      </template>\n      <template v-slot:body=\"props\">\n        <q-tr :props=\"props\">\n          <q-td key=\"id\" :props=\"props\">{{ props.row.id }}</q-td>\n          <q-td key=\"name\" :props=\"props\">\n            {{ props.row.name }}\n            <q-popup-edit\n              v-model=\"props.row.name\"\n              title=\"Cambiar nombre\"\n              v-slot=\"scope\"\n            >\n              <q-input\n                v-model=\"scope.value\"\n                dense\n                autofocus\n                counter\n                @keyup.enter=\"scope.set\"\n              >\n                <template v-slot:append>\n                  <q-icon name=\"edit\" />\n                </template>\n              </q-input>\n            </q-popup-edit>\n          </q-td>\n          <q-td key=\"options\" :props=\"props\">\n            <q-btn\n              flat\n              round\n              color=\"negative\"\n              icon=\"delete\"\n              @click=\"showDeleteDialog(props.row)\"\n            />\n          </q-td>\n        </q-tr>\n      </template>\n    </q-table>\n    <q-dialog v-model=\"showDelete\" persistent>\n      <q-card>\n        <q-card-section class=\"row items-center\">\n          <q-icon\n            name=\"delete\"\n            size=\"sm\"\n            color=\"negative\"\n            @click=\"showDelete = true\"\n          />\n          <span class=\"q-ml-sm\">\n            \u00bfEst\u00e1s seguro de que quieres borrar este elemento?\n          </span>\n        </q-card-section>\n\n        <q-card-actions align=\"right\">\n          <q-btn flat label=\"Cancelar\" color=\"primary\" v-close-popup />\n          <q-btn\n            flat\n            label=\"Confirmar\"\n            color=\"primary\"\n            v-close-popup\n            @click=\"deleteGame\"\n          />\n        </q-card-actions>\n      </q-card>\n    </q-dialog>\n    <q-dialog v-model=\"showAdd\">\n      <q-card style=\"min-width: 350px\">\n        <q-card-section>\n          <div class=\"text-h6\">Nombre del juego</div>\n        </q-card-section>\n\n        <q-card-section class=\"q-pt-none\">\n          <q-input dense v-model=\"nameToAdd\" autofocus @keyup.enter=\"addGame\" />\n        </q-card-section>\n\n        <q-card-actions align=\"right\" class=\"text-primary\">\n          <q-btn flat label=\"Cancelar\" v-close-popup />\n          <q-btn flat label=\"A\u00f1adir juego\" v-close-popup @click=\"addGame\" />\n        </q-card-actions>\n      </q-card>\n    </q-dialog>\n  </q-page>\n</template>\n\n<script lang=\"ts\">\nimport { ref } from 'vue';\nimport { defineComponent } from 'vue';\n\nconst columns = [\n  { name: 'id', align: 'left', label: 'ID', field: 'id', sortable: true },\n  {\n    name: 'name',\n    align: 'left',\n    label: 'Nombre',\n    field: 'name',\n    sortable: true,\n  },\n  { name: 'options', align: 'left', label: 'Options', field: 'options' },\n];\n\nconst data = [\n  { id: 1, name: 'Dados' },\n  { id: 2, name: 'Fichas' },\n  { id: 3, name: 'Cartas' },\n  { id: 4, name: 'Rol' },\n  { id: 5, name: 'Tableros' },\n  { id: 6, name: 'Tem\u00e1ticos' },\n  { id: 7, name: 'Europeos' },\n  { id: 8, name: 'Guerra' },\n  { id: 9, name: 'Abstractos' },\n];\n\nexport default defineComponent({\n  name: 'CatalogPage',\n\n  setup() {\n    const catalogData = ref(data);\n    const showDelete = ref(false);\n    const showAdd = ref(false);\n    const nameToAdd = ref('');\n    const selectedRow = ref({});\n\n    const deleteGame = () => {\n      catalogData.value.splice(\n        catalogData.value.findIndex((i) => i.id === selectedRow.value.id),\n        1\n      );\n      showDelete.value = false;\n    };\n\n    const showDeleteDialog = (item: any) => {\n      selectedRow.value = item;\n      showDelete.value = true;\n    };\n\n    const addGame = () => {\n      catalogData.value.push({\n        id: Math.max(...catalogData.value.map((o) => o.id)) + 1,\n        name: nameToAdd.value,\n      });\n      nameToAdd.value = '';\n      showAdd.value = false;\n    };\n\n    return {\n      catalogData,\n      columns: columns,\n      pagination: {\n        page: 1,\n        rowsPerPage: 0, // 0 means all rows\n      },\n      showDelete,\n      showAdd,\n      nameToAdd,\n      showDeleteDialog,\n      deleteGame,\n      addGame,\n    };\n  },\n});\n</script>\n
    "},{"location":"develop/basic/vuejs/#anadir-fila","title":"A\u00f1adir fila","text":"

    Para esto hemos necesitado el primer template dentro del componente tabla para mostrar un bot\u00f3n que har\u00e1 que se muestre un dialog para introducir el nombre del juego que es el \u00faltimo q-dialog mostrado en el componente. Tanto al pulsar en el bot\u00f3n como al pulsar Enter se ejecutar\u00e1 la funci\u00f3n para a\u00f1adirlo llamada addGame, que se encarga de a\u00f1adirlo poni\u00e9ndole un id superior a cualquiera de los ya creados, el nombre seleccionado almacenado en la variable nameToAdd y de dejar de mostrar el dialog una vez realizado el proceso.

    "},{"location":"develop/basic/vuejs/#editar-fila","title":"Editar fila","text":"

    Para esto hemos necesitado el segundo template de dentro del componente (a excepci\u00f3n del \u00faltimo q-td). Este hace que cuando sea la columna id simplemente muestre su valor, pero en cambio cuando sea la del nombre, en caso de que se pulse sobre esa casilla se muestre un dialog con un campo de texto con el valor de la casilla pulsada.

    "},{"location":"develop/basic/vuejs/#borrar-fila","title":"Borrar fila","text":"

    Por \u00faltimo, para el borrado hemos necesitado el q-td con la key de options para mostrar un bot\u00f3n para ejecutar la funci\u00f3n showDeleteDialog pas\u00e1ndole el item completo de la fila seleccionada, este hace que se muestre el dialog y se almacene el item seleccionado y por \u00faltimo el dialog se encarga de realizar la pregunta de confirmaci\u00f3n para su posterior borrado. En caso de confirmarlo, la funci\u00f3n deleteGame busca la posici\u00f3n del item seleccionado y lo borra. Una vez hecho eso, limpia el valor de fila seleccionada y deja de mostrar el dialog.

    "},{"location":"develop/basic/vuejs/#conexion-con-backend","title":"Conexi\u00f3n con backend","text":"

    Antes de nada, para poder realizar peticiones vamos a tener que instalar: @vueuse/core.

    "},{"location":"develop/basic/vuejs/#recuperacion-de-datos","title":"Recuperaci\u00f3n de datos","text":"

    Vamos a proceder a modificar lo m\u00ednimo e indispensable para que los datos mostrados no sean los mockeados y vengan del back mediante esta petici\u00f3n:

    const { data } = useFetch('http://localhost:8080/game').get().json();\nwhenever(data, () => (catalogData.value = data.value));\n

    Tambi\u00e9n tendremos que modificar los campos a mostrar, ya que ya no es name, si no title el nombre del juego. Y tambi\u00e9n habr\u00e1 que mostrar la edad, la categor\u00eda y el autor.

    "},{"location":"develop/basic/vuejs/#edicion-de-una-fila","title":"Edici\u00f3n de una fila","text":"

    Solo modificaremos los campos referidos al juego (de momento) para que sea lo m\u00e1s sencillo posible, es decir, solo se modificar\u00e1 el t\u00edtulo y la edad tal y como lo hab\u00edamos hecho antes con el q-popup-edit.

    "},{"location":"develop/basic/vuejs/#creacion-de-una-nueva-fila","title":"Creaci\u00f3n de una nueva fila","text":"

    Ya que no tenemos en el back de Node realizado el back necesario para poder borrar una fila, terminaremos con el a\u00f1adido de una nueva fila.

    Para esto, tendremos que modificar la funci\u00f3n para a\u00f1adir, adem\u00e1s de eliminar la variable nameToAdd y modificar el dialog. As\u00ed deber\u00eda quedar la funci\u00f3n:

    const addGame = async () => {\n  const response = await useFetch('http://localhost:8080/game', {\n    method: 'PUT',\n    redirect: 'manual',\n    headers: {\n      accept: '*/*',\n      origin: window.origin,\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify(gameToAdd.value),\n  })\n    .put()\n    .json();\n\n  getGames();\n  gameToAdd.value = newGame;\n};\n

    Y as\u00ed el dialog:

    <q-dialog v-model=\"showAdd\">\n      <q-card style=\"width: 300px\" class=\"q-px-sm q-pb-md\">\n        <q-card-section>\n          <div class=\"text-h6\">Nuevo juego</div>\n        </q-card-section>\n\n        <q-item-label header>T\u00edtulo</q-item-label>\n        <q-item dense>\n          <q-item-section avatar>\n            <q-icon name=\"sports_esports\" />\n          </q-item-section>\n          <q-item-section>\n            <q-input dense v-model=\"gameToAdd.title\" autofocus />\n          </q-item-section>\n        </q-item>\n\n        <q-item-label header>Edad</q-item-label>\n        <q-item dense>\n          <q-item-section avatar>\n            <q-icon name=\"cake\" />\n          </q-item-section>\n          <q-item-section>\n            <q-slider\n              color=\"teal\"\n              v-model=\"gameToAdd.age\"\n              :min=\"0\"\n              :max=\"100\"\n              :step=\"1\"\n              label\n              label-always\n            />\n          </q-item-section>\n        </q-item>\n\n        <q-item-label header>Categor\u00eda</q-item-label>\n        <q-item dense>\n          <q-item-section avatar>\n            <q-icon name=\"category\" />\n          </q-item-section>\n          <q-item-section>\n            <q-select\n              name=\"category\"\n              v-model=\"gameToAdd.category.id\"\n              :options=\"categories\"\n              filled\n              clearable\n              emit-value\n              map-options\n              option-disable=\"inactive\"\n              option-value=\"id\"\n              option-label=\"name\"\n              color=\"primary\"\n              label=\"Category\"\n            />\n          </q-item-section>\n        </q-item>\n\n        <q-item-label header>Autor</q-item-label>\n        <q-item dense>\n          <q-item-section avatar>\n            <q-icon name=\"face\" />\n          </q-item-section>\n          <q-item-section>\n            <q-select\n              name=\"author\"\n              v-model=\"gameToAdd.author.id\"\n              :options=\"authors\"\n              filled\n              clearable\n              emit-value\n              map-options\n              option-disable=\"inactive\"\n              option-value=\"id\"\n              option-label=\"name\"\n              color=\"primary\"\n              label=\"Author\"\n            />\n          </q-item-section>\n        </q-item>\n\n        <q-card-actions align=\"right\" class=\"text-primary\">\n          <q-btn flat label=\"Cancelar\" v-close-popup />\n          <q-btn flat label=\"A\u00f1adir juego\" v-close-popup @click=\"addGame\" />\n        </q-card-actions>\n      </q-card>\n    </q-dialog>\n
    "},{"location":"develop/basic/vuejs/#ultimo-paso","title":"\u00daltimo paso","text":"

    Este resultado vamos a copiarlo y pegarlo en las pantallas de Categor\u00eda y Autor para que tengamos exactamente el mismo formato cambiando todo donde diga \u201cjuego\u201d o \u201cgame\u201d por su traducci\u00f3n a \u201ccategor\u00eda\u201d o \u201cautor\u201d.

    "},{"location":"develop/basic/vuejs/#ejercicio","title":"Ejercicio","text":"

    Al realizar el cambio descrito anteriormente podremos ver que no todo funciona, ya que el objeto que se env\u00eda para modificar no ser\u00eda correcto adem\u00e1s de que las tablas de Categor\u00eda y Autor s\u00ed que tienen una funci\u00f3n para poder borrar esas filas.

    El ejercicio se va a realizar en la pantalla de Categor\u00eda. Consta en, despu\u00e9s de haber realizado todos los cambios, hacer que a\u00f1ada, edite y borre las filas seg\u00fan sea necesario.

    El c\u00f3digo resultante deber\u00eda ser algo parecido al siguiente c\u00f3digo:

    <template>\n  <q-page padding>\n    <q-table\n      hide-bottom\n      :rows=\"categoriesData\"\n      :columns=\"columns\"\n      v-model:pagination=\"pagination\"\n      title=\"Cat\u00e1logo\"\n      class=\"my-sticky-header-table\"\n      no-data-label=\"No hay resultados\"\n      row-key=\"id\"\n    >\n      <template v-slot:top>\n        <q-btn flat round color=\"primary\" icon=\"add\" @click=\"showAdd = true\" />\n      </template>\n      <template v-slot:body=\"props\">\n        <q-tr :props=\"props\">\n          <q-td key=\"id\" :props=\"props\">{{ props.row.id }}</q-td>\n          <q-td key=\"name\" :props=\"props\">\n            {{ props.row.name }}\n            <q-popup-edit\n              v-model=\"props.row.name\"\n              title=\"Cambiar nombre\"\n              v-slot=\"scope\"\n            >\n              <q-input\n                v-model=\"scope.value\"\n                dense\n                autofocus\n                counter\n                @keyup.enter=\"editRow(props, scope, 'name')\"\n              >\n                <template v-slot:append>\n                  <q-icon name=\"edit\" />\n                </template>\n              </q-input>\n            </q-popup-edit>\n          </q-td>\n          <q-td key=\"options\" :props=\"props\">\n            <q-btn\n              flat\n              round\n              color=\"negative\"\n              icon=\"delete\"\n              @click=\"showDeleteDialog(props.row)\"\n            />\n          </q-td>\n        </q-tr>\n      </template>\n    </q-table>\n    <q-dialog v-model=\"showDelete\" persistent>\n      <q-card>\n        <q-card-section class=\"row items-center\">\n          <q-icon\n            name=\"delete\"\n            size=\"sm\"\n            color=\"negative\"\n            @click=\"showDelete = true\"\n          />\n          <span class=\"q-ml-sm\">\n            \u00bfEst\u00e1s seguro de que quieres borrar este elemento?\n          </span>\n        </q-card-section>\n\n        <q-card-actions align=\"right\">\n          <q-btn flat label=\"Cancelar\" color=\"primary\" v-close-popup />\n          <q-btn\n            flat\n            label=\"Confirmar\"\n            color=\"primary\"\n            v-close-popup\n            @click=\"deleteCategory\"\n          />\n        </q-card-actions>\n      </q-card>\n    </q-dialog>\n    <q-dialog v-model=\"showAdd\">\n      <q-card style=\"width: 300px\" class=\"q-px-sm q-pb-md\">\n        <q-card-section>\n          <div class=\"text-h6\">Nueva categor\u00eda</div>\n        </q-card-section>\n\n        <q-item-label header>Nombre</q-item-label>\n        <q-item dense>\n          <q-item-section avatar>\n            <q-icon name=\"category\" />\n          </q-item-section>\n          <q-item-section>\n            <q-input\n              dense\n              v-model=\"categoryToAdd.name\"\n              autofocus\n              @keyup.enter=\"addCategory\"\n            />\n          </q-item-section>\n        </q-item>\n        <q-card-actions align=\"right\" class=\"text-primary\">\n          <q-btn flat label=\"Cancelar\" v-close-popup />\n          <q-btn\n            flat\n            label=\"A\u00f1adir categor\u00eda\"\n            v-close-popup\n            @click=\"addCategory\"\n          />\n        </q-card-actions>\n      </q-card>\n    </q-dialog>\n  </q-page>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from 'vue';\nimport { useFetch, whenever } from '@vueuse/core';\n\nconst columns = [\n  { name: 'id', align: 'left', label: 'ID', field: 'id', sortable: true },\n  {\n    name: 'name',\n    align: 'left',\n    label: 'Nombre',\n    field: 'name',\n    sortable: true,\n  },\n  { name: 'options', align: 'left', label: 'Options', field: 'options' },\n];\nconst pagination = {\n  page: 1,\n  rowsPerPage: 0,\n};\nconst newCategory = {\n  name: '',\n  id: '',\n};\n\nconst categoriesData = ref([]);\nconst showDelete = ref(false);\nconst showAdd = ref(false);\nconst selectedRow = ref({});\nconst categoryToAdd = ref({ ...newCategory });\n\nconst getCategories = () => {\n  const { data } = useFetch('http://localhost:8080/category').get().json();\n  whenever(data, () => (categoriesData.value = data.value));\n};\ngetCategories();\n\nconst showDeleteDialog = (item: any) => {\n  selectedRow.value = item;\n  showDelete.value = true;\n};\n\nconst addCategory = async () => {\n  await useFetch('http://localhost:8080/category', {\n    method: 'PUT',\n    redirect: 'manual',\n    headers: {\n      accept: '*/*',\n      origin: window.origin,\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify(categoryToAdd.value),\n  })\n    .put()\n    .json();\n\n  getCategories();\n  categoryToAdd.value = newCategory;\n  showAdd.value = false;\n};\n\nconst editRow = (props: any, scope: any, field: any) => {\n  const row = {\n    name: props.row.name,\n  };\n  row[field] = scope.value;\n  scope.set();\n  editCategory(props.row.id, row);\n};\n\nconst editCategory = async (id: string, reqBody: any) => {\n  await useFetch(`http://localhost:8080/category/${id}`, {\n    method: 'PUT',\n    redirect: 'manual',\n    headers: {\n      accept: '*/*',\n      origin: window.origin,\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify(reqBody),\n  })\n    .put()\n    .json();\n\n  getCategories();\n};\n\nconst deleteCategory = async () => {\n  await useFetch(`http://localhost:8080/category/${selectedRow.value.id}`, {\n    method: 'DELETE',\n    redirect: 'manual',\n    headers: {\n      accept: '*/*',\n      origin: window.origin,\n      'Content-Type': 'application/json',\n    },\n  })\n    .delete()\n    .json();\n\n  getCategories();\n};\n</script>\n
    "},{"location":"develop/basic/vuejs/#depuracion","title":"Depuraci\u00f3n","text":"

    Una parte muy importante del desarrollo es tener la capacidad de depurar nuestro c\u00f3digo, en este apartado vamos a explicar como se realiza debug en Front.

    Esta parte se puede realizar con nuestro navegador favorito, en este caso vamos a utilizar Chrome.

    El primer paso es abrir las herramientas del desarrollador del navegador presionando F12.

    En esta herramienta tenemos varias partes importantes:

    • Elements: Inspector de los elementos del DOM de nuestra aplicaci\u00f3n que nos ayuda identificar el c\u00f3digo generado.
    • Console: Consola donde podemos ver mensajes importantes que nos ayudan a identificar posibles problemas.
    • Source: El navegador de ficheros que componen nuestra aplicaci\u00f3n.
    • Network: El registro de peticiones que realiza nuestra aplicaci\u00f3n.

    Identificados los elementos importantes, vamos a depurar la operaci\u00f3n de crear categor\u00eda.

    Para ello nos dirigimos a la pesta\u00f1a de Source, en el \u00e1rbol de carpetas nos dirigimos a la ruta donde est\u00e1 localizado el c\u00f3digo de nuestra aplicaci\u00f3n webpack://src/app.

    Dentro de esta carpeta est\u00e9 localizado todo el c\u00f3digo fuente de la aplicaci\u00f3n, en nuestro caso vamos a localizar componente category-edit.component que crea una nueva categor\u00eda.

    Dentro del fichero ya podemos a\u00f1adir puntos de ruptura (breakpoint), en nuestro caso queremos comprobar que el nombre introducido se captura bien y se env\u00eda al service correctamente.

    Colocamos el breakpoint en la l\u00ednea de invocaci\u00f3n del service (click sobre el n\u00famero de la l\u00ednea) y desde la interfaz creamos una nueva categor\u00eda.

    Hecho esto, podemos observar que a nivel de interfaz, la aplicaci\u00f3n se detiene y aparece un panel de manejo de los puntos de interrupci\u00f3n:

    En cuanto a la herramienta del desarrollador nos lleva al punto exacto donde hemos a\u00f1adido el breakpoint y se para en este punto ofreci\u00e9ndonos la posibilidad de explorar el contenido de las variables del c\u00f3digo:

    Aqu\u00ed podemos comprobar que efectivamente la variable category tiene el valor que hemos introducido por pantalla y se propaga correctamente hacia el service.

    Para continuar con la ejecuci\u00f3n basta con darle al bot\u00f3n de play del panel de manejo de interrupci\u00f3n o al que aparece dentro de la herramienta de desarrollo (parte superior derecha).

    Por \u00faltimo, vamos a revisar que la petici\u00f3n REST se ha realizado correctamente al backend, para ello nos dirigimos a la pesta\u00f1a Network y comprobamos las peticiones realizadas:

    Aqu\u00ed podemos observar el registro de todas las peticiones y haciendo click sobre una de ellas, obtenemos el detalle de esta.

    • Header: Informaci\u00f3n de las cabeceras enviadas (aqu\u00ed podemos ver que se ha hecho un PUT a la ruta correcta).
    • Payload: El cuerpo de la petici\u00f3n (vemos el cuerpo del mensaje con el nombre enviado).
    • Preview: Respuesta de la petici\u00f3n normalizada (vemos la respuesta con el identificador creado para la nueva categor\u00eda).
    "},{"location":"develop/basic/vuejsold/","title":"Listado simple - VUE","text":"

    Ahora que ya tenemos listo el proyecto frontend de VUE, ya podemos empezar a codificar la soluci\u00f3n.

    "},{"location":"develop/basic/vuejsold/#primeros-pasos","title":"Primeros pasos","text":"

    Antes de empezar

    Quiero hacer hincapi\u00e9 que VUE tiene una documentaci\u00f3n muy extensa y completa, as\u00ed que te recomiendo que hagas uso de ella cuando tengas cualquier duda. En la propia web de documentaci\u00f3n de VUE puedes buscar casi cualquier ejemplo que necesites.

    Si abrimos el proyecto con el IDE que tengamos (Visual Studio Code en el caso del tutorial) podemos ver que en la carpeta src/ existen unos ficheros ya creados por defecto. Estos ficheros son:

    • App.vue \u2192 contiene el c\u00f3digo inicial del proyecto.
    • main.ts \u2192 es el punto de entrada a la aplicaci\u00f3n.

    Lo primero que vamos a hacer es instalar SASS para poder trabajar con este preprocesador CSS, para ello tendremos que irnos a la terminal, en la misma carpeta donde tenemos el proyecto y ejecutar el siguiente comando:

    npm install -D sass\n

    Con esto ya lo tendremos instalado y para usarlo es tan f\u00e1cil como poner la etiqueta style de esta manera:

    <style lang=\"scss\"></style>  <--->  con Sass activado\n<style></style>  <--->  sin Sass, css normal\n

    En los estilos tambi\u00e9n veremos la propiedad scoped en VUE, el atributo scoped se utiliza para limitar el \u00e1mbito de los estilos de un componente a los elementos del propio componente y no a los elementos hijos o padres, lo que ayuda a evitar conflictos de estilo entre los diferentes componentes de una aplicaci\u00f3n.

    Esto significa que los estilos definidos en una etiqueta <style scoped> solo se aplicar\u00e1n a los elementos dentro del componente actual, y no se propagar\u00e1n a otros componentes en la jerarqu\u00eda del DOM. De esta manera, se puede evitar que los estilos de un componente afecten a otros componentes en la aplicaci\u00f3n.

    <style scoped></style>  <--->  Estos estilos solo afectar\u00e1n al componente donde se aplican\n<style></style>  <--->  Estos estilos son generales y afectan a toda la aplicaci\u00f3n.\n

    Con estas cositas sobre los estilos en cabeza vamos lo primero a limpiar la aplicaci\u00f3n para poder empezar a trabajar desde cero.

    • Entraremos en la carpeta assets y borraremos todos los archivos excepto base.css.
    • Entraremos en la carpeta components y borraremos todos los archivos dejando solo la carpeta que usaremos m\u00e1s adelante.
    • La carpeta router la dejaremos tal cual esta, sin tocar nada.
    • Entraremos en la carpeta views y borraremos todos los archivos dejando solo la carpeta que usaremos m\u00e1s adelante.

    Con esto tenemos nuestra estructura preparada y quedar\u00eda tal que asi:

    Vamos a a\u00f1adir unas l\u00edneas al tsconfig.json para que el typescript deje de marcarnos lo como error, lo dejaremos asi:

    tsconfig.json
    {\n  \"extends\": \"@vue/tsconfig/tsconfig.web.json\",\n  \"include\": [\"env.d.ts\", \"src/**/*\", \"src/**/*.vue\"],\n  \"compilerOptions\": {\n    \"preserveValueImports\": false,\n    \"importsNotUsedAsValues\": \"remove\",\n    \"verbatimModuleSyntax\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.node.json\"\n    }\n  ]\n}\n

    Para que la aplicaci\u00f3n funcione de nuevo y poder empezar a trabajar faltar\u00eda hacer un par de cositas que os explico:

    • En \u00e9l base.css no hace falta cambiar nada para que funcione, pero tenemos muchas cosas que seguramente no vamos a usar, este archivo lo conservamos solamente para trabajar en variables css todo el tema de los colores de nuestra web o algunas otras cositas como el ancho del menu o del header, etc\u2026 Lo primero vamos a eliminar todas las variables CSS y crearnos las nuestras propias con nuestro color primario y secundario tanto para botones y dem\u00e1s como para texto y tambi\u00e9n para el background principal. Tenemos que dejar nuestro archivo de esta manera:
    base.css
    :root {\n  --primary: #2a6fa8;\n  --secondary: #12abdb;\n  --text-ligth: #2c3e50;\n  --text-dark: #fff;\n  --background-color: #fff;\n}\n\n*,\n*::before,\n*::after {\n  box-sizing: border-box;\n  margin: 0;\n  position: relative;\n  font-weight: normal;\n}\n\nbody {\n  min-height: 100vh;\n  color: var(--text-ligth);\n  background: var(--background-color);\n  line-height: 1.6;\n  font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,\n    Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;\n  font-size: 16px;\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n
    • Despu\u00e9s vamos a abrir el archivo main.ts y cambiaremos el import que hace del CSS por el base que es el que estamos usando, quedar\u00eda de esta manera:
    main.ts
    import { createApp } from 'vue'\nimport App from './App.vue'\nimport router from './router'\n\nimport './assets/base.css'\n\nconst app = createApp(App)\n\napp.use(router)\n\napp.mount('#app')\n
    • Luego abriremos el archivo App.vue y lo dejaremos solo como la entrada a la aplicaci\u00f3n, esto ya son maneras de trabajar de cada uno, pero a m\u00ed me gusta hacerlo asi para tener si hiciera falta diferentes layouts, uno con header y men\u00fa, otro sin header y men\u00fa, otro de la parte de admin, etc\u2026 Lo dejaremos exactamente asi:
    App.vue
    <script setup lang=\"ts\">\nimport { RouterView } from 'vue-router'\n</script>\n\n<template>\n    <RouterView />\n</template>\n
    • Por \u00faltimo crearemos nuestro layout principal al que iremos a\u00f1adiendo luego toda nuestra aplicaci\u00f3n. Lo primero nos pondremos en src y crearemos una nueva carpeta llamada layouts, dentro de esta carpeta crearemos otra que se llamara main-layout (esto lo hacemos por si luego tenemos m\u00e1s de un layout que cada uno tenga su carpeta para tener sus propias cosas) y dentro de la carpeta main-layout crearemos el archivo MainLayout.vue, nos deber\u00eda de quedar asi:

    Una vez tenemos el archivo MainLayout.vue creado lo abriremos y escribiremos el siguiente c\u00f3digo:

    MainLayout.vue
    <script setup lang=\"ts\">\n    const helloWorld = 'Hola Mundo';\n</script>\n\n<template>\n    <h1>{{ helloWorld }}</h1>\n</template>\n

    Vamos a intentar explicar este c\u00f3digo un poco:

    • Dentro de las etiquetas script metemos todo el c\u00f3digo Javascript, en este caso como vamos a trabajar con Typescript le ponemos la etiqueta Lang=\u201dts\u201d para que el compilador sepa que estamos trabajando con Typescript.
    • Ponemos la palabra setup porque estamos trabajando con la composition api, en VUE podemos trabajar con la options api y con la composition api, nosotros vamos a usar la composition api que aunque al principio cuesta un poco m\u00e1s, luego nos va a hacer la vida much\u00edsimo m\u00e1s f\u00e1cil, sobre todo en aplicaciones \"reales\".
    • Dentro de las etiquetas template va el HTML y como estamos usando el m\u00e9todo setup no necesitamos retornar nada para poder acceder a ello desde la plantilla.

    Las llaves dobles permiten hacen un binding entre el c\u00f3digo del componente y la plantilla. Es decir, en este caso ir\u00e1 al c\u00f3digo TypeScript y buscar\u00e1 el valor de la variable helloWorld.

    Consejo

    El binding tambi\u00e9n nos sirve para ejecutar los m\u00e9todos de TypeScript desde el c\u00f3digo HTML. Adem\u00e1s, si el valor que contiene la variable se modificar\u00e1 durante la ejecuci\u00f3n de alg\u00fan m\u00e9todo, autom\u00e1ticamente el c\u00f3digo HTML refrescar\u00eda el nuevo valor de la variable helloWorld.

    Ponemos en marcha la aplicaci\u00f3n con npm run dev.

    Si abrimos el navegador y accedemos a http://localhost:5173/ podremos ver el resultado del c\u00f3digo.

    "},{"location":"develop/basic/vuejsold/#layout-general","title":"Layout general","text":""},{"location":"develop/basic/vuejsold/#crear-componente","title":"Crear componente","text":"

    Lo primero que vamos a hacer es escoger un tema y una paleta de componentes para trabajar. VUE no tiene una librer\u00eda de componentes oficial al igual que, por ejemplo, Angular tiene Material, por lo que podremos elegir entre las diferentes opciones y ver la que m\u00e1s se ajusta a las necesidades del proyecto o crearnos la nuestra propia, si entramos en proyectos ya comenzados, seguramente este paso ya habr\u00e1 sido abordado y ya sabr\u00e1s con qu\u00e9 librer\u00eda de componentes trabajar, para este proyecto vamos a optar por PrimeVue, no tenemos ning\u00fan motivo especial para decidir esa en especial, pero la hemos usado en un curso anterior y optamos por seguir con la misma librer\u00eda.

    Para instalarla bastar\u00e1 con seguir los pasos de su documentaci\u00f3n.

    Vamos a hacerlo y la instalamos en nuestro proyecto:

    • Lo primero ejecutaremos el comando npm o yarn para instalarla, en mi caso lo hare con npm:
    • npm install primevue\n
    • Despu\u00e9s instalaremos PrimeVue con la funci\u00f3n use en el main.ts que es donde tenemos nuestra configuraci\u00f3n, quedando asi nuestro main.ts:

    • Despu\u00e9s a\u00f1adiremos los estilos necesarios a nuestro main.ts:

    • Por \u00faltimo en nuestro base.css cambiaremos la fuente del proyecto por la que trae el tema de PrimeVue, cambiando en el body la l\u00ednea:
    base.css
    ...\n font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,\n Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;\n...\n

    Por:

    base.css
    ...\nfont-family: (--font-family);\n...\n

    Recuerda

    Al a\u00f1adir una nueva librer\u00eda tenemos que parar el servidor y volver a arrancarlo para que compile y pre-cargue las nuevas dependencias.

    Una vez a\u00f1adida la dependencia, lo que queremos es crear una primera estructura inicial a la p\u00e1gina. Si te acuerdas cual era la estructura (y si no te acuerdas, vuelve a la secci\u00f3n Contexto de la aplicaci\u00f3n y lo revisas), ten\u00edamos una cabecera superior con un logo y t\u00edtulo y unas opciones de men\u00fa.

    Antes de empezar a crear y programar vamos a instalar unas extensiones en Visual Studio Code que nos har\u00e1n la vida mucho mas f\u00e1cil, en cada una de ellas podeis ver una descripci\u00f3n de que hacen y para que sirven, tu ya dices si la quieres instalar o no, nosotros vamos a trabajar con ellas y por eso te las recomendamos:

    • Vue Volar extension Pack
    • Vue Discovery
    • IntelliCode
    • npm Intellisense
    • Vue VSCode Snippets

    Para poder seguir trabajando con comodidad vamos a a\u00f1adir una fuente de iconos para todos los iconitos que usemos en la aplicaci\u00f3n, nosotros vamos a usar Material porque es la que estamos acostumbrados, para a\u00f1adirla tenemos una gu\u00eda.

    Lo haremos paso a paso:

    • Lo primero a\u00f1adimos al index.html la fuente a trav\u00e9s de Google fonts, hay muchas otras maneras de hacerlo, como bajarla y servirla desde local, pero para este tutorial vamos a usar esta por ser la m\u00e1s f\u00e1cil, para a\u00f1adirla pegaremos en el index.html esta l\u00ednea:
    <link href=\"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\" rel=\"stylesheet\" />\n

    Quedando de esta manera:

    index.html
    <!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" href=\"/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <link\n      href=\"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\"\n      rel=\"stylesheet\"\n    />\n    <title>Vite App</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/main.ts\"></script>\n  </body>\n</html>\n

    Para que no nos salga el error de comments, a\u00f1adiremos al eslintrc.js estas l\u00edneas:

    eslintrc.js
    ...\nrules: {\n    'vue/comment-directive': 'off'\n}\n...\n

    Despu\u00e9s nos iremos al fichero base.css y a\u00f1adiremos esto al final del archivo:

    base.css
    ...\n.material-symbols-outlined {\n  font-family: \"Material Symbols Outlined\", sans-serif;\n  font-weight: normal;\n  font-style: normal;\n  font-size: 24px;  /* Preferred icon size */\n  display: inline-block;\n  line-height: 1;\n  text-transform: none;\n  letter-spacing: normal;\n  word-wrap: normal;\n  white-space: nowrap;\n  direction: ltr;\n}\n

    Con esto ya tendremos a\u00f1adida la fuente material-symbols y podremos usar todos los iconos disponibles.

    • Despu\u00e9s instalaremos tambi\u00e9n los iconos de PrimeVue para poder usarlos f\u00e1cilmente en los componentes, lo primero pondremos:
    npm install primeicons\n

    Una vez instalados, importaremos los iconos en el main.ts poniendo este import debajo de todos los de css:

    main.ts
    ...\nimport 'primeicons/primeicons.css';\n...\n

    Con esto ya lo tendr\u00edamos todo.

    Pues vamos a ello, con las extensiones ya instaladas y la fuente para los iconos a\u00f1adida crearemos esa estructura com\u00fan para toda la aplicaci\u00f3n.

    Lo primero crearemos el componente header, dentro de la carpeta components al ser un m\u00f3dulo de la aplicaci\u00f3n y no especifico de una vista o p\u00e1gina. Para eso crearemos una nueva carpeta dentro de components que llamaremos header, nos situaremos encima de la carpeta header y crearemos el archivo HeaderComponent.vue, con el archivo vac\u00edo escribiremos, vbase-3-ts-setup y conforme lo escribimos nos aparecer\u00e1 esto:

    Consejo

    Esto nos aparece gracias a las extensiones que hemos instalado, aseg\u00farate de instalarlas para que aparezca o si no las quieres instalar lo puedes crear a mano. Si no te aparece y has instalado las extensiones, cierra vscode y vu\u00e9lvelo a abrir.

    Podemos seleccionar vbase-3-ts-setup, esto es un snippet que lo que har\u00e1 es generarnos todo el c\u00f3digo de un componente vac\u00edo y lo dejara asi:

    HeaderComponent.vue
    <template>\n    <div>\n\n    </div>\n</template>\n\n<script setup lang=\"ts\">\n\n</script>\n\n<style scoped>\n\n</style>\n

    Con esto solo nos faltar\u00eda agregar a la etiqueta style que vamos a trabajar con Sass y la dejar\u00edamos asi:

    HeaderComponent.vue
    ...\n<style lang=\"scss\" scoped>\n\n</style>\n...\n

    Si os dais cuenta hemos a\u00f1adido Lang=\u201dscss\u201d y con esto ya estamos preparados para crear nuestro componente.

    Para continuar cambiaremos el c\u00f3digo del HeaderComponent.vue por este:

    HeaderComponent.vue
    <template>\n  <div class=\"card relative z-2\">\n    <Menubar :model=\"items\">\n      <template #start>\n        <span class=\"material-symbols-outlined\">storefront</span>\n        <span class=\"title\">LUDOTECA TAN</span>\n      </template>\n      <template #end>\n        <Avatar icon=\"pi pi-user\" class=\"mr-2 avatar-image\" size=\"large\" shape=\"circle\" />\n        <span class=\"sign-text\">Sign in</span>\n      </template>\n    </Menubar>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport Menubar from \"primevue/menubar\";\nimport Avatar from \"primevue/avatar\";\n\nconst items = ref([\n  {\n    label: \"Cat\u00e1logo\",\n  },\n  {\n    label: \"Categor\u00edas\",\n  },\n  {\n    label: \"Autores\",\n  },\n]);\n</script>\n\n<style lang=\"scss\" scoped>\n.p-menubar {\n  padding: 0.5rem;\n  background: var(--primary);\n  color: var(--text-dark);\n  border: none;\n  border-radius: 0px;\n}\n\n.title {\n  margin-left: 1rem;\n  font-weight: 600;\n}\n\n.avatar-image {\n  background-color: var(--secondary);\n  color: var(--text-dark);\n  border: 1px solid var(--text-dark);\n  cursor: pointer;\n}\n\n.sign-text {\n  color: var(--text-dark);\n  margin-left: 1rem;\n  cursor: pointer;\n}\n\n:deep(.p-menubar-start) {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: center;\n  margin-right: 1rem;\n}\n\n:deep(.p-menubar-end) {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: center;\n}\n\n:deep(.p-menuitem-text) {\n  color: var(--text-dark) !important;\n}\n\n:deep(.p-menuitem-content:hover) {\n  background: var(--secondary) !important;\n}\n\n.material-symbols-outlined {\n  font-size: 36px;\n}\n</style>\n

    Intentaremos explicarlo un poco:

    En el template estamos a\u00f1adiendo el Menubar de la librer\u00eda de componentes que estamos utilizando, si queremos saber como se a\u00f1ade podemos verlo en este link.

    Veremos que lo primero que hacemos es el import dentro de las etiquetas <script> para poder tener el componente disponible y poder usarlo.

    HeaderComponent.vue
    ...\nimport Menubar from \"primevue/menubar\";\n...\n

    Luego, con el import ya hecho, podemos copiar el HTML que nos dan y ponerlo en nuestro componente:

    HeaderComponent.vue
    ...\n<div class=\"card relative z-2\">\n    <Menubar :model=\"items\">\n        <template #start>\n            <span class=\"material-symbols-outlined\">storefront</span>\n            <span class=\"title\">LUDOTECA TAN</span>\n        </template>\n        <template #end>\n            <Avatar icon=\"pi pi-user\" class=\"mr-2 avatar-image\" size=\"large\" shape=\"circle\" />\n            <span class=\"sign-text\">Sign in</span>\n        </template>\n    </Menubar>\n</div>\n...\n

    Si os dais cuenta es el c\u00f3digo que ellos nos dan retocado para cubrir nuestras necesidades, primero hemos metido un icono de material dentro del template #start que es lo que se situara al principio pegado a la izquierda del Menubar y tras el icono metemos el t\u00edtulo.

    El template #end se situar\u00e1 al final pegado a la derecha y alli estamos metiendo otro componente de la librer\u00eda de componentes, pod\u00e9is ver la info de como usarlo en este link.

    Este simplemente lo pegamos como esta y le a\u00f1adimos detr\u00e1s la frase Sign in.

    En la parte del script metemos todo nuestro Javascript/Typescript:

    HeaderComponent.vue
    ...\n<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport Menubar from \"primevue/menubar\";\nimport Avatar from \"primevue/avatar\";\n\nconst items = ref([\n  {\n    label: \"Cat\u00e1logo\",\n  },\n  {\n    label: \"Categor\u00edas\",\n  },\n  {\n    label: \"Autores\",\n  },\n]);\n</script>\n...\n

    Si os dais cuenta, lo \u00fanico que estamos haciendo son los imports necesarios para que todo funcione y creando una variable \u00edtems que es la que luego estamos usando en el men\u00fa para pintar los diferentes menus. Si os dais cuenta envolvemos el valor de la variable dentro de ref(). En Vue 3, la funci\u00f3n ref() se utiliza para crear una referencia reactiva a un valor. Una referencia reactiva es un objeto que puede ser pasado como prop, utilizado en una plantilla, y observado para detectar cambios en su valor.

    La funci\u00f3n ref() toma un valor como argumento y devuelve un objeto con una propiedad value que contiene el valor proporcionado. Por ejemplo, si queremos crear una referencia a un n\u00famero entero, podemos hacer lo siguiente:

    import { ref } from 'vue'\nconst myNumber = ref(42)\nconsole.log(myNumber.value) // 42\n

    La referencia myNumber es ahora un objeto con una propiedad value que contiene el valor 42. Si cambiamos el valor de la propiedad value, la referencia notificar\u00e1 a cualquier componente que est\u00e9 observando el valor que ha cambiado. Por ejemplo:

    myNumber.value = 21\nconsole.log(myNumber.value) // 21\n

    Cualquier componente que est\u00e9 utilizando myNumber se actualizar\u00e1 autom\u00e1ticamente para reflejar el nuevo valor. La funci\u00f3n ref() es muy \u00fatil en Vue 3 para crear referencias reactivas a valores que pueden cambiar con el tiempo.

    En los styles tenemos poco que explicar, simplemente estamos haciendo que se vea como nosotros queremos, que todos los colores y dem\u00e1s los traemos de las variables que hemos creado antes en el base.css y adem\u00e1s me gustar\u00eda mencionar una cosa:

    HeaderComponent.vue
    ...\n:deep(.p-menubar-start) {\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    justify-content: center;\n    margin-right: 1rem;\n}\n...\n

    Si os dais cuenta algunos estilos llevan el :Deep delante, como seguro ya sabes, puedes utilizar el atributo scoped dentro de la etiqueta <style> para escribir CSS y as\u00ed impedir que tus estilos afecten a posibles sub-componentes. Pero, \u00bfqu\u00e9 ocurre si necesitas que al menos una regla s\u00ed afecte a tu componente hijo?. Para ello puedes usar la pseudo-clase :deep de Vue 3.

    En este ejemplo lo hemos creado asi para que sepas de su existencia y busques un poco de informaci\u00f3n sobre ella y las otras que existen, este CSS lo podr\u00edamos poner en el styles.scss principal y no tendr\u00edamos que poner el :deep que seria lo mas recomendado. Es importante tener en cuenta que la directiva :deep puede tener un impacto en el rendimiento, ya que Vue necesita buscar en todo el \u00e1rbol de elementos para aplicar los estilos. Por lo tanto, se recomienda utilizar esta directiva con moderaci\u00f3n y solo en casos en los que sea necesario seleccionar elementos anidados de forma din\u00e1mica. Tenerlo en cuenta y solo usarla cuando de verdad sea necesario.

    Ya por \u00faltimo nos iremos a nuestro MainLayout.vue y a\u00f1adiremos el header que acabamos de crearnos:

    MainLayout.vue
    <script setup lang=\"ts\">\n    import HeaderComponent from '@/components/header/HeaderComponent.vue';\n\n    const helloWorld = 'Hola Mundo';\n</script>\n\n<template>\n    <HeaderComponent></HeaderComponent>\n    <h1>{{ helloWorld }}</h1>\n</template>\n

    Como antes, lo \u00fanico que hacemos es importar el componente en el script y usarlo en el HTML.

    Lo siguiente iremos a la carpeta router, al archivo index.ts y lo dejaremos asi:

    index.ts
    import { createRouter, createWebHistory } from 'vue-router'\nimport MainLayout from '@/layouts/main-layout/MainLayout.vue'\n\nconst router = createRouter({\n    history: createWebHistory(import.meta.env.BASE_URL),\n    routes: [\n        {\n            path: '/',\n            name: 'home',\n            component: MainLayout\n        }\n    ]\n})\n\nexport default router\n

    Hemos cambiado la ruta principal para que apunte a nuestro layout y nada m\u00e1s entrar en la aplicaci\u00f3n lo carguemos gracias al router de VUE.

    Si guardamos todo y ponemos en marcha el proyecto ya veremos algo como esto:

    "},{"location":"develop/basic/vuejsold/#creando-un-listado-basico","title":"Creando un listado b\u00e1sico","text":""},{"location":"develop/basic/vuejsold/#crear-componente_1","title":"Crear componente","text":"

    Ya tenemos la estructura principal, ahora vamos a crear nuestra primera pantalla. Vamos a empezar por la de Categor\u00edas que es la m\u00e1s sencilla, ya que se trata de un listado, que muestra datos sin filtrar ni paginar.

    Como categor\u00edas es un dominio funcional de la aplicaci\u00f3n, vamos a crear una nueva carpeta dentro de la carpeta views llamada categories, todas las pantallas, componentes y servicios que creemos, referidos a este dominio funcional, deber\u00e1n ir dentro del m\u00f3dulo categories. Dentro de esa carpeta crearemos un fichero que se llamara CategoriesView.vue y dentro nos crearemos el esqueleto de la misma manera que hicimos anteriormente.

    Escribiremos vbase-3-ts-setup, le daremos al enter y nos generara toda la estructura a la que solo faltara agregar a la etiqueta <style> Lang=\u201dscss\u201d para decirle que vamos a trabajar con SASS. Con esto tenemos nuestra vista preparada para empezar a trabajar.

    Lo primero vamos a conectar nuestro componente al router para que cuando hagamos click en el men\u00fa correspondiente podamos llegar hasta \u00e9l y tambi\u00e9n para poder ver lo que vamos trabajando. Para ello lo primero que vamos a hacer en el template de nuestro componente es a\u00f1adir cualquier cosa para saber que estamos donde toca, por ejemplo:

    CategoriesView.vue
    <template>\n  <div>SOY CATEGORIAS</div>\n</template>\n

    Con esto cuando entremos en la ruta de categor\u00edas deber\u00edamos ver SOY CATEGORIAS.

    Lo siguiente crearemos en el layout un sitio para cargar todas nuestras rutas que van a ir dentro de ese layout, para ello iremos al archivo MainLayout.vue y a\u00f1adiremos un <RouterView /> que ser\u00e1 el segundo de nuestra aplicaci\u00f3n, el primero lo tenemos en el App.vue que servir\u00e1 para cargar nuestras rutas principales (diferentes layouts, pagina 404, etc) y el segundo es este que acabamos de crear, podemos tener tantos como queramos en una aplicaci\u00f3n y cada uno tendr\u00e1 su cometido. Este que acabamos de crear ser\u00e1 donde se cargaran todas las rutas que quieran estar dentro del layout principal.

    Para crearlo importaremos \u00e9l RouterView dentro de los <script> desde vue-router:

    MainLayout.vue
    import { RouterView } from 'vue-router';\n

    Lo a\u00f1adiremos dentro de los <template> exactamente donde queramos cargar las rutas y si puede ser con un div padre que haga de contenedor asi podremos darle los estilos sin sufrir demasiado.

    MainLayout.vue
    <div class=\"outlet-container\">\n    <RouterView />\n</div>\n

    Y luego dentro de <style> le daremos estilo al contenedor padre de acuerdo a lo que necesitemos (grid, flex, etc\u2026) en este ejemplo para hacerlo f\u00e1cil lo haremos con flex, con todo esto quedar\u00eda asi:

    MainLayout.vue
    <script setup lang=\"ts\">\nimport { RouterView } from 'vue-router';\nimport HeaderComponent from \"@/components/header/HeaderComponent.vue\";\n</script>\n\n<template>\n  <HeaderComponent></HeaderComponent>\n  <div class=\"outlet-container\">\n    <RouterView />\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n.outlet-container {\n  display: flex;\n  flex-direction: column;\n  flex-grow: 1;\n  width: 100%;\n  min-height: calc(100vh - 65px);\n  padding: 1rem;\n}\n</style>\n

    Ahora vamos a a\u00f1adirlo a nuestras rutas, para ello nos vamos a la carpeta router y dentro tendremos el index.ts con nuestras rutas actuales, vamos a a\u00f1adir la nueva ruta como hija de layout para que siempre se muestre dentro del layout que hemos creado con \u00e9l header:

    index.ts
    import { createRouter, createWebHistory } from 'vue-router'\nimport MainLayout from '@/layouts/main-layout/MainLayout.vue'\n\nconst router = createRouter({\n    history: createWebHistory(import.meta.env.BASE_URL),\n    routes: [\n        {\n            path: '/',\n            name: 'home',\n            component: MainLayout,\n            children: [\n                {\n                    path: '/categories',\n                    name: 'categories',\n                    component: () => import('../views/categories/CategoriesView.vue')\n                }\n            ]\n        }\n    ]\n})\n\nexport default router\n

    Si os dais cuenta lo hemos a\u00f1adido como hijo de layout y adem\u00e1s lo hemos hecho con lazy loading, es decir, este componente solo se cargara cuando el usuario navegue a esa ruta, asi evitamos cargas much\u00edsimo m\u00e1s grandes al inicio de la aplicaci\u00f3n.

    Posteriormente nos iremos al HeaderComponent.vue y a\u00f1adiremos la ruta a los \u00edtems del men\u00fa de esta manera:

    HeaderComponent.vue
    const items = ref([\n    {\n        label: \"Cat\u00e1logo\",\n    },\n    {\n        label: \"Categor\u00edas\",\n        to: { name: 'categories'}\n    },\n    {\n        label: \"Autores\",\n    },\n]);\n

    Si nos fijamos hemos a\u00f1adido la navegaci\u00f3n por el nombre de ruta en el men\u00fa categor\u00edas para que sepa cuando apretemos ese men\u00fa donde nos tiene que llevar.

    Con todo esto si ponemos en marcha nuestra aplicaci\u00f3n, ya podremos navegar haciendo click en el men\u00fa Categor\u00edas a esta nueva ruta que hemos creado y ya ver\u00edamos el SOY CATEGORIAS pero tenemos un problemilla en los menus, cuando apretamos un men\u00fa se pone el fondo gris, lo cual no nos gusta y adem\u00e1s aunque estemos en categor\u00edas si apretamos en otro men\u00fa se pone el otro gris y se quita el categor\u00edas lo cual tampoco es lo deseado ya que queremos que se quede marcado el men\u00fa donde estamos actualmente para informaci\u00f3n del usuario. Para ello nos iremos al base.css y a\u00f1adiremos al final estas l\u00edneas:

    base.css
    ...\n.router-link-active {\n    background: var(--secondary);\n    border-radius: 5px;\n}\n\n.p-menuitem.p-focus > .p-menuitem-content:not(:hover) {\n    background: transparent !important;\n}\n

    En Vue 3, la directiva router-link-active se utiliza para establecer una clase CSS en un enlace de router activo, con esto ya tendremos resuelto el problema y todo estar\u00e1 funcionando como toca y poniendo en marcha la aplicaci\u00f3n y haciendo click en el men\u00fa Categor\u00edas ya deber\u00edamos ver esto:

    "},{"location":"develop/basic/vuejsold/#codigo-de-la-pantalla","title":"C\u00f3digo de la pantalla","text":"

    Ahora vamos a construir la pantalla. Para manejar la informaci\u00f3n del listado, necesitamos tipar los datos para que Typescript no se queje. Para ello crearemos un fichero en categories\\models\\category-interface.ts donde implementaremos la interface necesaria. Esta interface ser\u00e1 la que utilizaremos para tipar el c\u00f3digo de nuestro componente.

    category-interface.ts
    export interface Category {\n    id: number\n    name: string\n}\n

    Tambi\u00e9n, escribiremos el c\u00f3digo de CategoriesView.vue:

    CategoriesView.vue
    <template>\n  <div class=\"card\">\n    <DataTable\n      v-model:editingRows=\"editingRows\"\n      :value=\"categories\"\n      tableStyle=\"min-width: 50rem\"\n      editMode=\"row\"\n      dataKey=\"id\"\n      @row-edit-save=\"onRowEditSave\"\n    >\n      <Column field=\"id\" header=\"IDENTIFICADOR\">\n        <template #editor=\"{ data, field }\">\n          <InputText v-model=\"data[field]\" />\n        </template>\n      </Column>\n      <Column field=\"name\" header=\"NOMBRE CATEGOR\u00cdA\">\n        <template #editor=\"{ data, field }\">\n          <InputText v-model=\"data[field]\" />\n        </template>\n      </Column>\n      <Column\n        :rowEditor=\"true\"\n        style=\"width: 110px\"\n        bodyStyle=\"text-align:center\"\n      ></Column>\n      <Column\n        style=\"width: 30px; padding: 0px 2rem 0px 0px; color: red\"\n        bodyStyle=\"text-align:center\"\n      >\n        <template #body=\"{ data }\">\n          <i class=\"pi pi-times\" @click=\"onRowDelete(data)\"></i>\n        </template>\n      </Column>\n    </DataTable>\n  </div>\n  <div class=\"actions\">\n    <Button label=\"Nueva categor\u00eda\" />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport DataTable, { type DataTableRowEditSaveEvent } from \"primevue/datatable\";\nimport Column from \"primevue/column\";\nimport InputText from \"primevue/inputtext\";\nimport Button from 'primevue/button';\nimport type { CategoryInterface } from \"./model/category.interface\";\nconst categories = ref([]);\nconst editingRows = ref([]);\nconst onRowEditSave = (event: DataTableRowEditSaveEvent) => {\n  console.log(event);\n};\nconst onRowDelete = (data: CategoryInterface) => {\n  console.log(data);\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.actions {\n  display: flex;\n  flex-direction: row;\n  margin-top: 1rem;\n  justify-content: flex-end;\n}\n\n.p-button {\n  background: var(--primary);\n  border: 1px solid var(--primary);\n\n  &:enabled {\n    &:hover {\n      background: var(--secondary);\n      border-color: var(--secondary);\n    }\n  }\n}\n</style>\n

    Intentaremos explicar un poco el c\u00f3digo:

    Lo primero vamos a importar el componente DataTable desde la librer\u00eda de componentes que estamos usando, para ello podemos ver algunos ejemplos de como hacerlo en la documentaci\u00f3n oficial

    Nosotros hemos puesto las importaciones que necesitamos en el <script>:

    CategoriesView.vue
    import DataTable, { type DataTableRowEditSaveEvent } from \"primevue/datatable\";\nimport Column from \"primevue/column\";\n

    Hemos creado nuestra tabla con las exigencias de la aplicaci\u00f3n, hemos puesto dos columnas, la columna identificador donde en \u00e9l header=\u201d\u201d le ponemos que nombre se muestra en la cabecera y le hemos dicho que debe mostrar en ella el dato id poni\u00e9ndolo en \u00e9l field=\u201d\u201d.

    CategoriesView.vue
    <Column field=\"id\" header=\"IDENTIFICADOR\"></Column>\n

    Como a la tabla le hemos dicho que debe ser editable con:

    CategoriesView.vue
    editMode=\"row\"\n

    Le decimos a esta columna que debe hacer cuando entremos en modo de edici\u00f3n, con el template le decimos que mostrara un InputText que es otro componente de la librer\u00eda de componentes que viene a ser un input de toda la vida donde podemos escribir texto para editar el valor, quedando al final asi:

    CategoriesView.vue
    <Column field=\"id\" header=\"IDENTIFICADOR\">\n    <template #editor=\"{ data, field }\">\n        <InputText v-model=\"data[field]\" />\n    </template>\n</Column>\n

    Luego hemos creado dos columnas, una que tiene el l\u00e1piz y activa el modo edici\u00f3n:

    CategoriesView.vue
    <Column\n    :rowEditor=\"true\"\n    style=\"width: 110px\"\n    bodyStyle=\"text-align:center\">\n</Column>\n

    Y otra que tiene la X y lo que har\u00e1 ser\u00e1 borrar la fila:

    CategoriesView.vue
    <Column\n    style=\"width: 30px; padding: 0px 2rem 0px 0px; color: red\"\n    bodyStyle=\"text-align:center\"\n>\n    <template #body=\"{ data }\">\n    <i class=\"pi pi-times\" @click=\"onRowDelete(data)\"></i>\n    </template>\n</Column>\n

    Al final a\u00f1adimos otro contenedor que vale para alojar los botones como en nuestro caso el de crear nueva categor\u00eda, el bot\u00f3n es tambi\u00e9n un componente de la librer\u00eda por lo que tendremos que hacer su import en la etiqueta <script>:

    CategoriesView.vue
    <div class=\"actions\">\n    <Button label=\"Nueva categor\u00eda\" />\n</div>\n

    Si abrimos el navegador y accedemos a http://localhost:5173/ y pulsamos en el men\u00fa de Categor\u00edas obtendremos una pantalla con un listado vac\u00edo (solo con cabeceras) y un bot\u00f3n de crear Nueva Categor\u00eda que a\u00fan no hace nada.

    "},{"location":"develop/basic/vuejsold/#anadiendo-datos","title":"A\u00f1adiendo datos","text":"

    En este punto y para ver como responde el listado, vamos a a\u00f1adir datos. Si tuvi\u00e9ramos el backend implementado podr\u00edamos consultar los datos directamente de una operaci\u00f3n de negocio de backend, pero ahora mismo no lo tenemos implementado as\u00ed que para no bloquear el desarrollo vamos a mockear los datos.

    En Vue para conectar a APIS externas solemos usar una librer\u00eda llamada Axios, lo primero que haremos ser\u00e1 descargarla e instalarla como indica en su documentaci\u00f3n oficial.

    Para instalarla simplemente nos iremos a la terminal dentro de la carpeta donde tenemos el proyecto y pondremos:

    npm install axios\n

    Con esto ya podremos ver que se ha a\u00f1adido a nuestro package.json, luego crearemos una carpeta api dentro de la carpeta src y dentro de la carpeta api crearemos el archivo app-api.ts. Dentro de este archivo vamos a inicializar nuestra config de la API y guardaremos todos los par\u00e1metros iniciales conforme nos vayan haciendo falta, de momento pondremos solo este c\u00f3digo:

    app-api.ts
    import axios from 'axios';\n\nexport const appApi = axios.create({\n    baseURL: 'http://localhost:8080',\n});\n

    Si os dais cuenta lo \u00fanico que hacemos es importar axios que acabamos de instalarlo y definir nuestra url base del api para no tener que escribirla cada vez y para s\u00ed alg\u00fan d\u00eda cambia, tener que cambiarla solo en un sitio y no en todos los servicios que la usen.

    Para mockear los datos con axios usaremos una librer\u00eda que se llama axios-mock-adapter y la pod\u00e9is encontrar en este link.

    Para instalarla lo haremos con npm como siempre, pondremos esta orden en el terminal y enter:

    npm install axios-mock-adapter --save-dev\n

    Si nos vamos al package.json veremos que ya la tenemos en las devDependencies, la diferencia entre estas y las dependencias es que las dependencias las necesitamos en el proyecto y estar\u00e1n en nuestro bundle final que serviremos a la gente, las devDependencies se usan solo mientras programamos y no entraran en el bundle final. Los mocks los usaremos solo en el desarrollo y hasta que podamos conectar con la API real por eso los metemos en las devDependencies.

    "},{"location":"develop/basic/vuejsold/#mockeando-datos","title":"Mockeando datos","text":"

    Como hemos comentado anteriormente, el backend todav\u00eda no est\u00e1 implementado as\u00ed que vamos a mockear datos. Nos crearemos un fichero mock-categories.ts dentro de views/categories/mocks, con datos ficticios y crearemos una llamada a la API que nos devuelva estos datos. De esta forma, cuando tengamos implementada la operaci\u00f3n de negocio en backend, tan solo tenemos que sustituir el c\u00f3digo que devuelve datos est\u00e1ticos por una llamada HTTP.

    Dentro de la carpeta mocks crearemos el archivo mock-categories.ts con el siguiente c\u00f3digo:

    mock-categories.ts
    import type { Category } from \"@/views/categories/models/category-interface\";\n\nexport const CATEGORY_DATA_MOCK: Category[] = [\n    { id: 1, name: 'Dados' },\n    { id: 2, name: 'Fichas' },\n    { id: 3, name: 'Cartas' },\n    { id: 4, name: 'Rol' },\n    { id: 5, name: 'Tableros' },\n    { id: 6, name: 'Tem\u00e1ticos' },\n    { id: 7, name: 'Europeos' },\n    { id: 8, name: 'Guerra' },\n    { id: 9, name: 'Abstractos' },\n]\n

    Despu\u00e9s nos crearemos un composable que usaremos para llamar a la API y poder reutilizarlo en otros componentes si hiciera falta. Dentro de la carpeta categories crearemos otra carpeta llamada composables y dentro crearemos un archivo llamado categories-composable.ts, en ese archivo escribiremos este c\u00f3digo:

    categories-composable.ts
    import appApi from '@/api/app-api'\nimport MockAdapter from 'axios-mock-adapter';\nimport { CATEGORY_DATA_MOCK } from '@/views/categories/mocks/mock-categories'\n\nconst mock = new MockAdapter(appApi);\n\nconst useCategoriesApiComposable = () => {\nmock.onGet(\"/category\").reply(200, CATEGORY_DATA_MOCK);\n\n    const getCategories = async () => {\n        const categories = await appApi.get(\"/category\");\n        return categories.data;\n    };\n\n    return {\n        getCategories\n    }\n}\n\nexport default useCategoriesApiComposable\n

    A\u00f1adiremos \u00e9l composable a nuestro CategoriesView.vue dentro de las etiquetas <script>, lo primero en el import que ya tenemos desde Vue a\u00f1adiremos el m\u00e9todo onMounted dej\u00e1ndolo asi:

    CategoriesView.vue
    import { onMounted, ref } from 'vue'\n

    El m\u00e9todo onMounted es un ciclo de vida que se dispara nada m\u00e1s montarse el componente, despu\u00e9s a\u00f1adiremos al final el import del composable para poder usarlo:

    CategoriesView.vue
    import useCategoriesApiComposable from '@/views/categories/composables/categories-composable'\n

    Nos traeremos el m\u00e9todo del composable con la desestructuraci\u00f3n del objeto:

    CategoriesView.vue
    const { getCategories } = useCategoriesApiComposable()\n

    Crearemos una funci\u00f3n as\u00edncrona para llamar al composable y llamaremos al composable en el onMounted:

    CategoriesView.vue
    async function getInitCategories() {\n    categories.value = await getCategories()\n}\n\nonMounted(() => {\n    getInitCategories()\n})\n

    El CategoriesView.vue quedar\u00eda asi:

    CategoriesView.vue
    <template>\n  <div class=\"card\">\n    <DataTable\n      v-model:editingRows=\"editingRows\"\n      :value=\"categories\"\n      tableStyle=\"min-width: 50rem\"\n      editMode=\"row\"\n      dataKey=\"id\"\n      @row-edit-save=\"onRowEditSave\"\n    >\n      <Column field=\"id\" header=\"IDENTIFICADOR\">\n        <template #editor=\"{ data, field }\">\n          <InputText v-model=\"data[field]\" />\n        </template>\n      </Column>\n      <Column field=\"name\" header=\"NOMBRE CATEGOR\u00cdA\">\n        <template #editor=\"{ data, field }\">\n          <InputText v-model=\"data[field]\" />\n        </template>\n      </Column>\n      <Column :rowEditor=\"true\" style=\"width: 110px\" bodyStyle=\"text-align:center\"></Column>\n      <Column\n        style=\"width: 30px; padding: 0px 2rem 0px 0px; color: red\"\n        bodyStyle=\"text-align:center\"\n      >\n        <template #body=\"{ data }\">\n          <i class=\"pi pi-times\" @click=\"onRowDelete(data)\"></i>\n        </template>\n      </Column>\n    </DataTable>\n  </div>\n  <div class=\"actions\">\n    <Button label=\"Nueva categor\u00eda\" />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { onMounted, ref } from 'vue'\nimport DataTable, { type DataTableRowEditSaveEvent } from 'primevue/datatable'\nimport Column from 'primevue/column'\nimport InputText from 'primevue/inputtext'\nimport Button from 'primevue/button'\nimport type { CategoryInterface } from './model/category.interface'\nimport useCategoriesApiComposable from '@/views/categories/composables/categories-composable'\n\nconst categories = ref([])\nconst editingRows = ref([])\n\nconst { getCategories } = useCategoriesApiComposable()\n\nconst onRowEditSave = (event: DataTableRowEditSaveEvent) => {\n  console.log(event)\n}\n\nconst onRowDelete = (data: CategoryInterface) => {\n  console.log(data)\n}\n\nasync function getInitCategories() {\n  categories.value = await getCategories()\n}\n\nonMounted(() => {\n  getInitCategories()\n})\n</script>\n\n<style lang=\"scss\" scoped>\n.actions {\n  display: flex;\n  flex-direction: row;\n  margin-top: 1rem;\n  justify-content: flex-end;\n}\n\n.p-button {\n  background: var(--primary);\n  border: 1px solid var(--primary);\n\n  &:enabled {\n    &:hover {\n      background: var(--secondary);\n      border-color: var(--secondary);\n    }\n  }\n}\n</style>\n

    Si ahora refrescamos la p\u00e1gina web, veremos que el listado ya tiene datos con los que vamos a interactuar.

    "},{"location":"develop/basic/vuejsold/#simulando-las-otras-peticiones","title":"Simulando las otras peticiones","text":"

    Para terminar, vamos a simular las otras dos peticiones, la de editar y la de borrar para cuando tengamos que utilizarlas. \u00c9l composable debe quedar m\u00e1s o menos as\u00ed:

    categories-composable.ts
    import appApi from '@/api/app-api'\nimport MockAdapter from 'axios-mock-adapter'\nimport type { Category } from '@/views/categories/models/category-interface'\nimport { CATEGORY_DATA_MOCK } from '@/views/categories/mocks/mock-categories'\n\nconst mock = new MockAdapter(appApi)\n\nconst useCategoriesApiComposable = () => {\n  mock.onAny('/category').reply(200, CATEGORY_DATA_MOCK)\n\n  const getCategories = async () => {\n    const categories = await appApi.get('/category')\n    return categories.data\n  }\n\n  const saveCategory = async (category: Category) => {\n    const categoryEdit = await appApi.post('/category', category)\n    return categoryEdit.data\n  }\n\n  const editCategory = async (category: Category) => {\n    const categoryEdit = await appApi.put('/category', category)\n    return categoryEdit.data\n  }\n\n  const deleteCategory = async (categoryId: number) => {\n    const categoryEdit = await appApi.delete(`/category/${categoryId}`)\n    return categoryEdit.data\n  }\n\n  return {\n    getCategories,\n    saveCategory,\n    editCategory,\n    deleteCategory\n  }\n}\n\nexport default useCategoriesApiComposable\n
    "},{"location":"develop/filtered/angular/","title":"Listado filtrado - Angular","text":"

    En este punto ya tenemos dos listados, uno b\u00e1sico y otro paginado. Ahora vamos a implementar un listado un poco diferente, con filtros y con una presentaci\u00f3n un tanto distinta.

    Como ya conocemos como se debe desarrollar, en este ejemplo vamos a ir m\u00e1s r\u00e1pidos y nos vamos a centrar \u00fanicamente en las novedades.

    "},{"location":"develop/filtered/angular/#crear-componentes","title":"Crear componentes","text":"

    Vamos a desarrollar el listado de Juegos. Este listado es un tanto peculiar, porque no tiene una tabla como tal, sino que tiene una tabla con \"tiles\" para cada uno de los juegos. Necesitaremos un componente para el listado y otro componente para el detalle del juego. Tambi\u00e9n necesitaremos otro componente para el dialogo de edici\u00f3n / alta.

    Manos a la obra:

    ng generate module game\n\nng generate component game/game-list\nng generate component game/game-list/game-item\nng generate component game/game-edit\n\nng generate service game/game\n

    Y a\u00f1adimos el nuevo m\u00f3dulo al app.module.ts como hemos hecho con el resto de m\u00f3dulos.

    Game.ts
    import { NgModule } from '@angular/core';\nimport { BrowserModule } from '@angular/platform-browser';\n\nimport { AppRoutingModule } from './app-routing.module';\nimport { AppComponent } from './app.component';\nimport { BrowserAnimationsModule } from '@angular/platform-browser/animations';\nimport { CoreModule } from './core/core.module';\nimport { CategoryModule } from './category/category.module';\nimport { AuthorModule } from './author/author.module';\nimport { GameModule } from './game/game.module';\n\n@NgModule({\n    declarations: [\n        AppComponent\n    ],\n    imports: [\n        BrowserModule,\n        AppRoutingModule,\n        CoreModule,\n        CategoryModule,\n        AuthorModule,\n        GameModule,\n        BrowserAnimationsModule\n    ],\n    providers: [],\n    bootstrap: [AppComponent]\n})\nexport class AppModule { }\n
    "},{"location":"develop/filtered/angular/#crear-el-modelo","title":"Crear el modelo","text":"

    Lo primero que vamos a hacer es crear el modelo en game/model/Game.ts con todas las propiedades necesarias para trabajar con un juego:

    Game.ts
    import { Category } from \"src/app/category/model/Category\";\nimport { Author } from \"src/app/author/model/Author\";\n\nexport class Game {\n    id: number;\n    title: string;\n    age: number;\n    category: Category;\n    author: Author;\n}\n

    Como ves, el juego tiene dos objetos para mapear categor\u00eda y autor.

    "},{"location":"develop/filtered/angular/#anadir-el-punto-de-entrada","title":"A\u00f1adir el punto de entrada","text":"

    A\u00f1adimos la ruta al men\u00fa para que podamos navegar a esta pantalla:

    app-routing.module.ts
    import { NgModule } from '@angular/core';\nimport { Routes, RouterModule } from '@angular/router';\nimport { AuthorListComponent } from './author/author-list/author-list.component';\nimport { CategoryListComponent } from './category/category-list/category-list.component';\nimport { GameListComponent } from './game/game-list/game-list.component';\n\n\nconst routes: Routes = [\n    { path: '', redirectTo: '/games', pathMatch: 'full'},\n    { path: 'categories', component: CategoryListComponent },\n    { path: 'authors', component: AuthorListComponent },\n    { path: 'games', component: GameListComponent },\n];\n\n@NgModule({\n    imports: [RouterModule.forRoot(routes)],\n    exports: [RouterModule]\n})\nexport class AppRoutingModule { }\n

    Adem\u00e1s, hemos a\u00f1adido una regla adicional con el path vac\u00edo para indicar que si no pone ruta, por defecto la p\u00e1gina inicial redirija al path /games, que es nuevo path que hemos a\u00f1adido.

    "},{"location":"develop/filtered/angular/#implementar-servicio","title":"Implementar servicio","text":"

    A continuaci\u00f3n implementamos el servicio y mockeamos datos de ejemplo:

    mock-games.tsgame.service.ts
    import { Game } from \"./Game\";\n\nexport const GAME_DATA: Game[] = [\n    { id: 1, title: 'Juego 1', age: 6, category: { id: 1, name: 'Categor\u00eda 1' }, author: { id: 1, name: 'Autor 1', nationality: 'Nacionalidad 1' } },\n    { id: 2, title: 'Juego 2', age: 8, category: { id: 1, name: 'Categor\u00eda 1' }, author: { id: 2, name: 'Autor 2', nationality: 'Nacionalidad 2' } },\n    { id: 3, title: 'Juego 3', age: 4, category: { id: 1, name: 'Categor\u00eda 1' }, author: { id: 3, name: 'Autor 3', nationality: 'Nacionalidad 3' } },\n    { id: 4, title: 'Juego 4', age: 10, category: { id: 2, name: 'Categor\u00eda 2' }, author: { id: 1, name: 'Autor 1', nationality: 'Nacionalidad 1' } },\n    { id: 5, title: 'Juego 5', age: 16, category: { id: 2, name: 'Categor\u00eda 2' }, author: { id: 2, name: 'Autor 2', nationality: 'Nacionalidad 2' } },\n    { id: 6, title: 'Juego 6', age: 16, category: { id: 2, name: 'Categor\u00eda 2' }, author: { id: 3, name: 'Autor 3', nationality: 'Nacionalidad 3' } },\n    { id: 7, title: 'Juego 7', age: 12, category: { id: 3, name: 'Categor\u00eda 3' }, author: { id: 1, name: 'Autor 1', nationality: 'Nacionalidad 1' } },\n    { id: 8, title: 'Juego 8', age: 14, category: { id: 3, name: 'Categor\u00eda 3' }, author: { id: 2, name: 'Autor 2', nationality: 'Nacionalidad 2' } },\n]\n
    import { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Game } from './model/Game';\nimport { GAME_DATA } from './model/mock-games';\n\n@Injectable({\n    providedIn: 'root'\n})\nexport class GameService {\n\n    constructor() { }\n\n    getGames(title?: String, categoryId?: number): Observable<Game[]> {\n        return of(GAME_DATA);\n    }\n\n    saveGame(game: Game): Observable<void> {\n        return of(null);\n    }\n\n}\n
    "},{"location":"develop/filtered/angular/#implementar-listado","title":"Implementar listado","text":"

    Ya tenemos las operaciones del servicio con datoos, as\u00ed que ahora vamos a por el listado filtrado.

    game-list.component.htmlgame-list.component.scssgame-list.component.ts
    <div class=\"container\">\n    <h1>Cat\u00e1logo de juegos</h1>\n\n    <div class=\"filters\">\n        <form>\n            <mat-form-field>\n                <mat-label>T\u00edtulo del juego</mat-label>\n                <input type=\"text\" matInput placeholder=\"T\u00edtulo del juego\" [(ngModel)]=\"filterTitle\" name=\"title\">\n            </mat-form-field>\n\n            <mat-form-field>\n                <mat-label>Categor\u00eda del juego</mat-label>\n                <mat-select disableRipple [(ngModel)]=\"filterCategory\" name=\"category\">\n                    <mat-option *ngFor=\"let category of categories\" [value]=\"category\">{{category.name}}</mat-option>\n                </mat-select>\n            </mat-form-field>    \n        </form>\n\n        <div class=\"buttons\">\n            <button mat-stroked-button (click)=\"onCleanFilter()\">Limpiar</button> \n            <button mat-stroked-button (click)=\"onSearch()\">Filtrar</button> \n        </div>   \n    </div>   \n\n    <div class=\"game-list\">\n        <app-game-item *ngFor=\"let game of games; let i = index;\" (click)=\"editGame(game)\">\n        </app-game-item>\n    </div>\n\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\" (click)=\"createGame()\">Nuevo juego</button>            \n    </div>   \n</div>\n
    .container {\n    margin: 20px;\n\n    .filters {\n        display: flex;\n\n        mat-form-field {\n            width: 300px;\n            margin-right: 20px;\n        }\n\n        .buttons {\n            flex: auto;\n            align-self: center;\n\n            button {\n                margin-left: 15px;\n            }\n        }\n    }\n\n    .game-list { \n        margin-top: 20px;\n        margin-bottom: 20px;\n\n        display: flex;\n        flex-flow: wrap;\n        overflow: auto;  \n    }\n\n    .buttons {\n        text-align: right;\n    }\n}\n\nbutton {\n    width: 125px;\n}\n
    import { Component, OnInit } from '@angular/core';\nimport { MatDialog } from '@angular/material/dialog';\nimport { CategoryService } from 'src/app/category/category.service';\nimport { Category } from 'src/app/category/model/Category';\nimport { GameEditComponent } from '../game-edit/game-edit.component';\nimport { GameService } from '../game.service';\nimport { Game } from '../model/Game';\n\n@Component({\n    selector: 'app-game-list',\n    templateUrl: './game-list.component.html',\n    styleUrls: ['./game-list.component.scss']\n})\nexport class GameListComponent implements OnInit {\n\n    categories : Category[];\n    games: Game[];\n    filterCategory: Category;\n    filterTitle: string;\n\n    constructor(\n        private gameService: GameService,\n        private categoryService: CategoryService,\n        public dialog: MatDialog,\n    ) { }\n\n    ngOnInit(): void {\n\n        this.gameService.getGames().subscribe(\n            games => this.games = games\n        );\n\n        this.categoryService.getCategories().subscribe(\n            categories => this.categories = categories\n        );\n    }\n\n    onCleanFilter(): void {\n        this.filterTitle = null;\n        this.filterCategory = null;\n\n        this.onSearch();\n    }\n\n    onSearch(): void {\n\n        let title = this.filterTitle;\n        let categoryId = this.filterCategory != null ? this.filterCategory.id : null;\n\n        this.gameService.getGames(title, categoryId).subscribe(\n            games => this.games = games\n        );\n    }\n\n    createGame() {    \n        const dialogRef = this.dialog.open(GameEditComponent, {\n            data: {}\n        });\n\n        dialogRef.afterClosed().subscribe(result => {\n            this.ngOnInit();\n        });    \n    }  \n\n    editGame(game: Game) {\n        const dialogRef = this.dialog.open(GameEditComponent, {\n            data: { game: game }\n        });\n\n        dialogRef.afterClosed().subscribe(result => {\n            this.onSearch();\n        });\n    }\n}\n

    Recuerda, de nuevo, que todos los componentes de Angular que utilicemos hay que importarlos en el m\u00f3dulo padre correspondiente para que se puedan precargar correctamente.

    game.module.ts
    import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { GameListComponent } from './game-list/game-list.component';\nimport { GameEditComponent } from './game-edit/game-edit.component';\nimport { GameItemComponent } from './game-list/game-item/game-item.component';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatOptionModule } from '@angular/material/core';\nimport { MatDialogModule } from '@angular/material/dialog';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatPaginatorModule } from '@angular/material/paginator';\nimport { MatSelectModule } from '@angular/material/select';\nimport { MatTableModule } from '@angular/material/table';\nimport { MatCardModule } from '@angular/material/card';\n\n\n@NgModule({\ndeclarations: [\n    GameListComponent,\n    GameEditComponent,\n    GameItemComponent\n],\nimports: [\n    CommonModule,\n    MatTableModule,\n    MatIconModule, \n    MatButtonModule,\n    MatDialogModule,\n    MatFormFieldModule,\n    MatInputModule,\n    FormsModule,\n    ReactiveFormsModule,\n    MatPaginatorModule,\n    MatOptionModule,\n    MatSelectModule,\n    MatCardModule,\n]\n})\nexport class GameModule { }\n

    Con todos estos cambios y si refrescamos el navegador, deber\u00eda verse una pantalla similar a esta:

    Tenemos una pantalla con una secci\u00f3n de filtros en la parte superior, donde podemos introducir un texto o seleccionar una categor\u00eda de un dropdown, un listado que de momento tiene todos los componentes b\u00e1sicos en una fila uno detr\u00e1s del otro, y un bot\u00f3n para crear juegos nuevos.

    Dropdown

    El componente Dropdown es uno de los componentes m\u00e1s utilizados en las pantallas y formularios de Angular. Ves familiariz\u00e1ndote con \u00e9l porque lo vas a usar mucho. Es bastante potente y medianamente sencillo de utilizar. Los datos del listado pueden ser din\u00e1micos (desde servidor) o est\u00e1ticos (si los valores ya los tienes prefijados).

    "},{"location":"develop/filtered/angular/#implementar-detalle-del-item","title":"Implementar detalle del item","text":"

    Ahora vamos a implementar el detalle de cada uno de los items que forman el listado. Para ello lo primero que haremos ser\u00e1 pasarle la informaci\u00f3n del juego a cada componente como un dato de entrada Input hacia el componente.

    game-list.component.html
    <div class=\"container\">\n    <h1>Cat\u00e1logo de juegos</h1>\n\n    <div class=\"filters\">\n        <form>\n            <mat-form-field>\n                <mat-label>T\u00edtulo del juego</mat-label>\n                <input type=\"text\" matInput placeholder=\"T\u00edtulo del juego\" [(ngModel)]=\"filterTitle\" name=\"title\">\n            </mat-form-field>\n\n            <mat-form-field>\n                <mat-label>Categor\u00eda del juego</mat-label>\n                <mat-select disableRipple [(ngModel)]=\"filterCategory\" name=\"category\">\n                    <mat-option *ngFor=\"let category of categories\" [value]=\"category\">{{category.name}}</mat-option>\n                </mat-select>\n            </mat-form-field>    \n        </form>\n\n        <div class=\"buttons\">\n            <button mat-stroked-button (click)=\"onCleanFilter()\">Limpiar</button> \n            <button mat-stroked-button (click)=\"onSearch()\">Filtrar</button> \n        </div>   \n    </div>   \n\n    <div class=\"game-list\">\n        <app-game-item *ngFor=\"let game of games; let i = index;\" (click)=\"editGame(game)\" [game]=\"game\">\n        </app-game-item>\n    </div>\n\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\" (click)=\"createGame()\">Nuevo juego</button>            \n    </div>   \n</div>\n

    Tambi\u00e9n vamos a necesitar una foto de ejemplo para poner dentro de la tarjeta detalle de los juegos. Vamos a utilizar esta imagen:

    Desc\u00e1rgala y d\u00e9jala dentro del proyecto en assets/foto.png. Y ya para terminar, implementamos el componente de detalle:

    game-item.component.htmlgame-item.component.scssgame-item.component.ts
    <div class=\"container\">\n    <mat-card>\n        <div class=\"photo\">\n            <img src=\"./assets/foto.png\">\n        </div>\n        <div class=\"detail\">\n            <div class=\"title\">{{game.title}}</div>\n            <div class=\"properties\">\n                <div><i>Edad recomendada: </i>+{{game.age}}</div>\n                <div><i>Categor\u00eda: </i>{{game.category.name}}</div>\n                <div><i>Autor: </i>{{game.author.name}}</div>\n                <div><i>Nacionalidad: </i>{{game.author.nationality}}</div>\n            </div>\n        </div>\n    </mat-card>\n</div>\n
    .container {\n    display: flex;\n    width: 325px;\n\n    mat-card {\n        width: 100%;\n        margin: 10px;\n        display: flex;\n\n        .photo {\n            margin-right: 10px;\n\n            img {\n                width: 80px;\n                height: 80px;\n            }\n        }\n\n        .detail {\n            .title {\n                font-size: 14px;\n                font-weight: bold;\n            }\n\n            .properties {\n                font-size: 11px;\n\n                div {\n                    height: 15px;\n                }                \n            }\n        }\n    }\n}    \n
    import { Component, OnInit, Input } from '@angular/core';\nimport { Game } from '../../model/Game';\n\n@Component({\n    selector: 'app-game-item',\n    templateUrl: './game-item.component.html',\n    styleUrls: ['./game-item.component.scss']\n})\nexport class GameItemComponent implements OnInit {\n\n    @Input() game: Game;\n\n    constructor() { }\n\n    ngOnInit(): void {\n    }\n\n}\n

    Ahora si que deber\u00eda quedar algo similar a esta pantalla:

    "},{"location":"develop/filtered/angular/#implementar-dialogo-de-edicion","title":"Implementar dialogo de edici\u00f3n","text":"

    Ya solo nos falta el \u00faltimo paso, implementar el cuadro de edici\u00f3n / alta de un nuevo juego. Pero tenemos un peque\u00f1o problema, y es que al crear o editar un juego debemos seleccionar una Categor\u00eda y un Autor.

    Para la Categor\u00eda no tenemos ning\u00fan problema, pero para el Autor no tenemos un servicio que nos devuelva todos los autores, solo tenemos un servicio que nos devuelve una Page de autores.

    As\u00ed que lo primero que haremos ser\u00e1 implementar una operaci\u00f3n getAllAuthors para poder recuperar una lista.

    mock-authors-list.tsauthor.service.ts
    import { Author } from \"./Author\";\n\nexport const AUTHOR_DATA_LIST : Author[] = [\n    { id: 1, name: 'Klaus Teuber', nationality: 'Alemania' },\n    { id: 2, name: 'Matt Leacock', nationality: 'Estados Unidos' },\n    { id: 3, name: 'Keng Leong Yeo', nationality: 'Singapur' },\n    { id: 4, name: 'Gil Hova', nationality: 'Estados Unidos'},\n    { id: 5, name: 'Kelly Adams', nationality: 'Estados Unidos' },\n]    \n
    import { HttpClient } from '@angular/common/http';\nimport { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Pageable } from '../core/model/page/Pageable';\nimport { Author } from './model/Author';\nimport { AuthorPage } from './model/AuthorPage';\nimport { AUTHOR_DATA_LIST } from './model/mock-authors-list';\n\n@Injectable({\n    providedIn: 'root'\n})\nexport class AuthorService {\n\n    constructor(\n        private http: HttpClient\n    ) { }\n\n    getAuthors(pageable: Pageable): Observable<AuthorPage> {\n        return this.http.post<AuthorPage>('http://localhost:8080/author', {pageable:pageable});\n    }\n\n    saveAuthor(author: Author): Observable<void> {\n\n        let url = 'http://localhost:8080/author';\n        if (author.id != null) url += '/'+author.id;\n\n        return this.http.put<void>(url, author);\n    }\n\n    deleteAuthor(idAuthor : number): Observable<void> {\n        return this.http.delete<void>('http://localhost:8080/author/'+idAuthor);\n    }    \n\n    getAllAuthors(): Observable<Author[]> {\n        return of(AUTHOR_DATA_LIST);\n    }\n\n}\n

    Ahora s\u00ed que tenemos todo listo para implementar el cuadro de dialogo para dar de alta o editar juegos.

    game-edit.component.htmlgame-edit.component.scssgame-edit.component.ts
    <div class=\"container\">\n    <h1 *ngIf=\"game.id == null\">Crear juego</h1>\n    <h1 *ngIf=\"game.id != null\">Modificar juego</h1>\n\n    <form>\n        <mat-form-field>\n            <mat-label>Identificador</mat-label>\n            <input type=\"text\" matInput placeholder=\"Identificador\" [(ngModel)]=\"game.id\" name=\"id\" disabled>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>T\u00edtulo</mat-label>\n            <input type=\"text\" matInput placeholder=\"T\u00edtulo del juego\" [(ngModel)]=\"game.title\" name=\"title\" required>\n            <mat-error>El t\u00edtulo no puede estar vac\u00edo</mat-error>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Edad recomendada</mat-label>\n            <input type=\"number\" matInput placeholder=\"Edad recomendada\" [(ngModel)]=\"game.age\" name=\"age\" required>\n            <mat-error>La edad no puede estar vac\u00eda</mat-error>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Categor\u00eda</mat-label>\n            <mat-select disableRipple [(ngModel)]=\"game.category\" name=\"category\" required>\n                <mat-option *ngFor=\"let category of categories\" [value]=\"category\">{{category.name}}</mat-option>\n            </mat-select>\n            <mat-error>La categor\u00eda no puede estar vac\u00eda</mat-error>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Autor</mat-label>\n            <mat-select disableRipple [(ngModel)]=\"game.author\" name=\"author\" required>\n                <mat-option *ngFor=\"let author of authors\" [value]=\"author\">{{author.name}}</mat-option>\n            </mat-select>\n            <mat-error>El autor no puede estar vac\u00edo</mat-error>\n        </mat-form-field>\n    </form>\n\n    <div class=\"buttons\">\n        <button mat-stroked-button (click)=\"onClose()\">Cerrar</button>\n        <button mat-flat-button color=\"primary\" (click)=\"onSave()\">Guardar</button>\n    </div>\n</div>\n
    .container {\n    min-width: 350px;\n    max-width: 500px;\n    padding: 20px;\n\n    form {\n        display: flex;\n        flex-direction: column;\n        margin-bottom:20px;\n    }\n\n    .buttons {\n        text-align: right;\n\n        button {\n            margin-left: 10px;\n        }\n    }\n}\n
    import { Component, Inject, OnInit } from '@angular/core';\nimport { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';\nimport { AuthorService } from 'src/app/author/author.service';\nimport { Author } from 'src/app/author/model/Author';\nimport { CategoryService } from 'src/app/category/category.service';\nimport { Category } from 'src/app/category/model/Category';\nimport { GameService } from '../game.service';\nimport { Game } from '../model/Game';\n\n@Component({\n    selector: 'app-game-edit',\n    templateUrl: './game-edit.component.html',\n    styleUrls: ['./game-edit.component.scss']\n})\nexport class GameEditComponent implements OnInit {\n\n    game: Game; \n    authors: Author[];\n    categories: Category[];\n\n    constructor(\n        public dialogRef: MatDialogRef<GameEditComponent>,\n        @Inject(MAT_DIALOG_DATA) public data: any,\n        private gameService: GameService,\n        private categoryService: CategoryService,\n        private authorService: AuthorService,\n    ) { }\n\n    ngOnInit(): void {\n        if (this.data.game != null) {\n            this.game = Object.assign({}, this.data.game);\n        }\n        else {\n            this.game = new Game();\n        }\n\n        this.categoryService.getCategories().subscribe(\n            categories => {\n                this.categories = categories;\n\n                if (this.game.category != null) {\n                    let categoryFilter: Category[] = categories.filter(category => category.id == this.data.game.category.id);\n                    if (categoryFilter != null) {\n                        this.game.category = categoryFilter[0];\n                    }\n                }\n            }\n        );\n\n        this.authorService.getAllAuthors().subscribe(\n            authors => {\n                this.authors = authors\n\n                if (this.game.author != null) {\n                    let authorFilter: Author[] = authors.filter(author => author.id == this.data.game.author.id);\n                    if (authorFilter != null) {\n                        this.game.author = authorFilter[0];\n                    }\n                }\n            }\n        );\n    }\n\n    onSave() {\n        this.gameService.saveGame(this.game).subscribe(result => {\n            this.dialogRef.close();\n        });    \n    }  \n\n    onClose() {\n        this.dialogRef.close();\n    }\n\n}\n

    Como puedes ver, para rellenar los componentes seleccionables de dropdown, hemos realizado una consulta al servicio para recuperar todos los autores y categorias, y en la respuesta de cada uno de ellos, hemos buscado en los resultados cual es el que coincide con el ID enviado desde el listado, y ese es el que hemos fijado en el objeto Game.

    De esta forma, no estamos cogiendo directamente los datos del listado, sino que no estamos asegurando que los datos de autor y de categor\u00eda son los que vienen del servicio, siempre filtrando por su ID.

    "},{"location":"develop/filtered/angular/#conectar-con-backend","title":"Conectar con Backend","text":"

    Antes de seguir

    Antes de seguir con este punto, debes implementar el c\u00f3digo de backend en la tecnolog\u00eda que quieras (Springboot o Nodejs). Si has empezado este cap\u00edtulo implementando el frontend, por favor accede a la secci\u00f3n correspondiente de backend para poder continuar con el tutorial. Una vez tengas implementadas todas las operaciones para este listado, puedes volver a este punto y continuar con Angular.

    Una vez implementado front y back, lo que nos queda es modificar el servicio del front para que conecte directamente con las operaciones ofrecidas por el back.

    author-service.tsgame-service.ts
    import { HttpClient } from '@angular/common/http';\nimport { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Pageable } from '../core/model/page/Pageable';\nimport { Author } from './model/Author';\nimport { AuthorPage } from './model/AuthorPage';\n\n@Injectable({\n    providedIn: 'root'\n})\nexport class AuthorService {\n\n    constructor(\n        private http: HttpClient\n    ) { }\n\n    getAuthors(pageable: Pageable): Observable<AuthorPage> {\n        return this.http.post<AuthorPage>('http://localhost:8080/author', {pageable:pageable});\n    }\n\n    saveAuthor(author: Author): Observable<void> {\n\n        let url = 'http://localhost:8080/author';\n        if (author.id != null) url += '/'+author.id;\n\n        return this.http.put<void>(url, author);\n    }\n\n    deleteAuthor(idAuthor : number): Observable<void> {\n        return this.http.delete<void>('http://localhost:8080/author/'+idAuthor);\n    }    \n\n    getAllAuthors(): Observable<Author[]> {\n        return this.http.get<Author[]>('http://localhost:8080/author');\n    }\n\n}\n
    import { HttpClient } from '@angular/common/http';\nimport { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Game } from './model/Game';\n\n@Injectable({\n    providedIn: 'root'\n})\nexport class GameService {\n\n    constructor(\n        private http: HttpClient\n    ) { }\n\n    getGames(title?: String, categoryId?: number): Observable<Game[]> {            \n        return this.http.get<Game[]>(this.composeFindUrl(title, categoryId));\n    }\n\n    saveGame(game: Game): Observable<void> {\n        let url = 'http://localhost:8080/game';\n\n        if (game.id != null) {\n            url += '/'+game.id;\n        }\n\n        return this.http.put<void>(url, game);\n    }\n\n    private composeFindUrl(title?: String, categoryId?: number) : string {\n        let params = '';\n\n        if (title != null) {\n            params += 'title='+title;\n        }\n\n        if (categoryId != null) {\n            if (params != '') params += \"&\";\n            params += \"idCategory=\"+categoryId;\n        }\n\n        let url = 'http://localhost:8080/game'\n\n        if (params == '') return url;\n        else return url + '?'+params;\n    }\n}\n

    Y ahora si, podemos navegar por la web y ver el resultado completo.

    "},{"location":"develop/filtered/angular17/","title":"Listado filtrado - Angular","text":"

    En este punto ya tenemos dos listados, uno b\u00e1sico y otro paginado. Ahora vamos a implementar un listado un poco diferente, con filtros y con una presentaci\u00f3n un tanto distinta.

    Como ya conocemos como se debe desarrollar, en este ejemplo vamos a ir m\u00e1s r\u00e1pidos y nos vamos a centrar \u00fanicamente en las novedades.

    "},{"location":"develop/filtered/angular17/#crear-componentes","title":"Crear componentes","text":"

    Vamos a desarrollar el listado de Juegos. Este listado es un tanto peculiar, porque no tiene una tabla como tal, sino que tiene una tabla con \"tiles\" para cada uno de los juegos. Necesitaremos un componente para el listado y otro componente para el detalle del juego. Tambi\u00e9n necesitaremos otro componente para el di\u00e1logo de edici\u00f3n / alta.

    Manos a la obra:

    ng generate component game/game-list --type=page\nng generate component game/game-list/game-item\nng generate component game/game-edit\n\nng generate service game/game\n
    "},{"location":"develop/filtered/angular17/#crear-el-modelo","title":"Crear el modelo","text":"

    Lo primero que vamos a hacer es crear el modelo en game/model/Game.ts con todas las propiedades necesarias para trabajar con un juego:

    Game.ts
    import { Author } from \"../../author/model/Author\";\nimport { Category } from \"../../category/model/Category\";\n\nexport interface Game {\n    id: number;\n    title: string;\n    age: number;\n    category: Category;\n    author: Author;\n}\n

    Como ves, el juego tiene dos objetos para mapear categor\u00eda y autor.

    "},{"location":"develop/filtered/angular17/#anadir-el-punto-de-entrada","title":"A\u00f1adir el punto de entrada","text":"

    A\u00f1adimos la ruta al men\u00fa para que podamos navegar a esta pantalla:

    app.routes.ts
    import { Routes } from '@angular/router';\n\nexport const routes: Routes = [\n    { path: '', redirectTo: '/games', pathMatch: 'full'},\n    { path: 'categories', loadComponent: () => import('../category/category-list/category-list.page').then(m => m.CategoryListPage)},\n    { path: 'authors', loadComponent: () => import('../author/author-list/author-list.page').then(m => m.AuthorListPage)},\n    { path: 'games', loadComponent: () => import('../game/game-list/game-list.page').then(m => m.GameListPage)}\n];\n

    Adem\u00e1s, hemos a\u00f1adido una regla adicional con el path vac\u00edo para indicar que si no pone ruta, por defecto la p\u00e1gina inicial redirija al path /games, que es nuevo path que hemos a\u00f1adido.

    "},{"location":"develop/filtered/angular17/#implementar-servicio","title":"Implementar servicio","text":"

    A continuaci\u00f3n implementamos el servicio y mockeamos datos de ejemplo:

    mock-games.tsgame.service.ts
    import { Game } from \"./Game\";\n\nexport const GAME_DATA: Game[] = [\n    { id: 1, title: 'Juego 1', age: 6, category: { id: 1, name: 'Categor\u00eda 1' }, author: { id: 1, name: 'Autor 1', nationality: 'Nacionalidad 1' } },\n    { id: 2, title: 'Juego 2', age: 8, category: { id: 1, name: 'Categor\u00eda 1' }, author: { id: 2, name: 'Autor 2', nationality: 'Nacionalidad 2' } },\n    { id: 3, title: 'Juego 3', age: 4, category: { id: 1, name: 'Categor\u00eda 1' }, author: { id: 3, name: 'Autor 3', nationality: 'Nacionalidad 3' } },\n    { id: 4, title: 'Juego 4', age: 10, category: { id: 2, name: 'Categor\u00eda 2' }, author: { id: 1, name: 'Autor 1', nationality: 'Nacionalidad 1' } },\n    { id: 5, title: 'Juego 5', age: 16, category: { id: 2, name: 'Categor\u00eda 2' }, author: { id: 2, name: 'Autor 2', nationality: 'Nacionalidad 2' } },\n    { id: 6, title: 'Juego 6', age: 16, category: { id: 2, name: 'Categor\u00eda 2' }, author: { id: 3, name: 'Autor 3', nationality: 'Nacionalidad 3' } },\n    { id: 7, title: 'Juego 7', age: 12, category: { id: 3, name: 'Categor\u00eda 3' }, author: { id: 1, name: 'Autor 1', nationality: 'Nacionalidad 1' } },\n    { id: 8, title: 'Juego 8', age: 14, category: { id: 3, name: 'Categor\u00eda 3' }, author: { id: 2, name: 'Autor 2', nationality: 'Nacionalidad 2' } },\n]\n
    import { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Game } from './model/Game';\nimport { GAME_DATA } from './model/mock-games';\n\n@Injectable({\n    providedIn: 'root'\n})\nexport class GameService {\n\n    constructor() { }\n\n    getGames(title?: string, categoryId?: number): Observable<Game[]> {\n        return of(GAME_DATA);\n    }\n\n    saveGame(game: Game): Observable<void> {\n        return of(null);\n    }\n\n}\n
    "},{"location":"develop/filtered/angular17/#implementar-listado","title":"Implementar listado","text":"

    Ya tenemos las operaciones del servicio con datos, as\u00ed que ahora vamos a por el listado filtrado.

    game-list.component.htmlgame-list.component.scssgame-list.component.ts
    <div class=\"container\">\n    <h1>Cat\u00e1logo de juegos</h1>\n\n    <div class=\"filters\">\n        <form>\n            <mat-form-field>\n                <mat-label>T\u00edtulo del juego</mat-label>\n                <input\n                    type=\"text\"\n                    matInput\n                    placeholder=\"T\u00edtulo del juego\"\n                    [(ngModel)]=\"filterTitle\"\n                    name=\"title\"\n                />\n            </mat-form-field>\n\n            <mat-form-field>\n                <mat-label>Categor\u00eda del juego</mat-label>\n                <mat-select disableRipple [(ngModel)]=\"filterCategory\" name=\"category\">\n                    @for (category of categories(); track category.id) {\n                        <mat-option [value]=\"category\">{{ category.name }}</mat-option>\n                    }\n                </mat-select>\n            </mat-form-field>\n        </form>\n\n        <div class=\"buttons\">\n            <button mat-stroked-button (click)=\"onCleanFilter()\">Limpiar</button>\n            <button mat-stroked-button (click)=\"onSearch()\">Filtrar</button>\n        </div>\n    </div>\n\n    <div class=\"game-list\">\n        @for (game of games(); track game.id) {\n            <app-game-item (click)=\"editGame(game)\" />\n        }\n    </div>\n\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\" (click)=\"createGame()\">\n            Nuevo juego\n        </button>\n    </div>\n</div>\n
    .container {\n    margin: 20px;\n\n    .filters {\n        display: flex;\n\n        mat-form-field {\n            width: 300px;\n            margin-right: 20px;\n        }\n\n        .buttons {\n            flex: auto;\n            align-self: center;\n\n            button {\n                margin-left: 15px;\n            }\n        }\n    }\n\n    .game-list { \n        margin-top: 20px;\n        margin-bottom: 20px;\n\n        display: flex;\n        flex-flow: wrap;\n        overflow: auto;  \n    }\n\n    .buttons {\n        text-align: right;\n    }\n}\n\nbutton {\n    width: 125px;\n}\n
    import { Component, OnInit, inject, signal } from '@angular/core';\nimport { MatDialog } from '@angular/material/dialog';\nimport { GameEditComponent } from '../game-edit/game-edit.component';\nimport { GameService } from '../game.service';\nimport { Game } from '../model/Game';\nimport { CategoryService } from '../../category/category.service';\nimport { Category } from '../../category/model/Category';\nimport { CommonModule } from '@angular/common';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatTableModule } from '@angular/material/table';\nimport { FormsModule } from '@angular/forms';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatSelectModule } from '@angular/material/select';\nimport { GameItemComponent } from './game-item/game-item.component';\n\n@Component({\n    selector: 'app-game-list',\n    standalone: true,\n    imports: [\n        MatButtonModule,\n        MatIconModule,\n        MatTableModule,\n        CommonModule,\n        FormsModule,\n        MatFormFieldModule,\n        MatInputModule,\n        MatSelectModule,\n        GameItemComponent\n    ],\n    templateUrl: './game-list.component.html',\n    styleUrl: './game-list.component.scss',\n})\nexport class GameListComponent implements OnInit {\n    protected readonly categories = signal<Category[]>([]);\n    protected readonly games = signal<Game[]>([]);\n    protected readonly filterCategory = signal<Category | null>(null);\n    protected readonly filterTitle = signal<string>('');\n\n    protected readonly gameService = inject(GameService);\n    protected readonly categoryService = inject(CategoryService);\n    protected readonly dialog = inject(MatDialog);\n\n    ngOnInit(): void {\n        this.gameService.getGames().subscribe((games) => this.games.set(games));\n\n        this.categoryService\n            .getCategories()\n            .subscribe((categories) => this.categories.set(categories));\n    }\n\n    onCleanFilter(): void {\n        this.filterTitle.set('');\n        this.filterCategory.set(null);\n        this.onSearch();\n    }\n\n    onSearch(): void {\n        const title = this.filterTitle();\n        const categoryId =\n            this.filterCategory() != null ? this.filterCategory().id : null;\n\n        this.gameService\n            .getGames(title, categoryId)\n            .subscribe((games) => this.games.set(games));\n    }\n\n    createGame() {\n        const dialogRef = this.dialog.open(GameEditComponent, {\n            data: {},\n        });\n\n        dialogRef.afterClosed().subscribe((result) => {\n            if(!result) return;\n            this.onSearch();\n        });\n    }\n\n    editGame(game: Game) {\n        const dialogRef = this.dialog.open(GameEditComponent, {\n            data: { game: game },\n        });\n\n        dialogRef.afterClosed().subscribe((result) => {\n            if(!result) return;\n            this.onSearch();\n        });\n    }\n}\n

    Con todos estos cambios y si refrescamos el navegador, deber\u00eda verse una pantalla similar a esta:

    Tenemos una pantalla con una secci\u00f3n de filtros en la parte superior, donde podemos introducir un texto o seleccionar una categor\u00eda de un dropdown, un listado que de momento tiene todos los componentes b\u00e1sicos en una fila, uno detr\u00e1s del otro, y un bot\u00f3n para crear juegos nuevos.

    Dropdown

    El componente Dropdown es uno de los componentes m\u00e1s utilizados en las pantallas y formularios de Angular. Ves familiariz\u00e1ndote con \u00e9l porque lo vas a usar mucho. Es bastante potente y medianamente sencillo de utilizar. Los datos del listado pueden ser din\u00e1micos (desde servidor) o est\u00e1ticos (si los valores ya los tienes prefijados).

    "},{"location":"develop/filtered/angular17/#implementar-detalle-del-item","title":"Implementar detalle del item","text":"

    Ahora vamos a implementar el detalle de cada uno de los items que forman el listado. Para ello lo primero que haremos ser\u00e1 pasarle la informaci\u00f3n del juego a cada componente como un dato de entrada Input hacia el componente.

    game-list.component.html
    <div class=\"container\">\n    <h1>Cat\u00e1logo de juegos</h1>\n\n    <div class=\"filters\">\n        <form>\n            <mat-form-field>\n                <mat-label>T\u00edtulo del juego</mat-label>\n                <input\n                    type=\"text\"\n                    matInput\n                    placeholder=\"T\u00edtulo del juego\"\n                    [(ngModel)]=\"filterTitle\"\n                    name=\"title\"\n                />\n            </mat-form-field>\n\n            <mat-form-field>\n                <mat-label>Categor\u00eda del juego</mat-label>\n                <mat-select disableRipple [(ngModel)]=\"filterCategory\" name=\"category\">\n                    @for (category of categories(); track category.id) {\n                        <mat-option [value]=\"category\">{{ category.name }}</mat-option>\n                    }\n                </mat-select>\n            </mat-form-field>\n        </form>\n\n        <div class=\"buttons\">\n            <button mat-stroked-button (click)=\"onCleanFilter()\">Limpiar</button>\n            <button mat-stroked-button (click)=\"onSearch()\">Filtrar</button>\n        </div>\n    </div>\n\n    <div class=\"game-list\">\n        @for (game of games(); track game.id) {\n            <app-game-item (click)=\"editGame(game)\" [game]=\"game\" />\n        }\n    </div>\n\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\" (click)=\"createGame()\">\n            Nuevo juego\n        </button>\n    </div>\n</div>\n

    Tambi\u00e9n vamos a necesitar una foto de ejemplo para poner dentro de la tarjeta detalle de los juegos. Vamos a utilizar esta imagen:

    Desc\u00e1rgala y d\u00e9jala dentro del proyecto en public/img/foto.png. Y ya para terminar, implementamos el componente de detalle:

    game-item.component.htmlgame-item.component.scssgame-item.component.ts
    <div class=\"container\">\n    <mat-card>\n        <div class=\"photo\">\n            <img src=\"img/foto.png\">\n        </div>\n        <div class=\"detail\">\n            <div class=\"title\">{{game().title}}</div>\n            <div class=\"properties\">\n                <div><i>Edad recomendada: </i>+{{game().age}}</div>\n                <div><i>Categor\u00eda: </i>{{game().category.name}}</div>\n                <div><i>Autor: </i>{{game().author.name}}</div>\n                <div><i>Nacionalidad: </i>{{game().author.nationality}}</div>\n            </div>\n        </div>\n    </mat-card>\n</div>\n
    .container {\n    display: flex;\n    width: 325px;\n\n    mat-card {\n        width: 100%;\n        margin: 10px;\n        display: flex;\n        padding: 1rem;\n\n        .photo {\n            margin-right: 10px;\n\n            img {\n                width: 80px;\n                height: 80px;\n            }\n        }\n\n        .detail {\n            .title {\n                font-size: 14px;\n                font-weight: bold;\n            }\n\n            .properties {\n                font-size: 11px;\n\n                div {\n                    height: 15px;\n                }                \n            }\n        }\n    }\n}    \n
    import { Component, input } from '@angular/core';\nimport { Game } from '../../model/Game';\nimport {MatCardModule} from '@angular/material/card';\n\n@Component({\n    selector: 'app-game-item',\n    standalone: true,\n    imports: [MatCardModule],\n    templateUrl: './game-item.component.html',\n    styleUrl: './game-item.component.scss'\n})\nexport class GameItemComponent {\n    protected readonly game = input.required<Game>();\n}\n

    Ahora s\u00ed que deber\u00eda quedar algo similar a esta pantalla:

    "},{"location":"develop/filtered/angular17/#implementar-dialogo-de-edicion","title":"Implementar di\u00e1logo de edici\u00f3n","text":"

    Ya solo nos falta el \u00faltimo paso, implementar el cuadro de edici\u00f3n / alta de un nuevo juego. Pero tenemos un peque\u00f1o problema, y es que al crear o editar un juego debemos seleccionar una Categor\u00eda y un Autor.

    Para la Categor\u00eda no tenemos ning\u00fan problema, pero para el Autor no tenemos un servicio que nos devuelva todos los autores, solo tenemos un servicio que nos devuelve una Page de autores.

    As\u00ed que lo primero que haremos ser\u00e1 implementar una operaci\u00f3n getAllAuthors para poder recuperar una lista.

    mock-authors-list.tsauthor.service.ts
    import { Author } from \"./Author\";\n\nexport const AUTHOR_DATA_LIST : Author[] = [\n    { id: 1, name: 'Klaus Teuber', nationality: 'Alemania' },\n    { id: 2, name: 'Matt Leacock', nationality: 'Estados Unidos' },\n    { id: 3, name: 'Keng Leong Yeo', nationality: 'Singapur' },\n    { id: 4, name: 'Gil Hova', nationality: 'Estados Unidos'},\n    { id: 5, name: 'Kelly Adams', nationality: 'Estados Unidos' },\n]    \n
    import { Injectable, inject } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Pageable } from '../core/model/page/Pageable';\nimport { Author } from './model/Author';\nimport { PaginatedData } from 'src/app/core/model/page/PaginatedData';\nimport { HttpClient } from '@angular/common/http';\nimport { AUTHOR_DATA_LIST } from './model/mock-authors-list';\n\n@Injectable({\n    providedIn: 'root',\n})\nexport class AuthorService {\n    protected readonly http = inject(HttpClient);\n\n    private baseUrl = 'http://localhost:8080/author';\n\n    getAuthors(pageable: Pageable): Observable<PaginatedData<Author>> {\n        return this.http.post<PaginatedData<Author>>(this.baseUrl, { pageable: pageable });\n    }\n\n    saveAuthor(author: Author): Observable<Author> {\n        const { id } = author;\n        const url = id ? `${this.baseUrl}/${id}` : this.baseUrl;\n        return this.http.put<Author>(url, author);\n    }\n\n    deleteAuthor(idAuthor: number): Observable<void> {\n        return this.http.delete<void>(`${this.baseUrl}/${idAuthor}`);\n    }\n\n    getAllAuthors(): Observable<Author[]> {\n        return of(AUTHOR_DATA_LIST);\n    }\n}\n

    Ahora s\u00ed que tenemos todo listo para implementar el cuadro de di\u00e1logo para dar de alta o editar juegos.

    game-edit.component.htmlgame-edit.component.scssgame-edit.component.ts
    <div class=\"container\">\n    @if (id()) {\n        <h1>Modificar juego</h1>\n    } @else {\n        <h1>Crear juego</h1>\n    }\n\n    <form>\n        <mat-form-field>\n            <mat-label>Identificador</mat-label>\n            <input type=\"text\" matInput placeholder=\"Identificador\" [(ngModel)]=\"id\" name=\"id\" disabled>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>T\u00edtulo</mat-label>\n            <input type=\"text\" matInput placeholder=\"T\u00edtulo del juego\" [(ngModel)]=\"title\" name=\"title\" required>\n            <mat-error>El t\u00edtulo no puede estar vac\u00edo</mat-error>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Edad recomendada</mat-label>\n            <input type=\"number\" matInput placeholder=\"Edad recomendada\" [(ngModel)]=\"age\" name=\"age\" required>\n            <mat-error>La edad no puede estar vac\u00eda</mat-error>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Categor\u00eda</mat-label>\n            <mat-select disableRipple [(ngModel)]=\"categoryId\" name=\"category\" required>\n                @for (cat of categories(); track cat.id) {\n                    <mat-option [value]=\"cat.id\">{{cat.name}}</mat-option>\n                }\n            </mat-select>\n            <mat-error>La categor\u00eda no puede estar vac\u00eda</mat-error>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Autor</mat-label>\n            <mat-select disableRipple [(ngModel)]=\"authorId\" name=\"author\" required>\n                @for (aut of authors(); track aut.id) {\n                    <mat-option [value]=\"aut.id\">{{aut.name}}</mat-option>\n                }\n            </mat-select>\n            <mat-error>El autor no puede estar vac\u00edo</mat-error>\n        </mat-form-field>\n    </form>\n\n    <div class=\"buttons\">\n        <button mat-stroked-button (click)=\"onClose()\">Cerrar</button>\n        <button mat-flat-button color=\"primary\" (click)=\"onSave()\">Guardar</button>\n    </div>\n</div>\n
    .container {\n    min-width: 350px;\n    max-width: 500px;\n    padding: 20px;\n\n    form {\n        display: flex;\n        flex-direction: column;\n        margin-bottom:20px;\n    }\n\n    .buttons {\n        text-align: right;\n\n        button {\n            margin-left: 10px;\n        }\n    }\n}\n

    ``` TypeScript import { Component, inject, OnInit, signal } from '@angular/core'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { GameService } from '../game.service'; import { Game } from '../model/Game'; import { AuthorService } from '../../author/author.service'; import { Author } from '../../author/model/Author'; import { CategoryService } from '../../category/category.service'; import { Category } from '../../category/model/Category'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { validateFields } from '../../core/helpers/validation.helper';

    @Component({ selector: 'app-game-edit', standalone: true, imports: [FormsModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatButtonModule, MatSelectModule ], templateUrl: './game-edit.component.html', styleUrl: './game-edit.component.scss', }) export class GameEditComponent implements OnInit { protected readonly id = signal(null); protected readonly title = signal(null); protected readonly age = signal(null); protected readonly categoryId = signal(null); protected readonly authorId = signal(null); protected readonly categories = signal([]); protected readonly authors = signal([]);

    protected readonly dialogRef = inject(MatDialogRef<GameEditComponent>);\nprotected readonly data = inject(MAT_DIALOG_DATA);\nprotected readonly gameService = inject(GameService);\nprotected readonly categoryService = inject(CategoryService);\nprotected readonly authorService = inject(AuthorService);\n\nngOnInit(): void {\n    this.loadFormData(this.data.game ?? null);\n}\n\nloadFormData(initialData: Game | null): void {\n    this.id.set(initialData?.id ?? null);\n    this.title.set(initialData?.title ?? null);\n    this.age.set(initialData?.age ?? null);\n\n    this.categoryService.getCategories().subscribe((cats) => {\n        this.categories.set(cats);\n        this.categoryId.set(initialData?.category?.id ?? null);\n    });\n\n    this.authorService.getAllAuthors().subscribe((auts) => {\n        this.authors.set(auts);\n        this.authorId.set(initialData?.author?.id ?? null);\n    });\n}\n\nonSave() {\n    const id = this.id();\n    const title = this.title(); \n    const age = this.age(); \n    const categoryId = this.categoryId(); \n    const authorId = this.authorId();\n\n    const requiredFields = [\"title\", \"age\", \"categoryId\", \"authorId\"] as const\n    const data = { title, age, categoryId, authorId }\n\n    if (!validateFields(data, requiredFields)) {\n        return;\n    }\n\n    const game = {\n        id,\n        title,\n        age,\n        category: this.categories().find(c => c.id === categoryId) ?? null,\n        author: this.authors().find(a => a.id === authorId) ?? null,\n    } as Game;\n    this.gameService.saveGame(game).subscribe(() => {\n        this.dialogRef.close(true);\n    });\n}\n\nonClose() {\n    this.dialogRef.close();\n}\n

    }

    Como puedes ver, para rellenar los componentes seleccionables de dropdown, hemos realizado una consulta al servicio para recuperar todos los autores y categor\u00edas, y en la respuesta de cada uno de ellos, hemos buscado en los resultados cu\u00e1l es el que coincide con el ID enviado desde el listado, y ese es el que hemos fijado en el objeto Game.

    De esta forma, no estamos cogiendo directamente los datos del listado, sino que no estamos asegurando que los datos de autor y de categor\u00eda son los que vienen del servicio, siempre filtrando por su ID.

    "},{"location":"develop/filtered/angular17/#conectar-con-backend","title":"Conectar con Backend","text":"

    Antes de seguir

    Antes de seguir con este punto, debes implementar el c\u00f3digo de backend en la tecnolog\u00eda que quieras (Springboot o Nodejs). Si has empezado este cap\u00edtulo implementando el frontend, por favor accede a la secci\u00f3n correspondiente de backend para poder continuar con el tutorial. Una vez tengas implementadas todas las operaciones para este listado, puedes volver a este punto y continuar con Angular.

    Una vez implementado front y back, lo que nos queda es modificar el servicio del front para que conecte directamente con las operaciones ofrecidas por el back.

    author-service.tsgame-service.ts
    import { Injectable, inject } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Pageable } from '../core/model/page/Pageable';\nimport { Author } from './model/Author';\nimport { PaginatedData } from 'src/app/core/model/page/PaginatedData';\nimport { HttpClient } from '@angular/common/http';\n\n@Injectable({\n    providedIn: 'root',\n})\nexport class AuthorService {\n    protected readonly http = inject(HttpClient);\n\n    private baseUrl = 'http://localhost:8080/author';\n\n    getAuthors(pageable: Pageable): Observable<PaginatedData<Author>> {\n        return this.http.post<PaginatedData<Author>>(this.baseUrl, { pageable: pageable });\n    }\n\n    saveAuthor(author: Author): Observable<Author> {\n        const { id } = author;\n        const url = id ? `${this.baseUrl}/${id}` : this.baseUrl;\n        return this.http.put<Author>(url, author);\n    }\n\n    deleteAuthor(idAuthor: number): Observable<void> {\n        return this.http.delete<void>(`${this.baseUrl}/${idAuthor}`);\n    }\n\n    getAllAuthors(): Observable<Author[]> {\n        return this.http.get<Author[]>(this.baseUrl);\n    }\n}\n
    import { Injectable, inject } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Game } from './model/Game';\nimport { HttpClient } from '@angular/common/http';\n\n@Injectable({\nprovidedIn: 'root',\n})\nexport class GameService {\n    protected readonly http = inject(HttpClient);\n\n    private baseUrl = 'http://localhost:8080/game';\n\n    getGames(title?: string, categoryId?: number): Observable<Game[]> {\n        return this.http.get<Game[]>(this.composeFindUrl(title, categoryId));\n    }\n\n    saveGame(game: Game): Observable<void> {\n        const { id } = game;\n        const url = id ? `${this.baseUrl}/${id}` : this.baseUrl;\n\n        return this.http.put<void>(url, game);\n    }\n\n    private composeFindUrl(title?: string, categoryId?: number): string {\n        const params = new URLSearchParams();\n        if (title) {\n          params.set('title', title);\n        }  \n        if (categoryId) {\n            params.set('idCategory', categoryId.toString());\n        }\n        const queryString = params.toString();\n        return queryString ? `${this.baseUrl}?${queryString}` : this.baseUrl;\n    }\n}\n

    Y ahora si, podemos navegar por la web y ver el resultado completo.

    "},{"location":"develop/filtered/nodejs/","title":"Listado simple - Nodejs","text":"

    En este punto ya tenemos dos listados, uno b\u00e1sico y otro paginado. Ahora vamos a implementar un listado un poco diferente, este listado va a tener filtros de b\u00fasqueda.

    Como ya conocemos como se debe desarrollar, en este ejemplo vamos a ir m\u00e1s r\u00e1pidos y nos vamos a centrar \u00fanicamente en las novedades.

    "},{"location":"develop/filtered/nodejs/#crear-modelos","title":"Crear Modelos","text":"

    Lo primero que vamos a hacer es crear el modelo de author para trabajar con BBDD. En la carpeta schemas creamos el archivo game.schema.js:

    game.schema.js
    import mongoose from \"mongoose\";\nconst { Schema, model } = mongoose;\nimport normalize from 'normalize-mongoose';\n\nconst gameSchema = new Schema({\n    title: {\n        type: String,\n        require: true\n    },\n    age: {\n        type: Number,\n        require: true,\n        max: 99,\n        min: 0\n    },\n    category: {\n        type: Schema.Types.ObjectId,\n        ref: 'Category',\n        required: true\n    },\n    author: {\n        type: Schema.Types.ObjectId,\n        ref: 'Author',\n        required: true\n    }\n});\n\ngameSchema.plugin(normalize);\nconst GameModel = model('Game', gameSchema);\n\nexport default GameModel;\n

    Lo m\u00e1s novedoso aqu\u00ed es que ahora cada juego va a tener una categor\u00eda y un autor asociados. Para ello simplemente en el tipo del dato Category y Author tenemos que hacer referencia al id del esquema deseado.

    "},{"location":"develop/filtered/nodejs/#implementar-el-service","title":"Implementar el Service","text":"

    Creamos el service correspondiente game.service.js:

    game.service.js
    import GameModel from '../schemas/game.schema.js';\n\nexport const getGames = async (title, category) => {\n    try {\n        const regexTitle = new RegExp(title, 'i');\n        const find = category? { $and: [{ title: regexTitle }, { category: category }] } : { title: regexTitle };\n        return await GameModel.find(find).sort('id').populate('category').populate('author');\n    } catch(e) {\n        throw Error('Error fetching games');\n    }\n}\n\nexport const createGame = async (data) => {\n    try {\n        const game = new GameModel({\n            ...data,\n            category: data.category.id,\n            author: data.author.id,\n        });\n        return await game.save();\n    } catch (e) {\n        throw Error('Error creating game');\n    }\n}\n\nexport const updateGame = async (id, data) => {\n    try {\n        const game = await GameModel.findById(id);\n        if (!game) {\n            throw Error('There is no game with that Id');\n        }    \n        const gameToUpdate = {\n            ...data,\n            category: data.category.id,\n            author: data.author.id,\n        };\n        return await GameModel.findByIdAndUpdate(id, gameToUpdate, { new: false });\n    } catch (e) {\n        throw Error(e);\n    }\n}\n

    En este caso recibimos en el m\u00e9todo para recuperar juegos dos par\u00e1metros, el titulo del juego y la categor\u00eda. Aqu\u00ed vamos a utilizar una expresi\u00f3n regular para que podamos encontrar cualquier juego que contenga el titulo que pasemos en su nombre. Con la categor\u00eda tiene que ser el valor exacto de su id. El m\u00e9todo populate lo que hace es traernos toda la informaci\u00f3n de la categor\u00eda y del autor. Sino lo us\u00e1semos solo nos recuperar\u00eda el id.

    "},{"location":"develop/filtered/nodejs/#implementar-el-controller","title":"Implementar el Controller","text":"

    Creamos el controlador game.controller.js:

    game.controller.js
    import * as GameService from '../services/game.service.js';\n\nexport const getGames = async (req, res) => {\n    try {\n        const titleToFind = req.query?.title || '';\n        const categoryToFind = req.query?.idCategory || null;\n        const games = await GameService.getGames(titleToFind, categoryToFind);\n        res.status(200).json(games);\n    } catch(err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n\nexport const createGame = async (req, res) => {\n    try {\n        const game = await GameService.createGame(req.body);\n        res.status(200).json({\n            game\n        });\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n\nexport const updateGame = async (req, res) => {\n    const gameId = req.params.id;\n    try {\n        await GameService.updateGame(gameId, req.body);\n        res.status(200).json(1);\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n

    Los m\u00e9todos son muy parecidos al resto de los controllers. En este caso para recuperar los datos del filtro tendremos que hacerlo con req.query para leer los datos que nos lleguen como query params en la url. Por ejemplo: http://localhost:8080/game?title=trivial&category=1

    "},{"location":"develop/filtered/nodejs/#implementar-las-rutas","title":"Implementar las Rutas","text":"

    Y por \u00faltimo creamos nuestro archivo de rutas game.routes.js:

    game.routes.js
    import { Router } from 'express';\nimport { check } from 'express-validator';\nimport validateFields from '../middlewares/validateFields.js';\nimport { createGame, getGames, updateGame } from '../controllers/game.controller.js';\nconst gameRouter = Router();\n\ngameRouter.put('/:id', [\n    check('title').not().isEmpty(),\n    check('age').not().isEmpty(),\n    check('age').isNumeric(),\n    check('category.id').not().isEmpty(),\n    check('author.id').not().isEmpty(),\n    validateFields\n], updateGame);\n\ngameRouter.put('/', [\n    check('title').not().isEmpty(),\n    check('age').not().isEmpty(),\n    check('age').isNumeric(),\n    check('category.id').not().isEmpty(),\n    check('author.id').not().isEmpty(),\n    validateFields\n], createGame);\n\ngameRouter.get('/', getGames);\ngameRouter.get('/:query', getGames);\n\nexport default gameRouter;\n

    En este caso hemos tenido que meter dos rutas para get, una para cuando se informen los filtros y otra para cuando no vayan informados. Si lo hici\u00e9ramos con una \u00fanica ruta nos fallar\u00eda en el otro caso.

    Finalmente en nuestro archivo index.js vamos a a\u00f1adir el nuevo router:

    index.js
    ...\n\nimport gameRouter from './src/routes/game.routes.js';\n\n...\n\napp.use('/game', gameRouter);\n\n...\n
    "},{"location":"develop/filtered/nodejs/#probar-las-operaciones","title":"Probar las operaciones","text":"

    Y ahora que tenemos todo creado, ya podemos probarlo con Postman:

    Por un lado creamos juegos con:

    ** PUT /game **

    ** PUT /game/{id} **

    {\n    \"title\": \"Nuevo juego\",\n    \"age\": \"18\",\n    \"category\": {\n        \"id\": \"63e8b795f7dae4b980b63202\"\n },\n    \"author\": {\n        \"id\": \"63e8bda064c208e065667bfa\"\n    }\n}\n

    Tambi\u00e9n podemos filtrar y recuperar informaci\u00f3n:

    ** GET /game **

    ** GET /game?title=xxx **

    ** GET /game?idCategory=xxx **

    "},{"location":"develop/filtered/nodejs/#implementar-validaciones","title":"Implementar validaciones","text":"

    Ahora que ya tenemos todos nuestros CRUDs creados vamos a introducir unas peque\u00f1as validaciones.

    "},{"location":"develop/filtered/nodejs/#validacion-en-borrado","title":"Validaci\u00f3n en borrado","text":"

    La primera validaci\u00f3n sera para que no podamos borrar categor\u00edas ni autores que tengan un juego asociado. Para ello primero tendremos que crear un m\u00e9todo en el servicio de juegos para buscar los juegos que correspondan con un campo dado. En game.service.js a\u00f1adimos:

    game.service.js
    ...\nexport const getGame = async (field) => {\n    try {\n        return await GameModel.find(field);\n    } catch (e) {\n        throw Error('Error fetching games');\n    }\n}\n...\n

    Y ahora en category.service.js importamos el m\u00e9todo creado y modificamos el m\u00e9todo para borrar categor\u00edas:

    category.service.js
    ...\nimport { getGame } from './game.service.js';\n...\n\n...\nexport const deleteCategory = async (id) => {\n    try {\n        const category = await CategoryModel.findById(id);\n        if (!category) {\n            throw 'There is no category with that Id';\n        }\n        const games = await getGame({category});\n        if(games.length > 0) {\n            throw 'There are games related to this category';\n        }\n        return await CategoryModel.findByIdAndDelete(id);\n    } catch (e) {\n        throw Error(e);\n    }\n}\n...\n

    De este modo si encontramos alg\u00fan juego con esta categor\u00eda no nos dejar\u00e1 borrarla.

    Por \u00faltimo, hacemos lo mismo en author.service.js:

    author.service.js
    ...\nimport { getGame } from './game.service.js';\n...\n\n...\nexport const deleteAuthor = async (id) => {\n    try {\n        const author = await AuthorModel.findById(id);\n        if (!author) {\n            throw 'There is no author with that Id';\n        }\n        const games = await getGame({author});\n        if(games.length > 0) {\n            throw 'There are games related to this author';\n        }\n        return await AuthorModel.findByIdAndDelete(id);\n    } catch (e) {\n        throw Error(e);\n    }\n}\n...\n
    "},{"location":"develop/filtered/nodejs/#validacion-en-creacion","title":"Validaci\u00f3n en creaci\u00f3n","text":"

    En las creaciones es conveniente validad la existencia de las entidades relacionadas para garantizar la integridad de la BBDD.

    Para esto vamos a introducir una validaci\u00f3n en la creaci\u00f3n y edici\u00f3n de los juegos para garantizar que la categor\u00eda y el autor proporcionados existen.

    En primer lugar vamos a crear los servicios de consulta de categor\u00eda y autor:

    category.service.js
    ...\nexport const getCategory = async (id) => {\n    try {\n        return await CategoryModel.findById(id);\n    } catch (e) {\n        throw Error('There is no category with that Id');\n    }\n}\n...\n
    author.service.js
    ...\nexport const getAuthor = async (id) => {\n    try {\n        return await AuthorModel.findById(id);\n    } catch (e) {\n        throw Error('There is no author with that Id');\n    }\n}\n...\n

    Teniendo los servicios ya disponibles, vamos a a\u00f1adir las validaciones a los servicios de creaci\u00f3n y edici\u00f3n:

    game.service.js
    ...\nimport { getCategory } from './category.service.js';\nimport { getAuthor } from './author.service.js';\n...\n\n...\nexport const createGame = async (data) => {\n    try {\n        const category = await getCategory(data.category.id);\n        if (!category) {\n            throw Error('There is no category with that Id');\n        }\n\n        const author = await getAuthor(data.author.id);\n        if (!author) {\n            throw Error('There is no author with that Id');\n        }\n\n        const game = new GameModel({\n            ...data,\n            category: data.category.id,\n            author: data.author.id,\n        });\n        return await game.save();\n    } catch (e) {\n        throw Error(e);\n    }\n}\n...\n\n...\nexport const updateGame = async (id, data) => {\n    try {\n        const game = await GameModel.findById(id);\n        if (!game) {\n            throw Error('There is no game with that Id');\n        }\n\n        const category = await getCategory(data.category.id);\n        if (!category) {\n            throw Error('There is no category with that Id');\n        }\n\n        const author = await getAuthor(data.author.id);\n        if (!author) {\n            throw Error('There is no author with that Id');\n        }\n\n        const gameToUpdate = {\n            ...data,\n            category: data.category.id,\n            author: data.author.id,\n        };\n        return await GameModel.findByIdAndUpdate(id, gameToUpdate, { new: false });\n    } catch (e) {\n        throw Error(e);\n    }\n}\n...\n

    Con esto ya tendr\u00edamos acabado nuestro CRUD.

    "},{"location":"develop/filtered/react/","title":"Listado filtrado - Angular","text":"

    En este punto ya tenemos dos listados, uno b\u00e1sico y otro paginado. Ahora vamos a implementar un listado un poco diferente, con filtros y con una presentaci\u00f3n un tanto distinta.

    Como ya conocemos como se debe desarrollar, en este ejemplo vamos a ir m\u00e1s r\u00e1pidos y nos vamos a centrar \u00fanicamente en las novedades.

    Vamos a desarrollar el listado de Juegos. Este listado es un tanto peculiar, porque no tiene una tabla como tal, sino que vamos a mostrar los juegos como cards. Ya tenemos creado nuestro componentes pagina pero vamos a necesitar un componente para mostrar cada uno de los juegos y otro para crear y editar los juegos.

    "},{"location":"develop/filtered/react/#crear-componente-game","title":"Crear componente game","text":"

    Manos a la obra:

    Creamos el fichero Game.ts dentro de la carpeta types:

    Game.ts
    import { Category } from \"./Category\";\nimport { Author } from \"./Author\";\n\nexport interface Game {\n  id: string;\n  title: string;\n  age: number;\n  category?: Category;\n  author?: Author;\n}\n

    Modificamos nuestra api de Toolkit para a\u00f1adir los endpoints de juegos y aparte creamos un endpoint para recuperar los autores que necesitaremos para crear un nuevo juego, el fichero completo quedar\u00eda de esta manera:

    import { createApi, fetchBaseQuery } from \"@reduxjs/toolkit/query/react\";\nimport { Game } from \"../../types/Game\";\nimport { Category } from \"../../types/Category\";\nimport { Author, AuthorResponse } from \"../../types/Author\";\n\nexport const ludotecaAPI = createApi({\n  reducerPath: \"ludotecaApi\",\n  baseQuery: fetchBaseQuery({\n    baseUrl: \"http://localhost:8080\",\n  }),\n  tagTypes: [\"Category\", \"Author\", \"Game\"],\n  endpoints: (builder) => ({\n    getCategories: builder.query<Category[], null>({\n      query: () => \"category\",\n      providesTags: [\"Category\"],\n    }),\n    createCategory: builder.mutation({\n      query: (payload) => ({\n        url: \"/category\",\n        method: \"PUT\",\n        body: payload,\n        headers: {\n          \"Content-type\": \"application/json; charset=UTF-8\",\n        },\n      }),\n      invalidatesTags: [\"Category\"],\n    }),\n    deleteCategory: builder.mutation({\n      query: (id: string) => ({\n        url: `/category/${id}`,\n        method: \"DELETE\",\n      }),\n      invalidatesTags: [\"Category\"],\n    }),\n    updateCategory: builder.mutation({\n      query: (payload: Category) => ({\n        url: `category/${payload.id}`,\n        method: \"PUT\",\n        body: payload,\n      }),\n      invalidatesTags: [\"Category\"],\n    }),\n    getAllAuthors: builder.query<Author[], null>({\n      query: () => \"author\",\n      providesTags: [\"Author\"],\n    }),\n    getAuthors: builder.query<\n      AuthorResponse,\n      { pageNumber: number; pageSize: number }\n    >({\n      query: ({ pageNumber, pageSize }) => {\n        return {\n          url: \"author\",\n          method: \"POST\",\n          body: {\n            pageable: {\n              pageNumber,\n              pageSize,\n            },\n          },\n        };\n      },\n      providesTags: [\"Author\"],\n    }),\n    createAuthor: builder.mutation({\n      query: (payload) => ({\n        url: \"/author\",\n        method: \"PUT\",\n        body: payload,\n        headers: {\n          \"Content-type\": \"application/json; charset=UTF-8\",\n        },\n      }),\n      invalidatesTags: [\"Author\"],\n    }),\n    deleteAuthor: builder.mutation({\n      query: (id: string) => ({\n        url: `/author/${id}`,\n        method: \"DELETE\",\n      }),\n      invalidatesTags: [\"Author\"],\n    }),\n    updateAuthor: builder.mutation({\n      query: (payload: Author) => ({\n        url: `author/${payload.id}`,\n        method: \"PUT\",\n        body: payload,\n      }),\n      invalidatesTags: [\"Author\", \"Game\"],\n    }),\n    getGames: builder.query<Game[], { title: string; idCategory: string }>({\n      query: ({ title, idCategory }) => {\n        return {\n          url: \"game/\",\n          params: { title, idCategory },\n        };\n      },\n      providesTags: [\"Game\"],\n    }),\n    createGame: builder.mutation({\n      query: (payload: Game) => ({\n        url: \"/game\",\n        method: \"PUT\",\n        body: { ...payload },\n        headers: {\n          \"Content-type\": \"application/json; charset=UTF-8\",\n        },\n      }),\n      invalidatesTags: [\"Game\"],\n    }),\n    updateGame: builder.mutation({\n      query: (payload: Game) => ({\n        url: `game/${payload.id}`,\n        method: \"PUT\",\n        body: { ...payload },\n      }),\n      invalidatesTags: [\"Game\"],\n    }),\n\n  }),\n});\n\nexport const {\n  useGetCategoriesQuery,\n  useCreateCategoryMutation,\n  useDeleteCategoryMutation,\n  useUpdateCategoryMutation,\n  useCreateAuthorMutation,\n  useDeleteAuthorMutation,\n  useGetAllAuthorsQuery,\n  useGetAuthorsQuery,\n  useUpdateAuthorMutation,\n  useCreateGameMutation,\n  useGetGamesQuery,\n  useUpdateGameMutation\n} = ludotecaAPI;\n

    Creamos una nueva carpeta components dentro de src/pages/Game y dentro creamos un archivo llamado CreateGame.tsx con el siguiente contenido:

    CreateGame.tsx
    import { ChangeEvent, useContext, useEffect, useState } from \"react\";\nimport Button from \"@mui/material/Button\";\nimport MenuItem from \"@mui/material/MenuItem\";\nimport TextField from \"@mui/material/TextField\";\nimport Dialog from \"@mui/material/Dialog\";\nimport DialogActions from \"@mui/material/DialogActions\";\nimport DialogContent from \"@mui/material/DialogContent\";\nimport DialogTitle from \"@mui/material/DialogTitle\";\nimport {\n  useGetAllAuthorsQuery,\n  useGetCategoriesQuery,\n} from \"../../../redux/services/ludotecaApi\";\nimport { LoaderContext } from \"../../../context/LoaderProvider\";\nimport { Game } from \"../../../types/Game\";\nimport { Category } from \"../../../types/Category\";\nimport { Author } from \"../../../types/Author\";\n\ninterface Props {\n  game: Game | null;\n  closeModal: () => void;\n  create: (game: Game) => void;\n}\n\nconst initialState = {\n  id: \"\",\n  title: \"\",\n  age: 0,\n  category: undefined,\n  author: undefined,\n};\n\nexport default function CreateGame(props: Props) {\n  const [form, setForm] = useState<Game>(initialState);\n  const loader = useContext(LoaderContext);\n  const { data: categories, isLoading: isLoadingCategories } =\n    useGetCategoriesQuery(null);\n  const { data: authors, isLoading: isLoadingAuthors } =\n    useGetAllAuthorsQuery(null);\n\n  useEffect(() => {\n    setForm({\n      id: props.game?.id || \"\",\n      title: props.game?.title || \"\",\n      age: props.game?.age || 0,\n      category: props.game?.category,\n      author: props.game?.author,\n    });\n  }, [props?.game]);\n\n  useEffect(() => {\n    loader.showLoading(isLoadingCategories || isLoadingAuthors);\n  }, [isLoadingCategories, isLoadingAuthors]);\n\n  const handleChangeForm = (\n    event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n  ) => {\n    setForm({\n      ...form,\n      [event.target.id]: event.target.value,\n    });\n  };\n\n  const handleChangeSelect = (\n    event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n  ) => {\n    const values = event.target.name === \"category\" ? categories : authors;\n    setForm({\n      ...form,\n      [event.target.name]: values?.find((val) => val.id === event.target.value),\n    });\n  };\n\n  return (\n    <div>\n      <Dialog open={true} onClose={props.closeModal}>\n        <DialogTitle>\n          {props.game ? \"Actualizar Juego\" : \"Crear Juego\"}\n        </DialogTitle>\n        <DialogContent>\n          {props.game && (\n            <TextField\n              margin=\"dense\"\n              disabled\n              id=\"id\"\n              label=\"Id\"\n              fullWidth\n              value={props.game.id}\n              variant=\"standard\"\n            />\n          )}\n          <TextField\n            margin=\"dense\"\n            id=\"title\"\n            label=\"Titulo\"\n            fullWidth\n            variant=\"standard\"\n            onChange={handleChangeForm}\n            value={form.title}\n          />\n          <TextField\n            margin=\"dense\"\n            id=\"age\"\n            label=\"Edad Recomendada\"\n            fullWidth\n            type=\"number\"\n            variant=\"standard\"\n            onChange={handleChangeForm}\n            value={form.age}\n          />\n          <TextField\n            id=\"category\"\n            select\n            label=\"Categor\u00eda\"\n            defaultValue=\"''\"\n            fullWidth\n            variant=\"standard\"\n            name=\"category\"\n            value={form.category ? form.category.id : \"\"}\n            onChange={handleChangeSelect}\n          >\n            {categories &&\n              categories.map((option: Category) => (\n                <MenuItem key={option.id} value={option.id}>\n                  {option.name}\n                </MenuItem>\n              ))}\n          </TextField>\n          <TextField\n            id=\"author\"\n            select\n            label=\"Autor\"\n            defaultValue=\"''\"\n            fullWidth\n            variant=\"standard\"\n            name=\"author\"\n            value={form.author ? form.author.id : \"\"}\n            onChange={handleChangeSelect}\n          >\n            {authors &&\n              authors.map((option: Author) => (\n                <MenuItem key={option.id} value={option.id}>\n                  {option.name}\n                </MenuItem>\n              ))}\n          </TextField>\n        </DialogContent>\n        <DialogActions>\n          <Button onClick={props.closeModal}>Cancelar</Button>\n          <Button\n            onClick={() =>\n              props.create({\n                id: \"\",\n                title: form.title,\n                age: form.age,\n                category: form.category,\n                author: form.author,\n              })\n            }\n            disabled={\n              !form.title || !form.age || !form.category || !form.author\n            }\n          >\n            {props.game ? \"Actualizar\" : \"Crear\"}\n          </Button>\n        </DialogActions>\n      </Dialog>\n    </div>\n  );\n}\n

    Ahora en esa misma carpeta crearemos el componente GameCard.tsx para mostrar nuestros juegos con un dise\u00f1o de carta:

    GameCard.tsx
    import Card from \"@mui/material/Card\";\nimport CardContent from \"@mui/material/CardContent\";\nimport CardMedia from \"@mui/material/CardMedia\";\nimport CardHeader from \"@mui/material/CardHeader\";\nimport List from \"@mui/material/List\";\nimport ListItem from \"@mui/material/ListItem\";\nimport ListItemAvatar from \"@mui/material/ListItemAvatar\";\nimport ListItemText from \"@mui/material/ListItemText\";\nimport Avatar from \"@mui/material/Avatar\";\nimport PersonIcon from \"@mui/icons-material/Person\";\nimport LanguageIcon from \"@mui/icons-material/Language\";\nimport CardActionArea from \"@mui/material/CardActionArea\";\nimport red from \"@mui/material/colors/red\";\nimport imageGame from \"./../../../assets/foto.png\";\nimport { Game } from \"../../../types/Game\";\n\ninterface GameCardProps {\n  game: Game;\n}\n\nexport default function GameCard(props: GameCardProps) {\n  const { title, age, category, author } = props.game;\n  return (\n    <Card sx={{ maxWidth: 265 }}>\n      <CardHeader\n        sx={{\n          \".MuiCardHeader-title\": {\n            fontSize: \"20px\",\n          },\n        }}\n        avatar={\n          <Avatar sx={{ bgcolor: red[500] }} aria-label=\"age\">\n            +{age}\n          </Avatar>\n        }\n        title={title}\n        subheader={category?.name}\n      />\n      <CardActionArea>\n        <CardMedia\n          component=\"img\"\n          height=\"140\"\n          image={imageGame}\n          alt=\"game image\"\n        />\n        <CardContent>\n          <List dense={true}>\n            <ListItem>\n              <ListItemAvatar>\n                <Avatar>\n                  <PersonIcon />\n                </Avatar>\n              </ListItemAvatar>\n              <ListItemText primary={`Autor: ${author?.name}`} />\n            </ListItem>\n            <ListItem>\n              <ListItemAvatar>\n                <Avatar>\n                  <LanguageIcon />\n                </Avatar>\n              </ListItemAvatar>\n              <ListItemText primary={`Nacionalidad: ${author?.nationality}`} />\n            </ListItem>\n          </List>\n        </CardContent>\n      </CardActionArea>\n    </Card>\n  );\n}\n

    En la carpeta src/pages/game vamos a crear un fichero para los estilos llamado Game.module.css:

    Game.module.css
    .filter {\n    display: flex;\n    align-items: center;\n}\n\n.cards {\n    display: flex;\n    gap: 20px;\n    padding: 10px;\n    flex-wrap: wrap;\n}\n\n.card {\n    cursor: pointer;\n}\n\n@media (max-width: 800px) {\n    .cards {\n        display: flex;\n        flex-direction: column;\n        align-items: center;\n    }\n\n    .filter {\n        display: flex;\n        flex-direction: column;\n    }\n}\n

    Y por \u00faltimo modificamos nuestro componente p\u00e1gina Game y lo dejamos de esta manera:

    Game.tsx
    import { useState, useContext, useEffect } from \"react\";\nimport MenuItem from \"@mui/material/MenuItem\";\nimport FormControl from \"@mui/material/FormControl\";\nimport TextField from \"@mui/material/TextField\";\nimport Button from \"@mui/material/Button\";\nimport GameCard from \"./components/GameCard\";\nimport styles from \"./Game.module.css\";\nimport {\n  useCreateGameMutation,\n  useGetCategoriesQuery,\n  useGetGamesQuery,\n  useUpdateGameMutation,\n} from \"../../redux/services/ludotecaApi\";\nimport CreateGame from \"./components/CreateGame\";\nimport { LoaderContext } from \"../../context/LoaderProvider\";\nimport { useAppDispatch } from \"../../redux/hooks\";\nimport { setMessage } from \"../../redux/features/messagesSlice\";\nimport { Game as GameModel } from \"../../types/Game\";\nimport { Category } from \"../../types/Category\";\n\nexport const Game = () => {\n  const [openCreate, setOpenCreate] = useState(false);\n  const [filterTitle, setFilterTitle] = useState(\"\");\n  const [filterCategory, setFilterCategory] = useState(\"\");\n  const [gameToUpdate, setGameToUpdate] = useState<GameModel | null>(null);\n  const loader = useContext(LoaderContext);\n  const dispatch = useAppDispatch();\n\n  const { data, error, isLoading, isFetching } = useGetGamesQuery({\n    title: filterTitle,\n    idCategory: filterCategory,\n  });\n\n  const [updateGameApi, { isLoading: isLoadingUpdate, error: errorUpdate }] =\n    useUpdateGameMutation();\n\n  const { data: categories } = useGetCategoriesQuery(null);\n\n  const [createGameApi, { isLoading: isLoadingCreate, error: errorCreate }] =\n    useCreateGameMutation();\n\n  useEffect(() => {\n    loader.showLoading(\n      isLoadingCreate || isLoadingUpdate || isLoading || isFetching\n    );\n  }, [isLoadingCreate, isLoadingUpdate, isLoading, isFetching]);\n\n  useEffect(() => {\n    if (errorCreate || errorUpdate) {\n      setMessage({\n        text: \"Se ha producido un error al realizar la operaci\u00f3n\",\n        type: \"error\",\n      });\n    }\n  }, [errorUpdate, errorCreate]);\n\n  if (error) return <p>Error cargando!!!</p>;\n\n  const createGame = (game: GameModel) => {\n    setOpenCreate(false);\n    if (gameToUpdate) {\n      updateGameApi({\n        ...game,\n        id: gameToUpdate.id,\n      })\n        .then(() => {\n          dispatch(\n            setMessage({\n              text: \"Juego actualizado correctamente\",\n              type: \"ok\",\n            })\n          );\n          setGameToUpdate(null);\n        })\n        .catch((err) => console.log(err));\n    } else {\n      createGameApi(game)\n        .then(() => {\n          dispatch(\n            setMessage({\n              text: \"Juego creado correctamente\",\n              type: \"ok\",\n            })\n          );\n          setGameToUpdate(null);\n        })\n        .catch((err) => console.log(err));\n    }\n  };\n\n  return (\n    <div className=\"container\">\n      <h1>Cat\u00e1logo de juegos</h1>\n      <div className={styles.filter}>\n        <FormControl variant=\"standard\" sx={{ m: 1, minWidth: 220 }}>\n          <TextField\n            margin=\"dense\"\n            id=\"title\"\n            label=\"Titulo\"\n            fullWidth\n            value={filterTitle}\n            variant=\"standard\"\n            onChange={(event) => setFilterTitle(event.target.value)}\n          />\n        </FormControl>\n        <FormControl variant=\"standard\" sx={{ m: 1, minWidth: 220 }}>\n          <TextField\n            id=\"category\"\n            select\n            label=\"Categor\u00eda\"\n            defaultValue=\"''\"\n            fullWidth\n            variant=\"standard\"\n            name=\"author\"\n            value={filterCategory}\n            onChange={(event) => setFilterCategory(event.target.value)}\n          >\n            {categories &&\n              categories.map((option: Category) => (\n                <MenuItem key={option.id} value={option.id}>\n                  {option.name}\n                </MenuItem>\n              ))}\n          </TextField>\n        </FormControl>\n        <Button\n          variant=\"outlined\"\n          onClick={() => {\n            setFilterCategory(\"\");\n            setFilterTitle(\"\");\n          }}\n        >\n          Limpiar\n        </Button>\n      </div>\n      <div className={styles.cards}>\n        {data?.map((card) => (\n          <div\n            key={card.id}\n            className={styles.card}\n            onClick={() => {\n              setGameToUpdate(card);\n              setOpenCreate(true);\n            }}\n          >\n            <GameCard game={card} />\n          </div>\n        ))}\n      </div>\n      <div className=\"newButton\">\n        <Button variant=\"contained\" onClick={() => setOpenCreate(true)}>\n          Nuevo juego\n        </Button>\n      </div>\n      {openCreate && (\n        <CreateGame\n          create={createGame}\n          game={gameToUpdate}\n          closeModal={() => {\n            setGameToUpdate(null);\n            setOpenCreate(false);\n          }}\n        />\n      )}\n    </div>\n  );\n};\n

    Y por \u00faltimo descargamos la siguiente imagen y la guardamos en la carpeta src/assets.

    En este listado realizamos el filtro de manera din\u00e1mica, en el momento en que cambiamos el valor de la categor\u00eda o el t\u00edtulo a filtrar, como estas variables est\u00e1n asociadas al estado de nuestro componente, se vuelve a renderizar y por lo tanto se actualiza el valor de \"data\" modificando as\u00ed los resultados.

    El resto es muy parecido a lo que ya hemos realizado antes. Aqu\u00ed no tenemos una tabla, sino que mostramos nuestros juegos como Cards y si pulsamos sobre cualquier Card se mostrar\u00e1 el formulario de edici\u00f3n del juego.

    Si ahora arrancamos el proyecto y nos vamos a la pagina de juegos podremos crear y ver nuestros juegos.

    "},{"location":"develop/filtered/springboot/","title":"Listado filtrado - Spring Boot","text":"

    En este punto ya tenemos dos listados, uno b\u00e1sico y otro paginado. Ahora vamos a implementar un listado un poco diferente, este listado va a tener filtros de b\u00fasqueda.

    Como ya conocemos como se debe desarrollar, en este ejemplo vamos a ir m\u00e1s r\u00e1pidos y nos vamos a centrar \u00fanicamente en las novedades.

    "},{"location":"develop/filtered/springboot/#crear-modelos","title":"Crear Modelos","text":"

    Lo primero que vamos a hacer es crear los modelos para trabajar con BBDD y con peticiones hacia el front. Adem\u00e1s, tambi\u00e9n tenemos que a\u00f1adir datos al script de inicializaci\u00f3n de BBDD.

    Game.javaGameDto.javadata.sql
    package com.ccsw.tutorial.game.model;\n\nimport com.ccsw.tutorial.author.model.Author;\nimport com.ccsw.tutorial.category.model.Category;\n\nimport jakarta.persistence.*;\n\n\n/**\n * @author ccsw\n *\n */\n@Entity\n@Table(name = \"game\")\npublic class Game {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"id\", nullable = false)\n    private Long id;\n\n    @Column(name = \"title\", nullable = false)\n    private String title;\n\n    @Column(name = \"age\", nullable = false)\n    private String age;\n\n    @ManyToOne\n    @JoinColumn(name = \"category_id\", nullable = false)\n    private Category category;\n\n    @ManyToOne\n    @JoinColumn(name = \"author_id\", nullable = false)\n    private Author author;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return title\n     */\n    public String getTitle() {\n\n        return this.title;\n    }\n\n    /**\n     * @param title new value of {@link #getTitle}.\n     */\n    public void setTitle(String title) {\n\n        this.title = title;\n    }\n\n    /**\n     * @return age\n     */\n    public String getAge() {\n\n        return this.age;\n    }\n\n    /**\n     * @param age new value of {@link #getAge}.\n     */\n    public void setAge(String age) {\n\n        this.age = age;\n    }\n\n    /**\n     * @return category\n     */\n    public Category getCategory() {\n\n        return this.category;\n    }\n\n    /**\n     * @param category new value of {@link #getCategory}.\n     */\n    public void setCategory(Category category) {\n\n        this.category = category;\n    }\n\n    /**\n     * @return author\n     */\n    public Author getAuthor() {\n\n        return this.author;\n    }\n\n    /**\n     * @param author new value of {@link #getAuthor}.\n     */\n    public void setAuthor(Author author) {\n\n        this.author = author;\n    }\n\n}\n
    package com.ccsw.tutorial.game.model;\n\nimport com.ccsw.tutorial.author.model.AuthorDto;\nimport com.ccsw.tutorial.category.model.CategoryDto;\n\n/**\n * @author ccsw\n *\n */\npublic class GameDto {\n\n    private Long id;\n\n    private String title;\n\n    private String age;\n\n    private CategoryDto category;\n\n    private AuthorDto author;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return title\n     */\n    public String getTitle() {\n\n        return this.title;\n    }\n\n    /**\n     * @param title new value of {@link #getTitle}.\n     */\n    public void setTitle(String title) {\n\n        this.title = title;\n    }\n\n    /**\n     * @return age\n     */\n    public String getAge() {\n\n        return this.age;\n    }\n\n    /**\n     * @param age new value of {@link #getAge}.\n     */\n    public void setAge(String age) {\n\n        this.age = age;\n    }\n\n    /**\n     * @return category\n     */\n    public CategoryDto getCategory() {\n\n        return this.category;\n    }\n\n    /**\n     * @param category new value of {@link #getCategory}.\n     */\n    public void setCategory(CategoryDto category) {\n\n        this.category = category;\n    }\n\n    /**\n     * @return author\n     */\n    public AuthorDto getAuthor() {\n\n        return this.author;\n    }\n\n    /**\n     * @param author new value of {@link #getAuthor}.\n     */\n    public void setAuthor(AuthorDto author) {\n\n        this.author = author;\n    }\n\n}\n
    INSERT INTO category(name) VALUES ('Eurogames');\nINSERT INTO category(name) VALUES ('Ameritrash');\nINSERT INTO category(name) VALUES ('Familiar');\n\nINSERT INTO author(name, nationality) VALUES ('Alan R. Moon', 'US');\nINSERT INTO author(name, nationality) VALUES ('Vital Lacerda', 'PT');\nINSERT INTO author(name, nationality) VALUES ('Simone Luciani', 'IT');\nINSERT INTO author(name, nationality) VALUES ('Perepau Llistosella', 'ES');\nINSERT INTO author(name, nationality) VALUES ('Michael Kiesling', 'DE');\nINSERT INTO author(name, nationality) VALUES ('Phil Walker-Harding', 'US');\n\nINSERT INTO game(title, age, category_id, author_id) VALUES ('On Mars', '14', 1, 2);\nINSERT INTO game(title, age, category_id, author_id) VALUES ('Aventureros al tren', '8', 3, 1);\nINSERT INTO game(title, age, category_id, author_id) VALUES ('1920: Wall Street', '12', 1, 4);\nINSERT INTO game(title, age, category_id, author_id) VALUES ('Barrage', '14', 1, 3);\nINSERT INTO game(title, age, category_id, author_id) VALUES ('Los viajes de Marco Polo', '12', 1, 3);\nINSERT INTO game(title, age, category_id, author_id) VALUES ('Azul', '8', 3, 5);\n

    Relaciones anidadas

    F\u00edjate que tanto la Entity como el Dto tienen relaciones con Author y Category. Gracias a Spring JPA se pueden resolver de esta forma y tener toda la informaci\u00f3n de las relaciones hijas dentro del objeto padre. Muy importante recordar que en el mundo entity las relaciones ser\u00e1n con objetos Entity mientras que en el mundo dto las relaciones deben ser siempre con objetos Dto. La utilidad beanMapper ya har\u00e1 las conversiones necesarias, siempre que tengan el mismo nombre de propiedades.

    "},{"location":"develop/filtered/springboot/#tdd-pruebas","title":"TDD - Pruebas","text":"

    Para desarrollar todas las operaciones, empezaremos primero dise\u00f1ando las pruebas y luego implementando el c\u00f3digo necesario que haga funcionar correctamente esas pruebas. Para ir m\u00e1s r\u00e1pido vamos a poner todas las pruebas de golpe, pero realmente se deber\u00edan crear una a una e ir implementando el c\u00f3digo necesario para esa prueba. Para evitar tantas iteraciones en el tutorial las haremos todas de golpe.

    Vamos a pararnos a pensar un poco que necesitamos en la pantalla. En este caso solo tenemos dos operaciones:

    • Una consulta filtrada, que reciba datos de filtro opcionales (t\u00edtulo e idCategor\u00eda) y devuelva los datos ya filtrados
    • Una operaci\u00f3n de guardado y modificaci\u00f3n

    De nuevo tendremos que desglosar esto en varios casos de prueba:

    • Buscar un juego sin filtros
    • Buscar un t\u00edtulo que exista
    • Buscar una categor\u00eda que exista
    • Buscar un t\u00edtulo y una categor\u00eda que existan
    • Buscar un t\u00edtulo que no exista
    • Buscar una categor\u00eda que no exista
    • Buscar un t\u00edtulo y una categor\u00eda que no existan
    • Crear un juego nuevo (en realidad deber\u00edamos probar diferentes combinaciones y errores)
    • Modificar un juego que exista
    • Modificar un juego que no exista

    Tambi\u00e9n crearemos una clase GameController dentro del package de com.ccsw.tutorial.game con la implementaci\u00f3n de los m\u00e9todos vac\u00edos, para que no falle la compilaci\u00f3n.

    \u00a1Vamos a implementar test!

    GameController.javaGameIT.java
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.game.model.Game;\nimport com.ccsw.tutorial.game.model.GameDto;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Tag(name = \"Game\", description = \"API of Game\")\n@RequestMapping(value = \"/game\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class GameController {\n\n    /**\n     * M\u00e9todo para recuperar una lista de {@link Game}\n     *\n     * @param title t\u00edtulo del juego\n     * @param idCategory PK de la categor\u00eda\n     * @return {@link List} de {@link GameDto}\n     */\n    @Operation(summary = \"Find\", description = \"Method that return a filtered list of Games\")\n    @RequestMapping(path = \"\", method = RequestMethod.GET)\n    public List<GameDto> find(@RequestParam(value = \"title\", required = false) String title,\n                              @RequestParam(value = \"idCategory\", required = false) Long idCategory) {\n\n        return null;\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Game}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Game\")\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody GameDto dto) {\n\n    }\n\n}\n
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.author.model.AuthorDto;\nimport com.ccsw.tutorial.category.model.CategoryDto;\nimport com.ccsw.tutorial.game.model.GameDto;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.test.web.client.TestRestTemplate;\nimport org.springframework.boot.test.web.server.LocalServerPort;\nimport org.springframework.core.ParameterizedTypeReference;\nimport org.springframework.http.HttpEntity;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.test.annotation.DirtiesContext;\nimport org.springframework.web.util.UriComponentsBuilder;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)\n@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)\npublic class GameIT {\n\n    public static final String LOCALHOST = \"http://localhost:\";\n    public static final String SERVICE_PATH = \"/game\";\n\n    public static final Long EXISTS_GAME_ID = 1L;\n    public static final Long NOT_EXISTS_GAME_ID = 0L;\n    private static final String NOT_EXISTS_TITLE = \"NotExists\";\n    private static final String EXISTS_TITLE = \"Aventureros\";\n    private static final String NEW_TITLE = \"Nuevo juego\";\n    private static final Long NOT_EXISTS_CATEGORY = 0L;\n    private static final Long EXISTS_CATEGORY = 3L;\n\n    private static final String TITLE_PARAM = \"title\";\n    private static final String CATEGORY_ID_PARAM = \"idCategory\";\n\n    @LocalServerPort\n    private int port;\n\n    @Autowired\n    private TestRestTemplate restTemplate;\n\n    ParameterizedTypeReference<List<GameDto>> responseType = new ParameterizedTypeReference<List<GameDto>>(){};\n\n    private String getUrlWithParams(){\n    return UriComponentsBuilder.fromHttpUrl(LOCALHOST + port + SERVICE_PATH)\n    .queryParam(TITLE_PARAM, \"{\" + TITLE_PARAM +\"}\")\n    .queryParam(CATEGORY_ID_PARAM, \"{\" + CATEGORY_ID_PARAM +\"}\")\n    .encode()\n    .toUriString();\n    }\n\n    @Test\n    public void findWithoutFiltersShouldReturnAllGamesInDB() {\n\n          int GAMES_WITH_FILTER = 6;\n\n          Map<String, Object> params = new HashMap<>();\n          params.put(TITLE_PARAM, null);\n          params.put(CATEGORY_ID_PARAM, null);\n\n          ResponseEntity<List<GameDto>> response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n\n          assertNotNull(response);\n          assertEquals(GAMES_WITH_FILTER, response.getBody().size());\n    }\n\n    @Test\n    public void findExistsTitleShouldReturnGames() {\n\n          int GAMES_WITH_FILTER = 1;\n\n          Map<String, Object> params = new HashMap<>();\n          params.put(TITLE_PARAM, EXISTS_TITLE);\n          params.put(CATEGORY_ID_PARAM, null);\n\n          ResponseEntity<List<GameDto>> response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n\n          assertNotNull(response);\n          assertEquals(GAMES_WITH_FILTER, response.getBody().size());\n    }\n\n    @Test\n    public void findExistsCategoryShouldReturnGames() {\n\n          int GAMES_WITH_FILTER = 2;\n\n          Map<String, Object> params = new HashMap<>();\n          params.put(TITLE_PARAM, null);\n          params.put(CATEGORY_ID_PARAM, EXISTS_CATEGORY);\n\n          ResponseEntity<List<GameDto>> response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n\n          assertNotNull(response);\n          assertEquals(GAMES_WITH_FILTER, response.getBody().size());\n    }\n\n    @Test\n    public void findExistsTitleAndCategoryShouldReturnGames() {\n\n          int GAMES_WITH_FILTER = 1;\n\n          Map<String, Object> params = new HashMap<>();\n          params.put(TITLE_PARAM, EXISTS_TITLE);\n          params.put(CATEGORY_ID_PARAM, EXISTS_CATEGORY);\n\n          ResponseEntity<List<GameDto>> response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n\n          assertNotNull(response);\n          assertEquals(GAMES_WITH_FILTER, response.getBody().size());\n    }\n\n    @Test\n    public void findNotExistsTitleShouldReturnEmpty() {\n\n          int GAMES_WITH_FILTER = 0;\n\n          Map<String, Object> params = new HashMap<>();\n          params.put(TITLE_PARAM, NOT_EXISTS_TITLE);\n          params.put(CATEGORY_ID_PARAM, null);\n\n          ResponseEntity<List<GameDto>> response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n\n          assertNotNull(response);\n          assertEquals(GAMES_WITH_FILTER, response.getBody().size());\n    }\n\n    @Test\n    public void findNotExistsCategoryShouldReturnEmpty() {\n\n          int GAMES_WITH_FILTER = 0;\n\n          Map<String, Object> params = new HashMap<>();\n          params.put(TITLE_PARAM, null);\n          params.put(CATEGORY_ID_PARAM, NOT_EXISTS_CATEGORY);\n\n          ResponseEntity<List<GameDto>> response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n\n          assertNotNull(response);\n          assertEquals(GAMES_WITH_FILTER, response.getBody().size());\n    }\n\n    @Test\n    public void findNotExistsTitleOrCategoryShouldReturnEmpty() {\n\n          int GAMES_WITH_FILTER = 0;\n\n          Map<String, Object> params = new HashMap<>();\n          params.put(TITLE_PARAM, NOT_EXISTS_TITLE);\n          params.put(CATEGORY_ID_PARAM, NOT_EXISTS_CATEGORY);\n\n          ResponseEntity<List<GameDto>> response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n          assertNotNull(response);\n          assertEquals(GAMES_WITH_FILTER, response.getBody().size());\n\n          params.put(TITLE_PARAM, NOT_EXISTS_TITLE);\n          params.put(CATEGORY_ID_PARAM, EXISTS_CATEGORY);\n\n          response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n          assertNotNull(response);\n          assertEquals(GAMES_WITH_FILTER, response.getBody().size());\n\n          params.put(TITLE_PARAM, EXISTS_TITLE);\n          params.put(CATEGORY_ID_PARAM, NOT_EXISTS_CATEGORY);\n\n          response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n          assertNotNull(response);\n          assertEquals(GAMES_WITH_FILTER, response.getBody().size());\n    }\n\n    @Test\n    public void saveWithoutIdShouldCreateNewGame() {\n\n          GameDto dto = new GameDto();\n          AuthorDto authorDto = new AuthorDto();\n          authorDto.setId(1L);\n\n          CategoryDto categoryDto = new CategoryDto();\n          categoryDto.setId(1L);\n\n          dto.setTitle(NEW_TITLE);\n          dto.setAge(\"18\");\n          dto.setAuthor(authorDto);\n          dto.setCategory(categoryDto);\n\n          Map<String, Object> params = new HashMap<>();\n          params.put(TITLE_PARAM, NEW_TITLE);\n          params.put(CATEGORY_ID_PARAM, null);\n\n          ResponseEntity<List<GameDto>> response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n\n          assertNotNull(response);\n          assertEquals(0, response.getBody().size());\n\n          restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.PUT, new HttpEntity<>(dto), Void.class);\n\n          response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n\n          assertNotNull(response);\n          assertEquals(1, response.getBody().size());\n    }\n\n    @Test\n    public void modifyWithExistIdShouldModifyGame() {\n\n          GameDto dto = new GameDto();\n          AuthorDto authorDto = new AuthorDto();\n          authorDto.setId(1L);\n\n          CategoryDto categoryDto = new CategoryDto();\n          categoryDto.setId(1L);\n\n          dto.setTitle(NEW_TITLE);\n          dto.setAge(\"18\");\n          dto.setAuthor(authorDto);\n          dto.setCategory(categoryDto);\n\n          Map<String, Object> params = new HashMap<>();\n          params.put(TITLE_PARAM, NEW_TITLE);\n          params.put(CATEGORY_ID_PARAM, null);\n\n          ResponseEntity<List<GameDto>> response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n\n          assertNotNull(response);\n          assertEquals(0, response.getBody().size());\n\n          restTemplate.exchange(LOCALHOST + port + SERVICE_PATH + \"/\" + EXISTS_GAME_ID, HttpMethod.PUT, new HttpEntity<>(dto), Void.class);\n\n          response = restTemplate.exchange(getUrlWithParams(), HttpMethod.GET, null, responseType, params);\n\n          assertNotNull(response);\n          assertEquals(1, response.getBody().size());\n          assertEquals(EXISTS_GAME_ID, response.getBody().get(0).getId());\n    }\n\n    @Test\n    public void modifyWithNotExistIdShouldThrowException() {\n\n          GameDto dto = new GameDto();\n          dto.setTitle(NEW_TITLE);\n\n          ResponseEntity<?> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH + \"/\" + NOT_EXISTS_GAME_ID, HttpMethod.PUT, new HttpEntity<>(dto), Void.class);\n\n          assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());\n    }\n\n}\n

    B\u00fasquedas en BBDD

    Siempre deber\u00edamos buscar a los hijos por primary keys, nunca hay que hacerlo por una descripci\u00f3n libre, ya que el usuario podr\u00eda teclear el mismo nombre de diferentes formas y no habr\u00eda manera de buscar correctamente el resultado. As\u00ed que siempre que haya un dropdown, se debe filtrar por su ID.

    Si ahora ejecutas los jUnits, ver\u00e1s que en este caso hemos construido 10 pruebas, para cubrir los casos b\u00e1sicos del Controller, y todas ellas fallan la ejecuci\u00f3n. Vamos a seguir implementando el resto de capas para hacer que los test funcionen.

    "},{"location":"develop/filtered/springboot/#controller","title":"Controller","text":"

    De nuevo para poder compilar esta capa, nos hace falta delegar sus operaciones de l\u00f3gica de negocio en un Service as\u00ed que lo crearemos al mismo tiempo que lo vamos necesitando.

    GameService.javaGameController.java
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.game.model.Game;\nimport com.ccsw.tutorial.game.model.GameDto;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\npublic interface GameService {\n\n    /**\n     * Recupera los juegos filtrando opcionalmente por t\u00edtulo y/o categor\u00eda\n     *\n     * @param title t\u00edtulo del juego\n     * @param idCategory PK de la categor\u00eda\n     * @return {@link List} de {@link Game}\n     */\n    List<Game> find(String title, Long idCategory);\n\n    /**\n     * Guarda o modifica un juego, dependiendo de si el identificador est\u00e1 o no informado\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    void save(Long id, GameDto dto);\n\n}\n
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.game.model.Game;\nimport com.ccsw.tutorial.game.model.GameDto;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.modelmapper.ModelMapper;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * @author ccsw\n *\n */\n@Tag(name = \"Game\", description = \"API of Game\")\n@RequestMapping(value = \"/game\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class GameController {\n\n    @Autowired\n    GameService gameService;\n\n    @Autowired\n    ModelMapper mapper;\n\n    /**\n     * M\u00e9todo para recuperar una lista de {@link Game}\n     *\n     * @param title t\u00edtulo del juego\n     * @param idCategory PK de la categor\u00eda\n     * @return {@link List} de {@link GameDto}\n     */\n    @Operation(summary = \"Find\", description = \"Method that return a filtered list of Games\")\n    @RequestMapping(path = \"\", method = RequestMethod.GET)\n    public List<GameDto> find(@RequestParam(value = \"title\", required = false) String title,\n                              @RequestParam(value = \"idCategory\", required = false) Long idCategory) {\n\n        List<Game> games = gameService.find(title, idCategory);\n\n        return games.stream().map(e -> mapper.map(e, GameDto.class)).collect(Collectors.toList());\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Game}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Game\")\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody GameDto dto) {\n\n        gameService.save(id, dto);\n    }\n\n}\n

    En esta ocasi\u00f3n, para el m\u00e9todo de b\u00fasqueda hemos decidido utilizar par\u00e1metros en la URL de tal forma que nos quedar\u00e1 algo as\u00ed http://localhost:8080/game/?title=xxx&idCategoria=yyy. Queremos recuperar el recurso Game que es el raiz de la ruta, pero filtrado por cero o varios par\u00e1metros.

    "},{"location":"develop/filtered/springboot/#service","title":"Service","text":"

    Siguiente paso, la capa de l\u00f3gica de negocio, es decir el Service, que por tanto har\u00e1 uso de un Repository.

    GameServiceImpl.javaGameRepository.java
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.game.model.Game;\nimport com.ccsw.tutorial.game.model.GameDto;\nimport jakarta.transaction.Transactional;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\n\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Service\n@Transactional\npublic class GameServiceImpl implements GameService {\n\n    @Autowired\n    GameRepository gameRepository;\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public List<Game> find(String title, Long idCategory) {\n\n        return (List<Game>) this.gameRepository.findAll();\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void save(Long id, GameDto dto) {\n\n        Game game;\n\n        if (id == null) {\n            game = new Game();\n        } else {\n            game = this.gameRepository.findById(id).orElse(null);\n        }\n\n        BeanUtils.copyProperties(dto, game, \"id\", \"author\", \"category\");\n\n        this.gameRepository.save(game);\n    }\n\n}\n
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.game.model.Game;\nimport org.springframework.data.repository.CrudRepository;\n\n/**\n * @author ccsw\n *\n */\npublic interface GameRepository extends CrudRepository<Game, Long> {\n\n}\n

    Este servicio tiene dos peculiaridades, remarcadas en amarillo en la clase anterior. Por un lado tenemos la consulta, que no es un listado completo ni un listado paginado, sino que es un listado con filtros. Luego veremos como se hace eso, de momento lo dejaremos como un m\u00e9todo que recibe los dos filtros.

    La segunda peculiaridad es que de cliente nos est\u00e1 llegando un GameDto, que internamente tiene un AuthorDto y un CategoryDto, pero nosotros lo tenemos que traducir a entidades de BBDD. No sirve con copiar las propiedades tal cual, ya que entonces Spring lo que har\u00e1 ser\u00e1 crear un objeto nuevo y persistir ese objeto nuevo de Author y de Category. Adem\u00e1s, de cliente generalmente tan solo nos llega el ID de esos objetos hijo, y no el resto de informaci\u00f3n de la entidad. Por esos motivos lo hemos ignorado del copyProperties.

    Pero de alguna forma tendremos que asignarle esos valores a la entidad Game. Si conocemos sus ID que es lo que generalmente llega, podemos recuperar esos objetos de BBDD y asignarlos en el objeto Game. Si recuerdas las reglas b\u00e1sicas, un Repository debe pertenecer a un solo Service, por lo que en lugar de llamar a m\u00e9todos de los AuthorRepository y CategoryRepository desde nuestro GameServiceImpl, debemos llamar a m\u00e9todos expuestos en AuthorService y CategoryService, que son los que gestionan sus repositorios. Para ello necesitaremos crear esos m\u00e9todos get en los otros Services.

    Y ya sabes, para implementar nuevos m\u00e9todos, antes se deben hacer las pruebas jUnit, que en este caso, por variar, cubriremos con pruebas unitarias. Recuerda que los test van en src/test/java

    AuthorTest.javaAuthorService.javaAuthorServiceImpl.java
    package com.ccsw.tutorial.author;\n\nimport com.ccsw.tutorial.author.model.Author;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\nimport java.util.Optional;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\n@ExtendWith(MockitoExtension.class)\npublic class AuthorTest {\n\n    public static final Long EXISTS_AUTHOR_ID = 1L;\n    public static final Long NOT_EXISTS_AUTHOR_ID = 0L;\n\n    @Mock\n    private AuthorRepository authorRepository;\n\n    @InjectMocks\n    private AuthorServiceImpl authorService;\n\n    @Test\n    public void getExistsAuthorIdShouldReturnAuthor() {\n\n          Author author = mock(Author.class);\n          when(author.getId()).thenReturn(EXISTS_AUTHOR_ID);\n          when(authorRepository.findById(EXISTS_AUTHOR_ID)).thenReturn(Optional.of(author));\n\n          Author authorResponse = authorService.get(EXISTS_AUTHOR_ID);\n\n          assertNotNull(authorResponse);\n\n          assertEquals(EXISTS_AUTHOR_ID, authorResponse.getId());\n    }\n\n    @Test\n    public void getNotExistsAuthorIdShouldReturnNull() {\n\n          when(authorRepository.findById(NOT_EXISTS_AUTHOR_ID)).thenReturn(Optional.empty());\n\n          Author author = authorService.get(NOT_EXISTS_AUTHOR_ID);\n\n          assertNull(author);\n    }\n\n}\n
    package com.ccsw.tutorial.author;\n\nimport com.ccsw.tutorial.author.model.Author;\nimport com.ccsw.tutorial.author.model.AuthorDto;\nimport com.ccsw.tutorial.author.model.AuthorSearchDto;\nimport org.springframework.data.domain.Page;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\npublic interface AuthorService {\n\n    /**\n     * Recupera un {@link Author} a trav\u00e9s de su ID\n     *\n     * @param id PK de la entidad\n     * @return {@link Author}\n     */\n    Author get(Long id);\n\n    /**\n     * M\u00e9todo para recuperar un listado paginado de {@link Author}\n     *\n     * @param dto dto de b\u00fasqueda\n     * @return {@link Page} de {@link Author}\n     */\n    Page<Author> findPage(AuthorSearchDto dto);\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    void save(Long id, AuthorDto dto);\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     */\n    void delete(Long id) throws Exception;\n\n}\n
    package com.ccsw.tutorial.author;\n\nimport com.ccsw.tutorial.author.model.Author;\nimport com.ccsw.tutorial.author.model.AuthorDto;\nimport com.ccsw.tutorial.author.model.AuthorSearchDto;\nimport jakarta.transaction.Transactional;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.domain.Page;\nimport org.springframework.stereotype.Service;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Service\n@Transactional\npublic class AuthorServiceImpl implements AuthorService {\n\n    @Autowired\n    AuthorRepository authorRepository;\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public Author get(Long id) {\n\n        return this.authorRepository.findById(id).orElse(null);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public Page<Author> findPage(AuthorSearchDto dto) {\n\n        return this.authorRepository.findAll(dto.getPageable().getPageable());\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void save(Long id, AuthorDto data) {\n\n        Author author;\n\n        if (id == null) {\n            author = new Author();\n        } else {\n            author = this.get(id);\n        }\n\n        BeanUtils.copyProperties(data, author, \"id\");\n\n        this.authorRepository.save(author);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void delete(Long id) throws Exception {\n\n        if(this.get(id) == null){\n            throw new Exception(\"Not exists\");\n        }\n\n        this.authorRepository.deleteById(id);\n    }\n\n}\n

    Y lo mismo para categor\u00edas.

    CategoryTest.javaCategoryService.javaCategoryServiceImpl.java
    public static final Long NOT_EXISTS_CATEGORY_ID = 0L;\n\n@Test\npublic void getExistsCategoryIdShouldReturnCategory() {\n\n      Category category = mock(Category.class);\n      when(category.getId()).thenReturn(EXISTS_CATEGORY_ID);\n      when(categoryRepository.findById(EXISTS_CATEGORY_ID)).thenReturn(Optional.of(category));\n\n      Category categoryResponse = categoryService.get(EXISTS_CATEGORY_ID);\n\n      assertNotNull(categoryResponse);\n      assertEquals(EXISTS_CATEGORY_ID, category.getId());\n}\n\n@Test\npublic void getNotExistsCategoryIdShouldReturnNull() {\n\n      when(categoryRepository.findById(NOT_EXISTS_CATEGORY_ID)).thenReturn(Optional.empty());\n\n      Category category = categoryService.get(NOT_EXISTS_CATEGORY_ID);\n\n      assertNull(category);\n}\n
    package com.ccsw.tutorial.category;\n\nimport com.ccsw.tutorial.category.model.Category;\nimport com.ccsw.tutorial.category.model.CategoryDto;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\npublic interface CategoryService {\n\n    /**\n     * Recupera una {@link Category} a partir de su ID\n     *\n     * @param id PK de la entidad\n     * @return {@link Category}\n     */\n    Category get(Long id);\n\n    /**\n     * M\u00e9todo para recuperar todas las {@link Category}\n     *\n     * @return {@link List} de {@link Category}\n     */\n    List<Category> findAll();\n\n    /**\n     * M\u00e9todo para crear o actualizar una {@link Category}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    void save(Long id, CategoryDto dto);\n\n    /**\n     * M\u00e9todo para borrar una {@link Category}\n     *\n     * @param id PK de la entidad\n     */\n    void delete(Long id) throws Exception;\n\n}\n
    package com.ccsw.tutorial.category;\n\nimport com.ccsw.tutorial.category.model.Category;\nimport com.ccsw.tutorial.category.model.CategoryDto;\nimport jakarta.transaction.Transactional;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Service\n@Transactional\npublic class CategoryServiceImpl implements CategoryService {\n\n    @Autowired\n    CategoryRepository categoryRepository;\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public Category get(Long id) {\n\n          return this.categoryRepository.findById(id).orElse(null);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public List<Category> findAll() {\n\n          return (List<Category>) this.categoryRepository.findAll();\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void save(Long id, CategoryDto dto) {\n\n          Category category;\n\n          if (id == null) {\n             category = new Category();\n          } else {\n             category = this.get(id);\n          }\n\n          category.setName(dto.getName());\n\n          this.categoryRepository.save(category);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void delete(Long id) throws Exception {\n\n          if(this.get(id) == null){\n             throw new Exception(\"Not exists\");\n          }\n\n          this.categoryRepository.deleteById(id);\n    }\n\n}\n

    Clean Code

    A la hora de implementar m\u00e9todos nuevos, ten siempre presente el Clean Code. \u00a1No dupliques c\u00f3digo!, es muy importante de cara al futuro mantenimiento. Si en nuestro m\u00e9todo save hac\u00edamos uso de una operaci\u00f3n findById y ahora hemos creado una nueva operaci\u00f3n get, hagamos uso de esta nueva operaci\u00f3n y no repitamos el c\u00f3digo.

    Y ahora que ya tenemos los m\u00e9todos necesarios, ya podemos implementar correctamente nuestro GameServiceImpl.

    GameServiceImpl.java
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.author.AuthorService;\nimport com.ccsw.tutorial.category.CategoryService;\nimport com.ccsw.tutorial.game.model.Game;\nimport com.ccsw.tutorial.game.model.GameDto;\nimport jakarta.transaction.Transactional;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\n\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Service\n@Transactional\npublic class GameServiceImpl implements GameService {\n\n    @Autowired\n    GameRepository gameRepository;\n\n    @Autowired\n    AuthorService authorService;\n\n    @Autowired\n    CategoryService categoryService;\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public List<Game> find(String title, Long idCategory) {\n\n        return this.gameRepository.findAll();\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void save(Long id, GameDto dto) {\n\n        Game game;\n\n        if (id == null) {\n            game = new Game();\n        } else {\n            game = this.gameRepository.findById(id).orElse(null);\n        }\n\n        BeanUtils.copyProperties(dto, game, \"id\", \"author\", \"category\");\n\n        game.setAuthor(authorService.get(dto.getAuthor().getId()));\n        game.setCategory(categoryService.get(dto.getCategory().getId()));\n\n        this.gameRepository.save(game);\n    }\n\n}\n

    Ahora si que tenemos la capa de l\u00f3gica de negocio terminada, podemos pasar a la siguiente capa.

    "},{"location":"develop/filtered/springboot/#repository","title":"Repository","text":"

    Y llegamos a la \u00faltima capa donde, si recordamos, ten\u00edamos un m\u00e9todo que recibe dos par\u00e1metros. Necesitamos traducir esto en una consulta a la BBDD.

    Vamos a necesitar un listado filtrado por t\u00edtulo o por categor\u00eda, as\u00ed que necesitaremos pasarle esos datos y filtrar la query. Para el t\u00edtulo vamos a buscar por una cadena contenida, as\u00ed que el par\u00e1metro ser\u00e1 de tipo String, mientras que para la categor\u00eda vamos a buscar por su primary key, as\u00ed que el par\u00e1metro ser\u00e1 de tipo Long.

    Existen varias estrategias para abordar esta implementaci\u00f3n. Podr\u00edamos utilizar los QueryMethods para que Spring JPA haga su magia, pero en esta ocasi\u00f3n ser\u00eda bastante complicado encontrar un predicado correcto.

    Tambi\u00e9n podr\u00edamos hacer una implementaci\u00f3n de la interface y hacer la consulta directamente con Criteria.

    Por otro lado se podr\u00eda hacer uso de la anotaci\u00f3n @Query. Esta anotaci\u00f3n nos permite definir una consulta en SQL nativo o en JPQL (Java Persistence Query Language) y Spring JPA se encargar\u00e1 de realizar todo el mapeo y conversi\u00f3n de los datos de entrada y salida. Pero esta opci\u00f3n no es la m\u00e1s recomendable.

    "},{"location":"develop/filtered/springboot/#specifications","title":"Specifications","text":"

    En este caso vamos a hacer uso de las Specifications que es la opci\u00f3n m\u00e1s robusta y no presenta acoplamientos con el tipo de BBDD.

    Haciendo un resumen muy r\u00e1pido y con poco detalle, las Specifications sirven para generar de forma robusta las clausulas where de una consulta SQL. Estas clausulas se generar\u00e1n mediante Predicate (predicados) que realizar\u00e1n operaciones de comparaci\u00f3n entre un campo y un valor.

    En el siguiente ejemplo podemos verlo m\u00e1s claro: en la sentencia select * fromTablewherename = 'b\u00fasqueda' tenemos un solo predicado que es name = 'b\u00fasqueda'. En ese predicado diferenciamos tres etiquetas:

    • name \u2192 es el campo sobre el que hacemos el predicado
    • = \u2192 es la operaci\u00f3n que realizamos
    • 'b\u00fasqueda' \u2192 es el valor con el que realizamos la operaci\u00f3n

    Lo que trata de hacer Specifications es agregar varios predicados con AND o con OR de forma tipada en c\u00f3digo. Y \u00bfqu\u00e9 intentamos conseguir con esta forma de programar?, pues f\u00e1cil, intentamos hacer que si cambiamos alg\u00fan tipo o el nombre de alguna propiedad involucrada en la query, nos salte un fallo en tiempo de compilaci\u00f3n y nos demos cuenta de donde est\u00e1 el error. Si utiliz\u00e1ramos queries construidas directamente con String, al cambiar alg\u00fan tipo o el nombre de alguna propiedad involucrada, no nos dar\u00edamos cuenta hasta que saltara un fallo en tiempo de ejecuci\u00f3n.

    Por este motivo hay que programar con Specifications, porque son robustas ante cambios de c\u00f3digo y tenemos que tratar de evitar las construcciones a trav\u00e9s de cadenas de texto.

    Dicho esto, \u00a1vamos a implementar!

    Lo primero que necesitaremos ser\u00e1 una clase que nos permita guardar la informaci\u00f3n de un Predicate para luego generar facilmente la construcci\u00f3n. Para ello vamos a crear una clase que guarde informaci\u00f3n de los criterios de filtrado (campo, operaci\u00f3n y valor), por suerte esta clase ser\u00e1 gen\u00e9rica y la podremos usar en toda la aplicaci\u00f3n, as\u00ed que la vamos a crear en el paquete com.ccsw.tutorial.common.criteria

    SearchCriteria.java
    package com.ccsw.tutorial.common.criteria;\n\npublic class SearchCriteria {\n\n    private String key;\n    private String operation;\n    private Object value;\n\n    public SearchCriteria(String key, String operation, Object value) {\n\n        this.key = key;\n        this.operation = operation;\n        this.value = value;\n    }\n\n    public String getKey() {\n        return key;\n    }\n\n    public void setKey(String key) {\n        this.key = key;\n    }\n\n    public String getOperation() {\n        return operation;\n    }\n\n    public void setOperation(String operation) {\n        this.operation = operation;\n    }\n\n    public Object getValue() {\n        return value;\n    }\n\n    public void setValue(Object value) {\n        this.value = value;\n    }\n\n}\n

    Hecho esto pasamos a definir el Specification de nuestra clase la cual contendr\u00e1 la construcci\u00f3n de la consulta en funci\u00f3n de los criterios que se le proporcionan. No queremos construir los predicados directamente en nuestro Service ya que duplicariamos mucho c\u00f3digo, mucho mejor si hacemos una clase para centralizar la construcci\u00f3n de predicados.

    De esta forma vamos a crear una clase Specification por cada una de las Entity que queramos consultar. En nuestro caso solo vamos a generar queries para Game, as\u00ed que solo crearemos un GameSpecification donde construirmos los predicados.

    GameSpecification.java
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.common.criteria.SearchCriteria;\nimport com.ccsw.tutorial.game.model.Game;\nimport jakarta.persistence.criteria.*;\nimport org.springframework.data.jpa.domain.Specification;\n\n\npublic class GameSpecification implements Specification<Game> {\n\n    private static final long serialVersionUID = 1L;\n\n    private final SearchCriteria criteria;\n\n    public GameSpecification(SearchCriteria criteria) {\n\n        this.criteria = criteria;\n    }\n\n    @Override\n    public Predicate toPredicate(Root<Game> root, CriteriaQuery<?> query, CriteriaBuilder builder) {\n        if (criteria.getOperation().equalsIgnoreCase(\":\") && criteria.getValue() != null) {\n            Path<String> path = getPath(root);\n            if (path.getJavaType() == String.class) {\n                return builder.like(path, \"%\" + criteria.getValue() + \"%\");\n            } else {\n                return builder.equal(path, criteria.getValue());\n            }\n        }\n        return null;\n    }\n\n    private Path<String> getPath(Root<Game> root) {\n        String key = criteria.getKey();\n        String[] split = key.split(\"[.]\", 0);\n\n        Path<String> expression = root.get(split[0]);\n        for (int i = 1; i < split.length; i++) {\n            expression = expression.get(split[i]);\n        }\n\n        return expression;\n    }\n\n}\n

    Voy a tratar de explicar con calma cada una de las l\u00edneas marcadas, ya que son conceptos dificiles de entender hasta que no se utilizan.

    • Las dos primeras l\u00edneas marcadas hacen referencia a que cuando se crea un Specification, esta debe generar un predicado, con lo que necesita unos criterios de filtrado para poder generarlo. En el constructor le estamos pasando esos criterios de filtrado que luego utilizaremos.

    • La tercera l\u00ednea marcada est\u00e1 seleccionando el tipo de operaci\u00f3n. En nuestro caso solo vamos a utilizar operaciones de comparaci\u00f3n. Por convenio las operaciones de comparaci\u00f3n se marcan como \":\" ya que el s\u00edmbolo = est\u00e1 reservado. Aqu\u00ed es donde podr\u00edamos a\u00f1adir otro tipo de operaciones como \">\" o \"<>\" o cualquiera que queramos implementar. Gu\u00e1rdate esa informaci\u00f3n que te servir\u00e1 en el ejercicio final .

    • Las dos siguientes l\u00edneas, las de return est\u00e1n construyendo un Predicate al ser de tipo comparaci\u00f3n, si es un texto har\u00e1 un like y si no es texto (que es un n\u00famero o fecha) har\u00e1 un equals.

    • Por \u00faltimo, tenemos un m\u00e9todo getPath que invocamos dentro la generaci\u00f3n del predicado y que implementamos m\u00e1s abajo. Esta funci\u00f3n nos permite explorar las sub-entidades para realizar consultas sobre los atributos de estas. Por ejemplo, si queremos navegar hasta game.author.name, lo que har\u00e1 la exploraci\u00f3n ser\u00e1 recuperar el atributo name del objeto author de la entidad game.

    Una vez implementada nuestra clase de Specification, que lo \u00fanico que hace es recoger un criterio de filtrado y construir un predicado, y que en principio solo permite generar comparaciones de igualdad, vamos a utilizarlo dentro de nuestro Service:

    GameServiceImpl.javaGameRepository.java
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.author.AuthorService;\nimport com.ccsw.tutorial.category.CategoryService;\nimport com.ccsw.tutorial.common.criteria.SearchCriteria;\nimport com.ccsw.tutorial.game.model.Game;\nimport com.ccsw.tutorial.game.model.GameDto;\nimport jakarta.transaction.Transactional;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.jpa.domain.Specification;\nimport org.springframework.stereotype.Service;\n\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Service\n@Transactional\npublic class GameServiceImpl implements GameService {\n\n    @Autowired\n    GameRepository gameRepository;\n\n    @Autowired\n    AuthorService authorService;\n\n    @Autowired\n    CategoryService categoryService;\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public List<Game> find(String title, Long idCategory) {\n\n        GameSpecification titleSpec = new GameSpecification(new SearchCriteria(\"title\", \":\", title));\n        GameSpecification categorySpec = new GameSpecification(new SearchCriteria(\"category.id\", \":\", idCategory));\n\n        Specification<Game> spec = Specification.where(titleSpec).and(categorySpec);\n        // Desde la versi\u00f3n 3.5.0 de Spring Boot, la nueva manera es\n        Specification<Game> spec = titleSpec.and(categorySpec);\n\n        return this.gameRepository.findAll(spec);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void save(Long id, GameDto dto) {\n\n        Game game;\n\n        if (id == null) {\n            game = new Game();\n        } else {\n            game = this.gameRepository.findById(id).orElse(null);\n        }\n\n        BeanUtils.copyProperties(dto, game, \"id\", \"author\", \"category\");\n\n        game.setAuthor(authorService.get(dto.getAuthor().getId()));\n        game.setCategory(categoryService.get(dto.getCategory().getId()));\n\n        this.gameRepository.save(game);\n    }\n\n}\n
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.game.model.Game;\nimport org.springframework.data.jpa.domain.Specification;\nimport org.springframework.data.jpa.repository.EntityGraph;\nimport org.springframework.data.jpa.repository.JpaSpecificationExecutor;\nimport org.springframework.data.repository.CrudRepository;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\npublic interface GameRepository extends CrudRepository<Game, Long>, JpaSpecificationExecutor<Game> {\n\n}\n

    Lo que hemos hecho es crear los dos criterios de filtrado que necesit\u00e1bamos. En nuestro caso eran title, que es un atributo de la entidad Game y por otro lado el identificador de categor\u00eda, que en este caso, ya no es un atributo directo de la entidad, si no, de la categor\u00eda asociada, por lo que debemos navegar hasta el atributo id a trav\u00e9s del atributo category (para esto utilizamos el getPath que hemos visto anteriormente).

    A partir de estos dos predicados, podemos generar el Specification global para la consulta, uniendo los dos predicados mediante el operador AND.

    Una vez construido el Specification ya podemos usar el m\u00e9todo por defecto que nos proporciona Spring Data para dicho fin, tan solo tenemos que decirle a nuestro GameRepository que adem\u00e1s extender de CrudRepository debe extender de JpaSpecificationExecutor, para que pueda ejecutarlas.

    "},{"location":"develop/filtered/springboot/#mejoras-rendimiento","title":"Mejoras rendimiento","text":"

    Finalmente, de cara a mejorar el rendimiento de nuestros servicios vamos a hacer foco en la generaci\u00f3n de transacciones con la base de datos. Si ejecut\u00e1ramos esta petici\u00f3n tal cual lo tenemos implementado ahora mismo, en la consola ver\u00edamos lo siguiente:

    Hibernate: select g1_0.id,g1_0.age,g1_0.author_id,g1_0.category_id,g1_0.title from game g1_0\nHibernate: select a1_0.id,a1_0.name,a1_0.nationality from author a1_0 where a1_0.id=?\nHibernate: select c1_0.id,c1_0.name from category c1_0 where c1_0.id=?\nHibernate: select a1_0.id,a1_0.name,a1_0.nationality from author a1_0 where a1_0.id=?\nHibernate: select c1_0.id,c1_0.name from category c1_0 where c1_0.id=?\nHibernate: select a1_0.id,a1_0.name,a1_0.nationality from author a1_0 where a1_0.id=?\nHibernate: select a1_0.id,a1_0.name,a1_0.nationality from author a1_0 where a1_0.id=?\nHibernate: select a1_0.id,a1_0.name,a1_0.nationality from author a1_0 where a1_0.id=?\n

    Esto es debido a que no le hemos dado indicaciones a Spring Data de como queremos que construya las consultas con relaciones y por defecto est\u00e1 configurado para generar sub-consultas cuando tenemos tablas relacionadas.

    En nuestro caso la tabla Game est\u00e1 relacionada con Author y Category. Al realizar la consulta a Game realiza las sub-consultas por cada uno de los registros relacionados con los resultados Game.

    Para evitar tantas consultas contra la BBDD y realizar esto de una forma mucho m\u00e1s \u00f3ptima, podemos decirle a Spring Data el comportamiento que queremos, que en nuestro caso ser\u00e1 que haga una \u00fanica consulta y haga las sub-consultas mediante los join correspondientes.

    Para ello a\u00f1adimos una sobre-escritura del m\u00e9todo findAll, que ya ten\u00edamos implementado en JpaSpecificationExecutor y que utlizamos de forma heredada, pero en este caso le a\u00f1adimos la anotaci\u00f3n @EntityGraph con los atributos que queremos que se incluyan dentro de la consulta principal mediante join:

    GameRepository.java
    package com.ccsw.tutorial.game;\n\nimport com.ccsw.tutorial.game.model.Game;\nimport org.springframework.data.jpa.domain.Specification;\nimport org.springframework.data.jpa.repository.EntityGraph;\nimport org.springframework.data.jpa.repository.JpaSpecificationExecutor;\nimport org.springframework.data.repository.CrudRepository;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\npublic interface GameRepository extends CrudRepository<Game, Long>, JpaSpecificationExecutor<Game> {\n\n    @Override\n    @EntityGraph(attributePaths = {\"category\", \"author\"})\n    List<Game> findAll(Specification<Game> spec);\n\n}\n

    Tras realizar este cambio, podemos observar que la nueva consulta generada es la siguiente:

    Hibernate: select g1_0.id,g1_0.age,a1_0.id,a1_0.name,a1_0.nationality,c1_0.id,c1_0.name,g1_0.title from game g1_0 join author a1_0 on a1_0.id=g1_0.author_id join category c1_0 on c1_0.id=g1_0.category_id\n

    Como podemos observar, ahora se realiza una \u00fanica consulta con la correspondiente transacci\u00f3n con la BBDD, y se trae todos los datos necesarios de Game, Author y Category sin lanzar m\u00faltiples queries.

    "},{"location":"develop/filtered/springboot/#prueba-de-las-operaciones","title":"Prueba de las operaciones","text":"

    Si ahora ejecutamos de nuevo los jUnits, vemos que todos los que hemos desarrollado en GameIT ya funcionan correctamente, e incluso el resto de test de la aplicaci\u00f3n tambi\u00e9n funcionan correctamente.

    Pruebas jUnit

    Cada vez que desarrollemos un caso de uso nuevo, debemos relanzar todas las pruebas autom\u00e1ticas que tenga la aplicaci\u00f3n. Es muy com\u00fan que al implementar alg\u00fan desarrollo nuevo, interfiramos de alguna forma en el funcionamiento de otra funcionalidad. Si lanzamos toda la bater\u00eda de pruebas, nos daremos cuenta si algo ha dejado de funcionar y podremos solucionarlo antes de llevar ese error a Producci\u00f3n. Las pruebas jUnit son nuestra red de seguridad.

    Adem\u00e1s de las pruebas autom\u00e1ticas, podemos ver como se comporta la aplicaci\u00f3n y que respuesta nos ofrece, lanzando peticiones Rest con Postman, como hemos hecho en los casos anteriores. As\u00ed que podemos levantar la aplicaci\u00f3n y lanzar las operaciones:

    ** GET http://localhost:8080/game **

    ** GET http://localhost:8080/game?title=xxx **

    ** GET http://localhost:8080/game?idCategory=xxx **

    Nos devuelve un listado filtrado de Game. F\u00edjate bien en la petici\u00f3n donde enviamos los filtros y la respuesta que tiene los objetos Category y Author inclu\u00eddos.

    ** PUT http://localhost:8080/game ** ** PUT http://localhost:8080/game/{id} **

    {\n    \"title\": \"Nuevo juego\",\n    \"age\": \"18\",\n    \"category\": {\n        \"id\": 3\n    },\n    \"author\": {\n        \"id\": 1\n    }\n}\n

    Nos sirve para insertar un Game nuevo (si no tienen el id informado) o para actualizar un Game (si tienen el id informado). F\u00edjate que para enlazar Category y Author tan solo hace falta el id de cada no de ellos, ya que en el m\u00e9todo save se hace una consulta get para recuperarlos por su id. Adem\u00e1s que no tendr\u00eda sentido enviar toda la informaci\u00f3n de esas entidades ya que no est\u00e1s dando de alta una Category ni un Author.

    Rendimiento en las consultas JPA

    En este punto te recomiendo que visites el Anexo. Funcionamiento JPA para conocer un poco m\u00e1s como funciona por dentro JPA y alg\u00fan peque\u00f1o truco que puede mejorar el rendimiento.

    "},{"location":"develop/filtered/springboot/#implementar-listado-autores","title":"Implementar listado Autores","text":"

    Antes de poder conectar front con back, si recuerdas, en la edici\u00f3n de un Game, nos hac\u00eda falta un listado de Author y un listado de Category. El segundo ya lo tenemos ya que lo reutilizaremos del listado de categor\u00edas que implementamos. Pero el primero no lo tenemos, porque en la pantalla que hicimos, se mostraban de forma paginada.

    As\u00ed que necesitamos implementar esa funcionalidad, y como siempre vamos de la capa de testing hacia las siguientes capas. Deber\u00edamos a\u00f1adir los siguientes m\u00e9todos:

    AuthorIT.javaAuthorController.javaAuthorService.javaAuthorServiceImpl.java
    ...\n\nParameterizedTypeReference<List<AuthorDto>> responseTypeList = new ParameterizedTypeReference<List<AuthorDto>>(){};\n\n@Test\npublic void findAllShouldReturnAllAuthor() {\n\n      ResponseEntity<List<AuthorDto>> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.GET, null, responseTypeList);\n\n      assertNotNull(response);\n      assertEquals(TOTAL_AUTHORS, response.getBody().size());\n}\n\n...\n
    ...\n\n/**\n * Recupera un listado de autores {@link Author}\n *\n * @return {@link List} de {@link AuthorDto}\n */\n@Operation(summary = \"Find\", description = \"Method that return a list of Authors\")\n@RequestMapping(path = \"\", method = RequestMethod.GET)\npublic List<AuthorDto> findAll() {\n\n    List<Author> authors = this.authorService.findAll();\n\n    return authors.stream().map(e -> mapper.map(e, AuthorDto.class)).collect(Collectors.toList());\n}\n\n...\n
    ...\n\n/**\n * Recupera un listado de autores {@link Author}\n *\n * @return {@link List} de {@link Author}\n */\nList<Author> findAll();\n\n...\n
    ...\n\n/**\n * {@inheritDoc}\n */\n@Override\npublic List<Author> findAll() {\n\n    return (List<Author>) this.authorRepository.findAll();\n}\n\n\n...\n
    "},{"location":"develop/filtered/vuejs/","title":"Listado filtrado - VUE","text":"

    Aqu\u00ed vamos a volver a la pantalla de cat\u00e1logo para realizar un filtrado en la propia tabla.

    Empezaremos por modificar el template de la tabla que modificamos para a\u00f1adir el bot\u00f3n de a\u00f1adir nueva fila para a\u00f1adir tambi\u00e9n tres inputs: uno de texto para el nombre del juego y dos seleccionables para la categor\u00eda y el autor (les tendremos que asignar las opciones que haya en ese momento).

    Tambi\u00e9n a\u00f1adiremos un bot\u00f3n para que no se lance la petici\u00f3n cada vez que el usuario introduce una letra en el input de texto. Esto quedar\u00eda as\u00ed:

    <template v-slot:top>\n        <div class=\"q-table__title\">Cat\u00e1logo</div>\n        <q-btn flat round color=\"primary\" icon=\"add\" @click=\"showAdd = true\" />\n        <q-space />\n        <q-input dense v-model=\"filter.title\" placeholder=\"T\u00edtulo\">\n          <template v-slot:append>\n            <q-icon name=\"search\" />\n          </template>\n        </q-input>\n        <q-separator inset />\n        <div style=\"width: 10%\">\n          <q-select\n            dense\n            name=\"category\"\n            v-model=\"filter.category\"\n            :options=\"categories\"\n            emit-value\n            map-options\n            option-value=\"id\"\n            option-label=\"name\"\n            label=\"Categor\u00eda\"\n          />\n        </div>\n        <q-separator inset />\n        <div style=\"width: 10%\">\n          <q-select\n            dense\n            name=\"author\"\n            v-model=\"filter.author\"\n            :options=\"authors\"\n            emit-value\n            map-options\n            option-value=\"id\"\n            option-label=\"name\"\n            label=\"Autor\"\n          />\n        </div>\n        <q-separator inset />\n        <q-btn flat round color=\"primary\" icon=\"filter_alt\" @click=\"getGames\" />\n      </template>\n

    Adem\u00e1s, tambi\u00e9n vamos a a\u00f1adir un estado para todos los filtros juntos:

    const filter = ref({ title: '', category: '', author: '' });\n

    Por \u00faltimo, para no estar haciendo las tres peticiones (juegos, categor\u00edas y autores) las hemos extra\u00eddo en funciones diferentes de la siguiente manera:

    const getGames = () => {\n  const { data } = useFetch(url.value).get().json();\n  whenever(data, () => (catalogData.value = data.value));\n};\n\nconst getCategories = () => {\n  const { data: categoriesData } = useFetch('http://localhost:8080/category')\n    .get()\n    .json();\n  whenever(categoriesData, () => (categories.value = categoriesData.value));\n};\n\nconst getAuthors = () => {\n  const { data: authorsData } = useFetch('http://localhost:8080/author')\n    .get()\n    .json();\n  whenever(authorsData, () => (authors.value = authorsData.value));\n};\n\nconst firstLoad = () => {\n  getGames();\n  getCategories();\n  getAuthors();\n};\nfirstLoad();\n

    Y como podemos ver, ahora la petici\u00f3n de juegos no tiene la url. Esto es porque hemos hecho que sea una variable computada para a\u00f1adirle los par\u00e1metros de filtrado y ha quedado as\u00ed:

    const url = computed(() => {\n  const _url = new URL('http://localhost:8080/game');\n  _url.search = new URLSearchParams({\n    title: filter.value.title,\n    idCategory: filter.value.category ?? '',\n    idAuthor: filter.value.author ?? '',\n  });\n  return _url.toString();\n});\n
    "},{"location":"develop/paginated/angular/","title":"Listado paginado - Angular","text":"

    Ya tienes tu primer CRUD desarrollado. \u00bfHa sido sencillo, verdad?.

    Ahora vamos a implementar un CRUD un poco m\u00e1s complejo, este tiene datos paginados en servidor, esto quiere decir que no nos sirve un array de datos como en el anterior ejemplo. Para que un listado paginado en servidor funcione, el cliente debe enviar en cada petici\u00f3n que p\u00e1gina est\u00e1 solicitando y cual es el tama\u00f1o de la p\u00e1gina, para que el servidor devuelva solamente un subconjunto de datos, en lugar de devolver el listado completo.

    Como ya conocemos como se debe desarrollar, en este ejemplo vamos a ir m\u00e1s r\u00e1pidos y nos vamos a centrar \u00fanicamente en las novedades.

    "},{"location":"develop/paginated/angular/#crear-modulo-y-componentes","title":"Crear modulo y componentes","text":"

    Vamos a desarrollar el listado de Autores as\u00ed que, debemos crear los componentes:

    ng generate module author\nng generate component author/author-list\nng generate component author/author-edit\n\nng generate service author/author\n

    Este m\u00f3dulo lo vamos a a\u00f1adir a la aplicaci\u00f3n para que se cargue en el arranque. Abrimos el fichero app.module.ts y a\u00f1adimos el m\u00f3dulo:

    app.module.ts
    import { NgModule } from '@angular/core';\nimport { BrowserModule } from '@angular/platform-browser';\n\nimport { AppRoutingModule } from './app-routing.module';\nimport { AppComponent } from './app.component';\nimport { BrowserAnimationsModule } from '@angular/platform-browser/animations';\nimport { CoreModule } from './core/core.module';\nimport { CategoryModule } from './category/category.module';\nimport { AuthorModule } from './author/author.module';\n\n@NgModule({\ndeclarations: [\n    AppComponent\n],\nimports: [\n    BrowserModule,\n    AppRoutingModule,\n    CoreModule,\n    CategoryModule,\n    AuthorModule,\n    BrowserAnimationsModule\n],\nproviders: [],\nbootstrap: [AppComponent]\n})\nexport class AppModule { }\n
    "},{"location":"develop/paginated/angular/#crear-el-modelo","title":"Crear el modelo","text":"

    Creamos el modelo en author/model/Author.ts con las propiedades necesarias para trabajar con la informaci\u00f3n de un autor:

    Author.ts
    export class Author {\n    id: number;\n    name: string;\n    nationality: string;\n}\n
    "},{"location":"develop/paginated/angular/#anadir-el-punto-de-entrada","title":"A\u00f1adir el punto de entrada","text":"

    A\u00f1adimos la ruta al men\u00fa para que podamos acceder a la pantalla:

    app-routing.module.ts
    import { NgModule } from '@angular/core';\nimport { Routes, RouterModule } from '@angular/router';\nimport { CategoryListComponent } from './category/category-list/category-list.component';\nimport { AuthorListComponent } from './author/author-list/author-list.component';\n\nconst routes: Routes = [\n    { path: 'categories', component: CategoriesComponent },\n    { path: 'authors', component: AuthorListComponent },\n];\n\n@NgModule({\n    imports: [RouterModule.forRoot(routes)],\n    exports: [RouterModule]\n})\nexport class AppRoutingModule { }\n
    "},{"location":"develop/paginated/angular/#implementar-servicio","title":"Implementar servicio","text":"

    Y realizamos las diferentes implementaciones. Empezaremos por el servicio. En este caso, hay un cambio sustancial con el anterior ejemplo. Al tratarse de un listado paginado, la operaci\u00f3n getAuthors necesita informaci\u00f3n extra acerca de que p\u00e1gina de datos debe mostrar, adem\u00e1s de que el resultado ya no ser\u00e1 un listado sino una p\u00e1gina.

    Por defecto el esquema de datos de Spring para la paginaci\u00f3n es como el siguiente:

    Esquema de datos de paginaci\u00f3n
    {\n    \"content\": [ ... <listado con los resultados paginados> ... ],\n    \"pageable\": {\n        \"pageNumber\": <n\u00famero de p\u00e1gina empezando por 0>,\n        \"pageSize\": <tama\u00f1o de p\u00e1gina>,\n        \"sort\": [\n            { \n                \"property\": <nombre de la propiedad a ordenar>, \n                \"direction\": <direcci\u00f3n de la ordenaci\u00f3n ASC / DESC> \n            }\n        ]\n    },\n    \"totalElements\": <numero total de elementos en la tabla>\n}\n

    As\u00ed que necesitamos poder enviar y recuperar esa informaci\u00f3n desde Angular, nos hace falta crear esos objetos. Los objetos de paginaci\u00f3n al ser comunes a toda la aplicaci\u00f3n, vamos a crearlos en core/model/page, mientras que la paginaci\u00f3n de AuthorPage.ts la crear\u00e9 en su propio model dentro de author/model.

    SortPage.tsPageable.tsAuthorPage.ts
    export class SortPage {\n    property: String;\n    direction: String;\n}\n
    import { SortPage } from './SortPage';\n\nexport class Pageable {\n    pageNumber: number;\n    pageSize: number;\n    sort: SortPage[];\n}\n
    import { Pageable } from \"src/app/core/model/page/Pageable\";\nimport { Author } from \"./Author\";\n\nexport class AuthorPage {\n    content: Author[];\n    pageable: Pageable;\n    totalElements: number;\n}\n

    Con estos objetos creados ya podemos implementar el servicio y sus datos mockeados.

    mock-authors.tsauthor.service.ts
    import { AuthorPage } from \"./AuthorPage\";\n\nexport const AUTHOR_DATA: AuthorPage = {\n    content: [\n        { id: 1, name: 'Klaus Teuber', nationality: 'Alemania' },\n        { id: 2, name: 'Matt Leacock', nationality: 'Estados Unidos' },\n        { id: 3, name: 'Keng Leong Yeo', nationality: 'Singapur' },\n        { id: 4, name: 'Gil Hova', nationality: 'Estados Unidos'},\n        { id: 5, name: 'Kelly Adams', nationality: 'Estados Unidos' },\n        { id: 6, name: 'J. Alex Kavern', nationality: 'Estados Unidos' },\n        { id: 7, name: 'Corey Young', nationality: 'Estados Unidos' },\n    ],  \n    pageable : {\n        pageSize: 5,\n        pageNumber: 0,\n        sort: [\n            {property: \"id\", direction: \"ASC\"}\n        ]\n    },\n    totalElements: 7\n}\n
    import { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Pageable } from '../core/model/page/Pageable';\nimport { Author } from './model/Author';\nimport { AuthorPage } from './model/AuthorPage';\nimport { AUTHOR_DATA } from './model/mock-authors';\n\n@Injectable({\n    providedIn: 'root'\n})\nexport class AuthorService {\n\n    constructor() { }\n\n    getAuthors(pageable: Pageable): Observable<AuthorPage> {\n        return of(AUTHOR_DATA);\n    }\n\n    saveAuthor(author: Author): Observable<void> {\n        return of(null);\n    }\n\n    deleteAuthor(idAuthor : number): Observable<void> {\n        return of(null);\n    }    \n}\n
    "},{"location":"develop/paginated/angular/#implementar-listado","title":"Implementar listado","text":"

    Ya tenemos el servicio con los datos, ahora vamos a por el listado paginado.

    author-list.component.htmlauthor-list.component.scssauthor-list.component.ts
    <div class=\"container\">\n    <h1>Listado de Autores</h1>\n\n    <mat-table [dataSource]=\"dataSource\"> \n        <ng-container matColumnDef=\"id\">\n            <mat-header-cell *matHeaderCellDef> Identificador </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.id}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"name\">\n            <mat-header-cell *matHeaderCellDef> Nombre autor  </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.name}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"nationality\">\n            <mat-header-cell *matHeaderCellDef> Nacionalidad  </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.nationality}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"action\">\n            <mat-header-cell *matHeaderCellDef></mat-header-cell>\n            <mat-cell *matCellDef=\"let element\">\n                <button mat-icon-button color=\"primary\" (click)=\"editAuthor(element)\">\n                    <mat-icon>edit</mat-icon>\n                </button>\n                <button mat-icon-button color=\"accent\" (click)=\"deleteAuthor(element)\">\n                    <mat-icon>clear</mat-icon>\n                </button>\n            </mat-cell>\n        </ng-container>\n\n        <mat-header-row *matHeaderRowDef=\"displayedColumns; sticky: true\"></mat-header-row>\n        <mat-row *matRowDef=\"let row; columns: displayedColumns;\"></mat-row>\n    </mat-table> \n\n    <mat-paginator (page)=\"loadPage($event)\" [pageSizeOptions]=\"[5, 10, 20]\" [pageIndex]=\"pageNumber\" [pageSize]=\"pageSize\" [length]=\"totalElements\" showFirstLastButtons></mat-paginator>\n\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\" (click)=\"createAuthor()\">Nuevo autor</button> \n    </div>   \n</div>\n
    .container {\n    margin: 20px;\n\n    mat-table {\n        margin-top: 10px;\n        margin-bottom: 20px;\n\n        .mat-header-row {\n            background-color:#f5f5f5;\n\n            .mat-header-cell {\n                text-transform: uppercase;\n                font-weight: bold;\n                color: #838383;\n            }      \n        }\n\n        .mat-column-id {\n            flex: 0 0 20%;\n            justify-content: center;\n        }\n\n        .mat-column-action {\n            flex: 0 0 10%;\n            justify-content: center;\n        }\n    }\n\n    .buttons {\n        text-align: right;\n    }\n}\n
    import { Component, OnInit } from '@angular/core';\nimport { MatDialog } from '@angular/material/dialog';\nimport { PageEvent } from '@angular/material/paginator';\nimport { MatTableDataSource } from '@angular/material/table';\nimport { DialogConfirmationComponent } from 'src/app/core/dialog-confirmation/dialog-confirmation.component';\nimport { Pageable } from 'src/app/core/model/page/Pageable';\nimport { AuthorEditComponent } from '../author-edit/author-edit.component';\nimport { AuthorService } from '../author.service';\nimport { Author } from '../model/Author';\n\n@Component({\nselector: 'app-author-list',\ntemplateUrl: './author-list.component.html',\nstyleUrls: ['./author-list.component.scss']\n})\nexport class AuthorListComponent implements OnInit {\n\n    pageNumber: number = 0;\n    pageSize: number = 5;\n    totalElements: number = 0;\n\n    dataSource = new MatTableDataSource<Author>();\n    displayedColumns: string[] = ['id', 'name', 'nationality', 'action'];\n\n    constructor(\n        private authorService: AuthorService,\n        public dialog: MatDialog,\n    ) { }\n\n    ngOnInit(): void {\n        this.loadPage();\n    }\n\n    loadPage(event?: PageEvent) {\n\n        let pageable : Pageable =  {\n            pageNumber: this.pageNumber,\n            pageSize: this.pageSize,\n            sort: [{\n                property: 'id',\n                direction: 'ASC'\n            }]\n        }\n\n        if (event != null) {\n            pageable.pageSize = event.pageSize\n            pageable.pageNumber = event.pageIndex;\n        }\n\n        this.authorService.getAuthors(pageable).subscribe(data => {\n            this.dataSource.data = data.content;\n            this.pageNumber = data.pageable.pageNumber;\n            this.pageSize = data.pageable.pageSize;\n            this.totalElements = data.totalElements;\n        });\n\n    }  \n\n    createAuthor() {      \n        const dialogRef = this.dialog.open(AuthorEditComponent, {\n            data: {}\n        });\n\n        dialogRef.afterClosed().subscribe(result => {\n            this.ngOnInit();\n        });      \n    }  \n\n    editAuthor(author: Author) {    \n        const dialogRef = this.dialog.open(AuthorEditComponent, {\n            data: { author: author }\n        });\n\n        dialogRef.afterClosed().subscribe(result => {\n            this.ngOnInit();\n        });    \n    }\n\n    deleteAuthor(author: Author) {    \n        const dialogRef = this.dialog.open(DialogConfirmationComponent, {\n            data: { title: \"Eliminar autor\", description: \"Atenci\u00f3n si borra el autor se perder\u00e1n sus datos.<br> \u00bfDesea eliminar el autor?\" }\n        });\n\n        dialogRef.afterClosed().subscribe(result => {\n            if (result) {\n                this.authorService.deleteAuthor(author.id).subscribe(result =>  {\n                    this.ngOnInit();\n                }); \n            }\n        });\n    }  \n}\n

    F\u00edjate como hemos a\u00f1adido la paginaci\u00f3n.

    • Al HTML le hemos a\u00f1adido un componente nuevo mat-paginator, lo que nos va a obligar a a\u00f1adirlo al m\u00f3dulo tambi\u00e9n como dependencia. Ese componente le hemos definido un m\u00e9todo page que se ejecuta cada vez que la p\u00e1gina cambia, y unas propiedades con las que calcular\u00e1 la p\u00e1gina, el tama\u00f1o y el n\u00famero total de p\u00e1ginas.
    • Al Typescript le hemos tenido que a\u00f1adir esas variables y hemos creado un m\u00e9todo para cargar datos que lo que hace es construir un objeto pageable con los valores actuales del componente paginador y lanza la petici\u00f3n con esos datos en el body. Obviamente al ser un mock no funcionar\u00e1 el cambio de p\u00e1gina y dem\u00e1s.

    Como siempre, a\u00f1adimos las dependencias al m\u00f3dulo, vamos a intentar a\u00f1adir todas las que vamos a necesitar a futuro.

    author.module.ts
    import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { AuthorListComponent } from './author-list/author-list.component';\nimport { AuthorEditComponent } from './author-edit/author-edit.component';\nimport { MatTableModule } from '@angular/material/table';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatDialogModule, MAT_DIALOG_DATA } from '@angular/material/dialog';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatPaginatorModule } from '@angular/material/paginator';\n\n\n\n@NgModule({\ndeclarations: [\n    AuthorListComponent,\n    AuthorEditComponent\n],\nimports: [\n    CommonModule,\n    MatTableModule,\n    MatIconModule, \n    MatButtonModule,\n    MatDialogModule,\n    MatFormFieldModule,\n    MatInputModule,\n    FormsModule,\n    ReactiveFormsModule,\n    MatPaginatorModule,\n],\nproviders: [\n    {\n        provide: MAT_DIALOG_DATA,\n        useValue: {},\n    },\n]\n})\nexport class AuthorModule { }\n

    Deber\u00eda verse algo similar a esto:

    "},{"location":"develop/paginated/angular/#implementar-dialogo-edicion","title":"Implementar dialogo edici\u00f3n","text":"

    El \u00faltimo paso, es definir la pantalla de dialogo que realizar\u00e1 el alta y modificado de los datos de un Autor.

    author-edit.component.htmlauthor-edit.component.scssauthor-edit.component.ts
    <div class=\"container\">\n    <h1 *ngIf=\"author.id == null\">Crear autor</h1>\n    <h1 *ngIf=\"author.id != null\">Modificar autor</h1>\n\n    <form>\n        <mat-form-field>\n            <mat-label>Identificador</mat-label>\n            <input type=\"text\" matInput placeholder=\"Identificador\" [(ngModel)]=\"author.id\" name=\"id\" disabled>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Nombre</mat-label>\n            <input type=\"text\" matInput placeholder=\"Nombre del autor\" [(ngModel)]=\"author.name\" name=\"name\" required>\n            <mat-error>El nombre no puede estar vac\u00edo</mat-error>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Nacionalidad</mat-label>\n            <input type=\"text\" matInput placeholder=\"Nacionalidad del autor\" [(ngModel)]=\"author.nationality\" name=\"nationality\" required>\n            <mat-error>La nacionalidad no puede estar vac\u00eda</mat-error>\n        </mat-form-field>\n    </form>\n\n    <div class=\"buttons\">\n        <button mat-stroked-button (click)=\"onClose()\">Cerrar</button>\n        <button mat-flat-button color=\"primary\" (click)=\"onSave()\">Guardar</button>\n    </div>\n</div>\n
    .container {\n    min-width: 350px;\n    max-width: 500px;\n    padding: 20px;\n\n    form {\n        display: flex;\n        flex-direction: column;\n        margin-bottom:20px;\n    }\n\n    .buttons {\n    text-align: right;\n\n    button {\n        margin-left: 10px;\n    }\n    }\n}\n
    import { Component, Inject, OnInit } from '@angular/core';\nimport { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';\nimport { AuthorService } from '../author.service';\nimport { Author } from '../model/Author';\n\n@Component({\nselector: 'app-author-edit',\ntemplateUrl: './author-edit.component.html',\nstyleUrls: ['./author-edit.component.scss']\n})\nexport class AuthorEditComponent implements OnInit {\n\n    author : Author;\n\n    constructor(\n        public dialogRef: MatDialogRef<AuthorEditComponent>,\n        @Inject(MAT_DIALOG_DATA) public data: any,\n        private authorService: AuthorService\n    ) { }\n\n    ngOnInit(): void {\n        if (this.data.author != null) {\n            this.author = Object.assign({}, this.data.author);\n        }\n        else {\n            this.author = new Author();\n        }\n    }\n\n    onSave() {\n        this.authorService.saveAuthor(this.author).subscribe(result =>  {\n            this.dialogRef.close();\n        }); \n    }  \n\n    onClose() {\n        this.dialogRef.close();\n    }\n\n}\n

    Que deber\u00eda quedar algo as\u00ed:

    "},{"location":"develop/paginated/angular/#conectar-con-backend","title":"Conectar con Backend","text":"

    Antes de seguir

    Antes de seguir con este punto, debes implementar el c\u00f3digo de backend en la tecnolog\u00eda que quieras (Springboot o Nodejs). Si has empezado este cap\u00edtulo implementando el frontend, por favor accede a la secci\u00f3n correspondiente de backend para poder continuar con el tutorial. Una vez tengas implementadas todas las operaciones para este listado, puedes volver a este punto y continuar con Angular.

    Una vez implementado front y back, lo que nos queda es modificar el servicio del front para que conecte directamente con las operaciones ofrecidas por el back.

    author.service.ts
    import { HttpClient } from '@angular/common/http';\nimport { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Pageable } from '../core/model/page/Pageable';\nimport { Author } from './model/Author';\nimport { AuthorPage } from './model/AuthorPage';\n\n@Injectable({\nprovidedIn: 'root'\n})\nexport class AuthorService {\n\n    constructor(\n        private http: HttpClient\n    ) { }\n\n    getAuthors(pageable: Pageable): Observable<AuthorPage> {\n        return this.http.post<AuthorPage>('http://localhost:8080/author', {pageable:pageable});\n    }\n\n    saveAuthor(author: Author): Observable<void> {\n        let url = 'http://localhost:8080/author';\n        if (author.id != null) url += '/'+author.id;\n\n        return this.http.put<void>(url, author);\n    }\n\n    deleteAuthor(idAuthor : number): Observable<void> {\n        return this.http.delete<void>('http://localhost:8080/author/'+idAuthor);\n    }    \n}\n
    "},{"location":"develop/paginated/angular17/","title":"Listado paginado - Angular","text":"

    Ya tienes tu primer CRUD desarrollado. \u00bfHa sido sencillo, verdad?

    Ahora vamos a implementar un CRUD un poco m\u00e1s complejo, este tiene datos paginados en servidor, esto quiere decir que no nos sirve un array de datos como en el anterior ejemplo. Para que un listado paginado en servidor funcione, el cliente debe enviar en cada petici\u00f3n que p\u00e1gina est\u00e1 solicitando y cu\u00e1l es el tama\u00f1o de la p\u00e1gina, para que el servidor devuelva solamente un subconjunto de datos, en lugar de devolver el listado completo.

    Como ya conocemos como se debe desarrollar, en este ejemplo vamos a ir m\u00e1s r\u00e1pidos y nos vamos a centrar \u00fanicamente en las novedades.

    "},{"location":"develop/paginated/angular17/#crear-componentes","title":"Crear componentes","text":"

    Vamos a desarrollar el listado de Autores as\u00ed que, debemos crear los componentes:

    ng generate component author/author-list --type=page\nng generate component author/author-edit\n\nng generate service author/author\n
    "},{"location":"develop/paginated/angular17/#crear-el-modelo","title":"Crear el modelo","text":"

    Creamos el modelo en author/model/Author.ts con las propiedades necesarias para trabajar con la informaci\u00f3n de un autor:

    Author.ts
    export interface Author {\n    id: number;\n    name: string;\n    nationality: string;\n}\n
    "},{"location":"develop/paginated/angular17/#anadir-el-punto-de-entrada","title":"A\u00f1adir el punto de entrada","text":"

    A\u00f1adimos la ruta al men\u00fa para que podamos acceder a la pantalla:

    app.routes.ts
    import { Routes } from '@angular/router';\n\nexport const routes: Routes = [\n    { path: 'categories', loadComponent: () => import('../category/category-list/category-list.page').then(m => m.CategoryListPage)},\n    { path: 'authors', loadComponent: () => import('../author/author-list/author-list.page').then(m => m.AuthorListPage)},\n];\n
    "},{"location":"develop/paginated/angular17/#implementar-servicio","title":"Implementar servicio","text":"

    Y realizamos las diferentes implementaciones. Empezaremos por el servicio. En este caso, hay un cambio sustancial con el anterior ejemplo. Al tratarse de un listado paginado, la operaci\u00f3n getAuthors necesita informaci\u00f3n extra acerca de que p\u00e1gina de datos debe mostrar, adem\u00e1s de que el resultado ya no ser\u00e1 un listado sino una p\u00e1gina.

    Por defecto, el esquema de datos de Spring para la paginaci\u00f3n es como el siguiente:

    Esquema de datos de paginaci\u00f3n
    {\n    \"content\": [ ... <listado con los resultados paginados> ... ],\n    \"pageable\": {\n        \"pageNumber\": <n\u00famero de p\u00e1gina empezando por 0>,\n        \"pageSize\": <tama\u00f1o de p\u00e1gina>,\n        \"sort\": [\n            { \n                \"property\": <nombre de la propiedad a ordenar>, \n                \"direction\": <direcci\u00f3n de la ordenaci\u00f3n ASC / DESC> \n            }\n        ]\n    },\n    \"totalElements\": <numero total de elementos en la tabla>\n}\n

    As\u00ed que necesitamos poder enviar y recuperar esa informaci\u00f3n desde Angular, nos hace falta crear esos objetos. Los objetos de paginaci\u00f3n, al ser comunes a toda la aplicaci\u00f3n, vamos a crearlos en core/model/page.

    \u00bfPor qu\u00e9 en core/model/page y no dentro del componente author?

    SortPage, Pageable y PaginatedData no pertenecen al dominio de negocio de Author; son contratos t\u00e9cnicos que describen c\u00f3mo se comunica el frontend con el backend cuando los datos vienen paginados.

    Cualquier otro listado de la aplicaci\u00f3n \u2014categor\u00edas, pr\u00e9stamos, juegos\u2026\u2014 necesitar\u00e1 exactamente estos mismos objetos. Si los meti\u00e9ramos dentro de author/, el d\u00eda que quisi\u00e9ramos paginar otro listado tendr\u00edamos dos opciones igual de malas: duplicar el c\u00f3digo o importar clases de un m\u00f3dulo que no tiene nada que ver con tu entidad.

    Colocarlos en core/ sigue el principio de reutilizaci\u00f3n y responsabilidad \u00fanica:

    • Un \u00fanico lugar donde mantener el contrato de paginaci\u00f3n.
    • Si el backend cambia el esquema (p. ej. renombra pageNumber a page), s\u00f3lo hay que tocar un fichero.
    • Cualquier desarrollador que llegue al proyecto sabr\u00e1 d\u00f3nde buscarlos sin tener que adivinar en qu\u00e9 m\u00f3dulo de negocio est\u00e1n escondidos.
    SortPage.tsPageable.tsPaginatedData.ts
    export interface SortPage {\n    property: string;\n    direction: string;\n}\n
    import { SortPage } from './SortPage';\n\nexport interface Pageable {\n    pageNumber: number;\n    pageSize: number;\n    sort: SortPage[];\n}\n
    import { Pageable } from \"src/app/core/model/page/Pageable\";\n\nexport interface PaginatedData <TData>{\n    content: TData[];\n    pageable: Pageable;\n    totalElements: number;\n}\n

    \u00bfQu\u00e9 es <TData>?

    <TData> es un gen\u00e9rico de TypeScript (el equivalente a los generics de Java <T> o de C# <T>).

    Permite que PaginatedData sea una clase reutilizable con cualquier tipo de contenido, sin necesidad de crear una clase distinta para cada entidad:

    PaginatedData<Author>    // content ser\u00e1 Author[]\nPaginatedData<Category>  // content ser\u00e1 Category[]\nPaginatedData<Loan>      // content ser\u00e1 Loan[]\n

    TypeScript verifica el tipo en tiempo de compilaci\u00f3n, por lo que si intentas acceder a una propiedad que no existe en Author dentro de un PaginatedData<Author>, el compilador te avisar\u00e1 de inmediato. Obtienes reutilizaci\u00f3n y seguridad de tipos al mismo tiempo.

    Con estos objetos creados ya podemos implementar el servicio y sus datos mockeados.

    mock-authors.tsauthor.service.ts
    import { PaginatedData } from 'src/app/core/model/page/PaginatedData';\nimport { Author } from './Author';\n\nexport const AUTHOR_DATA: PaginatedData<Author> = {\n    content: [\n        { id: 1, name: 'Klaus Teuber', nationality: 'Alemania' },\n        { id: 2, name: 'Matt Leacock', nationality: 'Estados Unidos' },\n        { id: 3, name: 'Keng Leong Yeo', nationality: 'Singapur' },\n        { id: 4, name: 'Gil Hova', nationality: 'Estados Unidos' },\n        { id: 5, name: 'Kelly Adams', nationality: 'Estados Unidos' },\n        { id: 6, name: 'J. Alex Kavern', nationality: 'Estados Unidos' },\n        { id: 7, name: 'Corey Young', nationality: 'Estados Unidos' },\n    ],\n    pageable: {\n        pageSize: 5,\n        pageNumber: 0,\n        sort: [{ property: 'id', direction: 'ASC' }],\n    },\n    totalElements: 7,\n};\n
    import { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Pageable } from '../core/model/page/Pageable';\nimport { Author } from './model/Author';\nimport { PaginatedData } from 'src/app/core/model/page/PaginatedData';\nimport { AUTHOR_DATA } from './model/mock-authors';\n\n@Injectable({\n    providedIn: 'root',\n})\nexport class AuthorService {\n    constructor() {}\n\n    getAuthors(pageable: Pageable): Observable<PaginatedData<Author>> {\n        return of(AUTHOR_DATA);\n    }\n\n    saveAuthor(author: Author): Observable<void> {\n        return of(null);\n    }\n\n    deleteAuthor(idAuthor: number): Observable<void> {\n        return of(null);\n    }\n}\n
    "},{"location":"develop/paginated/angular17/#implementar-listado","title":"Implementar listado","text":"

    Ya tenemos el servicio con los datos, ahora vamos a por el listado paginado.

    author-list.component.htmlauthor-list.component.scssauthor-list.component.ts
    <div class=\"container\">\n    <h1>Listado de Autores</h1>\n\n    <mat-table [dataSource]=\"dataSource\"> \n        <ng-container matColumnDef=\"id\">\n            <mat-header-cell *matHeaderCellDef> Identificador </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.id}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"name\">\n            <mat-header-cell *matHeaderCellDef> Nombre autor  </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.name}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"nationality\">\n            <mat-header-cell *matHeaderCellDef> Nacionalidad  </mat-header-cell>\n            <mat-cell *matCellDef=\"let element\"> {{element.nationality}} </mat-cell>\n        </ng-container>\n\n        <ng-container matColumnDef=\"action\">\n            <mat-header-cell *matHeaderCellDef></mat-header-cell>\n            <mat-cell *matCellDef=\"let element\">\n                <button mat-icon-button color=\"primary\" (click)=\"editAuthor(element)\">\n                    <mat-icon>edit</mat-icon>\n                </button>\n                <button mat-icon-button color=\"accent\" (click)=\"deleteAuthor(element)\">\n                    <mat-icon>clear</mat-icon>\n                </button>\n            </mat-cell>\n        </ng-container>\n\n        <mat-header-row *matHeaderRowDef=\"displayedColumns; sticky: true\"></mat-header-row>\n        <mat-row *matRowDef=\"let row; columns: displayedColumns;\"></mat-row>\n    </mat-table> \n\n    <mat-paginator (page)=\"loadPage($event)\" [pageSizeOptions]=\"[5, 10, 20]\" [pageIndex]=\"pageNumber\" [pageSize]=\"pageSize\" [length]=\"totalElements\" showFirstLastButtons></mat-paginator>\n\n    <div class=\"buttons\">\n        <button mat-flat-button color=\"primary\" (click)=\"createAuthor()\">Nuevo autor</button> \n    </div>   \n</div>\n
    .container {\n    margin: 20px;\n\n    mat-table {\n        margin-top: 10px;\n        margin-bottom: 20px;\n\n        .mat-header-row {\n            background-color:#f5f5f5;\n\n            .mat-header-cell {\n                text-transform: uppercase;\n                font-weight: bold;\n                color: #838383;\n            }      \n        }\n\n        .mat-column-id {\n            flex: 0 0 20%;\n            justify-content: center;\n        }\n\n        .mat-column-action {\n            flex: 0 0 10%;\n            justify-content: center;\n        }\n    }\n\n    .buttons {\n        text-align: right;\n    }\n}\n
    import { Component, OnInit } from '@angular/core';\nimport { MatDialog } from '@angular/material/dialog';\nimport { PageEvent } from '@angular/material/paginator';\nimport { MatTableDataSource, MatTableModule } from '@angular/material/table';\nimport { AuthorEditComponent } from '../author-edit/author-edit.component';\nimport { AuthorService } from '../author.service';\nimport { Author } from '../model/Author';\nimport { Pageable } from '../../core/model/page/Pageable';\nimport { DialogConfirmationComponent } from '../../core/dialog-confirmation/dialog-confirmation.component';\nimport { CommonModule } from '@angular/common';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatIconModule } from '@angular/material/icon';\n\n@Component({\n    selector: 'app-author-list',\n    standalone: true,\n    imports: [MatButtonModule, MatIconModule, MatTableModule, CommonModule],\n    templateUrl: './author-list.component.html',\n    styleUrl: './author-list.component.scss',\n})\nexport class AuthorListComponent implements OnInit {\n    pageNumber: number = 0;\n    pageSize: number = 5;\n    totalElements: number = 0;\n\n    dataSource = new MatTableDataSource<Author>();\n    displayedColumns: string[] = ['id', 'name', 'nationality', 'action'];\n\n    constructor(private authorService: AuthorService, public dialog: MatDialog) {}\n\n    ngOnInit(): void {\n        this.loadPage();\n    }\n\n    loadPage(event?: PageEvent) {\n        const pageable: Pageable = {\n            pageNumber: this.pageNumber,\n            pageSize: this.pageSize,\n            sort: [\n                {\n                    property: 'id',\n                    direction: 'ASC',\n                },\n            ],\n        };\n\n        if (event != null) {\n            pageable.pageSize = event.pageSize;\n            pageable.pageNumber = event.pageIndex;\n        }\n\n        this.authorService.getAuthors(pageable).subscribe((data) => {\n            this.dataSource.data = data.content;\n            this.pageNumber = data.pageable.pageNumber;\n            this.pageSize = data.pageable.pageSize;\n            this.totalElements = data.totalElements;\n        });\n    }\n\n    createAuthor() {\n        const dialogRef = this.dialog.open(AuthorEditComponent, {\n            data: {},\n        });\n\n        dialogRef.afterClosed().subscribe((result) => {\n            this.ngOnInit();\n        });\n    }\n\n    editAuthor(author: Author) {\n        const dialogRef = this.dialog.open(AuthorEditComponent, {\n            data: { author: author },\n        });\n\n        dialogRef.afterClosed().subscribe((result) => {\n            this.ngOnInit();\n        });\n    }\n\n    deleteAuthor(author: Author) {\n        const dialogRef = this.dialog.open(DialogConfirmationComponent, {\n            data: {\n                title: 'Eliminar autor',\n                description:\n                    'Atenci\u00f3n si borra el autor se perder\u00e1n sus datos.<br> \u00bfDesea eliminar el autor?',\n            },\n        });\n\n        dialogRef.afterClosed().subscribe((result) => {\n            if (result) {\n                this.authorService.deleteAuthor(author.id).subscribe((result) => {\n                    this.ngOnInit();\n                });\n            }\n        });\n    }\n}\n

    F\u00edjate como hemos a\u00f1adido la paginaci\u00f3n.

    • Al HTML le hemos a\u00f1adido un componente nuevo mat-paginator, lo que nos va a obligar a a\u00f1adirlo al array de imports tambi\u00e9n como dependencia. Ese componente le hemos definido un m\u00e9todo page que se ejecuta cada vez que la p\u00e1gina cambia, y unas propiedades con las que calcular\u00e1 la p\u00e1gina, el tama\u00f1o y el n\u00famero total de p\u00e1ginas.
    • Al Typescript le hemos tenido que a\u00f1adir esas variables y hemos creado un m\u00e9todo para cargar datos que lo que hace es construir un objeto pageable con los valores actuales del componente paginador y lanza la petici\u00f3n con esos datos en el body. Obviamente, al ser un mock no funcionar\u00e1 el cambio de p\u00e1gina y dem\u00e1s.

    Deber\u00eda verse algo similar a esto:

    "},{"location":"develop/paginated/angular17/#implementar-dialogo-edicion","title":"Implementar di\u00e1logo edici\u00f3n","text":"

    El \u00faltimo paso es definir la pantalla de di\u00e1logo que realizar\u00e1 el alta y modificado de los datos de un Autor.

    author-edit.component.htmlauthor-edit.component.scssauthor-edit.component.ts
    <div class=\"container\">\n    @if (author.id) {\n        <h1>Modificar autor</h1>\n    } @else {\n        <h1>Crear autor</h1>\n    }\n\n    <form>\n        <mat-form-field>\n            <mat-label>Identificador</mat-label>\n            <input type=\"text\" matInput placeholder=\"Identificador\" [(ngModel)]=\"id\" name=\"id\" disabled>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Nombre</mat-label>\n            <input type=\"text\" matInput placeholder=\"Nombre del autor\" [(ngModel)]=\"name\" name=\"name\" required>\n            <mat-error>El nombre no puede estar vac\u00edo</mat-error>\n        </mat-form-field>\n\n        <mat-form-field>\n            <mat-label>Nacionalidad</mat-label>\n            <input type=\"text\" matInput placeholder=\"Nacionalidad del autor\" [(ngModel)]=\"nationality\" name=\"nationality\" required>\n            <mat-error>La nacionalidad no puede estar vac\u00eda</mat-error>\n        </mat-form-field>\n    </form>\n\n    <div class=\"buttons\">\n        <button mat-stroked-button (click)=\"onClose()\">Cerrar</button>\n        <button mat-flat-button color=\"primary\" (click)=\"onSave()\">Guardar</button>\n    </div>\n</div>\n
    .container {\n    min-width: 350px;\n    max-width: 500px;\n    padding: 20px;\n\n    form {\n        display: flex;\n        flex-direction: column;\n        margin-bottom:20px;\n    }\n\n    .buttons {\n    text-align: right;\n\n    button {\n        margin-left: 10px;\n    }\n    }\n}\n
    import { Component, inject, OnInit, signal } from '@angular/core';\nimport { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';\nimport { AuthorService } from '../author.service';\nimport { Author } from '../model/Author';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { validateFields } from '../../core/helpers/validation.helper';\n\n@Component({\n    selector: 'app-author-edit',\n    standalone: true,\n    imports: [FormsModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatButtonModule ],\n    templateUrl: './author-edit.component.html',\n    styleUrl: './author-edit.component.scss',\n})\nexport class AuthorEditComponent implements OnInit {\n    protected readonly authorService = inject(AuthorService);\n    protected readonly dialogRef = inject(MatDialogRef<AuthorEditComponent>);\n    protected readonly data = inject(MAT_DIALOG_DATA);\n\n    protected readonly id = signal<number | null>(null);\n    protected readonly name = signal<string | null>(null);\n    protected readonly nationality = signal<string | null>(null);\n\n    loadFormData(initialData: Author | null) {\n        this.id.set(initialData.id ?? null);\n        this.name.set(initialData.name ?? null);\n        this.nationality.set(initialData.nationality ?? null);\n    }\n\n    ngOnInit(): void {\n        this.loadFormData(this.data.author ?? null);\n    }\n\n    onSave() {\n        const id = this.id();\n        const name = this.name();\n        const nationality = this.nationality();\n\n        const requiredFields = [\"name\", \"nationality\"] as const\n        const data = { name, nationality }\n\n        if (!validateFields(data, requiredFields)) {\n            return;\n        }\n\n        const author = {\n            id,\n            name,\n            nationality,\n        } as Author;\n        this.authorService.saveAuthor(author).subscribe(() => {\n            this.dialogRef.close(true);\n        });\n    }\n\n    onClose() {\n        this.dialogRef.close(false);\n    }\n}\n

    Info

    Podemos usar el helper validateFields cuando haya varios campos para validar que sean requeridos

    Que deber\u00eda quedar algo as\u00ed:

    "},{"location":"develop/paginated/angular17/#conectar-con-backend","title":"Conectar con Backend","text":"

    Antes de seguir

    Antes de seguir con este punto, debes implementar el c\u00f3digo de backend en la tecnolog\u00eda que quieras (Springboot o Nodejs). Si has empezado este cap\u00edtulo implementando el frontend, por favor accede a la secci\u00f3n correspondiente de backend para poder continuar con el tutorial. Una vez tengas implementadas todas las operaciones para este listado, puedes volver a este punto y continuar con Angular.

    Una vez implementado front y back, lo que nos queda es modificar el servicio del front para que conecte directamente con las operaciones ofrecidas por el back.

    author.service.ts
    import { Injectable, inject } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { Pageable } from '../core/model/page/Pageable';\nimport { Author } from './model/Author';\nimport { PaginatedData } from 'src/app/core/model/page/PaginatedData';\nimport { AUTHOR_DATA } from './model/mock-authors';\nimport { HttpClient } from '@angular/common/http';\n\n@Injectable({\n    providedIn: 'root',\n})\nexport class AuthorService {\n    protected readonly http = inject(HttpClient);\n\n    private baseUrl = 'http://localhost:8080/author';\n\n    getAuthors(pageable: Pageable): Observable<PaginatedData<Author>> {\n        return this.http.post<PaginatedData<Author>>(this.baseUrl, { pageable: pageable });\n    }\n\n    saveAuthor(author: Author): Observable<Author> {\n        const { id } = author;\n        const url = id ? `${this.baseUrl}/${id}` : this.baseUrl;\n        return this.http.put<Author>(url, author);\n    }\n\n    deleteAuthor(idAuthor: number): Observable<void> {\n        return this.http.delete<void>(`${this.baseUrl}/${idAuthor}`);\n    }\n}\n
    "},{"location":"develop/paginated/nodejs/","title":"Listado paginado - Nodejs","text":"

    Ahora vamos a implementar las operaciones necesarias para ayudar al front a cubrir la funcionalidad del CRUD paginado en servidor. Recuerda que para que un listado paginado en servidor funcione, el cliente debe enviar en cada petici\u00f3n que p\u00e1gina est\u00e1 solicitando y cual es el tama\u00f1o de la p\u00e1gina, para que el servidor devuelva solamente un subconjunto de datos, en lugar de devolver el listado completo.

    Como ya conocemos como se debe desarrollar, en este ejemplo vamos a ir m\u00e1s r\u00e1pidos y nos vamos a centrar \u00fanicamente en las novedades.

    "},{"location":"develop/paginated/nodejs/#crear-modelos","title":"Crear modelos","text":"

    Lo primero que vamos a hacer es crear el modelo de author para trabajar con BBDD. En la carpeta schemas creamos el archivo author.schema.js:

    author.schema.js
    import mongoose from \"mongoose\";\nimport normalize from 'normalize-mongoose';\nimport mongoosePaginate from 'mongoose-paginate-v2';\nconst { Schema, model } = mongoose;\n\nconst authorSchema = new Schema({\n    name: {\n        type: String,\n        require: true\n    },\n    nationality: {\n        type: String,\n        require: true\n    }\n});\nauthorSchema.plugin(normalize);\nauthorSchema.plugin(mongoosePaginate);\n\nconst AuthorModel = model('Author', authorSchema);\n\nexport default AuthorModel;\n
    "},{"location":"develop/paginated/nodejs/#implementar-el-service","title":"Implementar el Service","text":"

    Creamos el service correspondiente author.service.js:

    author.service.js
    import AuthorModel from '../schemas/author.schema.js';\n\nexport const getAuthors = async () => {\n    try {\n        return await AuthorModel.find().sort('id');\n    } catch (e) {\n        throw Error('Error fetching authors');\n    }\n}\n\nexport const createAuthor = async (data) => {\n    const { name, nationality } = data;\n    try {\n        const author = new AuthorModel({ name, nationality });\n        return await author.save();\n    } catch (e) {\n        throw Error('Error creating author');\n    }\n}\n\nexport const updateAuthor = async (id, data) => {\n    try {\n        const author = await AuthorModel.findById(id);\n        if (!author) {\n            throw Error('There is no author with that Id');\n        }    \n        return await AuthorModel.findByIdAndUpdate(id, data);\n    } catch (e) {\n        throw Error(e);\n    }\n}\n\nexport const deleteAuthor = async (id) => {\n    try {\n        const author = await AuthorModel.findById(id);\n        if (!author) {\n            throw Error('There is no author with that Id');\n        }\n        return await AuthorModel.findByIdAndDelete(id);\n    } catch (e) {\n        throw Error(e);\n    }\n}\n\nexport const getAuthorsPageable = async (page, limit, sort) => {\n    const sortObj = {\n        [sort?.property || 'name']: sort?.direction === 'desc' ? 'desc' : 'asc'\n    };\n    try {\n       const options = {\n            page: parseInt(page) + 1,\n            limit,\n            sort: sortObj\n        };\n\n        return await AuthorModel.paginate({}, options);\n    } catch (e) {\n        throw Error('Error fetching authors page');\n    }    \n}\n

    Como podemos observar es muy parecido al servicio de categor\u00edas, pero hemos incluido un nuevo m\u00e9todo getAuthorsPageable. Este m\u00e9todo tendr\u00e1 como par\u00e1metros de entrada la p\u00e1gina que queramos mostrar, el tama\u00f1o de esta y las propiedades de ordenaci\u00f3n. Moongose nos proporciona el m\u00e9todo paginate que es muy parecido a find salvo que adem\u00e1s podemos pasar las opciones de paginaci\u00f3n y el solo realizar\u00e1 todo el trabajo.

    "},{"location":"develop/paginated/nodejs/#implementar-el-controller","title":"Implementar el Controller","text":"

    Creamos el controlador author.controller.js:

    author.controller.js
    import * as AuthorService from '../services/author.service.js';\n\nexport const getAuthors = async (req, res) => {\n    try {\n        const authors = await AuthorService.getAuthors();\n        res.status(200).json(\n            authors\n        );\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n\nexport const createAuthor = async (req, res) => {\n    try {\n        const author = await AuthorService.createAuthor(req.body);\n        res.status(200).json({\n            author\n        });\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n\nexport const updateAuthor = async (req, res) => {\n    const authorId = req.params.id;\n    try {\n        await AuthorService.updateAuthor(authorId, req.body);\n        res.status(200).json(1);\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n\nexport const deleteAuthor = async (req, res) => {\n    const authorId = req.params.id;\n    try {\n        const deletedAuthor = await AuthorService.deleteAuthor(authorId);\n        res.status(200).json({\n            author: deletedAuthor\n        });\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n\nexport const getAuthorsPageable = async (req, res) => {\n    const page = req.body.pageable.pageNumber || 0;\n    const limit = req.body.pageable.pageSize || 5;\n    const sort = req.body.pageable.sort || null;\n\n    try {\n        const response = await AuthorService.getAuthorsPageable(page, limit, sort);\n        res.status(200).json({\n            content: response.docs,\n            pageable: {\n                pageNumber: response.page - 1,\n                pageSize: response.limit\n            },\n            totalElements: response.totalDocs\n        });\n    } catch (err) {\n        res.status(400).json({\n            msg: err.toString()\n        });\n    }\n}\n

    Y vemos que el m\u00e9todo getAuthorsPageable lee los datos de la request, se los pasa al servicio y por \u00faltimo transforma la response con los datos obtenidos.

    "},{"location":"develop/paginated/nodejs/#implementar-las-rutas","title":"Implementar las Rutas","text":"

    Creamos nuestro archivo de rutas author.routes.js:

    author.routes.js
    import { Router } from 'express';\nimport { check } from 'express-validator';\nimport validateFields from '../middlewares/validateFields.js';\nimport { createAuthor, deleteAuthor, getAuthors, updateAuthor, getAuthorsPageable } from '../controllers/author.controller.js';\nconst authorRouter = Router();\n\nauthorRouter.put('/:id', [\n    check('name').not().isEmpty(),\n    check('nationality').not().isEmpty(),\n    validateFields\n], updateAuthor);\n\nauthorRouter.put('/', [\n    check('name').not().isEmpty(),\n    check('nationality').not().isEmpty(),\n    validateFields\n], createAuthor);\n\nauthorRouter.get('/', getAuthors);\nauthorRouter.delete('/:id', deleteAuthor);\n\nauthorRouter.post('/', [\n    check('pageable').not().isEmpty(),\n    check('pageable.pageSize').not().isEmpty(),\n    check('pageable.pageNumber').not().isEmpty(),\n    validateFields\n], getAuthorsPageable)\n\nexport default authorRouter;\n

    Podemos observar que si hacemos una petici\u00f3n con get a /author nos devolver\u00e1 todos los autores. Pero si hacemos una petici\u00f3n post con el objeto pageable en el body realizaremos el listado paginado.

    Finalmente en nuestro archivo index.js vamos a a\u00f1adir el nuevo router:

    index.js
    ...\n\nimport authorRouter from './src/routes/author.routes.js';\n\n...\n\napp.use('/author', authorRouter);\n\n...\n
    "},{"location":"develop/paginated/nodejs/#probar-las-operaciones","title":"Probar las operaciones","text":"

    Y ahora que tenemos todo creado, ya podemos probarlo con Postman:

    Por un lado creamos autores con:

    ** PUT /author **

    ** PUT /author/{id} **

    {\n    \"name\" : \"Nuevo autor\",\n    \"nationality\" : \"Nueva nacionalidad\"\n}\n

    Nos sirve para insertar Autores nuevas (si no tienen el id informado) o para actualizar Autores (si tienen el id informado en la URL). F\u00edjate que los datos que se env\u00edan est\u00e1n en el body como formato JSON (parte izquierda de la imagen). Si no te dar\u00e1 un error.

    ** DELETE /author/{id} ** nos sirve eliminar Autores. F\u00edjate que el dato del ID que se env\u00eda est\u00e1 en el path.

    Luego recuperamos los autores con el m\u00e9todo GET (antes tienes que crear unos cuantos para poder ver un listado):

    Y por \u00faltimo listamos los autores paginados:

    ** POST /author **

    {\n    \"pageable\": {\n        \"pageSize\" : 4,\n        \"pageNumber\" : 0,\n        \"sort\" : [\n            {\n                \"property\": \"name\",\n                \"direction\": \"asc\"\n            }\n        ]\n    }\n}\n

    Importante: direction tiene que ir en min\u00fasculas

    "},{"location":"develop/paginated/react/","title":"Listado paginado - React","text":"

    Ya tienes tu primer CRUD desarrollado. \u00bfHa sido sencillo, verdad?.

    Ahora vamos a implementar un CRUD un poco m\u00e1s complejo, este tiene datos paginados en servidor, esto quiere decir que no nos sirve un array de datos como en el anterior ejemplo. Para que un listado paginado en servidor funcione, el cliente debe enviar en cada petici\u00f3n que p\u00e1gina est\u00e1 solicitando y cual es el tama\u00f1o de la p\u00e1gina, para que el servidor devuelva solamente un subconjunto de datos, en lugar de devolver el listado completo.

    Como ya conocemos como se debe desarrollar, en este ejemplo vamos a ir m\u00e1s r\u00e1pidos y nos vamos a centrar \u00fanicamente en las novedades.

    "},{"location":"develop/paginated/react/#crear-componente-author","title":"Crear componente author","text":"

    Lo primero que vamos a hacer es crear una carpeta llamada types dentro de src. Aqu\u00ed crearemos los tipos de typescript. Creamos un nuevo fichero llamado Author.ts cuyo contenido ser\u00e1 el siguiente:

    Author.ts
    export interface Author {\n    id: string,\n    name: string,\n    nationality: string\n}\n\nexport interface AuthorResponse {\n    content: Author[];\n    totalElements: number;\n}\n

    Ahora vamos a crear un archivo de estilos que ser\u00e1 solo utilizado por la p\u00e1gina de author. Para ello dentro de la carpeta Author creamos un archivo llamado Author.module.css. Al llamar al archivo de esta manera React reconoce este archivo como un archivo \u00fanico para un componente y hace que sus reglas css sean m\u00e1s prioritarias, aunque por ejemplo exista una clase con el mismo nombre en el archivo index.css.

    El contenido de nuestro archivo css ser\u00e1 el siguiente:

    index.css
    .tableActions {\n    margin-right: 20px;\n    display: flex;\n    justify-content: flex-end;\n    align-content: flex-start;\n    gap: 19px;\n}\n

    Al igual que hicimos con categor\u00edas vamos a crear un nuevo componente para el formulario de alta y edici\u00f3n, para ello creamos una nueva carpeta llamada components en src/pages/Author y dentro de esta carpeta crearemos un fichero llamado CreateAuthor.tsx:

    CreateAuthor.tsx
    import { ChangeEvent, useEffect, useState } from \"react\";\nimport Button from \"@mui/material/Button\";\nimport TextField from \"@mui/material/TextField\";\nimport Dialog from \"@mui/material/Dialog\";\nimport DialogActions from \"@mui/material/DialogActions\";\nimport DialogContent from \"@mui/material/DialogContent\";\nimport DialogTitle from \"@mui/material/DialogTitle\";\nimport { Author } from \"../../../types/Author\";\n\ninterface Props {\n  author: Author | null;\n  closeModal: () => void;\n  create: (author: Author) => void;\n}\n\nconst initialState = {\n  name: \"\",\n  nationality: \"\",\n};\n\nexport default function CreateAuthor(props: Props) {\n  const [form, setForm] = useState(initialState);\n\n  useEffect(() => {\n    setForm(props?.author || initialState);\n  }, [props?.author]);\n\n  const handleChangeForm = (\n    event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n  ) => {\n    setForm({\n      ...form,\n      [event.target.id]: event.target.value,\n    });\n  };\n\n  return (\n    <div>\n      <Dialog open={true} onClose={props.closeModal}>\n        <DialogTitle>\n          {props.author ? \"Actualizar Autor\" : \"Crear Autor\"}\n        </DialogTitle>\n        <DialogContent>\n          {props.author && (\n            <TextField\n              margin=\"dense\"\n              disabled\n              id=\"id\"\n              label=\"Id\"\n              fullWidth\n              value={props.author.id}\n              variant=\"standard\"\n            />\n          )}\n          <TextField\n            margin=\"dense\"\n            id=\"name\"\n            label=\"Nombre\"\n            fullWidth\n            variant=\"standard\"\n            onChange={handleChangeForm}\n            value={form.name}\n          />\n          <TextField\n            margin=\"dense\"\n            id=\"nationality\"\n            label=\"Nacionalidad\"\n            fullWidth\n            variant=\"standard\"\n            onChange={handleChangeForm}\n            value={form.nationality}\n          />\n        </DialogContent>\n        <DialogActions>\n          <Button onClick={props.closeModal}>Cancelar</Button>\n          <Button\n            onClick={() =>\n              props.create({\n                id: props.author ? props.author.id : \"\",\n                name: form.name,\n                nationality: form.nationality,\n              })\n            }\n            disabled={!form.name || !form.nationality}\n          >\n            {props.author ? \"Actualizar\" : \"Crear\"}\n          </Button>\n        </DialogActions>\n      </Dialog>\n    </div>\n  );\n}\n

    Como los autores tienen m\u00e1s campos hemos a\u00f1adido un poco de funcionalidad extra que no ten\u00edamos en el formulario de categor\u00edas, pero no es demasiado complicada.

    Vamos a a\u00f1adir los m\u00e9todos necesarios para el crud de autores en el fichero src/redux/services/ludotecaApi.ts:

    ludotecaApi.ts
        getAllAuthors: builder.query<Author[], null>({\n  query: () => \"author\",\n  providesTags: [\"Author\" ],\n}),\ngetAuthors: builder.query<\n  AuthorResponse,\n  { pageNumber: number; pageSize: number }\n>({\n  query: ({ pageNumber, pageSize }) => {\n    return {\n      url: \"author\",\n      method: \"POST\",\n      body: {\n        pageable: {\n          pageNumber,\n          pageSize,\n        },\n      },\n    };\n  },\n  providesTags: [\"Author\"],\n}),\ncreateAuthor: builder.mutation({\n  query: (payload) => ({\n    url: \"/author\",\n    method: \"PUT\",\n    body: payload,\n    headers: {\n      \"Content-type\": \"application/json; charset=UTF-8\",\n    },\n  }),\n  invalidatesTags: [\"Author\"],\n}),\ndeleteAuthor: builder.mutation({\n  query: (id: string) => ({\n    url: `/author/${id}`,\n    method: \"DELETE\",\n  }),\n  invalidatesTags: [\"Author\"],\n}),\nupdateAuthor: builder.mutation({\n  query: (payload: Author) => ({\n    url: `author/${payload.id}`,\n    method: \"PUT\",\n    body: payload,\n  }),\n  invalidatesTags: [\"Author\", \"Game\"],\n}),\n

    A\u00f1adimos tambi\u00e9n los imports, tags y exports necesarios y guardamos.

    import { Author, AuthorResponse } from \"../../types/Author\";\n\ntagTypes: [\"Category\", \"Author\", \"Game\"],\n\nexport const {\n  useGetCategoriesQuery,\n  useCreateCategoryMutation,\n  useDeleteCategoryMutation,\n  useUpdateCategoryMutation,\n  useCreateAuthorMutation,\n  useDeleteAuthorMutation,\n  useGetAllAuthorsQuery,\n  useGetAuthorsQuery,\n  useUpdateAuthorMutation,\n} = ludotecaAPI;\n

    Y por \u00faltimo el contenido de nuestro fichero Author.tsx quedar\u00eda as\u00ed:

    Author.tsx
    import { useEffect, useState, useContext } from \"react\";\nimport Button from \"@mui/material/Button\";\nimport TableHead from \"@mui/material/TableHead\";\nimport Table from \"@mui/material/Table\";\nimport TableBody from \"@mui/material/TableBody\";\nimport TableCell from \"@mui/material/TableCell\";\nimport TableContainer from \"@mui/material/TableContainer\";\nimport TableFooter from \"@mui/material/TableFooter\";\nimport TablePagination from \"@mui/material/TablePagination\";\nimport TableRow from \"@mui/material/TableRow\";\nimport Paper from \"@mui/material/Paper\";\nimport IconButton from \"@mui/material/IconButton\";\nimport EditIcon from \"@mui/icons-material/Edit\";\nimport ClearIcon from \"@mui/icons-material/Clear\";\nimport styles from \"./Author.module.css\";\nimport CreateAuthor from \"./components/CreateAuthor\";\nimport { ConfirmDialog } from \"../../components/ConfirmDialog\";\nimport { useAppDispatch } from \"../../redux/hooks\";\nimport { setMessage } from \"../../redux/features/messageSlice\";\nimport { BackError } from \"../../types/appTypes\";\nimport { Author as AuthorModel } from \"../../types/Author\";\nimport {\n  useDeleteAuthorMutation,\n  useGetAuthorsQuery,\n  useCreateAuthorMutation,\n  useUpdateAuthorMutation,\n} from \"../../redux/services/ludotecaApi\";\nimport { LoaderContext } from \"../../context/LoaderProvider\";\n\nexport const Author = () => {\n  const [pageNumber, setPageNumber] = useState(0);\n  const [pageSize, setPageSize] = useState(5);\n  const [total, setTotal] = useState(0);\n  const [authors, setAuthors] = useState<AuthorModel[]>([]);\n  const [openCreate, setOpenCreate] = useState(false);\n  const [idToDelete, setIdToDelete] = useState(\"\");\n  const [authorToUpdate, setAuthorToUpdate] = useState<AuthorModel | null>(\n    null\n  );\n\n  const dispatch = useAppDispatch();\n  const loader = useContext(LoaderContext);\n\n  const handleChangePage = (\n    _event: React.MouseEvent<HTMLButtonElement> | null,\n    newPage: number\n  ) => {\n    setPageNumber(newPage);\n  };\n\n  const handleChangeRowsPerPage = (\n    event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n  ) => {\n    setPageNumber(0);\n    setPageSize(parseInt(event.target.value, 10));\n  };\n\n  const { data, error, isLoading } = useGetAuthorsQuery({\n    pageNumber,\n    pageSize,\n  });\n\n  const [deleteAuthorApi, { isLoading: isLoadingDelete, error: errorDelete }] =\n    useDeleteAuthorMutation();\n\n  const [createAuthorApi, { isLoading: isLoadingCreate }] =\n    useCreateAuthorMutation();\n\n  const [updateAuthorApi, { isLoading: isLoadingUpdate }] =\n    useUpdateAuthorMutation();\n\n  useEffect(() => {\n    loader.showLoading(\n      isLoadingCreate || isLoading || isLoadingDelete || isLoadingUpdate\n    );\n  }, [isLoadingCreate, isLoading, isLoadingDelete, isLoadingUpdate]);\n\n  useEffect(() => {\n    if (data) {\n      setAuthors(data.content);\n      setTotal(data.totalElements);\n    }\n  }, [data]);\n\n  useEffect(() => {\n    if (errorDelete) {\n      if (\"status\" in errorDelete) {\n        dispatch(\n          setMessage({\n            text: (errorDelete?.data as BackError).msg,\n            type: \"error\",\n          })\n        );\n      }\n    }\n  }, [errorDelete, dispatch]);\n\n  useEffect(() => {\n    if (error) {\n      dispatch(setMessage({ text: \"Se ha producido un error\", type: \"error\" }));\n    }\n  }, [error]);\n\n  const createAuthor = (author: AuthorModel) => {\n    setOpenCreate(false);\n    if (author.id) {\n      updateAuthorApi(author)\n        .then(() => {\n          dispatch(\n            setMessage({\n              text: \"Autor actualizado correctamente\",\n              type: \"ok\",\n            })\n          );\n          setAuthorToUpdate(null);\n        })\n        .catch((err) => console.log(err));\n    } else {\n      createAuthorApi(author)\n        .then(() => {\n          dispatch(\n            setMessage({ text: \"Autor creado correctamente\", type: \"ok\" })\n          );\n          setAuthorToUpdate(null);\n        })\n        .catch((err) => console.log(err));\n    }\n  };\n\n  const deleteAuthor = () => {\n    deleteAuthorApi(idToDelete)\n      .then(() => {\n        setIdToDelete(\"\");\n      })\n      .catch((err) => console.log(err));\n  };\n\n  return (\n    <div className=\"container\">\n      <h1>Listado de Autores</h1>\n      <TableContainer component={Paper}>\n        <Table sx={{ minWidth: 500 }} aria-label=\"custom pagination table\">\n          <TableHead\n            sx={{\n              \"& th\": {\n                backgroundColor: \"lightgrey\",\n              },\n            }}\n          >\n            <TableRow>\n              <TableCell>Identificador</TableCell>\n              <TableCell>Nombre Autor</TableCell>\n              <TableCell>Nacionalidad</TableCell>\n              <TableCell align=\"right\"></TableCell>\n            </TableRow>\n          </TableHead>\n          <TableBody>\n            {authors.map((author: AuthorModel) => (\n              <TableRow key={author.id}>\n                <TableCell component=\"th\" scope=\"row\">\n                  {author.id}\n                </TableCell>\n                <TableCell style={{ width: 160 }}>{author.name}</TableCell>\n                <TableCell style={{ width: 160 }}>\n                  {author.nationality}\n                </TableCell>\n                <TableCell align=\"right\">\n                  <div className={styles.tableActions}>\n                    <IconButton\n                      aria-label=\"update\"\n                      color=\"primary\"\n                      onClick={() => {\n                        setAuthorToUpdate(author);\n                        setOpenCreate(true);\n                      }}\n                    >\n                      <EditIcon />\n                    </IconButton>\n                    <IconButton\n                      aria-label=\"delete\"\n                      color=\"error\"\n                      onClick={() => {\n                        setIdToDelete(author.id);\n                      }}\n                    >\n                      <ClearIcon />\n                    </IconButton>\n                  </div>\n                </TableCell>\n              </TableRow>\n            ))}\n          </TableBody>\n          <TableFooter>\n            <TableRow>\n              <TablePagination\n                rowsPerPageOptions={[5, 10, 25]}\n                colSpan={4}\n                count={total}\n                rowsPerPage={pageSize}\n                page={pageNumber}\n                SelectProps={{\n                  inputProps: {\n                    \"aria-label\": \"rows per page\",\n                  },\n                  native: true,\n                }}\n                onPageChange={handleChangePage}\n                onRowsPerPageChange={handleChangeRowsPerPage}\n              />\n            </TableRow>\n          </TableFooter>\n        </Table>\n      </TableContainer>\n      <div className=\"newButton\">\n        <Button variant=\"contained\" onClick={() => setOpenCreate(true)}>\n          Nuevo autor\n        </Button>\n      </div>\n      {openCreate && (\n        <CreateAuthor\n          create={createAuthor}\n          author={authorToUpdate}\n          closeModal={() => {\n            setAuthorToUpdate(null);\n            setOpenCreate(false);\n          }}\n        />\n      )}\n      {!!idToDelete && (\n        <ConfirmDialog\n          title=\"Eliminar Autor\"\n          text=\"Atenci\u00f3n si borra el autor se perder\u00e1n sus datos. \u00bfDesea eliminar el autor?\"\n          confirm={deleteAuthor}\n          closeModal={() => setIdToDelete(\"\")}\n        />\n      )}\n    </div>\n  );\n};\n

    Al tratarse de un listado paginado hemos creado dos nuevas variables en nuestro estado para almacenar la p\u00e1gina y el n\u00famero de registros a mostrar en la p\u00e1gina. Cuando cambiamos estos valores en el navegador como estas variables van como par\u00e1metro en nuestro hook para recuperar datos autom\u00e1ticamente el listado se va a modificar.

    El resto de funcionalidad es muy parecida a la de categor\u00edas.

    "},{"location":"develop/paginated/springboot/","title":"Listado paginado - Spring Boot","text":"

    Ahora vamos a implementar las operaciones necesarias para ayudar al front a cubrir la funcionalidad del CRUD paginado en servidor. Recuerda que para que un listado paginado en servidor funcione, el cliente debe enviar en cada petici\u00f3n que p\u00e1gina est\u00e1 solicitando y cu\u00e1l es el tama\u00f1o de la p\u00e1gina, para que el servidor devuelva solamente un subconjunto de datos, en lugar de devolver el listado completo.

    Como ya conocemos como se debe desarrollar, en este ejemplo vamos a ir m\u00e1s r\u00e1pidos y nos vamos a centrar \u00fanicamente en las novedades.

    "},{"location":"develop/paginated/springboot/#crear-modelos","title":"Crear modelos","text":"

    Lo primero que vamos a hacer es crear los modelos para trabajar con BBDD y con peticiones hacia el front. Adem\u00e1s, tambi\u00e9n tenemos que a\u00f1adir datos al script de inicializaci\u00f3n de BBDD, siempre respetando la nomenclatura que le hemos dado a la tabla y columnas de BBDD.

    Author.javaAuthorDto.javadata.sql
    package com.ccsw.tutorial.author.model;\n\nimport jakarta.persistence.*;\n\n/**\n * @author ccsw\n *\n */\n@Entity\n@Table(name = \"author\")\npublic class Author {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"id\", nullable = false)\n    private Long id;\n\n    @Column(name = \"name\", nullable = false)\n    private String name;\n\n    @Column(name = \"nationality\")\n    private String nationality;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return name\n     */\n    public String getName() {\n\n        return this.name;\n    }\n\n    /**\n     * @param name new value of {@link #getName}.\n     */\n    public void setName(String name) {\n\n        this.name = name;\n    }\n\n    /**\n     * @return nationality\n     */\n    public String getNationality() {\n\n        return this.nationality;\n    }\n\n    /**\n     * @param nationality new value of {@link #getNationality}.\n     */\n    public void setNationality(String nationality) {\n\n        this.nationality = nationality;\n    }\n\n}\n
    package com.ccsw.tutorial.author.model;\n\n/**\n * @author ccsw\n *\n */\npublic class AuthorDto {\n\n    private Long id;\n\n    private String name;\n\n    private String nationality;\n\n    /**\n     * @return id\n     */\n    public Long getId() {\n\n        return this.id;\n    }\n\n    /**\n     * @param id new value of {@link #getId}.\n     */\n    public void setId(Long id) {\n\n        this.id = id;\n    }\n\n    /**\n     * @return name\n     */\n    public String getName() {\n\n        return this.name;\n    }\n\n    /**\n     * @param name new value of {@link #getName}.\n     */\n    public void setName(String name) {\n\n        this.name = name;\n    }\n\n    /**\n     * @return nationality\n     */\n    public String getNationality() {\n\n        return this.nationality;\n    }\n\n    /**\n     * @param nationality new value of {@link #getNationality}.\n     */\n    public void setNationality(String nationality) {\n\n        this.nationality = nationality;\n    }\n\n}\n
    INSERT INTO category(name) VALUES ('Eurogames');\nINSERT INTO category(name) VALUES ('Ameritrash');\nINSERT INTO category(name) VALUES ('Familiar');\n\nINSERT INTO author(name, nationality) VALUES ('Alan R. Moon', 'US');\nINSERT INTO author(name, nationality) VALUES ('Vital Lacerda', 'PT');\nINSERT INTO author(name, nationality) VALUES ('Simone Luciani', 'IT');\nINSERT INTO author(name, nationality) VALUES ('Perepau Llistosella', 'ES');\nINSERT INTO author(name, nationality) VALUES ('Michael Kiesling', 'DE');\nINSERT INTO author(name, nationality) VALUES ('Phil Walker-Harding', 'US');\n
    "},{"location":"develop/paginated/springboot/#implementar-tdd-pruebas","title":"Implementar TDD - Pruebas","text":"

    Para desarrollar todas las operaciones, empezaremos primero dise\u00f1ando las pruebas y luego implementando el c\u00f3digo necesario que haga funcionar correctamente esas pruebas. Para ir m\u00e1s r\u00e1pido vamos a poner todas las pruebas de golpe, pero realmente se deber\u00edan crear una a una e ir implementando el c\u00f3digo necesario para esa prueba. Para evitar tantas iteraciones en el tutorial las haremos todas de golpe.

    Vamos a pararnos a pensar un poco que necesitamos en la pantalla. Ahora mismo nos sirve con:

    • Una consulta paginada, que reciba datos de la p\u00e1gina a consultar y devuelva los datos paginados
    • Una operaci\u00f3n de guardado y modificaci\u00f3n
    • Una operaci\u00f3n de borrado

    Para la primera prueba que hemos descrito (consulta paginada) se necesita un objeto que contenga los datos de la p\u00e1gina a consultar. As\u00ed que crearemos una clase AuthorSearchDto para utilizarlo como 'paginador'.

    Para ello, en primer lugar, deberemos a\u00f1adir una clase que vamos a utilizar como envoltorio para las peticiones de paginaci\u00f3n en el proyecto. Hacemos esto para desacoplar la interface de Spring Boot de nuestro contrato de entrada. Crearemos esta clase en el paquete com.ccsw.tutorial.common.pagination.

    PageableRequest.java
    package com.ccsw.tutorial.common.pagination;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport org.springframework.data.domain.*;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\npublic class PageableRequest implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private int pageNumber;\n\n    private int pageSize;\n\n    private List<SortRequest> sort;\n\n    public PageableRequest() {\n\n        sort = new ArrayList<>();\n    }\n\n    public PageableRequest(int pageNumber, int pageSize) {\n\n        this();\n        this.pageNumber = pageNumber;\n        this.pageSize = pageSize;\n    }\n\n    public PageableRequest(int pageNumber, int pageSize, List<SortRequest> sort) {\n\n        this();\n        this.pageNumber = pageNumber;\n        this.pageSize = pageSize;\n        this.sort = sort;\n    }\n\n    public int getPageNumber() {\n        return pageNumber;\n    }\n\n    public void setPageNumber(int pageNumber) {\n        this.pageNumber = pageNumber;\n    }\n\n    public int getPageSize() {\n        return pageSize;\n    }\n\n    public void setPageSize(int pageSize) {\n        this.pageSize = pageSize;\n    }\n\n    public List<SortRequest> getSort() {\n        return sort;\n    }\n\n    public void setSort(List<SortRequest> sort) {\n        this.sort = sort;\n    }\n\n    @JsonIgnore\n    public Pageable getPageable() {\n\n        return PageRequest.of(this.pageNumber, this.pageSize, Sort.by(sort.stream().map(e -> new Sort.Order(e.getDirection(), e.getProperty())).collect(Collectors.toList())));\n    }\n\n    public static class SortRequest implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        private String property;\n\n        private Sort.Direction direction;\n\n        protected String getProperty() {\n            return property;\n        }\n\n        protected void setProperty(String property) {\n            this.property = property;\n        }\n\n        protected Sort.Direction getDirection() {\n            return direction;\n        }\n\n        protected void setDirection(Sort.Direction direction) {\n            this.direction = direction;\n        }\n    }\n\n}\n

    Adicionalmente necesitaremos una clase para deserializar las respuestas de Page recibidas en los test que vamos a implementar. Para ello creamos la clase necesaria dentro de la fuente de la carpeta de los test en el paquete com.ccsw.tutorial.config. Esto solo hace falta porque necesitamos leer la respuesta paginada en el test, si no hicieramos test, no har\u00eda falta.

    ResponsePage.java
    package com.ccsw.tutorial.config;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport org.springframework.data.domain.PageImpl;\nimport org.springframework.data.domain.PageRequest;\nimport org.springframework.data.domain.Pageable;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@JsonIgnoreProperties(ignoreUnknown = true)\npublic class ResponsePage<T> extends PageImpl<T> {\n\n    private static final long serialVersionUID = 1L;\n\n    @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)\n    public ResponsePage(@JsonProperty(\"content\") List<T> content,\n                        @JsonProperty(\"number\") int number,\n                        @JsonProperty(\"size\") int size,\n                        @JsonProperty(\"totalElements\") Long totalElements,\n                        @JsonProperty(\"pageable\") JsonNode pageable,\n                        @JsonProperty(\"last\") boolean last,\n                        @JsonProperty(\"totalPages\") int totalPages,\n                        @JsonProperty(\"sort\") JsonNode sort,\n                        @JsonProperty(\"first\") boolean first,\n                        @JsonProperty(\"numberOfElements\") int numberOfElements) {\n\n        super(content, PageRequest.of(number, size), totalElements);\n    }\n\n    public ResponsePage(List<T> content, Pageable pageable, long total) {\n        super(content, pageable, total);\n    }\n\n    public ResponsePage(List<T> content) {\n        super(content);\n    }\n\n    public ResponsePage() {\n        super(new ArrayList<>());\n    }\n\n}\n

    Paginaci\u00f3n en Springframework

    Cuando utilicemos paginaci\u00f3n en Springframework, debemos recordar que ya vienen implementados algunos objetos que podemos utilizar y que nos facilitan la vida. Es el caso de Pageable y Page.

    • El objeto Pageable no es m\u00e1s que una interface que le permite a Spring JPA saber que p\u00e1gina se quiere buscar, cual es el tama\u00f1o de p\u00e1gina y cuales son las propiedades de ordenaci\u00f3n que se debe lanzar en la consulta.
    • El objeto PageRequest es una utilidad que permite crear objetos de tipo Pageable de forma sencilla. Se utiliza mucho para codificaci\u00f3n de test.
    • El objeto Page no es m\u00e1s que un contenedor que engloba la informaci\u00f3n b\u00e1sica de la p\u00e1gina que se est\u00e1 consultando (n\u00famero de p\u00e1gina, tama\u00f1o de p\u00e1gina, n\u00famero total de resultados) y el conjunto de datos de la BBDD que contiene esa p\u00e1gina una vez han sido buscados y ordenados.

    Tambi\u00e9n crearemos una clase AuthorController dentro del package de com.ccsw.tutorial.author con la implementaci\u00f3n de los m\u00e9todos vac\u00edos, para que no falle la compilaci\u00f3n.

    \u00a1Vamos a implementar test!

    AuthorSearchDto.javaAuthorController.javaAuthorIT.java
    package com.ccsw.tutorial.author.model;\n\nimport com.ccsw.tutorial.common.pagination.PageableRequest;\n\n/**\n * @author ccsw\n *\n */\npublic class AuthorSearchDto {\n\n    private PageableRequest pageable;\n\n    public PageableRequest getPageable() {\n        return pageable;\n    }\n\n    public void setPageable(PageableRequest pageable) {\n        this.pageable = pageable;\n    }\n}\n
    package com.ccsw.tutorial.author;\n\nimport com.ccsw.tutorial.author.model.Author;\nimport com.ccsw.tutorial.author.model.AuthorDto;\nimport com.ccsw.tutorial.author.model.AuthorSearchDto;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.springframework.data.domain.Page;\nimport org.springframework.web.bind.annotation.*;\n\n/**\n * @author ccsw\n *\n */\n@Tag(name = \"Author\", description = \"API of Author\")\n@RequestMapping(value = \"/author\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class AuthorController {\n\n    /**\n     * M\u00e9todo para recuperar un listado paginado de {@link Author}\n     *\n     * @param dto dto de b\u00fasqueda\n     * @return {@link Page} de {@link AuthorDto}\n     */\n    @Operation(summary = \"Find Page\", description = \"Method that return a page of Authors\")\n    @RequestMapping(path = \"\", method = RequestMethod.POST)\n    public Page<AuthorDto> findPage(@RequestBody AuthorSearchDto dto) {\n\n        return null;\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Author\")\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody AuthorDto dto) {\n\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     */\n    @Operation(summary = \"Delete\", description = \"Method that deletes a Author\")\n    @RequestMapping(path = \"/{id}\", method = RequestMethod.DELETE)\n    public void delete(@PathVariable(\"id\") Long id) throws Exception {\n\n    }\n\n}\n
    package com.ccsw.tutorial.author;\n\nimport com.ccsw.tutorial.author.model.AuthorDto;\nimport com.ccsw.tutorial.author.model.AuthorSearchDto;\nimport com.ccsw.tutorial.common.pagination.PageableRequest;\nimport com.ccsw.tutorial.config.ResponsePage;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.test.web.client.TestRestTemplate;\nimport org.springframework.boot.test.web.server.LocalServerPort;\nimport org.springframework.core.ParameterizedTypeReference;\nimport org.springframework.http.*;\nimport org.springframework.test.annotation.DirtiesContext;\n\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)\n@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)\npublic class AuthorIT {\n\n    public static final String LOCALHOST = \"http://localhost:\";\n    public static final String SERVICE_PATH = \"/author\";\n\n    public static final Long DELETE_AUTHOR_ID = 6L;\n    public static final Long MODIFY_AUTHOR_ID = 3L;\n    public static final String NEW_AUTHOR_NAME = \"Nuevo Autor\";\n    public static final String NEW_NATIONALITY = \"Nueva Nacionalidad\";\n\n    private static final int TOTAL_AUTHORS = 6;\n    private static final int PAGE_SIZE = 5;\n\n    @LocalServerPort\n    private int port;\n\n    @Autowired\n    private TestRestTemplate restTemplate;\n\n    ParameterizedTypeReference<ResponsePage<AuthorDto>> responseTypePage = new ParameterizedTypeReference<ResponsePage<AuthorDto>>(){};\n\n    @Test\n    public void findFirstPageWithFiveSizeShouldReturnFirstFiveResults() {\n\n          AuthorSearchDto searchDto = new AuthorSearchDto();\n          searchDto.setPageable(new PageableRequest(0, PAGE_SIZE));\n\n          ResponseEntity<ResponsePage<AuthorDto>> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.POST, new HttpEntity<>(searchDto), responseTypePage);\n\n          assertNotNull(response);\n          assertEquals(TOTAL_AUTHORS, response.getBody().getTotalElements());\n          assertEquals(PAGE_SIZE, response.getBody().getContent().size());\n    }\n\n    @Test\n    public void findSecondPageWithFiveSizeShouldReturnLastResult() {\n\n          int elementsCount = TOTAL_AUTHORS - PAGE_SIZE;\n\n          AuthorSearchDto searchDto = new AuthorSearchDto();\n          searchDto.setPageable(new PageableRequest(1, PAGE_SIZE));\n\n          ResponseEntity<ResponsePage<AuthorDto>> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.POST, new HttpEntity<>(searchDto), responseTypePage);\n\n          assertNotNull(response);\n          assertEquals(TOTAL_AUTHORS, response.getBody().getTotalElements());\n          assertEquals(elementsCount, response.getBody().getContent().size());\n    }\n\n    @Test\n    public void saveWithoutIdShouldCreateNewAuthor() {\n\n          long newAuthorId = TOTAL_AUTHORS + 1;\n          long newAuthorSize = TOTAL_AUTHORS + 1;\n\n          AuthorDto dto = new AuthorDto();\n          dto.setName(NEW_AUTHOR_NAME);\n          dto.setNationality(NEW_NATIONALITY);\n\n          restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.PUT, new HttpEntity<>(dto), Void.class);\n\n          AuthorSearchDto searchDto = new AuthorSearchDto();\n          searchDto.setPageable(new PageableRequest(0, (int) newAuthorSize));\n\n          ResponseEntity<ResponsePage<AuthorDto>> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.POST, new HttpEntity<>(searchDto), responseTypePage);\n\n          assertNotNull(response);\n          assertEquals(newAuthorSize, response.getBody().getTotalElements());\n\n          AuthorDto author = response.getBody().getContent().stream().filter(item -> item.getId().equals(newAuthorId)).findFirst().orElse(null);\n          assertNotNull(author);\n          assertEquals(NEW_AUTHOR_NAME, author.getName());\n    }\n\n    @Test\n    public void modifyWithExistIdShouldModifyAuthor() {\n\n          AuthorDto dto = new AuthorDto();\n          dto.setName(NEW_AUTHOR_NAME);\n          dto.setNationality(NEW_NATIONALITY);\n\n          restTemplate.exchange(LOCALHOST + port + SERVICE_PATH + \"/\" + MODIFY_AUTHOR_ID, HttpMethod.PUT, new HttpEntity<>(dto), Void.class);\n\n          AuthorSearchDto searchDto = new AuthorSearchDto();\n          searchDto.setPageable(new PageableRequest(0, PAGE_SIZE));\n\n          ResponseEntity<ResponsePage<AuthorDto>> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.POST, new HttpEntity<>(searchDto), responseTypePage);\n\n          assertNotNull(response);\n          assertEquals(TOTAL_AUTHORS, response.getBody().getTotalElements());\n\n          AuthorDto author = response.getBody().getContent().stream().filter(item -> item.getId().equals(MODIFY_AUTHOR_ID)).findFirst().orElse(null);\n          assertNotNull(author);\n          assertEquals(NEW_AUTHOR_NAME, author.getName());\n          assertEquals(NEW_NATIONALITY, author.getNationality());\n    }\n\n    @Test\n    public void modifyWithNotExistIdShouldThrowException() {\n\n          long authorId = TOTAL_AUTHORS + 1;\n\n          AuthorDto dto = new AuthorDto();\n          dto.setName(NEW_AUTHOR_NAME);\n\n          ResponseEntity<?> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH + \"/\" + authorId, HttpMethod.PUT, new HttpEntity<>(dto), Void.class);\n\n          assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());\n    }\n\n    @Test\n    public void deleteWithExistsIdShouldDeleteCategory() {\n\n          long newAuthorsSize = TOTAL_AUTHORS - 1;\n\n          restTemplate.exchange(LOCALHOST + port + SERVICE_PATH + \"/\" + DELETE_AUTHOR_ID, HttpMethod.DELETE, null, Void.class);\n\n          AuthorSearchDto searchDto = new AuthorSearchDto();\n          searchDto.setPageable(new PageableRequest(0, TOTAL_AUTHORS));\n\n          ResponseEntity<ResponsePage<AuthorDto>> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH, HttpMethod.POST, new HttpEntity<>(searchDto), responseTypePage);\n\n          assertNotNull(response);\n          assertEquals(newAuthorsSize, response.getBody().getTotalElements());\n    }\n\n    @Test\n    public void deleteWithNotExistsIdShouldThrowException() {\n\n          long deleteAuthorId = TOTAL_AUTHORS + 1;\n\n          ResponseEntity<?> response = restTemplate.exchange(LOCALHOST + port + SERVICE_PATH + \"/\" + deleteAuthorId, HttpMethod.DELETE, null, Void.class);\n\n          assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());\n    }\n\n}\n

    Cuidado con las clases de Test

    Recuerda que el c\u00f3digo de aplicaci\u00f3n debe ir en src/main/java, mientras que las clases de test deben ir en src/test/java para que no se mezclen unas con otras y se empaquete todo en el artefacto final. En este caso AuthorIT.java va en el directorio de test src/test/java.

    Si ejecutamos los test, el resultado ser\u00e1 7 maravillosos test que fallan su ejecuci\u00f3n. Es normal, puesto que no hemos implementado nada de c\u00f3digo de aplicaci\u00f3n para corresponder esos test.

    "},{"location":"develop/paginated/springboot/#implementar-controller","title":"Implementar Controller","text":"

    Si recuerdas, esta capa de Controller es la que tiene los endpoints de entrada a la aplicaci\u00f3n. Nosotros ya tenemos definidas 3 operaciones, que hemos dise\u00f1ado directamente desde los tests. Ahora vamos a implementar esos m\u00e9todos con el c\u00f3digo necesario para que los test funcionen correctamente, y teniendo en mente que debemos apoyarnos en las capas inferiores Service y Repository para repartir l\u00f3gica de negocio y acceso a datos.

    AuthorController.javaAuthorService.java
    package com.ccsw.tutorial.author;\n\nimport com.ccsw.tutorial.author.model.Author;\nimport com.ccsw.tutorial.author.model.AuthorDto;\nimport com.ccsw.tutorial.author.model.AuthorSearchDto;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.modelmapper.ModelMapper;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.domain.Page;\nimport org.springframework.data.domain.PageImpl;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * @author ccsw\n *\n */\n@Tag(name = \"Author\", description = \"API of Author\")\n@RequestMapping(value = \"/author\")\n@RestController\n@CrossOrigin(origins = \"*\")\npublic class AuthorController {\n\n    @Autowired\n    AuthorService authorService;\n\n    @Autowired\n    ModelMapper mapper;\n\n    /**\n     * M\u00e9todo para recuperar un listado paginado de {@link Author}\n     *\n     * @param dto dto de b\u00fasqueda\n     * @return {@link Page} de {@link AuthorDto}\n     */\n    @Operation(summary = \"Find Page\", description = \"Method that return a page of Authors\")\n    @RequestMapping(path = \"\", method = RequestMethod.POST)\n    public Page<AuthorDto> findPage(@RequestBody AuthorSearchDto dto) {\n\n        Page<Author> page = this.authorService.findPage(dto);\n\n        return new PageImpl<>(page.getContent().stream().map(e -> mapper.map(e, AuthorDto.class)).collect(Collectors.toList()), page.getPageable(), page.getTotalElements());\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    @Operation(summary = \"Save or Update\", description = \"Method that saves or updates a Author\")\n    @RequestMapping(path = { \"\", \"/{id}\" }, method = RequestMethod.PUT)\n    public void save(@PathVariable(name = \"id\", required = false) Long id, @RequestBody AuthorDto dto) {\n\n        this.authorService.save(id, dto);\n    }\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     */\n    @Operation(summary = \"Delete\", description = \"Method that deletes a Author\")\n    @RequestMapping(path = \"/{id}\", method = RequestMethod.DELETE)\n    public void delete(@PathVariable(\"id\") Long id) throws Exception {\n\n        this.authorService.delete(id);\n    }\n\n}\n
    package com.ccsw.tutorial.author;\n\nimport com.ccsw.tutorial.author.model.Author;\nimport com.ccsw.tutorial.author.model.AuthorDto;\nimport com.ccsw.tutorial.author.model.AuthorSearchDto;\nimport org.springframework.data.domain.Page;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\npublic interface AuthorService {\n\n    /**\n     * M\u00e9todo para recuperar un listado paginado de {@link Author}\n     *\n     * @param dto dto de b\u00fasqueda\n     * @return {@link Page} de {@link Author}\n     */\n    Page<Author> findPage(AuthorSearchDto dto);\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     * @param dto datos de la entidad\n     */\n    void save(Long id, AuthorDto dto);\n\n    /**\n     * M\u00e9todo para crear o actualizar un {@link Author}\n     *\n     * @param id PK de la entidad\n     */\n    void delete(Long id) throws Exception;\n\n}\n

    Si te fijas, hemos trasladado toda la l\u00f3gica a llamadas al AuthorService que hemos inyectado, y para que no falle la compilaci\u00f3n hemos creado una interface con los m\u00e9todos necesarios.

    En la clase AuthorController es donde se hacen las conversiones de cara al cliente, pasaremos de un Page<Author> (modelo entidad) a un Page<AuthorDto> (modelo DTO) con la ayuda del beanMapper. Recuerda que al cliente no le deben llegar modelos entidades sino DTOs.

    Adem\u00e1s, el m\u00e9todo de carga findPage ya no es un m\u00e9todo de tipo GET, ahora es de tipo POST porque le tenemos que enviar los datos de la paginaci\u00f3n para que Spring JPA pueda hacer su magia.

    Ahora debemos implementar la siguiente capa.

    "},{"location":"develop/paginated/springboot/#implementar-service","title":"Implementar Service","text":"

    La siguiente capa que vamos a implementar es justamente la capa que contiene toda la l\u00f3gica de negocio, hace uso del Repository para acceder a los datos, y recibe llamadas generalmente de los Controller.

    AuthorServiceImpl.javaAuthorRepository.java
    package com.ccsw.tutorial.author;\n\nimport com.ccsw.tutorial.author.model.Author;\nimport com.ccsw.tutorial.author.model.AuthorDto;\nimport com.ccsw.tutorial.author.model.AuthorSearchDto;\nimport jakarta.transaction.Transactional;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.domain.Page;\nimport org.springframework.stereotype.Service;\n\nimport java.util.List;\n\n/**\n * @author ccsw\n *\n */\n@Service\n@Transactional\npublic class AuthorServiceImpl implements AuthorService {\n\n    @Autowired\n    AuthorRepository authorRepository;\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public Page<Author> findPage(AuthorSearchDto dto) {\n\n        return this.authorRepository.findAll(dto.getPageable().getPageable());\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void save(Long id, AuthorDto data) {\n\n        Author author;\n\n        if (id == null) {\n            author = new Author();\n        } else {\n            author = this.authorRepository.findById(id).orElse(null);\n        }\n\n        BeanUtils.copyProperties(data, author, \"id\");\n\n        this.authorRepository.save(author);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void delete(Long id) throws Exception {\n\n        if(this.authorRepository.findById(id).orElse(null) == null){\n            throw new Exception(\"Not exists\");\n        }\n\n        this.authorRepository.deleteById(id);\n    }\n\n}\n
    package com.ccsw.tutorial.author;\n\nimport com.ccsw.tutorial.author.model.Author;\nimport org.springframework.data.domain.Page;\nimport org.springframework.data.domain.Pageable;\nimport org.springframework.data.repository.CrudRepository;\n\n/**\n * @author ccsw\n *\n */\npublic interface AuthorRepository extends CrudRepository<Author, Long> {\n\n}\n

    De nuevo pasa lo mismo que con la capa anterior, aqu\u00ed delegamos muchas operaciones de consulta y guardado de datos en AuthorRepository. Hemos tenido que crearlo como interface para que no falle la compilaci\u00f3n. Recuerda que cuando creamos un Repository es de gran ayuda hacerlo extender de CrudRepository<T, ID> ya que tiene muchos m\u00e9todos implementados de base que nos pueden servir, como el delete o el save.

    F\u00edjate tambi\u00e9n que cuando queremos copiar m\u00e1s de un dato de una clase a otra, tenemos una utilidad llamada BeanUtils que nos permite realizar esa copia (siempre que las propiedades de ambas clases se llamen igual). Adem\u00e1s, en nuestro ejemplo hemos ignorado el 'id' para que no nos copie un null a la clase destino.

    "},{"location":"develop/paginated/springboot/#implementar-repository","title":"Implementar Repository","text":"

    Y llegamos a la \u00faltima capa, la que est\u00e1 m\u00e1s cerca de los datos finales. Tenemos la siguiente interface:

    AuthorRepository.java
    package com.ccsw.tutorial.author;\n\nimport com.ccsw.tutorial.author.model.Author;\nimport org.springframework.data.domain.Page;\nimport org.springframework.data.domain.Pageable;\nimport org.springframework.data.repository.CrudRepository;\n\n/**\n * @author ccsw\n *\n */\npublic interface AuthorRepository extends CrudRepository<Author, Long> {\n\n    /**\n     * M\u00e9todo para recuperar un listado paginado de {@link Author}\n     *\n     * @param pageable pageable\n     * @return {@link Page} de {@link Author}\n     */\n    Page<Author> findAll(Pageable pageable);\n\n}\n

    Si te fijas, este Repository ya no est\u00e1 vac\u00edo como el anterior, no nos sirve con las operaciones b\u00e1sicas del CrudRepository en este caso hemos tenido que a\u00f1adir un m\u00e9todo nuevo al que pasandole un objeto de tipo Pageable nos devuelva una Page.

    Pues bien, resulta que la m\u00e1gina de Spring JPA en este caso har\u00e1 su trabajo y nosotros no necesitamos implementar ninguna query, Spring ya entiende que un findAll significa que debe recuperar todos los datos de la tabla Author (que es la tabla que tiene como generico en CrudRepository) y adem\u00e1s deben estar paginados ya que el m\u00e9todo devuelve un objeto tipo Page. Nos ahorra tener que generar una sql para buscar una p\u00e1gina concreta de datos y hacer un count de la tabla para obtener el total de resultados. Para ver otros ejemplos y m\u00e1s informaci\u00f3n, visita la p\u00e1gina de QueryMethods. Realmente se puede hacer much\u00edsimas cosas con solo escribir el nombre del m\u00e9todo, sin tener que pensar ni teclear ninguna sql.

    Con esto ya lo tendr\u00edamos todo.

    "},{"location":"develop/paginated/springboot/#probar-las-operaciones","title":"Probar las operaciones","text":"

    Si ahora ejecutamos los test jUnit, veremos que todos funcionan y est\u00e1n en verde. Hemos implementado todas nuestras pruebas y la aplicaci\u00f3n es correcta.

    Aun as\u00ed, debemos realizar pruebas con el postman para ver los resultados que nos ofrece el back. Para ello, tienes que levantar la aplici\u00f3n y ejecutar las siguientes operaciones:

    ** POST /author **

    {\n    \"pageable\": {\n        \"pageSize\" : 4,\n        \"pageNumber\" : 0,\n        \"sort\" : [\n            {\n                \"property\": \"name\",\n                \"direction\": \"ASC\"\n            }\n        ]\n    }\n}\n
    Nos devuelve un listado paginado de Autores. F\u00edjate que los datos que se env\u00edan est\u00e1n en el body como formato JSON (parte izquierda de la imagen). Si no env\u00edas datos con formato Pageable, te dar\u00e1 un error. Tambi\u00e9n f\u00edjate que la respuesta es de tipo Page. Prueba a jugar con los datos de paginaci\u00f3n e incluso de ordenaci\u00f3n. No hemos programado ninguna SQL pero Spring hace su magia.

    ** PUT /author **

    ** PUT /author/{id} **

    {\n    \"name\" : \"Nuevo autor\",\n    \"nationality\" : \"Nueva nacionalidad\"\n}\n
    Nos sirve para insertar Autores nuevas (si no tienen el id informado) o para actualizar Autores (si tienen el id informado en la URL). F\u00edjate que los datos que se env\u00edan est\u00e1n en el body como formato JSON (parte izquierda de la imagen). Si no te dar\u00e1 un error.

    ** DELETE /author/{id} ** nos sirve eliminar Autores. F\u00edjate que el dato del ID que se env\u00eda est\u00e1 en el path.

    "},{"location":"develop/paginated/vuejs/","title":"Listado paginado - VUE","text":"

    Ahora nos ponemos con la pantalla de autores y vamos a realizar los cambios para poder realizar un paginado en la tabla de autores, adem\u00e1s de realizar los cambios oportunos para poder a\u00f1adir, editar y borrar autores.

    "},{"location":"develop/paginated/vuejs/#acciones-posibles","title":"Acciones posibles","text":""},{"location":"develop/paginated/vuejs/#anadir-una-fila","title":"A\u00f1adir una fila","text":"

    Para poder a\u00f1adir una fila, vamos a tener que a\u00f1adir al componente de dialog de adici\u00f3n un nuevo campo que ser\u00e1 la nacionalidad habiendo quitado los que hab\u00edamos copiado del cat\u00e1logo dejando finalmente solo dos: el nombre y la nacionalidad.

    Veremos el estado del c\u00f3digo en el apartado de borrado.

    "},{"location":"develop/paginated/vuejs/#editar-una-fila","title":"Editar una fila","text":"

    A la hora de editar una fila, modificaremos la columna de \u201cedad\u201d para reutilizarla con la nacionalidad, reutilizaremos la columna de \u201cnombre\u201d tal cual est\u00e1 y borraremos las dem\u00e1s exceptuando la de opciones que ah\u00ed pondremos el bot\u00f3n para el borrado.

    Veremos el estado del c\u00f3digo en el apartado de borrado.

    "},{"location":"develop/paginated/vuejs/#borrar-una-fila","title":"Borrar una fila","text":"

    Y, por \u00faltimo, haremos lo mismo que hicimos en la pantalla de categor\u00edas, que es a\u00f1adir la funci\u00f3n delete despu\u00e9s del dialog de confirmaci\u00f3n.

    El estado del c\u00f3digo ahora mismo quedar\u00eda as\u00ed:

    <template>\n  <q-page padding>\n    <q-table\n      hide-bottom\n      :rows=\"authorsData\"\n      :columns=\"columns\"\n      v-model:pagination=\"pagination\"\n      title=\"Cat\u00e1logo\"\n      class=\"my-sticky-header-table\"\n      no-data-label=\"No hay resultados\"\n      row-key=\"id\"\n    >\n      <template v-slot:top>\n        <q-btn flat round color=\"primary\" icon=\"add\" @click=\"showAdd = true\" />\n      </template>\n      <template v-slot:body=\"props\">\n        <q-tr :props=\"props\">\n          <q-td key=\"id\" :props=\"props\">{{ props.row.id }}</q-td>\n          <q-td key=\"name\" :props=\"props\">\n            {{ props.row.name }}\n            <q-popup-edit\n              v-model=\"props.row.name\"\n              title=\"Cambiar nombre\"\n              v-slot=\"scope\"\n            >\n              <q-input\n                v-model=\"scope.value\"\n                dense\n                autofocus\n                counter\n                @keyup.enter=\"editRow(props, scope, 'name')\"\n              >\n                <template v-slot:append>\n                  <q-icon name=\"edit\" />\n                </template>\n              </q-input>\n            </q-popup-edit>\n          </q-td>\n          <q-td key=\"nationality\" :props=\"props\">\n            {{ props.row.nationality }}\n            <q-popup-edit\n              v-model=\"props.row.nationality\"\n              title=\"Cambiar nacionalidad\"\n              v-slot=\"scope\"\n            >\n              <q-input\n                v-model=\"scope.value\"\n                dense\n                autofocus\n                counter\n                @keyup.enter=\"editRow(props, scope, 'nationality')\"\n              >\n                <template v-slot:append>\n                  <q-icon name=\"edit\" />\n                </template>\n              </q-input>\n            </q-popup-edit>\n          </q-td>\n          <q-td key=\"options\" :props=\"props\">\n            <q-btn\n              flat\n              round\n              color=\"negative\"\n              icon=\"delete\"\n              @click=\"showDeleteDialog(props.row)\"\n            />\n          </q-td>\n        </q-tr>\n      </template>\n    </q-table>\n    <q-dialog v-model=\"showDelete\" persistent>\n      <q-card>\n        <q-card-section class=\"row items-center\">\n          <q-icon\n            name=\"delete\"\n            size=\"sm\"\n            color=\"negative\"\n            @click=\"showDelete = true\"\n          />\n          <span class=\"q-ml-sm\">\n            \u00bfEst\u00e1s seguro de que quieres borrar este elemento?\n          </span>\n        </q-card-section>\n\n        <q-card-actions align=\"right\">\n          <q-btn flat label=\"Cancelar\" color=\"primary\" v-close-popup />\n          <q-btn\n            flat\n            label=\"Confirmar\"\n            color=\"primary\"\n            v-close-popup\n            @click=\"deleteAuthor\"\n          />\n        </q-card-actions>\n      </q-card>\n    </q-dialog>\n    <q-dialog v-model=\"showAdd\">\n      <q-card style=\"width: 300px\" class=\"q-px-sm q-pb-md\">\n        <q-card-section>\n          <div class=\"text-h6\">Nuevo autor</div>\n        </q-card-section>\n\n        <q-item-label header>Nombre</q-item-label>\n        <q-item dense>\n          <q-item-section avatar>\n            <q-icon name=\"badge\" />\n          </q-item-section>\n          <q-item-section>\n            <q-input dense v-model=\"authorToAdd.name\" autofocus />\n          </q-item-section>\n        </q-item>\n\n        <q-item-label header>Nacionalidad</q-item-label>\n        <q-item dense>\n          <q-item-section avatar>\n            <q-icon name=\"flag\" />\n          </q-item-section>\n          <q-item-section>\n            <q-input\n              dense\n              v-model=\"authorToAdd.nationality\"\n              autofocus\n              @keyup.enter=\"addAuthor\"\n            />\n          </q-item-section>\n        </q-item>\n\n        <q-card-actions align=\"right\" class=\"text-primary\">\n          <q-btn flat label=\"Cancelar\" v-close-popup />\n          <q-btn flat label=\"A\u00f1adir autor\" v-close-popup @click=\"addAuthor\" />\n        </q-card-actions>\n      </q-card>\n    </q-dialog>\n  </q-page>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from 'vue';\nimport { useFetch, whenever } from '@vueuse/core';\n\nconst columns = [\n  { name: 'id', align: 'left', label: 'ID', field: 'id', sortable: true },\n  {\n    name: 'name',\n    align: 'left',\n    label: 'Nombre',\n    field: 'name',\n    sortable: true,\n  },\n  {\n    name: 'nationality',\n    align: 'left',\n    label: 'Nacionalidad',\n    field: 'nationality',\n    sortable: true,\n  },\n  { name: 'options', align: 'left', label: 'Options', field: 'options' },\n];\nconst pagination = {\n  page: 1,\n  rowsPerPage: 0,\n};\nconst newAuthor = {\n  name: '',\n  nationality: '',\n};\n\nconst authorsData = ref([]);\nconst showDelete = ref(false);\nconst showAdd = ref(false);\nconst selectedRow = ref({});\nconst authorToAdd = ref({ ...newAuthor });\n\nconst getAuthors = () => {\n  const { data } = useFetch('http://localhost:8080/author').get().json();\n  whenever(data, () => (authorsData.value = data.value));\n};\ngetAuthors();\n\nconst showDeleteDialog = (item: any) => {\n  selectedRow.value = item;\n  showDelete.value = true;\n};\n\nconst addAuthor = async () => {\n  await useFetch('http://localhost:8080/author', {\n    method: 'PUT',\n    redirect: 'manual',\n    headers: {\n      accept: '*/*',\n      origin: window.origin,\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify(authorToAdd.value),\n  })\n    .put()\n    .json();\n\n  getAuthors();\n  authorToAdd.value = newAuthor;\n  showAdd.value = false;\n};\n\nconst editRow = (props: any, scope: any, field: any) => {\n  const row = {\n    name: props.row.name,\n    nationality: props.row.nationality,\n  };\n  row[field] = scope.value;\n  scope.set();\n  editAuthor(props.row.id, row);\n};\n\nconst editAuthor = async (id: string, reqBody: any) => {\n  await useFetch(`http://localhost:8080/author/${id}`, {\n    method: 'PUT',\n    redirect: 'manual',\n    headers: {\n      accept: '*/*',\n      origin: window.origin,\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify(reqBody),\n  })\n    .put()\n    .json();\n\n  getAuthors();\n};\n\nconst deleteAuthor = async () => {\n  await useFetch(`http://localhost:8080/author/${selectedRow.value.id}`, {\n    method: 'DELETE',\n    redirect: 'manual',\n    headers: {\n      accept: '*/*',\n      origin: window.origin,\n      'Content-Type': 'application/json',\n    },\n  })\n    .delete()\n    .json();\n\n  getAuthors();\n};\n</script>\n

    "},{"location":"develop/paginated/vuejs/#paginado","title":"Paginado","text":"

    Lo primero que tenemos que hacer es usar las nuevas caracter\u00edsticas de nuestra tabla para poder a\u00f1adir datos y as\u00ed hacer funcionar el paginado correctamente.

    Lo primero que vamos a hacer es cambiar el objeto de paginaci\u00f3n para que tenga lo siguiente:

    const pagination = ref({\n  page: 0,\n  rowsPerPage: 5,\n  rowsNumber: 10,\n});\n

    Y debido a que la tabla y el back requieren de formatos diferentes para la paginaci\u00f3n, vamos a tener que realizar una funci\u00f3n que formatee el objeto para enviarlo al back. Esta funci\u00f3n ser\u00e1, m\u00e1s o menos, as\u00ed:

    const formatPageableBody = (props: any) => {\n  return {\n    pageable: {\n      pageSize:\n        props.pagination.rowsPerPage !== 0\n          ? props.pagination.rowsPerPage\n          : props.pagination.rowsNumber,\n      pageNumber: props.pagination.page - 1,\n      sort: [\n        {\n          property: 'name',\n          direction: 'ASC',\n        },\n      ],\n    },\n  };\n};\n

    Tal y como podemos ver, se realiza una condici\u00f3n en el formato ya que, si el usuario selecciona que quiere ver todas las filas de golpe el valor de dicha variable ser\u00e1 0 y el back necesitar\u00e1 el valor del n\u00famero m\u00e1ximo de filas para que nosotros recibamos todas.

    Y por \u00faltimo vamos a hacer que la funci\u00f3n de recibir los datos reciba por par\u00e1metro el paginado (siempre habr\u00e1 uno por defecto) y que cuando todo haya ido bien se actualice la paginaci\u00f3n local.

    const updateLocalPagination = (props: any) => {\n  pagination.value.page = props.pagination.page;\n  pagination.value.rowsPerPage = props.pagination.rowsPerPage;\n};\n\nconst getAuthors = (props: any = { pagination: pagination.value }) => {\n  const { data } = useFetch('http://localhost:8080/author', {\n    method: 'POST',\n    redirect: 'manual',\n    headers: {\n      accept: '*/*',\n      origin: window.origin,\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify(formatPageableBody(props)),\n  })\n    .post()\n    .json();\n  whenever(data, () => {\n    updateLocalPagination(props);\n    authorsData.value = data.value.content;\n    pagination.value.rowsNumber = data.value.totalElements;\n  });\n};\n

    Importante

    En la primera de las peticiones (y si quieres en las dem\u00e1s tambi\u00e9n) se ha de recoger el atributo de filas totales y setearlo en el objeto de paginaci\u00f3n con el nombre de rowsNumber. Esto se realiza en la zona subrayada anterior.

    Y por \u00faltimo, hacemos que se realicen peticiones siempre que el usuario cambie par\u00e1metros de la tabla, como el cambio de p\u00e1gina o el cambio de filas mostradas. Esto se realiza a\u00f1adiendo a la creaci\u00f3n de la tabla la siguiente l\u00ednea:

    <q-table\n      :rows=\"authorsData\"\n      :columns=\"columns\"\n      v-model:pagination=\"pagination\"\n      title=\"Autores\"\n      class=\"my-sticky-header-table\"\n      no-data-label=\"No hay resultados\"\n      row-key=\"id\"\n      @request=\"getAuthors\"\n    >\n

    Con estos cambios, la pantalla deber\u00eda funcionar correctamente con el paginado funcionando y todas sus funciones b\u00e1sicas.

    "},{"location":"install/angular/","title":"Entorno de desarrollo - Angular","text":""},{"location":"install/angular/#instalacion-de-herramientas","title":"Instalaci\u00f3n de herramientas","text":"

    Las herramientas b\u00e1sicas que vamos a utilizar para esta tecnolog\u00eda son:

    • Visual Studio Code
    • Scoop.sh
    • Nodejs
    • Angular CLI
    "},{"location":"install/angular/#visual-studio-code","title":"Visual Studio Code","text":"

    Lo primero de todo es instalar el IDE para el desarrollo front.

    Te recomiendo utilizar Visual Studio Code, en un IDE que a nosotros nos gusta mucho y tiene muchos plugins configurables. Puedes entrar en su p\u00e1gina y descargarte la versi\u00f3n estable.

    Si no tuvieras permisos para instalar la herramienta por restricciones en el port\u00e1til existe una alternativa para poder instalarlo, a trav\u00e9s del \"Portal de Empresa\" que tenemos instalado en nuestro port\u00e1til. Para ello teclea en el buscador de Windows (o en el men\u00fa de inicio) el texto \"Portal de empresa\". Deber\u00eda aparecerte una app instalada en tu ordenador, tan solo tendr\u00e1s que hacer click en ella:

    Una vez dentro del portal de empresa, ver\u00e1s una aplicaci\u00f3n que se llama \"Pre-Approved Catalogue\". Deber\u00e1s instalarla, de hecho cada vez que quieras acceder a ella, tendr\u00e1s que instalarla para que se descargue el nuevo cat\u00e1logo.

    Despu\u00e9s de unos minutos de instalaci\u00f3n, entrar\u00e1s en un listado de las aplicaciones que est\u00e1n pre-aprobadas por la empresa. Solo tendr\u00e1s que buscar \"Visual Studio Code\" e instalarla.

    Pasados unos minutos, ya tendr\u00e1s instalado el IDE en tu port\u00e1til.

    "},{"location":"install/angular/#scoopsh","title":"Scoop.sh","text":"

    Muchas de las herramientas que necesitar\u00e1s a lo largo de tu estancia en los proyectos, no podr\u00e1s instalarlas por temas de permisos de seguridad en nuestros port\u00e1tiles. Una forma de evitar estos permisos de seguridad y poder instalar herramientas (sobre todo a nivel de consola), es utilizando a Scoop.sh.

    Para instalar scoop.sh tan solo necesitas abrir un terminal de PowerShell (OJO que es PowerShell y no una consola de msdos) y ejecutar los siguientes comandos:

    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser\nInvoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression\n

    Esto nos instalar\u00e1 scoop.sh en nuestros port\u00e1tiles. Adem\u00e1s de la instalaci\u00f3n, y debido a las restricciones de seguridad que tenemos, necesitaremos activar el lessmsi para que las aplicaciones que necesitemos instalar no intenten ejecutar los .exe de instalaci\u00f3n, sino que descompriman el zip. Para ello deberemos ejecutar el comando:

    scoop config use_lessmsi true\n

    A partir de este punto, ya tenemos listo el port\u00e1til para instalar herramientas y aplicaciones.

    "},{"location":"install/angular/#nodejs","title":"Nodejs","text":"

    El siguiente paso ser\u00e1 instalar el motor de Nodejs. Para esto vamos a usar scoop.sh ya que lo tenemos instalado, y vamos a pedirle que nos instal\u00e9 el motor de Nodejs. Abriremos una consola de msdos y ejecutaremos el comando:

    scoop install main/nodejs\n

    Con esto, scoop ya nos instalar\u00e1 todo lo necesario.

    "},{"location":"install/angular/#angular-cli","title":"Angular CLI","text":"

    El siguiente pas\u00f3 ser\u00e1 instalar una capa de gesti\u00f3n por encima de Nodejs que nos ayudar\u00e1 en concreto con la funcionalidad de Angular. Para poder instalarlo, tan solo hay que abrir una consola de msdos y ejecutar el comando y Nodejs ya har\u00e1 el resto:

    npm install -g @angular/cli\n

    Si en el comando no indicamos la versi\u00f3n se instalar\u00e1 la \u00faltima del CLI, pero si queremos podemos elegir una versi\u00f3n en concreto a\u00f1adiendo '@' y el n\u00famero de la versi\u00f3n correspondiente.

    npm install -g @angular/cli@16\n

    Muy

    Si vas a realizar el tutorial de Angular 17+ deber\u00e1s instalar la \u00faltima versi\u00f3n, en el caso de que por alguna raz\u00f3n optes por el tutorial Angular deber\u00e1s instalar la versi\u00f3n 16.

    Y con esto ya tendremos todo instalado, listo para empezar a crear los proyectos.

    Aviso para navegantes corporativos

    Si tienes alg\u00fan problema para ejecutar el comando ng ... puede deberse a que no se ha podido a\u00f1adir al PATH.

    Pero \u00a1\u00a1no te preocupes!! te explicamos c\u00f3mo puedes instal\u00e1rtelo paso a paso:

    1. Aseg\u00farate de que tienes instalado Git Bash, \u00bfc\u00f3mo?
      1. Clic derecho en una carpeta \u2013 la que sea
      2. \"M\u00e1s opciones\" / \"More options\"
      3. Si no te aparece, deber\u00e1s instal\u00e1rtelo desde el Portal de Empresa
    2. Elige una carpeta sobre la que tengas permisos como destino de la instalaci\u00f3n
      • npm install @angular/cli <- no ponemos -g
    3. Ahora viene lo confuso, pero te guiamos. Tienes que crear el siguiente alias
      • echo alias ng=\\'node RUTA_EN_LA_QUE_ESTAS/node_modules/@angular/cli/bin/ng.js\\' >> .bashrc <- esto crear\u00e1 el alias
      • source ~/.bashrc <- esto actualizar\u00e1 el perfil (resetea el diccionario con los alias disponibles)
    4. Ahora ya deber\u00edas poder ejecutar ng desde Git bash

    \u00bfTienes alg\u00fan problema en la instalaci\u00f3n? Cont\u00e1ctanos y te ayudaremos en la medida de lo que podamos

    "},{"location":"install/angular/#creacion-de-proyecto","title":"Creaci\u00f3n de proyecto","text":"

    La mayor\u00eda de los proyectos con Angular en los que trabajamos normalmente, suelen ser proyectos web usando las librer\u00edas mas comunes de angular, como Angular Material.

    Crear un proyecto de Angular es muy sencillo si tienes instalado el CLI de Angular. Lo primero abrir una consola de msdos y posicionarte en el directorio raiz donde quieres crear tu proyecto Angular, y ejecutamos lo siguiente:

    ng new tutorial --strict=false\n

    El propio CLI nos ir\u00e1 realizando una serie de preguntas que pueden cambiar dependiendo de la versi\u00f3n.

    Would you like to add Angular routing? (y/N)

    Preferiblemente: y

    Which stylesheet format would you like to use?

    Preferiblemente: SCSS

    Do you want to enable Server-Side Rendering (SSR)

    Preferiblemente: N

    En el caso del tutorial como vamos a tener dos proyectos para nuestra aplicaci\u00f3n (front y back), para poder seguir correctamente las explicaciones, voy a renombrar la carpeta para poder diferenciarla del otro proyecto. A partir de ahora se llamar\u00e1 client.

    Info

    Si durante el desarrollo del proyecto necesitas a\u00f1adir nuevos m\u00f3dulos al proyecto Angular, ser\u00e1 necesario resolver las dependencias antes de arrancar el servidor. Esto se puede realizar mediante el gestor de dependencias de Nodejs, directamente en consola ejecuta el comando npm update y descargar\u00e1 e instalar\u00e1 las nuevas dependencias.

    "},{"location":"install/angular/#arrancar-el-proyecto","title":"Arrancar el proyecto","text":"

    Para arrancar el proyecto, tan solo necesitamos ejecutar en consola el siguiente comando siempre dentro del directorio creado por Angular CLI:

    ng serve\n

    Angular compilar\u00e1 el c\u00f3digo fuente, levantar\u00e1 un servidor local al que podremos acceder por defecto mediante la URL: http://localhost:4200/

    Y ya podemos empezar a trabajar con Angular.

    Info

    Cuando se trata de un proyecto nuevo recien descargado de un repositorio, recuerda que ser\u00e1 necesario resolver las dependencias antes de arrancar el servidor. Esto se puede realizar mediante el gestor de dependencias de Nodejs, directamente en consola ejecuta el comando npm update y descargar\u00e1 e instalar\u00e1 las nuevas dependencias.

    Comandos de Angular CLI

    Si necesitas m\u00e1s informaci\u00f3n sobre los comandos que ofrece Angular CLI para poder crear aplicaciones, componentes, servicios, etc. los tienes disponibles en: https://angular.io/cli#command-overview

    "},{"location":"install/angular/#angular-17","title":"Angular 17+","text":"

    Con la llegada de Angular 17, se han introducido importantes novedades que impactan la manera en que se desarrollan aplicaciones web. A diferencia de las versiones anteriores, Angular 17 trae mejoras enfocadas en la simplicidad, el rendimiento y la flexibilidad del desarrollo. En este tutorial, te guiaremos a trav\u00e9s de estas nuevas caracter\u00edsticas, permiti\u00e9ndote elegir si deseas enfocarte en las versiones m\u00e1s recientes o adaptarlo a versiones anteriores.

    Las principales diferencias entre Angular 17+ y sus versiones anteriores incluyen:

    "},{"location":"install/angular/#componentes-standalone-por-defecto","title":"Componentes standalone por defecto","text":"

    Una de las novedades m\u00e1s importantes de Angular 17 es el uso de componentes standalone de forma predeterminada. En versiones anteriores, los m\u00f3dulos (NgModules) eran el n\u00facleo de la estructura de una aplicaci\u00f3n Angular. Ahora, con los componentes standalone, puedes crear y usar componentes sin necesidad de definir un m\u00f3dulo expl\u00edcito, lo que simplifica significativamente la configuraci\u00f3n inicial y mejora la modularidad. Esto facilita la creaci\u00f3n de aplicaciones m\u00e1s ligeras y modulares.

    "},{"location":"install/angular/#directivas-simplificadas","title":"Directivas Simplificadas","text":"

    En Angular 17, algunas de las directivas m\u00e1s utilizadas han sido actualizadas para simplificar su uso y mejorar la legibilidad del c\u00f3digo. Una de las principales mejoras es la introducci\u00f3n de @if, que reemplaza la tradicional ngIf. Esta nueva sintaxis hace que las condiciones sean m\u00e1s claras y f\u00e1ciles de aplicar en las plantillas. Del mismo modo, la directiva ngFor, utilizada para iterar sobre listas, tambi\u00e9n ha sido optimizada, ofreciendo una experiencia m\u00e1s fluida y mejor manejo de colecciones din\u00e1micas.

    Adem\u00e1s, se ha reducido la complejidad en el uso de otras directivas estructurales como ngSwitch y ngClass, haciendo m\u00e1s intuitivo el control del comportamiento y la apariencia de los elementos en las vistas. Con estas mejoras, Angular 17 ofrece una sintaxis m\u00e1s limpia y directa, permitiendo a los desarrolladores concentrarse en la l\u00f3gica de su aplicaci\u00f3n sin la sobrecarga de c\u00f3digo innecesario.

    "},{"location":"install/angular/#bloques-de-carga-deferred","title":"Bloques de carga deferred","text":"

    Los bloques de carga deferred (carga diferida) son una de las caracter\u00edsticas m\u00e1s esperadas en Angular 17. Esta funcionalidad permite retrasar la carga de ciertas partes de la aplicaci\u00f3n hasta que realmente sean necesarias, lo que optimiza el rendimiento al reducir el tama\u00f1o inicial del paquete que se descarga al navegador. Con esta t\u00e9cnica, es posible mejorar el tiempo de respuesta inicial de las aplicaciones y cargar los m\u00f3dulos o componentes bajo demanda, favoreciendo una mejor experiencia de usuario.

    "},{"location":"install/angular/#esbuild-y-vite","title":"ESBuild y Vite","text":"

    Otra de las mejoras clave de Angular 17 es la integraci\u00f3n de ESBuild y Vite como opciones de construcci\u00f3n (build). Estos dos motores de compilaci\u00f3n permiten una construcci\u00f3n mucho m\u00e1s r\u00e1pida y eficiente de aplicaciones, mejorando significativamente los tiempos de desarrollo y compilaci\u00f3n. ESBuild es un bundler de JavaScript que se enfoca en la velocidad, mientras que Vite proporciona una experiencia de desarrollo m\u00e1s \u00e1gil con recarga en caliente y un flujo de trabajo optimizado. Ambas herramientas ofrecen una alternativa moderna y r\u00e1pida a Webpack, especialmente para proyectos grandes o aplicaciones en tiempo real.

    "},{"location":"install/nodejs/","title":"Entorno de desarrollo - Nodejs","text":""},{"location":"install/nodejs/#instalacion-de-herramientas","title":"Instalaci\u00f3n de herramientas","text":"

    Las herramientas b\u00e1sicas que vamos a utilizar para esta tecnolog\u00eda son:

    • Visual Studio Code
    • Scoop.sh
    • Nodejs
    • MongoDB Atlas
    • Postman
    "},{"location":"install/nodejs/#visual-studio-code","title":"Visual Studio Code","text":"

    Lo primero de todo es instalar el IDE para el desarrollo en node si no lo has hecho previamente.

    Te recomiendo utilizar Visual Studio Code, en un IDE que a nosotros nos gusta mucho y tiene muchos plugins configurables. Puedes entrar en su p\u00e1gina y descargarte la versi\u00f3n estable.

    "},{"location":"install/nodejs/#scoopsh","title":"Scoop.sh","text":"

    Muchas de las herramientas que necesitar\u00e1s a lo largo de tu estancia en los proyectos, no podr\u00e1s instalarlas por temas de permisos de seguridad en nuestros port\u00e1tiles. Una forma de evitar estos permisos de seguridad y poder instalar herramientas (sobre todo a nivel de consola), es utilizando a Scoop.sh.

    Para instalar scoop.sh tan solo necesitas abrir un termina de PowerShell (OJO que es PowerShell y no una consola de msdos) y ejecutar los siguientes comandos:

    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser\nInvoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression\n

    Esto nos instalar\u00e1 scoop.sh en nuestros port\u00e1tiles. Adem\u00e1s de la instalaci\u00f3n, y debido a las restricciones de seguridad que tenemos, necesitaremos activar el lessmsi para que las aplicaciones que necesitemos instalar no intenten ejecutar los .exe de instalaci\u00f3n, sino que descompriman el zip. Para ello deberemos ejecutar el comando:

    scoop config use_lessmsi true\n

    A partir de este punto, ya tenemos listo el port\u00e1til para instalar herramientas y aplicaciones.

    "},{"location":"install/nodejs/#nodejs","title":"Nodejs","text":"

    El siguiente paso ser\u00e1 instalar el motor de Nodejs. Para esto vamos a usar scoop.sh ya que lo tenemos instalado, y vamos a pedirle que nos instal\u00e9 el motor de Nodejs. Abriremos una consola de msdos y ejecutaremos el comando:

    scoop install main/nodejs\n

    Con esto, scoop ya nos instalar\u00e1 todo lo necesario.

    "},{"location":"install/nodejs/#mongodb-atlas","title":"MongoDB Atlas","text":"

    Tambi\u00e9n necesitaremos crear una cuenta de MongoDB Atlas para crear nuestra base de datos MongoDB en la nube.

    Accede a la URL, registrate gr\u00e1tis con cualquier cuenta de correo y elige el tipo de cuenta gratuita \ud83d\ude0a:

    Configura el cluster a tu gusto (selecciona la opci\u00f3n gratuita en el cloud que m\u00e1s te guste) y ya tendr\u00edas una BBDD en cloud para hacer pruebas. Lo primero que se muestra es el dashboard que se ver\u00e1 algo similar a lo siguiente:

    A continuaci\u00f3n, pulsamos en la opci\u00f3n Database del men\u00fa y, sobre el Cluster0, pulsamos tambi\u00e9n el bot\u00f3n Connect. Se nos abrir\u00e1 el siguiente pop-up donde tendremos que elegir la opci\u00f3n Connect your application:

    En el siguiente paso es donde se nos muestra la url que tendremos que utilizar en nuestra aplicaci\u00f3n. La copiamos y guardamos para m\u00e1s tarde:

    Pulsamos Close y la BBDD ya estar\u00eda creada.

    Nota: Al crear la base de datos te aprecer\u00e1 un aviso para introducir tu IP en la whitelist, aseg\u00farate no estar en la VPN cuando lo hagas, de lo contrario no tendr\u00e1s conexi\u00f3n posteriormente.

    "},{"location":"install/nodejs/#herramientas-para-pruebas","title":"Herramientas para pruebas","text":"

    Para poder probar las operaciones de negocio que vamos a crear, lo mejor es utilizar una herramienta que permita realizar llamadas a API Rest. Para ello te propongo utilizar Postman, en su versi\u00f3n web o en su versi\u00f3n desktop, cualquiera de las dos sirve.

    Con esta herramienta se puede generar peticiones GET, POST, PUT, DELETE contra el servidor y pasarle par\u00e1metros de forma muy sencilla y visual. Lo usaremos durante el tutorial.

    "},{"location":"install/nodejs/#creacion-de-proyecto","title":"Creaci\u00f3n de proyecto","text":"

    Para la creaci\u00f3n de nuestro proyecto Node nos crearemos una carpeta con el nombre que deseemos y accederemos a ella con la consola de comandos de windows. Una vez dentro ejecutaremos el siguiente comando para inicializar nuestro proyecto con npm:

    npm init\n

    Cuando ejecutemos este comando nos pedir\u00e1 los valores para distintos par\u00e1metros de nuestro proyecto. Aconsejo solo cambiar el nombre y el resto dejarlo por defecto pulsando enter para cada valor. Una vez que hayamos terminado se nos habr\u00e1 generado un fichero package.json que contendr\u00e1 informaci\u00f3n b\u00e1sica de nuestro proyecto. Dentro de este fichero tendremos que a\u00f1adir un nuevo par\u00e1metro type con el valor module, esto nos permitir\u00e1 importar nuestros m\u00f3dulos con el est\u00e1ndar ES:

    {\n  \"name\": \"tutorialNode\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"type\": \"module\"\n}\n
    "},{"location":"install/nodejs/#instalar-dependencias","title":"Instalar dependencias","text":"

    En ese fichero aparte de la informaci\u00f3n de nuestro proyecto tambi\u00e9n tendremos que a\u00f1adir las dependencias que usara nuestra aplicaci\u00f3n.

    Para a\u00f1adir las dependencias, desde la consola de comandos y situados en la misma carpeta donde se haya creado el fichero package.json vamos a teclear los siguientes comandos:

    npm i express\nnpm i express-validator\nnpm i dotenv\nnpm i mongoose\nnpm i mongoose-paginate-v2\nnpm i normalize-mongoose\nnpm i cors\nnpm i nodemon --save-dev\n

    Tambi\u00e9n podr\u00edamos haber instalado todas a la vez en dos l\u00edneas:

    npm i express express-validator dotenv  mongoose mongoose-paginate-v2 normalize-mongoose cors\nnpm i nodemon --save-dev\n

    Las dependencias que acabamos de instalar son las siguientes:

    • Express es un framework de Node que nos facilitara mucho la tarea a la hora de crear nuestra aplicaci\u00f3n.
    • Dotenv es una librer\u00eda para usar variables de entorno.
    • Mongoose es una librer\u00eda ODM que nos ayudara a los accesos a BBDD.
    • Nodemon es una herramienta que nos ayuda reiniciando nuestro servidor cuando detecta un cambio en alguno de nuestros ficheros y as\u00ed no tener que hacerlo manualmente.
    • Cors es una herramienta que nos ayuda a configurar el CORS de nuestra app para que posteriormente podemos conectarlo al front.

    Ahora podemos fijarnos en nuestro fichero package.json donde se habr\u00e1n a\u00f1adido dos nuevos par\u00e1metros: dependencies y devDependencies. La diferencia est\u00e1 en que las devDependencies solo se utilizar en la fase de desarrollo de nuestro proyecto y las dependencies se utilizar\u00e1n en todo momento.

    "},{"location":"install/nodejs/#configurar-la-bbdd","title":"Configurar la BBDD","text":"

    A partir de aqu\u00ed ya podemos abrir Visual Studio Code, el IDE recomendado, y abrir la carpeta del proyecto para poder configurarlo y programarlo. Lo primero ser\u00e1 configurar el acceso con la BBDD.

    Para ello vamos a crear en la ra\u00edz de nuestro proyecto una carpeta config dentro de la cual crearemos un archivo llamado db.js. Este archivo exportar\u00e1 una funci\u00f3n que recibe una url de nuestra BBDD y la conectar\u00e1 con mongoose. El contenido de este archivo debe ser el siguiente:

    db.js
    import mongoose from 'mongoose';\n\nconst connectDB = async (url) => {\n\n    try {\n        await mongoose.connect(url);\n        console.log('BBDD connected');\n    } catch (error) {\n        throw new Error('Error initiating BBDD:' + error);\n    }\n}\n\nexport default connectDB;\n

    Ahora vamos a crear en la ra\u00edz de nuestro proyecto un archivo con el nombre .env. Este archivo tendr\u00e1 las variables de entorno de nuestro proyecto. Es aqu\u00ed donde pondremos la url que obtuvimos al crear nuestra BBDD. As\u00ed pues, crearemos una nueva variable y pegaremos la URL. Tambi\u00e9n vamos a configurar el puerto del servidor.

    .env
    MONGODB_URL='mongodb+srv://<user>:<pass>@<url>.mongodb.net/?retryWrites=true&w=majority'\nPORT='8080'\n
    "},{"location":"install/nodejs/#arrancar-el-proyecto","title":"Arrancar el proyecto","text":"

    Con toda esa configuraci\u00f3n, ahora ya podemos crear nuestra p\u00e1gina inicial. Dentro del fichero package.json, en concreto en el contenido de main vemos que nos indica el valor de index.js. Este ser\u00e1 el punto de entrada a nuestra aplicaci\u00f3n, pero este fichero todav\u00eda no existe, as\u00ed que lo crearemos con el siguiente contenido:

    index.js
    import express from 'express';\nimport cors from 'cors';\nimport connectDB from './config/db.js';\nimport { config } from 'dotenv';\n\nconfig();\nconnectDB(process.env.MONGODB_URL);\nconst app = express();\n\napp.use(cors({\n    origin: '*'\n}));\n\napp.listen(process.env.PORT, () => {\n    console.log(`Server running on port ${process.env.PORT}`);\n});\n

    El funcionamiento de este c\u00f3digo, resumiendo mucho, es el siguiente. Configurar la base de datos, configurar el CORS para que posteriormente podamos realizar peticiones desde el front y crea un servidor con express en el puerto 8080.

    Pero antes, para poder ejecutar nuestro servidor debemos modificar el fichero package.json, y a\u00f1adir un script de arranque. A\u00f1adiremos la siguiente l\u00ednea:

    \"dev\": \"nodemon ./index.js\"\n

    Y ahora s\u00ed, desde la consola de comando ya podemos ejecutar el siguiente comando:

    npm run dev\n

    y ya podremos ver en la consola como la aplicaci\u00f3n ha arrancado correctamente con el mensaje que le hemos a\u00f1adido.

    "},{"location":"install/react/","title":"Entorno de desarrollo - React","text":""},{"location":"install/react/#instalacion-de-herramientas","title":"Instalaci\u00f3n de herramientas","text":"

    Las herramientas b\u00e1sicas que vamos a utilizar para esta tecnolog\u00eda son:

    • Visual Studio Code
    • Scoop.sh
    • Nodejs
    "},{"location":"install/react/#visual-studio-code","title":"Visual Studio Code","text":"

    Lo primero de todo es instalar el IDE para el desarrollo front.

    Te recomiendo utilizar Visual Studio Code, en un IDE que a nosotros nos gusta mucho y tiene muchos plugins configurables. Puedes entrar en su p\u00e1gina y descargarte la versi\u00f3n estable.

    "},{"location":"install/react/#scoopsh","title":"Scoop.sh","text":"

    Muchas de las herramientas que necesitar\u00e1s a lo largo de tu estancia en los proyectos, no podr\u00e1s instalarlas por temas de permisos de seguridad en nuestros port\u00e1tiles. Una forma de evitar estos permisos de seguridad y poder instalar herramientas (sobre todo a nivel de consola), es utilizando a Scoop.sh.

    Para instalar scoop.sh tan solo necesitas abrir un termina de PowerShell (OJO que es PowerShell y no una consola de msdos) y ejecutar los siguientes comandos:

    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser\nInvoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression\n

    Esto nos instalar\u00e1 scoop.sh en nuestros port\u00e1tiles. Adem\u00e1s de la instalaci\u00f3n, y debido a las restricciones de seguridad que tenemos, necesitaremos activar el lessmsi para que las aplicaciones que necesitemos instalar no intenten ejecutar los .exe de instalaci\u00f3n, sino que descompriman el zip. Para ello deberemos ejecutar el comando:

    scoop config use_lessmsi true\n

    A partir de este punto, ya tenemos listo el port\u00e1til para instalar herramientas y aplicaciones.

    "},{"location":"install/react/#nodejs","title":"Nodejs","text":"

    El siguiente paso ser\u00e1 instalar el motor de Nodejs. Para esto vamos a usar scoop.sh ya que lo tenemos instalado, y vamos a pedirle que nos instal\u00e9 el motor de Nodejs. Abriremos una consola de msdos y ejecutaremos el comando:

    scoop install main/nodejs\n

    Con esto, scoop ya nos instalar\u00e1 todo lo necesario.

    "},{"location":"install/react/#creacion-de-proyecto","title":"Creaci\u00f3n de proyecto","text":"

    Hasta ahora para la generaci\u00f3n de un proyecto React se ha utilizado la herramienta \u201ccreate-react-app\u201d pero \u00faltimamente se usa m\u00e1s vite debido a su velocidad para desarrollar y su optimizaci\u00f3n en tiempos de construcci\u00f3n. En realidad, para realizar nuestro proyecto da igual una herramienta u otra m\u00e1s all\u00e1 de un poco de configuraci\u00f3n, pero para este proyecto elegiremos vite por su velocidad.

    Para generar nuestro proyecto react con Vite abrimos una consola de Windows y escribimos lo siguiente en la carpeta donde queramos localizar nuestro proyecto:

    npm create vite@latest\n

    Con esto se nos lanzara un wizard para la creaci\u00f3n de nuestro proyecto donde elegiremos el nombre del proyecto (en mi caso ludoteca-react), el framework (react evidentemente) y en la variante elegiremos typescript. Tras estos pasos instalaremos las dependencias base de nuestro proyecto. Primero accedemos a la ra\u00edz y despu\u00e9s ejecutaremos el comando install de npm.

    cd ludoteca-react\n
    npm install\n

    \u00f3

    npm i\n
    "},{"location":"install/react/#arrancar-el-proyecto","title":"Arrancar el proyecto","text":"

    Para arrancar el proyecto, tan solo necesitamos ejecutar en consola el siguiente comando siempre dentro del directorio creado por Vite:

    npm run dev\n

    Vite compilar\u00e1 el c\u00f3digo fuente, levantar\u00e1 un servidor local al que podremos acceder por defecto mediante la URL: http://localhost:5173/

    Y ya podemos empezar a trabajar en nuestro proyecto React.

    Info

    Cuando se trata de un proyecto nuevo recien descargado de un repositorio, recuerda que ser\u00e1 necesario resolver las dependencias antes de arrancar el servidor. Esto se puede realizar mediante el gestor de dependencias de Nodejs, directamente en consola ejecuta el comando npm update y descargar\u00e1 e instalar\u00e1 las nuevas dependencias.

    "},{"location":"install/springboot/","title":"Entorno de desarrollo - Spring Boot","text":""},{"location":"install/springboot/#instalacion-de-herramientas","title":"Instalaci\u00f3n de herramientas","text":"

    Las herramientas b\u00e1sicas que vamos a utilizar para esta tecnolog\u00eda son:

    • IntelliJ o Eclipse IDE (el que m\u00e1s te guste)
    • Java 17 o superior
    • Postman
    "},{"location":"install/springboot/#instalacion-de-intellij-idea","title":"Instalaci\u00f3n de IntelliJ IDEA","text":"

    Nuestra preferencia es utilizar IntelliJ ya que es un IDE m\u00e1s moderno que Eclise IDE, pero cualquiera de los dos es v\u00e1lido para hacer el tutorial. Debido a las restricciones que tenemos en nuestros port\u00e1tiles no ser\u00e1 posible descargarnos una versi\u00f3n de la web e instalarlo, aunque existe otra forma de hacerlo.

    Deberemos acceder al \"Portal de Empresa\" que tenemos instalado en nuestro port\u00e1til. Teclear en el buscador de Windows (o en el men\u00fa de inicio) el texto \"Portal de empresa\". Deber\u00eda aparecerte una app instalada en tu ordenador y hacer click en ella:

    Una vez dentro del portal de empresa, ver\u00e1s una aplicaci\u00f3n que se llama \"Pre-Approved Catalogue\". Deber\u00e1s instalarla, de hecho cada vez que quieras acceder a ella, tendr\u00e1s que instalarla para que se descargue el nuevo cat\u00e1logo.

    Despu\u00e9s de unos minutos de instalaci\u00f3n, entrar\u00e1s en un listado de las aplicaciones que est\u00e1n pre-aprobadas por la empresa. Solo tendr\u00e1s que buscar \"IntelliJ IDEA Community Edition\" e instalarla.

    Pasados unos minutos, ya tendr\u00e1s instalado el IDE en tu port\u00e1til.

    "},{"location":"install/springboot/#configuracion-del-ide","title":"Configuraci\u00f3n del IDE","text":"

    Como complemento al IntelliJ, con el fin de crear c\u00f3digo homog\u00e9neo y mantenible, vamos a configurar el formateador de c\u00f3digo autom\u00e1tico.

    Para ello de nuevo abrimos el men\u00fa Customize -> All Settings o el men\u00fa Settings si estamos en un proyecto, nos vamos a la secci\u00f3n Editor -> Code Style -> Java y aparecer\u00e1 una pantalla similar a esta:

    En el bot\u00f3n de opciones, nos permitir\u00e1 \"Importar esquema\" desde Intellij IDEA:

    Nos descargamos el fichero de Formmatter Profile IntelliJ y lo importamos en IntelliJ.

    Una vez cofigurado el nuevo formateador debemos activar que se aplique en el guardado. Para ello volvemos acceder a las preferencias de IntelliJ y nos dirigimos a la sub secci\u00f3n Tools -> Actions os Save. Es posible que esta secci\u00f3n solo est\u00e9 disponible cuando creemos o importemos un proyecto, as\u00ed que volveremos m\u00e1s adelante aqu\u00ed.

    Hay que activar la opci\u00f3n Reformat code y Optimize imports.

    "},{"location":"install/springboot/#obsoleto-instalacion-de-eclipse-ide","title":"(Obsoleto) Instalaci\u00f3n de Eclipse IDE","text":"

    Si no te gusta IntelliJ, puedes utilizar Eclipse IDE y la m\u00e1quina virtual de java necesaria para ejecutar el c\u00f3digo. Recomendamos Java 17 o superior, que es la versi\u00f3n con la que est\u00e1 desarrollado y probado el tutorial.

    Para instalar el IDE deber\u00e1s acceder a la web de Eclipse IDE y descargarte la \u00faltima versi\u00f3n del instalador. Una vez lo ejecutes te pedir\u00e1 el tipo de instalaci\u00f3n que deseas instalar. Por lo general con la de \"Eclipse IDE for Java Developers\" es suficiente. Con esta versi\u00f3n ya tiene integrado los plugins de Maven y Git.

    Pero recuerda que tendr\u00e1s que instalar una versi\u00f3n acorde de Java ya que Eclipse viene con una versi\u00f3n vieja.

    "},{"location":"install/springboot/#instalacion-de-java","title":"Instalaci\u00f3n de Java","text":"

    Si has instalado IntelliJ, te puedes saltar este punto.

    Si has instalado Eclise IDE, debes asegurarte que est\u00e1 usando por defecto la versi\u00f3n de Java 17 o superior y para ello deber\u00e1s instalarla. Desc\u00e1rgala del siguiente enlace. Es posible que te pida un registro de correo, utiliza el email que quieras (corporativo o personal). Revisa bien el enlace para buscar y descargar la versi\u00f3n 17 para Windows.

    OJO no instales el ejecutable .exe ya que no funcionar\u00e1 debido a nuestras medidas de seguridad. Debes descargarte el .zip y descomprimirlo en alg\u00fan directorio local.

    Ya solo queda a\u00f1adir Java al Eclipse. Para ello, abre el men\u00fa Window -> Preferences:

    y dentro de la secci\u00f3n Java - Installed JREs a\u00f1ade la versi\u00f3n que acabas de descargar, siempre pulsando el bot\u00f3n Add... y buscando el directorio home de la instalaci\u00f3n de Java. Adem\u00e1s, la debes marcar como default.

    "},{"location":"install/springboot/#configuracion-de-eclipse","title":"Configuraci\u00f3n de Eclipse","text":"

    Como complemento al Eclipse, con el fin de crear c\u00f3digo homog\u00e9neo y mantenible, vamos a configurar el formateador de c\u00f3digo autom\u00e1tico.

    Para ello de nuevo abrimos el men\u00fa Window -> Preferences, nos vamos a la secci\u00f3n Formatter de Java:

    Aqu\u00ed crearemos un nuevo perfil heredando la configuraci\u00f3n por defecto.

    En el nuevo perfil configuramos que se use espacios en vez de tabuladores con sangrado de 4 caracteres.

    Una vez cofigurado el nuevo formateador debemos activar que se aplique en el guardado. Para ello volvemos acceder a las preferencias de Eclipse y nos dirigimos a la sub secci\u00f3n Save Actions del la secci\u00f3n Editor nuevamente de Java.

    Aqu\u00ed aplicamos la configuraci\u00f3n deseada.

    "},{"location":"install/springboot/#herramientas-para-pruebas","title":"Herramientas para pruebas","text":"

    Para poder probar las operaciones de negocio que vamos a crear, lo mejor es utilizar una herramienta que permita realizar llamadas a API Rest. Para ello te propongo utilizar Postman, en su versi\u00f3n web o en su versi\u00f3n desktop, cualquiera de las dos sirve.

    Con esta herramienta se puede generar peticiones GET, POST, PUT, DELETE contra el servidor y pasarle par\u00e1metros de forma muy sencilla y visual. Lo usaremos durante el tutorial.

    "},{"location":"install/springboot/#creacion-de-proyecto","title":"Creaci\u00f3n de proyecto","text":"

    La mayor\u00eda de los proyectos Spring Boot en los que trabajamos normalmente, suelen ser proyectos web sencillos con pocas dependencias de terceros o incluso proyectos basados en micro-servicios que ejecutan pocas acciones. Ahora tienes que preparar el proyecto SpringBoot,

    "},{"location":"install/springboot/#crear-con-initilizr","title":"Crear con Initilizr","text":"

    Vamos a ver como configurar paso a paso un proyecto de cero, con las librer\u00edas que vamos a utilizar en el tutorial.

    "},{"location":"install/springboot/#como-usarlo","title":"\u00bfComo usarlo?","text":"

    Spring ha creado una p\u00e1gina interactiva que permite crear y configurar proyectos en diferentes lenguajes, con diferentes versiones de Spring Boot y a\u00f1adi\u00e9ndole los m\u00f3dulos que nosotros queramos.

    Esta p\u00e1gina est\u00e1 disponible desde Spring Initializr. Para seguir el ejemplo del tutorial, entraremos en la web y seleccionaremos los siguientes datos:

    • Tipo de proyecto: Maven
    • Lenguage: Java
    • Versi\u00f3n Spring boot: 3.2.4 (o alguna similar que no sea SNAPSHOPT y que sea 3.x)
    • Group: com.ccsw
    • ArtifactId: tutorial
    • Versi\u00f3n Java: 17 (o superior)
    • Dependencias: Spring Web, Spring Data JPA, H2 Database

    Esto nos generar\u00e1 un proyecto que ya vendr\u00e1 configurado con Spring Web, JPA y H2 para crear una BBDD en memoria de ejemplo con la que trabajaremos durante el tutorial.

    "},{"location":"install/springboot/#importar-en-intellij","title":"Importar en IntelliJ","text":"

    El siguiente paso, es descomprimir el proyecto generado e importarlo en el IDE. Abrimos IntelliJ, pulsamos en \"Open\" y buscamos la carpeta donde hemos descomprimido el proyecto.

    Una vez importado, recuerda darle al men\u00fa File \u2192 Settings y configurar las acciones de Actions on save que se explicar\u00f3n en el punto Configuraci\u00f3n del IDE.

    "},{"location":"install/springboot/#importar-en-eclipse","title":"Importar en Eclipse","text":"

    El siguiente paso, es descomprimir el proyecto generado e importarlo como proyecto Maven. Abrimos el eclipse, pulsamos en File \u2192 Import y seleccionamos Existing Maven Projects. Buscamos el proyecto y le damos a importar.

    "},{"location":"install/springboot/#configurar-las-dependencias","title":"Configurar las dependencias","text":"

    Lo primero que vamos a hacer es a\u00f1adir las dependencias a algunas librer\u00edas que vamos a utilizar. Abriremos el fichero pom.xml que nos ha generado el Spring Initilizr y a\u00f1adiremos las siguientes l\u00edneas:

    pom.xml
    <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\nxsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n<modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>3.2.4</version>\n        <relativePath/> <!-- lookup parent from repository -->\n    </parent>\n\n    <groupId>com.ccsw</groupId>\n    <artifactId>tutorial</artifactId>\n    <version>0.0.1-SNAPSHOT</version>\n    <name>tutorial</name>\n    <description>Tutorial project for Spring Boot</description>\n\n    <properties>\n        <java.version>17</java.version>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-jpa</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springdoc</groupId>\n            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>\n            <version>2.0.3</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.hibernate</groupId>\n            <artifactId>hibernate-validator</artifactId>\n            <version>8.0.0.Final</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.modelmapper</groupId>\n            <artifactId>modelmapper</artifactId>\n            <version>3.1.1</version>\n        </dependency>\n\n        <dependency>\n            <groupId>com.h2database</groupId>\n            <artifactId>h2</artifactId>\n            <scope>runtime</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n

    Hemos a\u00f1adido las dependencias de que nos permite utilizar Open API para documentar nuestras APIs. Adem\u00e1s de esa dependencia, hemos a\u00f1adido una utilidad para hacer mapeos entre objetos y para configurar los servicios Rest. M\u00e1s adelante veremos como se utilizan.

    "},{"location":"install/springboot/#configurar-librerias","title":"Configurar librer\u00edas","text":"

    El siguiente punto es crear las clases de configuraci\u00f3n para las librer\u00edas que hemos a\u00f1adido. Para ello vamos a crear un package de configuraci\u00f3n general de la aplicaci\u00f3n com.ccsw.tutorial.config donde crearemos una clase que llamaremos ModelMapperConfig y usaremos para configurar el bean de ModelMapper.

    ModelMapperConfig.java
    package com.ccsw.tutorial.config;\n\nimport org.modelmapper.ModelMapper;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n/**\n * @author ccsw\n *\n */\n@Configuration\npublic class ModelMapperConfig {\n\n    @Bean\n    public ModelMapper getModelMapper() {\n\n        return new ModelMapper();\n    }\n\n}\n

    Esta configuraci\u00f3n nos permitir\u00e1 luego hacer transformaciones entre objetos de forma muy sencilla. Ya lo iremos viendo m\u00e1s adelante. Listo, ya podemos empezar a desarrollar nuestros servicios.

    "},{"location":"install/springboot/#configurar-la-bbdd","title":"Configurar la BBDD","text":"

    Por \u00faltimo, vamos a dejar configurada la BBDD en memoria. Para ello crearemos un fichero, de momento en blanco, dentro de src/main/resources/:

    • data.sql \u2192 Ser\u00e1 el fichero que utilizaremos para rellenar con datos iniciales el esquema de BBDD

    Este fichero no puede estar vac\u00edo, ya que si no dar\u00e1 un error al arrancar. Puedes a\u00f1adirle la siguiente query (que no hace nada) para que pueda arrancar el proyecto.

    select 1 from dual;

    Y ahora le vamos a decir a Spring Boot que la BBDD ser\u00e1 en memoria, que use un motor de H2 y que la cree autom\u00e1ticamente desde el modelo y que utilice el fichero data.sql (por defecto) para cargar datos en esta. Para ello hay que configurar el fichero application.properties que est\u00e1 dentro de src/main/resources/:

    application.properties
      #Database\n  spring.datasource.url=jdbc:h2:mem:testdb\n  spring.datasource.username=sa\n  spring.datasource.password=sa\n  spring.datasource.driver-class-name=org.h2.Driver\n\n  spring.jpa.database-platform=org.hibernate.dialect.H2Dialect\n  spring.jpa.defer-datasource-initialization=true\n  spring.jpa.show-sql=true\n\n  spring.h2.console.enabled=true\n
    "},{"location":"install/springboot/#arrancar-el-proyecto","title":"Arrancar el proyecto","text":"

    Por \u00faltimo ya solo nos queda arrancar el proyecto creado. Para ello buscaremos la clase TutorialApplication.java (o la clase principal del proyecto) y con el bot\u00f3n derecho seleccionaremos Run As \u2192 Java Application. La aplicaci\u00f3n al estar basada en Spring Boot arrancar\u00e1 internamente un Tomcat embebido donde se despliega el proyecto.

    Si hab\u00e9is seguido el tutorial la aplicaci\u00f3n estar\u00e1 disponible en http://localhost:8080, aunque de momento a\u00fan no tenemos nada accesible y nos dar\u00e1 una p\u00e1gina de error Whitelabel Error Page, error 404. Eso significa que el Tomcat embedido nos ha contestado pero no sabe que devolvernos porque no hemos implementado todav\u00eda nada.

    "},{"location":"install/vuejs/","title":"Entorno de desarrollo - Vue.js","text":""},{"location":"install/vuejs/#instalacion-de-herramientas","title":"Instalaci\u00f3n de herramientas","text":"

    Las herramientas b\u00e1sicas que vamos a utilizar para esta tecnolog\u00eda son:

    • Visual Studio Code
    • Scoop.sh
    • Nodejs
    • Quasar CLI
    "},{"location":"install/vuejs/#visual-studio-code","title":"Visual Studio Code","text":"

    Lo primero de todo es instalar el IDE para el desarrollo front.

    Te recomiendo utilizar Visual Studio Code, en un IDE que a nosotros nos gusta mucho y tiene muchos plugins configurables. Puedes entrar en su p\u00e1gina y descargarte la versi\u00f3n estable.

    "},{"location":"install/vuejs/#scoopsh","title":"Scoop.sh","text":"

    Muchas de las herramientas que necesitar\u00e1s a lo largo de tu estancia en los proyectos, no podr\u00e1s instalarlas por temas de permisos de seguridad en nuestros port\u00e1tiles. Una forma de evitar estos permisos de seguridad y poder instalar herramientas (sobre todo a nivel de consola), es utilizando a Scoop.sh.

    Para instalar scoop.sh tan solo necesitas abrir un termina de PowerShell (OJO que es PowerShell y no una consola de msdos) y ejecutar los siguientes comandos:

    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser\nInvoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression\n

    Esto nos instalar\u00e1 scoop.sh en nuestros port\u00e1tiles. Adem\u00e1s de la instalaci\u00f3n, y debido a las restricciones de seguridad que tenemos, necesitaremos activar el lessmsi para que las aplicaciones que necesitemos instalar no intenten ejecutar los .exe de instalaci\u00f3n, sino que descompriman el zip. Para ello deberemos ejecutar el comando:

    scoop config use_lessmsi true\n

    A partir de este punto, ya tenemos listo el port\u00e1til para instalar herramientas y aplicaciones.

    "},{"location":"install/vuejs/#nodejs","title":"Nodejs","text":"

    El siguiente paso ser\u00e1 instalar el motor de Nodejs. Para esto vamos a usar scoop.sh ya que lo tenemos instalado, y vamos a pedirle que nos instal\u00e9 el motor de Nodejs. Abriremos una consola de msdos y ejecutaremos el comando:

    scoop install main/nodejs\n

    Con esto, scoop ya nos instalar\u00e1 todo lo necesario.

    "},{"location":"install/vuejs/#creacion-de-proyecto","title":"Creaci\u00f3n de proyecto","text":""},{"location":"install/vuejs/#generar-scaffolding","title":"Generar scaffolding","text":"

    Lo primero que haremos ser\u00e1 generar un proyecto mediante la librer\u00eda \"Quasar CLI\" para ello ejecutamos en consola el siguiente comando:

    npm init quasar\n

    Este comando detectar\u00e1 si tienes el CLI de Quasar instalado y en caso contrario te preguntar\u00e1 si deseas instalarlo. Debes responder que s\u00ed, que lo instale.

    Una vez instalado, aparecer\u00e1 un wizzard en el que se ir\u00e1n preguntando una serie de datos para crear la aplicaci\u00f3n:

    Y tendremos que elegir lo siguiente:

    What would you like to build?

    App with Quasar CLI, let's go!

    Project folder

    tutorial-vue

    Pick Quasar version

    Quasar v2 (Vue 3 | latest and greatest)

    Pick script type

    Typescript

    Pick Quasar App CLI variant

    Quasar App CLI with Vite

    Package name

    tutorial-vue

    Project product name

    Ludoceta Tan

    Project description

    Proyecto tutorial Ludoteca Tan

    Author

    <por defecto el email>

    Pick a Vue component style

    Composition API

    Pick your CSS preprocessor

    Sass with SCSS syntax

    Check the features needed for your project

    ESLint

    Pick an ESLint preset

    Prettier

    Install project dependencies?

    Yes, use npm

    "},{"location":"install/vuejs/#arrancar-el-proyecto","title":"Arrancar el proyecto","text":"

    Cuando todo ha terminado el propio scaffolding te dice lo que tienes que hacer para poner el proyecto en marcha y ver lo que te ha generado, solo tienes que seguir esos pasos.

    Accedes al directorio que acabas de crear y ejecutas

    npx quasar dev\n

    Esto arrancar\u00e1 el servidor y abrir\u00e1 un navegador en el puerto 9000 donde se mostrar\u00e1 la template creada.

    Tambi\u00e9n podemos navegar nosotros mismos a la URL http://localhost:9000/.

    Info

    Si durante el desarrollo del proyecto necesitas a\u00f1adir nuevos m\u00f3dulos al proyecto Vue.js, ser\u00e1 necesario resolver las dependencias antes de arrancar el servidor. Esto se puede realizar mediante el gestor de dependencias de Nodejs, directamente en consola ejecuta el comando npm install y descargar\u00e1 e instalar\u00e1 las nuevas dependencias..

    Proyecto descargado

    Cuando se trata de un proyecto nuevo recien descargado de un repositorio, recuerda que ser\u00e1 necesario resolver las dependencias antes de arrancar el servidor. Esto se puede realizar mediante el gestor de dependencias de Nodejs, directamente en consola ejecuta el comando npm install y descargar\u00e1 e instalar\u00e1 las nuevas dependencias.

    "},{"location":"specs/customers_free/","title":"Gesti\u00f3n de clientes (modelo gratuito)","text":"

    Warning

    Esta secci\u00f3n se encuentra en desarrollo \ud83d\udea7. NO se recomienda realizarla a menos que te lo hayan indicado expresamente.

    "},{"location":"specs/customers_free/#punto-de-partida","title":"Punto de partida","text":"

    Si has llegado hasta aqu\u00ed, entiendo que ya has le\u00eddo tanto la introducci\u00f3n como la instalaci\u00f3n del entorno. A partir de ahora voy a dar por hecho que partimos todos desde el mismo punto y que tenemos ya la estructura de directorios creada y los proyectos descargados.

    Dicho esto, nos vamos a la consola (o desde el terminal de tu IDE), nos situamos en el directorio ra\u00edz y lanzamos el inicializador de OpenSpec.

    openspec init\n

    Seleccionamos GitHub Copilot, pulsamos Enter para a\u00f1adirlo y despu\u00e9s Tab para validar la selecci\u00f3n.

    Esto instalar\u00e1 las plantillas necesarias para poder trabajar con OpenSpec + GitHub Copilot.

    "},{"location":"specs/customers_free/#requisitos-funcionales","title":"Requisitos funcionales","text":"
    • CRUD de clientes.
    • Entidad cliente con id y name.
    • Listado simple sin filtros ni paginaci\u00f3n.
    • Alta/edici\u00f3n en modal.
    • El nombre es el \u00fanico campo editable.
    • No se permite guardar clientes con nombre duplicado.
    "},{"location":"specs/customers_free/#estrategia-del-modo-gratuito","title":"Estrategia del modo gratuito","text":"

    Vamos a trabajar con un modelo gratuito, as\u00ed que es importante tener claras sus limitaciones:

    • El contexto es muy limitado
    • El n\u00famero de operaciones mensuales tambi\u00e9n lo es
    • El n\u00famero de operaciones por hora tambi\u00e9n

    As\u00ed que, te pido paciencia ya que posiblemente no puedas hacer todo el tutorial completo en el mismo d\u00eda, deber\u00e1s trocearlo por exceso de l\u00edmite de peticiones.

    Para intengar mitigar un poco esto, vamos a:

    • Dividir el trabajo en tareas peque\u00f1as
    • Ser muy expl\u00edcitos en los prompts
    • Dar m\u00e1s contexto \u201cartificial\u201d al modelo

    Con un modelo de pago podr\u00edamos plantear el ejercicio de forma m\u00e1s generalista y con menos fragmentaci\u00f3n.

    El modelo que utilizaremos ser\u00e1 Claude Haiku. Para ello debes:

    1. Hacer login con tu cuenta de GitHub
    2. Activar GitHub Copilot
    3. En el chat, abrir el tercer desplegable
    4. Seleccionar el modelo Claude Haiku

    En cualquier momento puedes ver el consumo mensual de tu cuenta pulsando el icono de la rana \ud83d\udc38 en la esquina inferior derecha. El contador se reinicia cada mes.

    Vamos a dividir el ejercicio en dos grandes bloques:

    1. Primero trabajaremos \u00fanicamente con el backend
    2. Despu\u00e9s abordaremos el frontend

    De esta forma limitamos el contexto a un solo proyecto y facilitamos el trabajo al modelo.

    Adem\u00e1s, recuerda que el comportamiento del modelo no es determinista. Si a ti te genera algo diferente a lo que ves aqu\u00ed, probablemente seguir\u00e1 siendo v\u00e1lido. No te frustres y ajusta los prompts si es necesario.

    "},{"location":"specs/customers_free/#flujo-de-trabajo-openspec","title":"Flujo de trabajo OpenSpec","text":"

    Seguiremos el ciclo completo de :

    1. Explore\n2. Propose\n3. Apply\n4. Archive\n
    "},{"location":"specs/customers_free/#generacion-de-backend","title":"Generaci\u00f3n de backend","text":""},{"location":"specs/customers_free/#explore","title":"Explore","text":"

    El objetivo de esta fase es analizar el sistema existente, sin modificar nada.

    Buscamos:

    • Entender la estructura actual
    • Identificar patrones reutilizables
    • Comprender c\u00f3mo se comunican frontend y backend

    Aspectos a revisar:

    Organizaci\u00f3n por dominios

    • Estructura de carpetas
    • Dominios existentes

    Angular

    • Componentes
    • Servicios
    • Modelos
    • Routing

    Spring Boot

    • Controller
    • Service
    • Repository
    • Entity
    • DTO

    Patr\u00f3n CRUD

    • Listados
    • Creaci\u00f3n / edici\u00f3n
    • Borrado

    Conexi\u00f3n frontend-backend

    • Endpoints
    • URLs
    • DTOs

    Reutilizaci\u00f3n

    • C\u00f3digo com\u00fan
    • Patrones repetidos

    \u26a0\ufe0f En esta fase:

    • NO se escribe c\u00f3digo
    • NO se dise\u00f1a la soluci\u00f3n
    • NO se inventan estructuras nuevas

    Solo se analiza el sistema actual.

    \ud83d\udcdc Prompt

    Lo que haremos ser\u00e1 escribir en el chat de Visual Studio Code el comando y las instrucciones que queramos darle. Recuerda haber elegido Claude Haiku.

    /opsx:explore\n\nAnaliza el proyecto actual que est\u00e1 en el directorio 'backend', es una aplicaci\u00f3n Spring Boot. Una vez analizado, responde:\n\n1. \u00bfC\u00f3mo est\u00e1n implementados los CRUD existentes?\n- Controller\n- Service\n- Repository\n\n2. \u00bfQu\u00e9 estructura siguen los dominios?\n\n3. \u00bfC\u00f3mo se implementan las operaciones?\n- Listado\n- Creaci\u00f3n/edici\u00f3n\n- Borrado\n\n4. \u00bfQu\u00e9 formato tienen los endpoints y que relaci\u00f3n tiene con los m\u00e9todos HTTP?\n\n5. \u00bfQu\u00e9 patrones o estructuras comunes se repiten en los CRUD existentes?\n- Clases reutilizables\n- L\u00f3gica repetida\n- Estructuras comunes entre dominios\n\n6. \u00bfExisten test unitarios y de integraci\u00f3n? \u00bfC\u00f3mo est\u00e1n implementados? \u00bfUtiliza algo especial al arrancar o al mockear?\n\nAnaliza \u00fanicamente la parte de backend (Spring Boot)\nNO propongas soluciones.\nNO dise\u00f1es nuevas funcionalidades.\nSolo analiza el sistema actual.\nEscr\u00edbe el resultado del contexto dentro del fichero backend-explore.md en el directorio de las specs para poder utilizarlo en siguientes fases.\n

    Este comando realizar\u00e1 un an\u00e1lisis exhaustivo de tu sistema y lo dejar\u00e1 escrito dentro de la carpeta specs en un fichero llamado backend-explore.md. Al final te mostrar\u00e1 un texto con el resumen del sistema y adem\u00e1s escribir\u00e1 el fichero de explore.

    Sobre los permisos

    Es posible que durante el an\u00e1lisis te pida permiso para hacer ciertas tareas. Le puedes ir dando permiso una a una o darle permiso en todo el workspace, eso lo dejamos a tu elecci\u00f3n.

    En cualquier momento puedes ver el consumo de la ventana de contexto para saber si todo el conocimiento del sistema est\u00e1 en memoria o no. En el icono de la gr\u00e1fica de pastel que est\u00e1 dentro del chat en la parte superior derecha.

    "},{"location":"specs/customers_free/#propose","title":"Propose","text":"

    En esta fase definimos qu\u00e9 vamos a construir, bas\u00e1ndonos estrictamente en el resultado del Explore.

    Durante esta fase debes especificar.

    Descripci\u00f3n funcional

    • Qu\u00e9 hace la funcionalidad
    • Qu\u00e9 problema resuelve

    Reglas de negocio

    • Validaciones
    • Restricciones
    • Comportamientos esperados

    Dise\u00f1o backend

    • Endpoints necesarios
    • Estructura del dominio (Entity, DTO, Service, Repository)
    • Tipo de operaciones (listado, creaci\u00f3n, edici\u00f3n, borrado)

    Dise\u00f1o frontend

    • Componentes necesarios
    • Flujo de usuario (listado, abrir modal, guardar, borrar)
    • Servicios Angular

    Decisiones t\u00e9cnicas

    • Qu\u00e9 patrones existentes se reutilizan
    • Qu\u00e9 se mantiene igual que en otros dominios
    • Qu\u00e9 diferencias introduce esta funcionalidad

    Plan de implementaci\u00f3n

    • Tareas ordenadas
    • Separaci\u00f3n backend / frontend

    Aqu\u00ed dejamos claro:

    • Qu\u00e9 funcionalidad se va a a\u00f1adir
    • Qu\u00e9 reglas de negocio existen
    • Qu\u00e9 piezas del sistema se ven afectadas
    • Qu\u00e9 tareas habr\u00e1 que ejecutar

    \u26a0\ufe0f En esta fase:

    • NO se implementa c\u00f3digo
    • NO se redefine el sistema

    \ud83d\udcdc Prompt

    Para nuestro ejemplo, lo que haremos ser\u00e1 escribir en el chat de Visual Studio Code el siguiente prompt:

    /opsx:propose manage-customers-backend\n\nDefine la funcionalidad de gesti\u00f3n de clientes bas\u00e1ndote en el sistema actual y en los patrones identificados en la fase Explore, tienes el resultado en el fichero \"backend-explore.md\".\n\nNos han pedido esta nueva funcionalidad.\n\nPor un lado necesita poder tener una base de datos de sus clientes. Para ello nos ha pedido que si podemos crearle una pantalla de CRUD sencilla, al igual que hicimos con las categor\u00edas donde \u00e9l pueda dar de alta a sus clientes.\n\nNos ha pasado un esquema muy sencillo de lo que quiere, tan solo quiere guardar un listado de los nombres de sus clientes para tenerlos fichados, y nos ha hecho un par de pantallas sencillas muy similares a Categor\u00edas.\n\nUn listado sin filtros de ning\u00fan tipo ni paginaci\u00f3n.\n\nUn formulario de edici\u00f3n / alta, cuyo \u00fanico dato editable sea el nombre. Adem\u00e1s, la \u00fanica restricci\u00f3n que nos ha pedido es que NO podamos dar de alta a un cliente con el mismo nombre que otro existente. As\u00ed que deberemos comprobar el nombre, antes de guardar el cliente.\n\nPara empezar te dar\u00e9 unos consejos:\n- Recuerda crear la tabla de la BBDD y sus datos\n- Intenta primero hacer el listado completo, en el orden que m\u00e1s te guste: frontend o backend.\n- Completa el listado conectando ambas capas.\n- Termina el caso de uso haciendo las funcionalidades de edici\u00f3n, nuevo y borrado. Presta atenci\u00f3n a la validaci\u00f3n a la hora de guardar un cliente, NO se puede guardar si el nombre ya existe.\n\n\nTe voy a dar otras directrices que pienso que te pueden servir:\n- Se necesita un CRUD de clientes\n- Un cliente solo tiene: id, name\n- El listado ser\u00e1 simple, sin filtros ni paginaci\u00f3n\n- Existir\u00e1 un formulario de alta / edici\u00f3n en modal\n- El \u00fanico campo editable ser\u00e1 el nombre\n- No se puede crear un cliente con un nombre ya existente, ser\u00e1 una validaci\u00f3n obligatoria que deberemos cumplir en el guardado\n\n\nNecesito que definas:\n\n1. Descripci\u00f3n de la funcionalidad\n\n2. Reglas de negocio\n\n3. Dise\u00f1o backend:\n- Endpoints necesarios\n- Estructura del dominio (Entity, DTO, Service, Repository)\n\n4. Decisiones t\u00e9cnicas:\n- Qu\u00e9 patrones del sistema actual se reutilizan\n\n\nNO implementes c\u00f3digo.\nNO analices de nuevo el proyecto.\nBasa la propuesta en los patrones detectados en la fase Explore.\nHaz la propuesta \u00fanicamente de backend.\nComo \u00faltima tarea a\u00f1ade al fichero de tasks generar un resumen del cambio realizado, con el contrato de los endpoints y la informaci\u00f3n necesaria para que luego el frontend pueda implementar sus llamadas de forma sencilla.\n\nTendr\u00e1s que escribir los ficheros de proposal, design, spec y tasks en la propuesta correspondiente.\n

    De nuevo al ser un modelo gratuito tenemos que delimitarle mucho las tareas y recordarle que debe generar los ficheros de proposal, design, spec y tasks, adem\u00e1s de basarse en el fichero de backend-explore. Tambi\u00e9n debemos centrarle para que SOLO genere la parte de backend.

    Por \u00faltimo si te fijas en el prompt hay una tarea que le indica claramente que genere un fichero con los contratos de los endpoints para poder implementar, en un futuro, la parte frontend. Esto es solamente una idea de generar un resumen para que el frontend sepa como comunicarse con el backend.

    Este paso debe generar una propuesta dentro de changes con los artefactos proposal, design, spec y tasks.

    Si necesitas recordar el papel de cada artefacto, revisa la introducci\u00f3n.

    Responsabilidades como developer IA

    En este punto la IA te ha hecho una propuesta que puede ser correcta o no, recordemos que se trata de un modelo matem\u00e1tico-probabil\u00edstico. Si hay algo de lo propuesto que no te encaja o es err\u00f3neo deber\u00edas comentarlo mediante el chat o corregirlo de forma manual en el fichero que corresponda. Por ejemplo si quieres a\u00f1adir una tarea porqu\u00e9 se te ha olvidado incluirla en el prompt original, deber\u00edas decirle al modelo que te incluya la nueva tarea.

    Una vez estemos de acuerdo con la propuesta que nos ha hecho la IA, podemos pasar al siguiente punto.

    "},{"location":"specs/customers_free/#apply","title":"Apply","text":"

    Una vez validada la propuesta, ejecutamos la implementaci\u00f3n:

    El objetivo de esta fase es transformar los artefactos generados (proposal.md, design.md, spec.md, tasks.md) en c\u00f3digo funcional, asegurando que:

    • Se respetan los requisitos funcionales definidos en spec.md
    • Se siguen las decisiones t\u00e9cnicas establecidas en design.md
    • Se ejecutan las tareas en el orden definido en tasks.md

    \ud83d\udcdc Prompt

    Esto es tan f\u00e1cil como escribir en el chat de Visual Studio Code el siguiente prompt:

    /opsx:apply\n

    El agente empezar\u00e1 a realizar un mont\u00f3n de tareas y pedirnos permisos. Es posible que algunas de esas tareas fallen y \u00e9l mismo lo reintente de otra forma. El resultado deber\u00eda ser el c\u00f3digo generado e implementado dentro de la carpeta de backend y un resumen de todas las tareas realizadas y checkeadas por la IA.

    "},{"location":"specs/customers_free/#verificacion-del-backend","title":"Verificaci\u00f3n del backend","text":"

    Un paso que no pertenece a OpenSpec pero que es altamente recomendable es probar los cambios realizados. Arranca el backend y verifica:

    • Que el servidor levanta
    • Que los endpoints existen y funcionan
    • Que los tests pasan

    Ojo no te fies

    Ojo no te fies de todo lo que construya la IA. Tu est\u00e1s al mando, tu debes decidir si el sistema est\u00e1 correctamente implementado o no. Es tu responsabilidad.

    Si NO est\u00e1s a gusto con la implementaci\u00f3n o se ha dejado algo por hacer, es el momento de escribirlo por el chat indic\u00e1ndole exactamente que es lo que falta. Cuanto m\u00e1s preciso y conciso seas, mejor implementar\u00e1 la IA.

    "},{"location":"specs/customers_free/#archive","title":"Archive","text":"

    Y llegamos a la \u00faltima etapa que nos define OpenSpec, donde se archiva el cambio y se da por finalizada la funcionalidad.

    El objetivo de esta fase es marcar la funcionalidad como completada, consolidar todos los artefactos generados durante el proceso y dejar el sistema en un estado estable, coherente y preparado para nuevas evoluciones.

    En esta fase se asegura que:

    • La funcionalidad ha sido correctamente implementada y validada
    • No existen incidencias cr\u00edticas pendientes
    • La documentaci\u00f3n asociada al cambio est\u00e1 completa y actualizada
    • Existe una trazabilidad entre requisitos, dise\u00f1o e implementaci\u00f3n

    Aunque parezca mentira, este paso es muy importante ya que nos servir\u00e1 para actualizar el contexto del sistema y archivar todos los cambios para futuras consultas.

    \ud83d\udcdc Prompt

    De nuevo nos vamos al chat de Visual Studio Code el siguiente prompt:

    /opsx:archive\n

    En este punto el sistema pedir\u00e1 confirmaci\u00f3n para sincronizar requisitos antes de archivar.

    Sincronizar es obligatorio si quieres mantener trazabilidad entre lo implementado y los specs oficiales.

    \ud83d\udcdc Actualizaci\u00f3n del contexto

    Adem\u00e1s, para forzar al modelo gratuito y dejarlo todo listo, es recomendable lanzar un \u00faltimo prompt que nos actualice el fichero de backend-explore.md

    Actualiza el fichero de backend-explore con los nuevos datos implementados\n
    "},{"location":"specs/customers_free/#generacion-de-frontend","title":"Generaci\u00f3n de frontend","text":"

    Bueno, pues ahora que ya tenemos el backend implementado, realizaremos de nuevo un ciclo completo de OpenSpec pero est\u00e1 vez para frontend:

    1. Explore\n2. Propose\n3. Apply\n4. Archive\n

    Es muy importante que cada nuevo cambio que hagamos, lo empecemos en un chat nuevo, para limpiar el contexto anterior y no arrastrar posibles errores o incoherencias.

    "},{"location":"specs/customers_free/#explore_1","title":"Explore","text":"

    De nuevo el objetivo de esta fase es analizar el sistema existente, sin modificar nada, pero esta vez nos centraremos en el frontend.

    \ud83d\udcdc Prompt

    Vamos a un nuevo chat de Visual Studio Code y escribimos el comando:

    opsx:explore\n\nAnaliza el proyecto actual que est\u00e1 en el directorio \"frontend\", es una aplicaci\u00f3n Angular. Ojo no escanees la carpeta de \"node_modules\" no tiene sentido. Una vez analizado, responde:\n\n1. \u00bfC\u00f3mo est\u00e1n implementados los CRUD existentes?\n- Componentes\n- Servicios\n- Modelos\n\n2. \u00bfQu\u00e9 estructura siguen los dominios?\n\n3. \u00bfC\u00f3mo se implementan las operaciones?\n- Listado\n- Creaci\u00f3n/edici\u00f3n\n- Borrado\n- C\u00f3mo funcionan las ventanas de creaci\u00f3n y edici\u00f3n (modales)\n\n4. \u00bfComo se comunican frontend con backend?\n- Servicios en Angular\n- Construcci\u00f3n de URLs\n\n5. \u00bfQu\u00e9 patrones o estructuras comunes se repiten en los CRUD existentes?\n- Clases reutilizables\n- L\u00f3gica repetida\n- Estructuras comunes entre dominios\n\n\nAnaliza \u00fanicamente la parte de frontend (Angular)\nNO propongas soluciones.\nNO dise\u00f1es nuevas funcionalidades.\nSolo analiza el sistema actual.\nEscr\u00edbe el resultado del contexto dentro del fichero frontend-explore.md en el directorio de las specs para poder utilizarlo en siguientes fases.\n

    Este comando realizar\u00e1 un an\u00e1lisis exhaustivo de tu sistema y lo dejar\u00e1 escrito dentro de la carpeta specs en un fichero llamado frontend-explore.md. Al final te mostrar\u00e1 un texto con el resumen del sistema y adem\u00e1s escribir\u00e1 el fichero de explore. Este an\u00e1lisis estar\u00e1 m\u00e1s centrado en el frontend y debes pedirle que compruebe como se comunica con el backend para que lo tenga en cuenta.

    "},{"location":"specs/customers_free/#propose_1","title":"Propose","text":"

    Una vez definido el an\u00e1lisis inicial, lo siguiente es pedirle una propuesta de lo que queremos construir.

    \ud83d\udcdc Prompt

    De nuevo en el chat de Visual Studio Code escribimos el comando:

    /opsx:propose manage-customers-frontend\n\nDefine la funcionalidad de gesti\u00f3n de clientes bas\u00e1ndote en el sistema actual y en los patrones identificados en la fase Explore, tienes el resultado en el fichero \"frontend-explore.md\". Adem\u00e1s tendr\u00e1s que ver el cambio realizado en la spec de \"manage-customers-backend\", sobre todo los endpoints generados que lo tienes definido en \"FRONTEND_API_CONTRACT.md\"\n\nNos han pedido esta nueva funcionalidad.\n\nPor un lado necesita poder tener una base de datos de sus clientes. Para ello nos ha pedido que si podemos crearle una pantalla de CRUD sencilla, al igual que hicimos con las categor\u00edas donde \u00e9l pueda dar de alta a sus clientes.\n\nNos ha pasado un esquema muy sencillo de lo que quiere, tan solo quiere guardar un listado de los nombres de sus clientes para tenerlos fichados, y nos ha hecho un par de pantallas sencillas muy similares a Categor\u00edas.\n\nUn listado sin filtros de ning\u00fan tipo ni paginaci\u00f3n.\n\nUn formulario de edici\u00f3n / alta, cuyo \u00fanico dato editable sea el nombre. Adem\u00e1s, la \u00fanica restricci\u00f3n que nos ha pedido es que NO podamos dar de alta a un cliente con el mismo nombre que otro existente. As\u00ed que deberemos comprobar el nombre, antes de guardar el cliente.\n\nPara empezar te dar\u00e9 unos consejos:\n- Recuerda crear la tabla de la BBDD y sus datos\n- Intenta primero hacer el listado completo, en el orden que m\u00e1s te guste: frontend o backend.\n- Completa el listado conectando ambas capas.\n- Termina el caso de uso haciendo las funcionalidades de edici\u00f3n, nuevo y borrado. Presta atenci\u00f3n a la validaci\u00f3n a la hora de guardar un cliente, NO se puede guardar si el nombre ya existe.\n\n\nTe voy a dar otras directrices que pienso que te pueden servir:\n- Se necesita un CRUD de clientes\n- Un cliente solo tiene: id, name\n- El listado ser\u00e1 simple, sin filtros ni paginaci\u00f3n\n- Existir\u00e1 un formulario de alta / edici\u00f3n en modal\n- El \u00fanico campo editable ser\u00e1 el nombre\n- No se puede crear un cliente con un nombre ya existente, ser\u00e1 una validaci\u00f3n obligatoria que deberemos cumplir en el guardado\n\n\nNecesito que definas:\n\n1. Descripci\u00f3n de la funcionalidad\n\n2. Reglas de negocio\n\n3. Dise\u00f1o frontend:\n- Componentes necesarios\n- Flujos de interacci\u00f3n (listado, abrir modal, guardar borrar)\n\n4. Uso de endpoints para llamar a backend\n\n5. Decisiones t\u00e9cnicas:\n- Qu\u00e9 patrones del sistema actual se reutilizan\n\n\nNO implementes c\u00f3digo.\nNO analices de nuevo el proyecto.\nBasa la propuesta en los patrones detectados en la fase Explore.\nHaz la propuesta \u00fanicamente de frontend.\nOlv\u00eddate de los test, en frontend no tenemos tests.\nA\u00f1ade el nuevo punto de men\u00fa en el header para que se pueda acceder.\nNo te inventes estilos, respeta los estilos de las pantallas (anchuras, alturas, colores, disposici\u00f3n de las tablas).\nUtiliza los alert dialog de Angular Material para las alertas, no uses la opci\u00f3n alert() del navegador.\n\nTendr\u00e1s que escribir los ficheros de proposal, design, spec y tasks en la propuesta correspondiente.\n

    Puntos a destacar de este prompt:

    • Adem\u00e1s del contexto inicial le hemos pedido que busque el fichero de FRONTEND_API_CONTRACT.md que es el fichero que generamos junto con el backend y que tiene las reglas de los endpoints del backend. Esto lo tenemos que hacer as\u00ed ya que el modelo es gratuito y tiene limitaciones, no puede analizar frontend y backend a la vez en el mismo contexto.
    • Tambi\u00e9n le hemos pedido que NO invente estilos y que se fije en las pantallas existentes, adem\u00e1s de decirle que utilice componentes de Angular Material. A veces los modelos gratuitos tienden a inventar mucho, por falta de contexto. De nuevo cuanto m\u00e1s concretos y precisos seamos, mejor implementar\u00e1.
    "},{"location":"specs/customers_free/#apply_1","title":"Apply","text":"

    Cuando estemos de acuerdo con la propuesta que nos ha hecho la IA y sobre todo con las tasks que propone realizar, lanzamos la fase de implementaci\u00f3n.

    \ud83d\udcdc Prompt

    Esto es tan f\u00e1cil como escribir en el chat de Visual Studio Code el siguiente prompt:

    /opsx:apply\n

    El agente empezar\u00e1 a realizar un mont\u00f3n de tareas y pedirnos permisos. Es posible que algunas de esas tareas fallen y \u00e9l mismo lo reintente de otra forma. El resultado deber\u00eda ser el c\u00f3digo generado e implementado dentro de la carpeta de frontend y un resumen de todas las tareas realizadas y checkeadas por la IA.

    "},{"location":"specs/customers_free/#verificacion-del-frontend","title":"Verificaci\u00f3n del frontend","text":"

    Ahora s\u00ed, prueba de \ud83d\udd25 fuego \ud83d\udd25. Es hora de levantar el sistema completo, frontend y backend, navegar por la aplicaci\u00f3n y comprobar que todo funciona.

    Si algo no encaja, es buen momento para conversarlo con la IA y que realice los cambios necesarios hasta conseguir que todo funcione perfectamente.

    "},{"location":"specs/customers_free/#archive_1","title":"Archive","text":"

    Una vez tengamos todo funcionando y perfectamente implementado, pasamos a la \u00faltima etapa para archivar y sincronizar nuestro cambio.

    \ud83d\udcdc Prompt

    De nuevo nos vamos al chat de Visual Studio Code el siguiente prompt:

    /opsx:archive\n

    En ese caso, el sistema solicita confirmaci\u00f3n para sincronizar los requisitos antes de archivar el cambio.

    \ud83d\udcdc Actualizaci\u00f3n del contexto

    Y por \u00faltimo forzamos una actualizaci\u00f3n del contexto inicial.

    Actualiza el fichero de frontend-explore con los nuevos datos implementados\n
    "},{"location":"specs/customers_paid/","title":"Gesti\u00f3n de clientes (modelo con licencia)","text":"

    Warning

    Esta secci\u00f3n se encuentra en desarrollo \ud83d\udea7. NO se recomienda realizarla a menos que te lo hayan indicado expresamente.

    "},{"location":"specs/customers_paid/#punto-de-partida","title":"Punto de partida","text":"

    Si has llegado hasta aqu\u00ed, entiendo que ya has le\u00eddo tanto la introducci\u00f3n como la instalaci\u00f3n del entorno. A partir de ahora voy a dar por hecho que partimos todos desde el mismo punto y que tenemos ya la estructura de directorios creada y los proyectos descargados.

    Dicho esto, nos vamos a la consola (o desde el terminal de tu IDE), nos situamos en el directorio ra\u00edz y lanzamos el inicializador de OpenSpec.

    openspec init\n

    Seleccionamos GitHub Copilot, pulsamos Enter para a\u00f1adirlo y despu\u00e9s Tab para validar la selecci\u00f3n.

    Esto instalar\u00e1 las plantillas necesarias para poder trabajar con OpenSpec + GitHub Copilot.

    "},{"location":"specs/customers_paid/#requisitos-funcionales","title":"Requisitos funcionales","text":"
    • CRUD de clientes.
    • Entidad cliente con id y name.
    • Listado simple sin filtros ni paginaci\u00f3n.
    • Alta/edici\u00f3n en modal.
    • El nombre es el \u00fanico campo editable.
    • No se permite guardar clientes con nombre duplicado.
    "},{"location":"specs/customers_paid/#estrategia-del-modo-con-licencia","title":"Estrategia del modo con licencia","text":"

    Vamos a trabajar con un modelo de pago, por lo que es importante entender qu\u00e9 ventajas nos ofrece frente al modelo gratuito.

    En este caso podremos:

    • Trabajar con mayor contexto
    • Analizar frontend y backend simult\u00e1neamente
    • Reducir la fragmentaci\u00f3n de tareas
    • Obtener propuestas e implementaciones m\u00e1s completas y robustas

    El modelo que utilizaremos ser\u00e1 Claude Sonnet 4.6. Para ello debes:

    1. Tener una cuenta premium con acceso a modelos de pago
    2. Hacer login con tu cuenta de GitHub
    3. Activar GitHub Copilot
    4. En el chat, abrir el tercer desplegable
    5. Seleccionar el modelo Claude Sonnet 4.6

    En cualquier momento puedes ver el consumo mensual de tu cuenta pulsando el icono de la rana \ud83d\udc38 en la esquina inferior derecha. El contador se reinicia cada mes.

    Vamos a abordar el ejercicio como un \u00fanico bloque de trabajo, analizando y construyendo la funcionalidad de forma simult\u00e1nea en backend y frontend.

    De esta manera aprovechamos el mayor contexto del modelo de pago, permitiendo:

    1. Analizar backend y frontend al mismo tiempo
    2. Dise\u00f1ar la funcionalidad de forma coherente en ambas capas desde el inicio

    Esto nos permite mantener una visi\u00f3n global del sistema durante todo el proceso y reducir la necesidad de dividir artificialmente el trabajo en fases independientes por capa.

    Adem\u00e1s, recuerda que el comportamiento del modelo no es determinista. Si a ti te genera algo diferente a lo que ves aqu\u00ed, probablemente seguir\u00e1 siendo v\u00e1lido. No te frustres y ajusta los prompts si es necesario.

    "},{"location":"specs/customers_paid/#flujo-de-trabajo-openspec","title":"Flujo de trabajo OpenSpec","text":"

    Seguiremos el ciclo completo de OpenSpec:

    1. Explore\n2. Propose\n3. Apply\n4. Archive\n
    "},{"location":"specs/customers_paid/#explore","title":"Explore","text":"

    El objetivo de esta fase es analizar el sistema existente, sin modificar nada.

    Buscamos:

    • Entender la estructura actual de la aplicaci\u00f3n
    • Identificar patrones y estructuras reutilizables
    • Comprender c\u00f3mo se comunican frontend y backend

    Aspectos a revisar:

    Organizaci\u00f3n por dominios

    • C\u00f3mo est\u00e1n estructurados los dominios existentes (category, author, game\u2026)
    • Qu\u00e9 carpetas existen en frontend y backend
    • C\u00f3mo se relacionan los dominios entre capas

    Angular

    • Componentes
    • Servicios
    • Modelos
    • Routing

    Spring Boot

    • Controller
    • Service
    • Repository
    • Entity
    • DTO

    Patr\u00f3n CRUD

    • C\u00f3mo se implementan los listados
    • C\u00f3mo se implementan las operaciones de creaci\u00f3n, edici\u00f3n y borrado
    • C\u00f3mo funcionan las ventanas de creaci\u00f3n y edici\u00f3n (modales)

    Conexi\u00f3n frontend-backend

    • C\u00f3mo Angular llama a los endpoints
    • C\u00f3mo se construyen las URLs
    • Qu\u00e9 DTOs se utilizan en la comunicaci\u00f3n

    Reutilizaci\u00f3n

    • C\u00f3digo com\u00fan entre dominios
    • Patrones repetidos
    • Estructuras compartidas entre diferentes funcionalidades

    \u26a0\ufe0f En esta fase:

    • NO se escribe c\u00f3digo
    • NO se dise\u00f1a la soluci\u00f3n
    • NO se inventan estructuras nuevas

    Solo se analiza el sistema actual.

    \ud83d\udcdc Prompt

    Lo que haremos ser\u00e1 escribir en el chat de Visual Studio Code el comando y las instrucciones que queramos darle. Recuerda haber elegido Claude Sonnet 4.6 y estar trabajando en modo Agent.

    En este caso, hemos a\u00f1adido las carpetas del proyecto frontend y backend al contexto, por lo que el an\u00e1lisis se realizar\u00e1 sobre el sistema completo.

    Para ello, desde el propio Chat de Copilot, pulsando el bot\u00f3n \u201c+\u201d, puedes seleccionar y a\u00f1adir tanto archivos individuales como directorios completos del proyecto. Tambi\u00e9n es posible a\u00f1adirlos arrastr\u00e1ndolos directamente al chat.

    /opsx:explore\n\nAnaliza el proyecto actual (Angular 17 + Spring Boot) que se ha a\u00f1adido al contexto. Una vez analizado, responde:\n\n1. \u00bfC\u00f3mo est\u00e1n implementados los CRUD existentes?\n- Backend: Controller, Service, Repository\n- Frontend: componentes, servicios y modelo\n\n2. \u00bfQu\u00e9 estructura siguen los dominios en backend y en frontend?\n\n3. \u00bfC\u00f3mo se implementan las operaciones?\n- Listado\n- Creaci\u00f3n/edici\u00f3n\n- Borrado\n- C\u00f3mo funcionan las ventanas de creaci\u00f3n y edici\u00f3n (modales)\n\n4. \u00bfC\u00f3mo se comunican frontend y backend?\n- Servicios Angular\n- Construcci\u00f3n de URLs\n\n5. \u00bfQu\u00e9 patrones o estructuras comunes se repiten en los CRUD existentes?\n- Clases reutilizables\n- L\u00f3gica repetida\n- Estructuras comunes entre dominios\n\nNO propongas soluciones.\nNO dise\u00f1es nuevas funcionalidades.\nSolo analiza el sistema actual.\n

    Este comando realizar\u00e1 un an\u00e1lisis exhaustivo de tu sistema que servir\u00e1 como base para definir la nueva funcionalidad en la siguiente fase.

    Sobre los permisos

    Es posible que durante el an\u00e1lisis te pida permiso para hacer ciertas tareas. Le puedes ir dando permiso una a una o darle permiso en todo el workspace, eso lo dejamos a tu elecci\u00f3n.

    En cualquier momento puedes ver el consumo de la ventana de contexto para saber si todo el conocimiento del sistema est\u00e1 en memoria o no. En el icono de la gr\u00e1fica circular que est\u00e1 situada en la parte inferior derecha del chat.

    "},{"location":"specs/customers_paid/#propose","title":"Propose","text":"

    Una vez analizado el sistema en la fase Explore, el siguiente paso es definir de forma clara y estructurada la nueva funcionalidad a implementar.

    En esta fase establecemos qu\u00e9 vamos a construir, bas\u00e1ndonos estrictamente en el resultado del Explore y aprovechando que el modelo dispone de una visi\u00f3n completa del sistema (frontend y backend).

    Esta fase act\u00faa como puente entre el an\u00e1lisis y la implementaci\u00f3n, permitiendo dise\u00f1ar la soluci\u00f3n antes de escribir c\u00f3digo y reduciendo el riesgo de errores durante el desarrollo.

    Durante esta fase debes especificar:

    Descripci\u00f3n funcional

    • Qu\u00e9 hace la funcionalidad
    • Qu\u00e9 problema resuelve

    Reglas de negocio

    • Validaciones
    • Restricciones
    • Comportamientos esperados

    Dise\u00f1o backend

    • Endpoints necesarios
    • Estructura del dominio (Entity, DTO, Service, Repository)
    • Tipo de operaciones (listado, creaci\u00f3n, edici\u00f3n, borrado)

    Dise\u00f1o frontend

    • Componentes necesarios
    • Flujo de usuario (listado, abrir modal, guardar, borrar)
    • Servicios Angular

    Decisiones t\u00e9cnicas

    • Qu\u00e9 patrones existentes se reutilizan
    • Qu\u00e9 se mantiene igual que en otros dominios
    • Qu\u00e9 diferencias introduce esta funcionalidad

    Plan de implementaci\u00f3n

    • Tareas ordenadas
    • Separaci\u00f3n backend / frontend

    Aqu\u00ed dejamos claro:

    • Qu\u00e9 funcionalidad se va a a\u00f1adir
    • Qu\u00e9 reglas de negocio existen
    • Qu\u00e9 piezas del sistema se ven afectadas
    • Qu\u00e9 tareas habr\u00e1 que ejecutar

    \u26a0\ufe0f En esta fase:

    • NO se implementa c\u00f3digo
    • NO se redefine el sistema

    \ud83d\udcdc Prompt

    Recuerda que seguimos trabajando en modo Agent, con las carpetas del proyecto frontend y backend a\u00f1adidas al contexto.

    Para nuestro ejemplo, lo que haremos ser\u00e1 escribir en el chat de Visual Studio Code el siguiente prompt:

    /opsx:propose client\n\nDefine la funcionalidad de gesti\u00f3n de clientes bas\u00e1ndote en el sistema actual (Angular 17 + Spring Boot) y en los patrones identificados en la fase Explore.\n\nRequisitos funcionales:\n- Se necesita un CRUD de clientes\n- Un cliente solo tiene: id, name\n- El listado ser\u00e1 simple, sin filtros ni paginaci\u00f3n\n- Existir\u00e1 un formulario de alta / edici\u00f3n en modal\n- El \u00fanico campo editable ser\u00e1 el nombre\n- No se puede crear un cliente con un nombre ya existente (validaci\u00f3n obligatoria)\n\nDefine:\n\n1. Descripci\u00f3n de la funcionalidad\n\n2. Reglas de negocio\n\n3. Dise\u00f1o backend:\n- Endpoints necesarios\n- Estructura del dominio (Entity, DTO, Service, Repository)\n\n4. Dise\u00f1o frontend:\n- Componentes necesarios\n- Flujo de interacci\u00f3n (listado, abrir modal, guardar, borrar)\n\n5. Decisiones t\u00e9cnicas:\n- Qu\u00e9 patrones del sistema actual se reutilizan\n\nNO implementes c\u00f3digo.\nNO analices de nuevo el proyecto.\nBasa la propuesta en los patrones detectados en la fase Explore.\n

    Este comando debe generar una propuesta en changes con los artefactos proposal, design, spec y tasks.

    Si necesitas recordar el papel de cada artefacto, revisa la introducci\u00f3n.

    Responsabilidades como developer IA

    En este punto la IA te ha hecho una propuesta que puede ser correcta o no, recordemos que se trata de un modelo matem\u00e1tico-probabil\u00edstico. Si hay algo de lo propuesto que no te encaja o es err\u00f3neo deber\u00edas comentarlo mediante el chat o corregirlo de forma manual en el fichero que corresponda. Por ejemplo si quieres a\u00f1adir una tarea porqu\u00e9 se te ha olvidado incluirla en el prompt original, deber\u00edas decirle al modelo que te incluya la nueva tarea.

    Una vez estemos de acuerdo con la propuesta que nos ha hecho la IA, podemos pasar al siguiente punto.

    "},{"location":"specs/customers_paid/#apply","title":"Apply","text":"

    Una vez validada la propuesta, ejecutamos la implementaci\u00f3n:

    El objetivo de esta fase es transformar los artefactos generados (proposal.md, design.md, spec.md, tasks.md) en c\u00f3digo funcional, asegurando que:

    • Se respetan los requisitos funcionales definidos en spec.md
    • Se siguen las decisiones t\u00e9cnicas establecidas en design.md
    • Se ejecutan las tareas en el orden definido en tasks.md

    \ud83d\udcdc Prompt

    Esto es tan f\u00e1cil como escribir en el chat de Visual Studio Code el siguiente prompt:

    /opsx:apply\n

    El agente empezar\u00e1 a realizar un mont\u00f3n de tareas y pedirnos permisos. Es posible que algunas de esas tareas fallen y \u00e9l mismo lo reintente de otra forma. El resultado deber\u00eda ser el c\u00f3digo generado e implementado tanto en la carpeta backend como en la carpeta frontend y un resumen de todas las tareas realizadas y checkeadas por la IA.

    "},{"location":"specs/customers_paid/#verificacion","title":"Verificaci\u00f3n","text":"

    Un paso que no pertenece a OpenSpec pero que es altamente recomendable es probar los cambios realizados.

    Arranca el backend y el frontend y verifica:

    • La aplicaci\u00f3n levanta correctamente
    • Las nuevas funcionalidades a\u00f1adidas est\u00e1n accesibles
    • Los flujos principales definidos en spec.md funcionan como se espera

    Ojo no te fies

    Ojo no te fies de todo lo que construya la IA. Tu est\u00e1s al mando, tu debes decidir si el sistema est\u00e1 correctamente implementado o no. Es tu responsabilidad.

    Si NO est\u00e1s a gusto con la implementaci\u00f3n o se ha dejado algo por hacer, es el momento de escribirlo por el chat indic\u00e1ndole exactamente que es lo que falta. Cuanto m\u00e1s preciso y conciso seas, mejor implementar\u00e1 la IA.

    "},{"location":"specs/customers_paid/#archive","title":"Archive","text":"

    Y llegamos a la \u00faltima etapa que nos define OpenSpec, donde se archiva el cambio y se da por finalizada la funcionalidad.

    El objetivo de esta fase es marcar la funcionalidad como completada, consolidar todos los artefactos generados durante el proceso y dejar el sistema en un estado estable, coherente y preparado para nuevas evoluciones.

    En esta fase se asegura que:

    • La funcionalidad ha sido correctamente implementada y validada
    • No existen incidencias cr\u00edticas pendientes
    • La documentaci\u00f3n asociada al cambio est\u00e1 completa y actualizada
    • Existe una trazabilidad entre requisitos, dise\u00f1o e implementaci\u00f3n

    Aunque parezca mentira, este paso es muy importante ya que nos servir\u00e1 para actualizar el contexto del sistema y archivar todos los cambios para futuras consultas.

    \ud83d\udcdc Prompt

    De nuevo nos vamos al chat de Visual Studio Code el siguiente prompt:

    /opsx:archive\n

    En este punto el sistema pedir\u00e1 confirmaci\u00f3n para sincronizar requisitos antes de archivar.

    Sincronizar es obligatorio si quieres mantener trazabilidad entre lo implementado y los specs oficiales.

    "},{"location":"specs/intro/","title":"Introducci\u00f3n a Spec-Driven Development","text":"

    Warning

    Esta secci\u00f3n se encuentra en desarrollo \ud83d\udea7. NO se recomienda realizarla a menos que te lo hayan indicado expresamente.

    "},{"location":"specs/intro/#contexto","title":"Contexto","text":"

    En este bloque del tutorial vamos a implementar nuevas funcionalidades usando Spec-Driven Development (SDD) con OpenSpec.

    El objetivo no es \u00fanicamente desarrollar nuevas funcionalidades, sino hacerlo siguiendo un proceso estructurado que permita separar claramente las distintas fases del desarrollo y garantizar trazabilidad entre an\u00e1lisis, definici\u00f3n, implementaci\u00f3n y validaci\u00f3n.

    Para llevar a cabo este enfoque de manera pr\u00e1ctica, se utiliza la metodolog\u00eda OpenSpec, que proporciona un flujo de trabajo claro y repetible para definir, implementar y consolidar cambios en el sistema.

    Las funcionalidades abordadas se organizan en dos bloques principales:

    • Gesti\u00f3n de clientes
    • Gesti\u00f3n de pr\u00e9stamos

    Ambas se implementan siguiendo las fases definidas por OpenSpec, reutilizando los patrones existentes del sistema y manteniendo coherencia t\u00e9cnica y funcional con el resto de la aplicaci\u00f3n.

    "},{"location":"specs/intro/#que-es-spec-driven-development","title":"\u00bfQu\u00e9 es Spec-Driven Development?","text":"

    Spec-Driven Development (SDD) es un enfoque de desarrollo en el que el comportamiento del sistema se define de forma expl\u00edcita antes de escribir c\u00f3digo.

    En lugar de comenzar directamente con la implementaci\u00f3n, SDD propone describir primero:

    • Qu\u00e9 debe hacer el sistema
    • Qu\u00e9 reglas y restricciones deben cumplirse
    • Qu\u00e9 comportamiento se espera en cada escenario

    Las especificaciones (specs) se convierten en el eje central del desarrollo y sirven como referencia com\u00fan durante todo el proceso.

    Este enfoque permite:

    • Reducir ambig\u00fcedades sobre el comportamiento esperado
    • Detectar errores de dise\u00f1o de forma temprana
    • Mantener coherencia en sistemas que evolucionan con el tiempo
    • Facilitar la comunicaci\u00f3n entre personas y herramientas implicadas en el desarrollo
    "},{"location":"specs/intro/#openspec-como-metodologia-de-trabajo","title":"OpenSpec como metodolog\u00eda de trabajo","text":"

    OpenSpec es una metodolog\u00eda que materializa el enfoque de Spec-Driven Development, proporcionando un flujo de trabajo estructurado para implementar cambios de forma controlada y trazable.

    OpenSpec organiza el desarrollo en una serie de fases bien definidas que permiten:

    • Analizar el contexto y el alcance del cambio
    • Definir el comportamiento funcional esperado
    • Implementar la soluci\u00f3n de forma alineada con lo definido
    • Cerrar y consolidar el cambio de manera ordenada

    A lo largo de esta gu\u00eda se utilizar\u00e1 OpenSpec como marco de trabajo para aplicar SDD en la implementaci\u00f3n de las funcionalidades de gesti\u00f3n de clientes y gesti\u00f3n de pr\u00e9stamos.

    SDD y agentes de IA

    Al trabajar con agentes de IA, el c\u00f3digo puede generarse r\u00e1pidamente, pero sin una gu\u00eda clara existe el riesgo de que la IA tome decisiones no deseadas para que \u201cel c\u00f3digo funcione\u201d.

    OpenSpec traslada el foco a las especificaciones, que definen expl\u00edcitamente el comportamiento esperado del sistema y sirven como contrato para la IA, reduciendo ambig\u00fcedades y asegurando trazabilidad entre lo definido y lo implementado.

    Es sumamente importante que se defina de forma concreta y muy concisa los requisitos y las reglas que debe seguir la IA a la hora de analizar y generar.

    "},{"location":"specs/intro/#fases-de-openspec","title":"Fases de OpenSpec","text":"

    El flujo de trabajo de OpenSpec se estructura en cuatro fases principales:

    1. Explore\n2. Propose\n3. Apply\n4. Archive\n

    Cada una de estas fases cumple un prop\u00f3sito espec\u00edfico y se apoya en la anterior, formando un ciclo completo de definici\u00f3n, implementaci\u00f3n y cierre del cambio.

    "},{"location":"specs/intro/#explore","title":"Explore","text":"

    Fase inicial orientada a comprender el contexto en el que se va a trabajar.

    Objetivo

    • Analizar la informaci\u00f3n disponible
    • Entender la necesidad, iniciativa o cambio a abordar
    • Identificar posibles limitaciones, dependencias y patrones existentes

    Esta fase no implica necesariamente la existencia de un sistema previo.

    Puede consistir en:

    • Analizar un sistema existente
    • Revisar documentaci\u00f3n o requisitos
    • Definir el contexto cuando se parte desde cero

    Resultado

    Una comprensi\u00f3n clara del punto de partida y del alcance del cambio a realizar.

    "},{"location":"specs/intro/#propose","title":"Propose","text":"

    Fase orientada a la definici\u00f3n de la soluci\u00f3n a implementar.

    Objetivo

    • Definir qu\u00e9 se va a construir
    • Delimitar el alcance del cambio (qu\u00e9 se incluye y qu\u00e9 queda fuera)
    • Establecer el comportamiento funcional esperado

    Resultado

    Una propuesta clara, estructurada y alineada con el objetivo del cambio, que servir\u00e1 como base para su implementaci\u00f3n. En esta fase se deber\u00edan generar 4 ficheros.

    \ud83d\udcc4 proposal.md

    Define la funcionalidad a alto nivel.

    Incluye:

    • El problema que se quiere resolver (Why)
    • Qu\u00e9 cambios se van a introducir (What Changes)
    • El alcance funcional
    • El impacto en la aplicaci\u00f3n

    Responde a: \u00bfQu\u00e9 se va a construir y por qu\u00e9?

    \ud83d\udcc4 design.md

    Describe el dise\u00f1o t\u00e9cnico de la soluci\u00f3n.

    Incluye:

    • Contexto del sistema actual
    • Objetivos (Goals / Non-Goals)
    • Decisiones t\u00e9cnicas y su justificaci\u00f3n
    • Alternativas consideradas
    • Riesgos y trade-offs

    Responde a: \u00bfC\u00f3mo se va a construir y por qu\u00e9 se ha elegido este enfoque?

    \ud83d\udcc4 spec.md

    Define el comportamiento funcional esperado.

    Incluye:

    • Requisitos funcionales
    • Casos de uso expresados como escenarios (WHEN / THEN)
    • Reglas de negocio
    • Validaciones y restricciones

    Responde a: \u00bfQu\u00e9 debe hacer el sistema?

    \ud83d\udcc4 tasks.md

    Descompone la implementaci\u00f3n en tareas ejecutables. Quiz\u00e1 es el fichero m\u00e1s importante.

    Incluye:

    • Lista ordenada de tareas
    • Pasos concretos para implementar la funcionalidad

    Responde a: \u00bfC\u00f3mo se implementa paso a paso?

    Relaci\u00f3n entre los artefactos

    Cada uno de los ficheros generados cumple un rol espec\u00edfico dentro del flujo de OpenSpec:

    • spec.md \u2192 define el comportamiento esperado (qu\u00e9 debe hacer el sistema)
    • design.md \u2192 define la soluci\u00f3n t\u00e9cnica (c\u00f3mo se va a construir)
    • proposal.md \u2192 aporta contexto y alcance (por qu\u00e9 se construye)
    • tasks.md \u2192 gu\u00eda la ejecuci\u00f3n paso a paso (c\u00f3mo se implementa)

    Esta separaci\u00f3n de responsabilidades permite:

    • Evitar mezclar requisitos con implementaci\u00f3n
    • Revisar cada nivel de forma independiente
    • Detectar errores e inconsistencias antes de escribir c\u00f3digo

    Estos artefactos constituyen la base para la siguiente fase.

    "},{"location":"specs/intro/#apply","title":"Apply","text":"

    Fase en la que se lleva a cabo la implementaci\u00f3n de la soluci\u00f3n definida en la fase Propose.

    Objetivo

    • Desarrollar la soluci\u00f3n definida
    • Asegurar la coherencia entre lo definido y lo implementado
    • Integrar y validar funcionalmente el resultado

    Resultado

    Una soluci\u00f3n implementada, coherente con la propuesta definida y preparada para su validaci\u00f3n final.

    "},{"location":"specs/intro/#archive","title":"Archive","text":"

    Fase final de cierre y consolidaci\u00f3n del cambio.

    Objetivo

    • Confirmar que el cambio est\u00e1 completo
    • Consolidar la documentaci\u00f3n generada durante el proceso
    • Garantizar la trazabilidad para futuras evoluciones

    Resultado

    Un cambio finalizado, validado y correctamente documentado. En esta fase se pedir\u00e1 sincronizar los requisitos antes de archivar y consolidar.

    \u00bfQu\u00e9 significa sincronizar?

    Al seleccionar la opci\u00f3n de sincronizaci\u00f3n:

    • Se integran los nuevos requisitos definidos en spec.md
    • Se crea o actualiza el spec definitivo
    • Los requisitos pasan a formar parte oficial del sistema

    Es decir, los requisitos pasan de ser un cambio temporal a formar parte permanente del sistema.

    \u00bfQu\u00e9 ocurre si no se sincroniza?

    Si se decide no sincronizar:

    • El c\u00f3digo permanece implementado
    • Los requisitos no se registran en los specs principales

    Esto puede provocar:

    • P\u00e9rdida de trazabilidad
    • Dificultad para futuras evoluciones
    • Desalineaci\u00f3n entre c\u00f3digo y documentaci\u00f3n

    Tras completar el proceso de Archive:

    • La funcionalidad queda documentada como completada
    • El cambio deja de formar parte de los cambios activos
    • Los requisitos quedan integrados definitivamente en el sistema (si se ha sincronizado)
    "},{"location":"specs/intro/#principios-de-calidad","title":"Principios de calidad","text":"

    SDD y agentes de IA

    Con agentes de IA se genera c\u00f3digo muy r\u00e1pido, pero la responsabilidad t\u00e9cnica sigue siendo tuya.

    Durante todo el proceso:

    • revisa siempre la propuesta antes de aplicar,
    • valida funcionalmente lo implementado,
    • corrige tareas o requisitos cuando detectes desviaciones,
    • no asumas que la primera respuesta de la IA es correcta.
    "},{"location":"specs/loans_free/","title":"Gesti\u00f3n de pr\u00e9stamos (modelo gratuito)","text":"

    Warning

    Esta secci\u00f3n se encuentra en desarrollo \ud83d\udea7. NO se recomienda realizarla a menos que te lo hayan indicado expresamente.

    "},{"location":"specs/loans_free/#punto-de-partida","title":"Punto de partida","text":"

    Si has llegado hasta aqu\u00ed, entiendo que ya has completado la funcionalidad de gesti\u00f3n de clientes utilizando el modelo gratuito.

    A partir de ahora vamos a dar por hecho que partimos de ese estado del sistema, donde:

    • Existe un CRUD funcional de clientes
    • La funcionalidad est\u00e1 implementada, validada y archivada
    • Los patrones de backend y frontend introducidos ya forman parte del sistema

    Una vez llegados a este punto, asumimos que el proyecto ya est\u00e1 descargado y configurado, y que hemos trabajado previamente sobre la funcionalidad de gesti\u00f3n de clientes.

    Por tanto, continuaremos utilizando los mismos proyectos y directorios, sin realizar ninguna instalaci\u00f3n ni configuraci\u00f3n adicional.

    En este tutorial seguiremos trabajando sobre:

    • server-springboot como backend
    • client-angular17 como frontend
    "},{"location":"specs/loans_free/#requisitos-funcionales","title":"Requisitos funcionales","text":"
    • Gesti\u00f3n de pr\u00e9stamos entre clientes y juegos.
    • Listado paginado con filtros por juego, cliente y fecha.
    • Alta/edici\u00f3n en modal con campos obligatorios (salvo identificador).
    • Validaciones de fechas y restricciones de solapamiento.
    • M\u00e1ximo 14 d\u00edas por pr\u00e9stamo.
    • Un juego no puede estar prestado a dos clientes en el mismo d\u00eda.
    • Un cliente no puede tener m\u00e1s de dos pr\u00e9stamos activos en el mismo d\u00eda.
    "},{"location":"specs/loans_free/#estrategia-del-modo-gratuito","title":"Estrategia del modo gratuito","text":"

    Continuaremos trabajando con un modelo gratuito, utilizando Claude Haiku y el mismo workspace que en la funcionalidad de gesti\u00f3n de clientes.

    Antes de comenzar, ten en cuenta lo siguiente:

    • Para cada nueva funcionalidad, es recomendable iniciar una nueva conversaci\u00f3n de chat dentro del mismo proyecto

    Esto ayuda a mantener el contexto limpio y a que el modelo se centre exclusivamente en la funcionalidad que vamos a abordar.

    Recuerda que en cualquier momento puedes ver el consumo mensual de tu cuenta pulsando el icono de la rana \ud83d\udc38 en la esquina inferior derecha. El contador se reinicia cada mes.

    Al igual que el ejercicio anterior, vamos a dividir el ejercicio en dos grandes bloques:

    1. Primero trabajaremos \u00fanicamente con el backend
    2. Despu\u00e9s abordaremos el frontend

    De esta forma limitamos el contexto a un solo proyecto y facilitamos el trabajo al modelo.

    Adem\u00e1s, recuerda que el comportamiento del modelo no es determinista. Si a ti te genera algo diferente a lo que ves aqu\u00ed, probablemente seguir\u00e1 siendo v\u00e1lido. No te frustres y ajusta los prompts si es necesario.

    "},{"location":"specs/loans_free/#flujo-de-trabajo-openspec","title":"Flujo de trabajo OpenSpec","text":"

    Seguiremos el ciclo completo de :

    1. Explore\n2. Propose\n3. Apply\n4. Archive\n
    "},{"location":"specs/loans_free/#generacion-de-backend","title":"Generaci\u00f3n de backend","text":"

    Aunque no es obligatorio, es altamente recomendable volver a ejecutar la fase de Explore. El sistema ha podido cambiar desde tu \u00faltimo cambio, alguien ha podido hacer modificaciones, etc. En tu caso no ser\u00eda necesario ya que est\u00e1s trabajando tu solo y no has cambiado nada, pero es buena pr\u00e1ctica hacerlo siempre.

    "},{"location":"specs/loans_free/#explore","title":"Explore","text":"

    El objetivo de esta fase es analizar el sistema existente, sin modificar nada.

    A diferencia de la gesti\u00f3n de clientes, este caso de uso introduce una mayor complejidad, principalmente por:

    • Relaciones entre entidades (cliente, juego, pr\u00e9stamo)
    • Uso de paginaci\u00f3n en los listados
    • Aplicaci\u00f3n de filtros combinados
    • Necesidad de validaciones de negocio m\u00e1s complejas

    En esta fase se analizar\u00e1 qu\u00e9 partes del sistema actual ya resuelven este tipo de problemas y pueden reutilizarse, y qu\u00e9 aspectos no est\u00e1n implementados y deber\u00e1n abordarse en fases posteriores.

    Aspectos a revisar:

    Paginaci\u00f3n

    • C\u00f3mo se implementa en backend (uso de Page)

    Filtros

    • C\u00f3mo se implementa en el cat\u00e1logo de juegos
    • C\u00f3mo se implementan filtros por rangos de fechas (si existen)
    • DTOs de filtro utilizados
    • Construcci\u00f3n de queries en backend
    • C\u00f3mo se construyen queries con condiciones combinadas y operadores distintos de igualdad

    Relaciones entre entidades

    • C\u00f3mo se modelan relaciones en JPA
    • Ejemplos existentes en el proyecto
    • C\u00f3mo se representan en DTOs
    • C\u00f3mo se cargan y exponen los datos relacionados

    Validaciones en backend

    • D\u00f3nde se implementan (Service)
    • C\u00f3mo se gestionan errores
    • C\u00f3mo se propagan al frontend
    • C\u00f3mo se implementan validaciones sobre rangos de fechas
    • C\u00f3mo se validan restricciones que dependen de registros existentes (solapamientos, l\u00edmites por cliente, etc.)

    \u26a0\ufe0f En esta fase:

    • NO se escribe c\u00f3digo
    • NO se dise\u00f1a la soluci\u00f3n
    • NO se inventan estructuras nuevas

    Solo se analiza el sistema actual.

    \ud83d\udcdc Prompt

    Lo que haremos ser\u00e1 escribir en el chat de Visual Studio Code el comando y las instrucciones que queramos darle. Recuerda haber elegido Claude Haiku y estar trabajando en modo Agent.

    /opsx:explore\n\nAnaliza el proyecto actual que est\u00e1 en el directorio \"backend\", es una aplicaci\u00f3n Spring Boot. Una vez analizado, responde:\n\n1. \u00bfC\u00f3mo est\u00e1n implementados los CRUD existentes?\n- Controller\n- Service\n- Repository\n- Paginaci\u00f3n y respuestas paginadas\n\n2. \u00bfQu\u00e9 estructura siguen los dominios?\n\n3. \u00bfC\u00f3mo se implementan las operaciones?\n- Listado\n- Creaci\u00f3n/edici\u00f3n\n- Borrado\n- DTOs de filtro utilizados y construcci\u00f3n de queries en backend\n- Uso de condiciones combinadas (no solo igualdad)\n- Ejemplos de filtros por rango de fechas (si existen)\n- Ejemplos de consultas donde una fecha debe estar contenida dentro de un rango (si existen)\n\n4. \u00bfC\u00f3mo se gestionan relaciones entre entidades?\n- Modelado en JPA\n- Ejemplos en el proyecto\n- C\u00f3mo se representan en DTOs\n- C\u00f3mo se exponen los datos relacionados\n\n5. \u00bfC\u00f3mo se implementan validaciones en backend?\n- D\u00f3nde se ubican (Service)\n- C\u00f3mo se gestionan errores\n- C\u00f3mo se propagan al frontend\n- Si existen validaciones que dependan de m\u00faltiples registros o condiciones\n- Si existen validaciones relacionadas con fechas o rangos\n- C\u00f3mo se validan restricciones basadas en datos existentes\n\n6. \u00bfQu\u00e9 formato tienen los endpoints y que relaci\u00f3n tiene con los m\u00e9todos HTTP?\n\n7. \u00bfQu\u00e9 patrones o estructuras comunes se repiten en los CRUD existentes?\n- Clases reutilizables\n- L\u00f3gica repetida\n- Estructuras comunes entre dominios\n\n8. \u00bfExisten test unitarios y de integraci\u00f3n? \u00bfC\u00f3mo est\u00e1n implementados? \u00bfUtiliza algo especial al arrancar o al mockear?\n\n\nAnaliza \u00fanicamente la parte de backend (Spring Boot)\nNO propongas soluciones.\nNO dise\u00f1es nuevas funcionalidades.\nSolo analiza el sistema actual.\nYa tienes un contexto previo en el fichero backend-explore.md en el directorio de las specs, utilizalo y lo actualizas con lo que analices y no est\u00e9.\n

    Si te fijas, le hemos indicado que aproveche el contexto previo generado en el ejercicio anterior y le hemos pedido que lo actualice con los cambios que considere.

    Este comando realizar\u00e1 un an\u00e1lisis exhaustivo de tu sistema que servir\u00e1 como base para definir la nueva funcionalidad en la siguiente fase.

    Sobre los permisos

    Es posible que durante el an\u00e1lisis te pida permiso para hacer ciertas tareas. Le puedes ir dando permiso una a una o darle permiso en todo el workspace, eso lo dejamos a tu elecci\u00f3n.

    "},{"location":"specs/loans_free/#propose","title":"Propose","text":"

    Una vez analizado el sistema en la fase Explore, el siguiente paso es definir de forma clara y estructurada la nueva funcionalidad a implementar.

    En esta fase establecemos qu\u00e9 vamos a construir, apoy\u00e1ndonos en el conocimiento ya consolidado del sistema y en el resultado del Explore.

    Esta fase act\u00faa como puente entre el an\u00e1lisis y la implementaci\u00f3n, permitiendo dise\u00f1ar la soluci\u00f3n antes de escribir c\u00f3digo y reduciendo el riesgo de errores durante el desarrollo.

    Durante esta fase debes especificar:

    Descripci\u00f3n funcional

    • Qu\u00e9 hace la funcionalidad
    • Qu\u00e9 problema resuelve

    Reglas de negocio

    • Validaciones sobre fechas:
      • La fecha de fin no podr\u00e1 ser anterior a la fecha de inicio
    • Restricciones de duraci\u00f3n del pr\u00e9stamo:
      • El per\u00edodo de pr\u00e9stamo m\u00e1ximo solo podr\u00e1 ser de 14 d\u00edas
    • Validaciones de solapamiento de pr\u00e9stamos:
      • El mismo juego no puede estar prestado a m\u00e1s de un cliente para ninguno de los d\u00edas incluidos en el rango del pr\u00e9stamo
    • L\u00edmites de pr\u00e9stamos simult\u00e1neos por cliente:
      • Un mismo cliente no puede tener m\u00e1s de 2 pr\u00e9stamos activos para ninguno de los d\u00edas incluidos en el rango del pr\u00e9stamo

    Dise\u00f1o backend

    • Endpoints necesarios
    • Estructura del dominio (Entity, DTO, Service, Repository)
    • Tipo de operaciones (listado, creaci\u00f3n, edici\u00f3n, borrado)
    • Estrategia para filtros por fecha dentro de rangos

    Decisiones t\u00e9cnicas

    • Qu\u00e9 patrones existentes se reutilizan
    • Qu\u00e9 partes deben extenderse
    • C\u00f3mo se gestionar\u00e1n los filtros de fecha y condiciones combinadas
    • C\u00f3mo se implementar\u00e1n validaciones basadas en m\u00faltiples registros (solapamientos y l\u00edmites)

    Plan de implementaci\u00f3n

    • Tareas ordenadas
    • Separaci\u00f3n backend / frontend
    • Prioridad de desarrollo (listado \u2192 filtros \u2192 validaciones)

    Aqu\u00ed dejamos claro:

    • Qu\u00e9 funcionalidad se va a a\u00f1adir
    • Qu\u00e9 reglas de negocio existen
    • Qu\u00e9 piezas del sistema se ven afectadas
    • Qu\u00e9 tareas habr\u00e1 que ejecutar

    \u26a0\ufe0f En esta fase:

    • NO se implementa c\u00f3digo
    • NO se redefine el sistema

    \ud83d\udcdc Prompt

    Para nuestro ejemplo, lo que haremos ser\u00e1 escribir en el chat de Visual Studio Code el siguiente prompt:

    /opsx:propose manage-loans-backend\n\nDefine la funcionalidad de gesti\u00f3n de pr\u00e9stamos de juegos bas\u00e1ndote en el sistema actual y en los patrones identificados en la fase Explore, tienes el resultado en el fichero \"backend-explore.md\".\n\nNos han pedido esta nueva funcionalidad.\n\nSe quiere hacer uso de su cat\u00e1logo de juegos y de sus clientes, y quiere saber que juegos ha prestado a cada cliente. Para ello nos ha pedido una p\u00e1gina bastante compleja donde se podr\u00e1 consultar diferente informaci\u00f3n y se permitir\u00e1 realizar el pr\u00e9stamo de los juegos.\n\nNos ha pasado el siguiente boceto y requisitos.\n\nLa pantalla tendr\u00e1 dos zonas:\n\n- Una zona de filtrado donde se permitir\u00e1 filtrar por:\n    - T\u00edtulo del juego, que deber\u00e1 ser un combo seleccionable con los juegos del cat\u00e1logo de la Ludoteca.\n    - Cliente, que deber\u00e1 ser un combo seleccionable con los clientes dados de alta en la aplicaci\u00f3n.\n    - Fecha, que deber\u00e1 ser de tipo Datepicker y que permitir\u00e1 elegir una fecha de b\u00fasqueda. Al elegir un d\u00eda nos deber\u00e1 mostrar que juegos est\u00e1n prestados para dicho d\u00eda. OJO que los pr\u00e9stamos son con fecha de inicio y de fin, si elijo un d\u00eda intermedio deber\u00eda aparecer el elemento en la tabla.\n- Una zona de listado paginado que deber\u00e1 mostrar\n    - El identificador del pr\u00e9stamo\n    - El nombre del juego prestado\n    - El nombre del cliente que lo solicit\u00f3\n    - La fecha de inicio del pr\u00e9stamo\n    - La fecha de fin del pr\u00e9stamo\n    - Un bot\u00f3n que permite eliminar el pr\u00e9stamo\n\n\nAl pulsar el bot\u00f3n de Nuevo pr\u00e9stamo se abrir\u00e1 una pantalla donde se podr\u00e1 ingresar la siguiente informaci\u00f3n, toda ella obligatoria:\n- Identificador, inicialmente vac\u00edo y en modo lectura\n- Nombre del cliente, mediante un combo seleccionable\n- Nombre del juego, mediante un combo seleccionable\n- Fechas del pr\u00e9stamo, donde se podr\u00e1 introducir dos fechas, de inicio y fin del pr\u00e9stamo.\n\nLas validaciones son sencillas aunque laboriosas:\n- La fecha de fin NO podr\u00e1 ser anterior a la fecha de inicio\n- El periodo de pr\u00e9stamo m\u00e1ximo solo podr\u00e1 ser de 14 d\u00edas. Si el usuario quiere un pr\u00e9stamo para m\u00e1s de 14 d\u00edas la aplicaci\u00f3n no debe permitirlo mostrando una alerta al intentar guardar.\n- El mismo juego no puede estar prestado a dos clientes distintos en un mismo d\u00eda. OJO que los pr\u00e9stamos tienen fecha de inicio y fecha fin, el juego no puede estar prestado a m\u00e1s de un cliente para ninguno de los d\u00edas que contemplan las fechas actuales del rango.\n- Un mismo cliente no puede tener prestados m\u00e1s de 2 juegos en un mismo d\u00eda. OJO que los pr\u00e9stamos tienen fecha de inicio y fecha fin, el cliente no puede tener m\u00e1s de dos pr\u00e9stamos para ninguno de los d\u00edas que contemplan las fechas actuales del rango.\n\nPara empezar te dar\u00e9 unos consejos:\n\n- Recuerda crear la tabla de la BBDD y sus datos\n- Intenta primero hacer el listado paginado sin filtros, en el orden que m\u00e1s te guste: frontend o backend. Recuerda que se trata de un listado paginado, as\u00ed que deber\u00e1s utilizar el objeto Page.\n- Completa el listado conectando ambas capas.\n- Ahora implementa los filtros, presta atenci\u00f3n al filtro de fecha, es el m\u00e1s complejo.\n- Para la paginaci\u00f3n filtrada solo tienes que mezclar los conceptos que hemos visto en los puntos del tutorial anteriores.\n- Si hiciste el backend en Springboot recuerda revisar Baeldung por si tienes dudas sobre las queries y recuerda que las Specifications son muy \u00fatiles, pero en este caso deber\u00e1s implementar otro tipo de operaciones, no te sirve solo con la operaci\u00f3n de igualdad :, que ya vimos en el tutorial.\n- Implementa la pantalla de alta de pr\u00e9stamo, sin ninguna validaci\u00f3n.\n- Cuando ya te funcione, intenta ir a\u00f1adiendo una a una las validaciones. Algunas de ellas pueden hacerse en frontend, mientras que otras deber\u00e1n validarse en backend\n- Os recordamos que han de poder crearse y editarse pr\u00e9stamos seg\u00fan las reglas de validaci\u00f3n indicadas anteriormente. Aplican las mismas reglas para ambas operaciones.\n- El Backend ha de validar siempre, independientemente de que el Frontend ya lo haya validado. Nunca conf\u00edes de manera exclusiva en terceras partes (Frontend o en otro Backend).\n\n\n\n\nTe voy a dar otras directrices que pienso que te pueden servir:\n- Se necesita un CRUD de prestamos\n- Debe tener una b\u00fasqueda y una paginaci\u00f3n, todo en el mismo endpoint\n- F\u00edjate en como est\u00e1n relacionadas las entidades del modelo ya que aqu\u00ed tendr\u00e1s que relacionar juego y cliente\n- Tienes que implementar las validaciones dentro del m\u00e9todo de guardado y creaci\u00f3n y, siempre que se pueda, la validaci\u00f3n se debe delegar en una query de BBDD.\n\n\nNecesito que definas:\n\n1. Descripci\u00f3n de la funcionalidad\n\n2. Reglas de negocio\n\n3. Dise\u00f1o backend:\n- Endpoints necesarios\n- Estructura del dominio (Entity, DTO, Service, Repository)\n\n4. Decisiones t\u00e9cnicas:\n- Qu\u00e9 patrones del sistema actual se reutilizan\n\n\nNO implementes c\u00f3digo.\nNO analices de nuevo el proyecto.\nBasa la propuesta en los patrones detectados en la fase Explore.\nHaz la propuesta \u00fanicamente de backend.\nComo \u00faltima tarea a\u00f1ade al fichero de tasks generar un resumen del cambio realizado, con el contrato de los endpoints y la informaci\u00f3n necesaria para que luego el frontend pueda implementar sus llamadas de forma sencilla.\n\nTendr\u00e1s que escribir los ficheros de proposal, design, spec y tasks en la propuesta correspondiente.\n

    Igual que en la gesti\u00f3n de clientes, este comando genera dentro del directorio changes la propuesta correspondiente, que incluye los siguientes ficheros: proposal.md, design.md, spec.md, tasks.md.

    Estos artefactos est\u00e1n adaptados a la funcionalidad de gesti\u00f3n de pr\u00e9stamos, incorporando las reglas de negocio, filtros y validaciones espec\u00edficas de este caso de uso.

    Constituyen la base para la siguiente fase: Apply, donde se ejecutar\u00e1 la implementaci\u00f3n siguiendo las tareas definidas.

    Responsabilidades como developer IA

    En este punto la IA te ha hecho una propuesta que puede ser correcta o no, recordemos que se trata de un modelo matem\u00e1tico-probabil\u00edstico. Si hay algo de lo propuesto que no te encaja o es err\u00f3neo deber\u00edas comentarlo mediante el chat o corregirlo de forma manual en el fichero que corresponda. Por ejemplo si quieres a\u00f1adir una tarea porqu\u00e9 se te ha olvidado incluirla en el prompt original, deber\u00edas decirle al modelo que te incluya la nueva tarea.

    Una vez estemos de acuerdo con la propuesta que nos ha hecho la IA, podemos pasar al siguiente punto.

    "},{"location":"specs/loans_free/#apply","title":"Apply","text":"

    Una vez validada la propuesta, ejecutamos la implementaci\u00f3n:

    El objetivo de esta fase es transformar los artefactos generados (proposal.md, design.md, spec.md, tasks.md) en c\u00f3digo funcional, asegurando que:

    • Se respetan los requisitos funcionales definidos en spec.md
    • Se siguen las decisiones t\u00e9cnicas establecidas en design.md
    • Se ejecutan las tareas en el orden definido en tasks.md

    \ud83d\udcdc Prompt

    Esto es tan f\u00e1cil como escribir en el chat de Visual Studio Code el siguiente prompt:

    /opsx:apply\n

    El agente empezar\u00e1 a realizar un mont\u00f3n de tareas y pedirnos permisos. Es posible que algunas de esas tareas fallen y \u00e9l mismo lo reintente de otra forma. El resultado deber\u00eda ser el c\u00f3digo generado e implementado dentro de la carpeta de backend y un resumen de todas las tareas realizadas y checkeadas por la IA.

    "},{"location":"specs/loans_free/#verificacion-del-backend","title":"Verificaci\u00f3n del backend","text":"

    Un paso que no pertenece a OpenSpec pero que es altamente recomendable es probar los cambios realizados. Arranca el backend y verifica:

    • Que el servidor levanta
    • Que los endpoints existen y funcionan
    • Que los tests pasan

    Ojo no te fies

    Ojo no te fies de todo lo que construya la IA. Tu est\u00e1s al mando, tu debes decidir si el sistema est\u00e1 correctamente implementado o no. Es tu responsabilidad.

    Si NO est\u00e1s a gusto con la implementaci\u00f3n o se ha dejado algo por hacer, es el momento de escribirlo por el chat indic\u00e1ndole exactamente que es lo que falta. Cuanto m\u00e1s preciso y conciso seas, mejor implementar\u00e1 la IA.

    "},{"location":"specs/loans_free/#archive","title":"Archive","text":"

    Y llegamos a la \u00faltima etapa que nos define OpenSpec, donde se archiva el cambio y se da por finalizada la funcionalidad.

    El objetivo de esta fase es marcar la funcionalidad como completada, consolidar todos los artefactos generados durante el proceso y dejar el sistema en un estado estable, coherente y preparado para nuevas evoluciones.

    \ud83d\udcdc Prompt

    De nuevo nos vamos al chat de Visual Studio Code el siguiente prompt:

    /opsx:archive\n

    Durante el proceso de Archive, el sistema solicitar\u00e1 confirmaci\u00f3n para sincronizar los requisitos antes de archivar el cambio.

    Recuerda que al sincronizar, los requisitos definidos en spec.md pasan de ser un cambio temporal a formar parte permanente del sistema.

    Si no se sincroniza, el c\u00f3digo queda implementado, pero los requisitos no se registran en los specs principales afectando a la trazabilidad y futuras evoluciones del sistema.

    \ud83d\udcdc Actualizaci\u00f3n del contexto

    Adem\u00e1s, para forzar al modelo gratuito y dejarlo todo listo, es recomendable lanzar un \u00faltimo prompt que nos actualice el fichero de backend-explore.md

    Actualiza el fichero de backend-explore con los nuevos datos implementados\n
    "},{"location":"specs/loans_free/#generacion-de-frontend","title":"Generaci\u00f3n de frontend","text":"

    Una vez implementado el backend, nos ponemos a trabajar con el frontend. De nuevo recordar que es muy importante que cada nuevo cambio que hagamos, lo empecemos en un chat nuevo, para limpiar el contexto anterior y no arrastrar posibles errores o incoherencias.

    "},{"location":"specs/loans_free/#explore_1","title":"Explore","text":"

    Al igual que en backend, aqu\u00ed tambi\u00e9n lanzamos una exploraci\u00f3n del sistema por si hubiera alg\u00fan cambio con respecto a la anterior versi\u00f3n.

    \ud83d\udcdc Prompt

    Vamos a un nuevo chat de Visual Studio Code y escribimos el comando:

    /opsx:explore\n\nAnaliza el proyecto actual que est\u00e1 en el directorio \"frontend\", es una aplicaci\u00f3n Angular. Ojo no escanees la carpeta de \"node_modules\" no tiene sentido. Una vez analizado, responde:\n\n1. \u00bfC\u00f3mo est\u00e1n implementados los CRUD existentes?\n- Componentes\n- Servicios\n- Modelos\n\n2. \u00bfQu\u00e9 estructura siguen los dominios?\n\n3. \u00bfC\u00f3mo se implementan las operaciones?\n- Listado\n- Creaci\u00f3n/edici\u00f3n\n- Borrado\n- C\u00f3mo funcionan las ventanas de creaci\u00f3n y edici\u00f3n (modales)\n\n4. \u00bfComo se comunican frontend con backend?\n- Servicios en Angular\n- Construcci\u00f3n de URLs\n\n5. \u00bfC\u00f3mo se implementa la paginaci\u00f3n?\n- Consumo de datos paginados\n- Integraci\u00f3n en tablas\n\n\n6. \u00bfC\u00f3mo se implementan los filtros en los listados?\n- Especialmente en el cat\u00e1logo de juegos\n- C\u00f3mo se env\u00edan los filtros desde Angular\n\n7. \u00bfC\u00f3mo se cargan datos en combos (selects) en frontend?\n- Servicios Angular utilizados\n- C\u00f3mo se obtienen los datos\n- Flujo de carga en componentes\n\n\n8. \u00bfQu\u00e9 patrones o estructuras comunes se repiten en los CRUD existentes?\n- Clases reutilizables\n- L\u00f3gica repetida\n- Estructuras comunes entre dominios\n\n\nAnaliza \u00fanicamente la parte de frontend (Angular)\nNO propongas soluciones.\nNO dise\u00f1es nuevas funcionalidades.\nSolo analiza el sistema actual.\nYa tienes un contexto previo en el fichero frontend-explore.md en el directorio de las specs, utilizalo y lo actualizas con lo que analices y no est\u00e9.\n

    Si te fijas en este explore hemos a\u00f1adido tanto la paginaci\u00f3n como los filtros. Al finalizar deber\u00eda actualizar el fichero explore de frontend y adem\u00e1s ofrecernos un resumen.

    "},{"location":"specs/loans_free/#propose_1","title":"Propose","text":"

    Una vez analizado el sistema en la fase Explore, el siguiente paso es definir de forma clara y estructurada la nueva funcionalidad a implementar.

    \ud83d\udcdc Prompt

    De nuevo en el chat de Visual Studio Code escribimos el siguiente prompt:

    /opsx:propose manage-loans-frontend\n\nDefine la funcionalidad de gesti\u00f3n de pr\u00e9stamos de juegos bas\u00e1ndote en el sistema actual y en los patrones identificados en la fase Explore, tienes el resultado en el fichero \"frontend-explore.md\". Adem\u00e1s tendr\u00e1s que ver el cambio realizado en la spec de \"manage-loans-backend\", sobre todo los endpoints generados. Por si acaso tambi\u00e9n deber\u00edas tener en cuenta el fichero de \"backend-explore.md\".\n\n\nNos han pedido esta nueva funcionalidad.\n\nSe quiere hacer uso de su cat\u00e1logo de juegos y de sus clientes, y quiere saber que juegos ha prestado a cada cliente. Para ello nos ha pedido una p\u00e1gina bastante compleja donde se podr\u00e1 consultar diferente informaci\u00f3n y se permitir\u00e1 realizar el pr\u00e9stamo de los juegos.\n\nNos ha pasado el siguiente boceto y requisitos.\n\nLa pantalla tendr\u00e1 dos zonas:\n\n- Una zona de filtrado donde se permitir\u00e1 filtrar por:\n    - T\u00edtulo del juego, que deber\u00e1 ser un combo seleccionable con los juegos del cat\u00e1logo de la Ludoteca.\n    - Cliente, que deber\u00e1 ser un combo seleccionable con los clientes dados de alta en la aplicaci\u00f3n.\n    - Fecha, que deber\u00e1 ser de tipo Datepicker y que permitir\u00e1 elegir una fecha de b\u00fasqueda. Al elegir un d\u00eda nos deber\u00e1 mostrar que juegos est\u00e1n prestados para dicho d\u00eda. OJO que los pr\u00e9stamos son con fecha de inicio y de fin, si elijo un d\u00eda intermedio deber\u00eda aparecer el elemento en la tabla.\n- Una zona de listado paginado que deber\u00e1 mostrar\n    - El identificador del pr\u00e9stamo\n    - El nombre del juego prestado\n    - El nombre del cliente que lo solicit\u00f3\n    - La fecha de inicio del pr\u00e9stamo\n    - La fecha de fin del pr\u00e9stamo\n    - Un bot\u00f3n que permite eliminar el pr\u00e9stamo\n\n\nAl pulsar el bot\u00f3n de Nuevo pr\u00e9stamo se abrir\u00e1 una pantalla donde se podr\u00e1 ingresar la siguiente informaci\u00f3n, toda ella obligatoria:\n- Identificador, inicialmente vac\u00edo y en modo lectura\n- Nombre del cliente, mediante un combo seleccionable\n- Nombre del juego, mediante un combo seleccionable\n- Fechas del pr\u00e9stamo, donde se podr\u00e1 introducir dos fechas, de inicio y fin del pr\u00e9stamo.\n\nLas validaciones son sencillas aunque laboriosas:\n- La fecha de fin NO podr\u00e1 ser anterior a la fecha de inicio\n- El periodo de pr\u00e9stamo m\u00e1ximo solo podr\u00e1 ser de 14 d\u00edas. Si el usuario quiere un pr\u00e9stamo para m\u00e1s de 14 d\u00edas la aplicaci\u00f3n no debe permitirlo mostrando una alerta al intentar guardar.\n- El mismo juego no puede estar prestado a dos clientes distintos en un mismo d\u00eda. OJO que los pr\u00e9stamos tienen fecha de inicio y fecha fin, el juego no puede estar prestado a m\u00e1s de un cliente para ninguno de los d\u00edas que contemplan las fechas actuales del rango.\n- Un mismo cliente no puede tener prestados m\u00e1s de 2 juegos en un mismo d\u00eda. OJO que los pr\u00e9stamos tienen fecha de inicio y fecha fin, el cliente no puede tener m\u00e1s de dos pr\u00e9stamos para ninguno de los d\u00edas que contemplan las fechas actuales del rango.\n\nPara empezar te dar\u00e9 unos consejos:\n\n- Recuerda crear la tabla de la BBDD y sus datos\n- Intenta primero hacer el listado paginado sin filtros, en el orden que m\u00e1s te guste: frontend o backend. Recuerda que se trata de un listado paginado, as\u00ed que deber\u00e1s utilizar el objeto Page.\n- Completa el listado conectando ambas capas.\n- Ahora implementa los filtros, presta atenci\u00f3n al filtro de fecha, es el m\u00e1s complejo.\n- Para la paginaci\u00f3n filtrada solo tienes que mezclar los conceptos que hemos visto en los puntos del tutorial anteriores.\n- Si hiciste el backend en Springboot recuerda revisar Baeldung por si tienes dudas sobre las queries y recuerda que las Specifications son muy \u00fatiles, pero en este caso deber\u00e1s implementar otro tipo de operaciones, no te sirve solo con la operaci\u00f3n de igualdad :, que ya vimos en el tutorial.\n- Implementa la pantalla de alta de pr\u00e9stamo, sin ninguna validaci\u00f3n.\n- Cuando ya te funcione, intenta ir a\u00f1adiendo una a una las validaciones. Algunas de ellas pueden hacerse en frontend, mientras que otras deber\u00e1n validarse en backend\n- Os recordamos que han de poder crearse y editarse pr\u00e9stamos seg\u00fan las reglas de validaci\u00f3n indicadas anteriormente. Aplican las mismas reglas para ambas operaciones.\n- El Backend ha de validar siempre, independientemente de que el Frontend ya lo haya validado. Nunca conf\u00edes de manera exclusiva en terceras partes (Frontend o en otro Backend).\n\n\n\n\nTe voy a dar otras directrices que pienso que te pueden servir:\n- Se necesita un CRUD de prestamos\n- Debe tener una b\u00fasqueda y una paginaci\u00f3n, as\u00ed que f\u00edjate en como est\u00e1 hecho en otras pantallas\n- Todo lo que se pueda tendr\u00e1 que estar con componentes de tipo dropdown\n- Es posible que tengas que implementar alg\u00fan nuevo endpoint para rellenar los componentes dropdown, dise\u00f1a eso tambi\u00e9n para el backend.\n\n\nNecesito que definas:\n\n1. Descripci\u00f3n de la funcionalidad\n\n2. Reglas de negocio\n\n3. Dise\u00f1o frontend:\n- Componentes necesarios\n- Flujos de interacci\u00f3n (listado, abrir modal, guardar borrar)\n\n4. Uso de endpoints para llamar a backend\n\n5. Decisiones t\u00e9cnicas:\n- Qu\u00e9 patrones del sistema actual se reutilizan\n\n\nNO implementes c\u00f3digo.\nNO analices de nuevo el proyecto.\nBasa la propuesta en los patrones detectados en la fase Explore.\nHaz la propuesta \u00fanicamente de frontend.\nOlv\u00eddate de los test, en frontend no tenemos tests.\nA\u00f1ade el nuevo punto de men\u00fa en el header para que se pueda acceder.\nNo te inventes estilos, respeta los estilos de las pantallas (anchuras, alturas, colores, disposici\u00f3n de las tablas).\nUtiliza los componentes de Angular Material para todo lo que puedas, no componentes nativos del navegador.\n\nTendr\u00e1s que escribir los ficheros de proposal, design, spec y tasks en la propuesta correspondiente.\n

    Aqu\u00ed es importante destacar que debe tener en cuenta:

    • debe coger el contexto generado anteriormente
    • adem\u00e1s, le debe sumar el contexto del \u00faltimo cambio de backend con los endpoints
    • debe respetar estilos y componentes de Angular material y no inventar
    • debe revisar como se rellenan los dropdown

    De nuevo este comando genera dentro del directorio changes la propuesta correspondiente, que incluye los siguientes ficheros: proposal.md, design.md, spec.md, tasks.md. Que deberemos revisar.

    No nos cansaremos de decirlo

    Esta es la fase m\u00e1s importante, aqu\u00ed es donde debes revisar toda la propuesta y si algo no te encaja o es err\u00f3neo deber\u00edas comentarlo mediante el chat o corregirlo de forma manual en el fichero que corresponda. Es t\u00fa responsabilidad.

    Una vez estemos de acuerdo con la propuesta que nos ha hecho la IA, podemos pasar al siguiente punto.

    "},{"location":"specs/loans_free/#apply_1","title":"Apply","text":"

    Una vez validado todo, pasamos a ejecutarlo.

    \ud83d\udcdc Prompt

    Esto es tan f\u00e1cil como escribir en el chat de Visual Studio Code el siguiente prompt:

    /opsx:apply\n

    El agente empezar\u00e1 a realizar un mont\u00f3n de tareas y pedirnos permisos. Es posible que algunas de esas tareas fallen y \u00e9l mismo lo reintente de otra forma. El resultado deber\u00eda ser el c\u00f3digo generado e implementado tanto en la carpeta backend como en la carpeta frontend y un resumen de todas las tareas realizadas y checkeadas por la IA.

    "},{"location":"specs/loans_free/#verificacion-del-frontend","title":"Verificaci\u00f3n del frontend","text":"

    Un paso que no pertenece a OpenSpec pero que es altamente recomendable es probar los cambios realizados.

    Arranca el backend y el frontend y verifica:

    • La aplicaci\u00f3n levanta correctamente
    • Las nuevas funcionalidades a\u00f1adidas est\u00e1n accesibles
    • Los flujos principales definidos en spec.md funcionan como se espera

    Ojo no te fies

    Ojo no te fies de todo lo que construya la IA. Tu est\u00e1s al mando, tu debes decidir si el sistema est\u00e1 correctamente implementado o no. Es tu responsabilidad.

    Si NO est\u00e1s a gusto con la implementaci\u00f3n o se ha dejado algo por hacer, es el momento de escribirlo por el chat indic\u00e1ndole exactamente que es lo que falta. Cuanto m\u00e1s preciso y conciso seas, mejor implementar\u00e1 la IA.

    "},{"location":"specs/loans_free/#archive_1","title":"Archive","text":"

    Y llegamos a la \u00faltima etapa que nos define OpenSpec, donde se archiva el cambio y se da por finalizada la funcionalidad.

    El objetivo de esta fase es marcar la funcionalidad como completada, consolidar todos los artefactos generados durante el proceso y dejar el sistema en un estado estable, coherente y preparado para nuevas evoluciones.

    \ud83d\udcdc Prompt

    De nuevo nos vamos al chat de Visual Studio Code el siguiente prompt:

    /opsx:archive\n

    Durante el proceso de Archive, el sistema solicitar\u00e1 confirmaci\u00f3n para sincronizar los requisitos antes de archivar el cambio.

    Recuerda que al sincronizar, los requisitos definidos en spec.md pasan de ser un cambio temporal a formar parte permanente del sistema.

    Si no se sincroniza, el c\u00f3digo queda implementado, pero los requisitos no se registran en los specs principales afectando a la trazabilidad y futuras evoluciones del sistema.

    "},{"location":"specs/loans_paid/","title":"Gesti\u00f3n de pr\u00e9stamos (modelo con licencia)","text":"

    Warning

    Esta secci\u00f3n se encuentra en desarrollo \ud83d\udea7. NO se recomienda realizarla a menos que te lo hayan indicado expresamente.

    "},{"location":"specs/loans_paid/#punto-de-partida","title":"Punto de partida","text":"

    Si has llegado hasta aqu\u00ed, entiendo que ya has completado la funcionalidad de gesti\u00f3n de clientes utilizando el modelo con licencia.

    A partir de ahora vamos a dar por hecho que partimos de ese estado del sistema, donde:

    • Existe un CRUD funcional de clientes
    • La funcionalidad est\u00e1 implementada, validada y archivada
    • Los patrones de backend y frontend introducidos ya forman parte del sistema

    Una vez llegados a este punto, asumimos que el proyecto ya est\u00e1 descargado y configurado, y que hemos trabajado previamente sobre la funcionalidad de gesti\u00f3n de clientes.

    Por tanto, continuaremos utilizando los mismos proyectos y directorios, sin realizar ninguna instalaci\u00f3n ni configuraci\u00f3n adicional.

    En este tutorial seguiremos trabajando sobre:

    • server-springboot como backend
    • client-angular17 como frontend
    "},{"location":"specs/loans_paid/#requisitos-funcionales","title":"Requisitos funcionales","text":"
    • Gesti\u00f3n de pr\u00e9stamos entre clientes y juegos.
    • Listado paginado con filtros por juego, cliente y fecha.
    • Alta/edici\u00f3n en modal con campos obligatorios (salvo identificador).
    • Validaciones de fechas y restricciones de solapamiento.
    • M\u00e1ximo 14 d\u00edas por pr\u00e9stamo.
    • Un juego no puede estar prestado a dos clientes en el mismo d\u00eda.
    • Un cliente no puede tener m\u00e1s de dos pr\u00e9stamos activos en el mismo d\u00eda.
    "},{"location":"specs/loans_paid/#estrategia-del-modo-con-licencia","title":"Estrategia del modo con licencia","text":"

    Continuaremos trabajando con un modelo de pago, utilizando Claude Sonnet 4.6 y el mismo workspace que en la funcionalidad de gesti\u00f3n de clientes.

    Antes de comenzar, ten en cuenta lo siguiente:

    • Para cada nueva funcionalidad, es recomendable iniciar una nueva conversaci\u00f3n de chat dentro del mismo proyecto

    Esto ayuda a mantener el contexto limpio y a que el modelo se centre exclusivamente en la funcionalidad que vamos a abordar.

    Recuerda que en cualquier momento puedes ver el consumo mensual de tu cuenta pulsando el icono de la rana \ud83d\udc38 en la esquina inferior derecha. El contador se reinicia cada mes.

    Vamos a abordar el ejercicio como un \u00fanico bloque de trabajo, analizando y construyendo la funcionalidad de forma simult\u00e1nea en backend y frontend.

    De esta manera aprovechamos el mayor contexto del modelo de pago, permitiendo:

    1. Analizar backend y frontend al mismo tiempo
    2. Dise\u00f1ar la funcionalidad de forma coherente en ambas capas desde el inicio

    Esto nos permite mantener una visi\u00f3n global del sistema durante todo el proceso y reducir la necesidad de dividir artificialmente el trabajo en fases independientes por capa.

    Adem\u00e1s, recuerda que el comportamiento del modelo no es determinista. Si a ti te genera algo diferente a lo que ves aqu\u00ed, probablemente seguir\u00e1 siendo v\u00e1lido. No te frustres y ajusta los prompts si es necesario.

    "},{"location":"specs/loans_paid/#flujo-de-trabajo-openspec","title":"Flujo de trabajo OpenSpec","text":"

    Seguiremos el ciclo completo de OpenSpec:

    1. Explore\n2. Propose\n3. Apply\n4. Archive\n
    "},{"location":"specs/loans_paid/#backend-y-frontend","title":"Backend y frontend","text":"

    Aunque no es obligatorio, es altamente recomendable volver a ejecutar la fase de Explore. El sistema ha podido cambiar desde tu \u00faltimo cambio, alguien ha podido hacer modificaciones, etc. En tu caso no ser\u00eda necesario ya que est\u00e1s trabajando tu solo y no has cambiado nada, pero es buena pr\u00e1ctica hacerlo siempre.

    "},{"location":"specs/loans_paid/#explore","title":"Explore","text":"

    El objetivo de esta fase es analizar el sistema existente, sin modificar nada.

    A diferencia de la gesti\u00f3n de clientes, este caso de uso introduce una mayor complejidad, principalmente por:

    • Relaciones entre entidades (cliente, juego, pr\u00e9stamo)
    • Uso de paginaci\u00f3n en los listados
    • Aplicaci\u00f3n de filtros combinados
    • Necesidad de validaciones de negocio m\u00e1s complejas

    En esta fase se analizar\u00e1 qu\u00e9 partes del sistema actual ya resuelven este tipo de problemas y pueden reutilizarse, y qu\u00e9 aspectos no est\u00e1n implementados y deber\u00e1n abordarse en fases posteriores.

    Aspectos a revisar:

    Paginaci\u00f3n

    • C\u00f3mo se implementa en backend (uso de Page)
    • C\u00f3mo se consume en frontend
    • C\u00f3mo se integra en tablas

    Filtros

    • C\u00f3mo se implementa en el cat\u00e1logo de juegos
    • C\u00f3mo se implementan filtros por rangos de fechas (si existen)
    • DTOs de filtro utilizados
    • Construcci\u00f3n de queries en backend
    • C\u00f3mo se construyen queries con condiciones combinadas y operadores distintos de igualdad
    • C\u00f3mo se env\u00edan los filtros desde Angular

    Relaciones entre entidades

    • C\u00f3mo se modelan relaciones en JPA
    • Ejemplos existentes en el proyecto
    • C\u00f3mo se representan en DTOs
    • C\u00f3mo se cargan y exponen los datos relacionados

    Validaciones en backend

    • D\u00f3nde se implementan (Service)
    • C\u00f3mo se gestionan errores
    • C\u00f3mo se propagan al frontend
    • C\u00f3mo se implementan validaciones sobre rangos de fechas
    • C\u00f3mo se validan restricciones que dependen de registros existentes (solapamientos, l\u00edmites por cliente, etc.)

    Combos (selects) en frontend - C\u00f3mo se cargan datos (clientes, juegos) - Uso de servicios Angular - Flujo de carga en componentes

    \u26a0\ufe0f En esta fase:

    • NO se escribe c\u00f3digo
    • NO se dise\u00f1a la soluci\u00f3n
    • NO se inventan estructuras nuevas

    Solo se analiza el sistema actual.

    \ud83d\udcdc Prompt

    Lo que haremos ser\u00e1 escribir en el chat de Visual Studio Code el comando y las instrucciones que queramos darle. Recuerda haber elegido Claude Sonnet 4.6 y estar trabajando en modo Agent.

    En este caso, hemos a\u00f1adido las carpetas del proyecto frontend y backend al contexto, por lo que el an\u00e1lisis se realizar\u00e1 sobre el sistema completo.

    Para ello, desde el propio Chat de Copilot, pulsando el bot\u00f3n \u201c+\u201d, puedes seleccionar y a\u00f1adir tanto archivos individuales como directorios completos del proyecto. Tambi\u00e9n es posible a\u00f1adirlos arrastr\u00e1ndolos directamente al chat.

    /opsx:explore\n\nAnaliza el proyecto actual (Angular 17 + Spring Boot) centr\u00e1ndote en las funcionalidades necesarias para implementar la gesti\u00f3n de pr\u00e9stamos y responde:\n\n1. \u00bfC\u00f3mo se implementa la paginaci\u00f3n?\n- Backend: uso de Page y construcci\u00f3n de respuestas paginadas\n- Frontend: consumo de datos paginados\n- Integraci\u00f3n en tablas\n\n2. \u00bfC\u00f3mo se implementan los filtros en los listados?\n- Especialmente en el cat\u00e1logo de juegos\n- DTOs de filtro utilizados\n- Construcci\u00f3n de queries en backend\n- Uso de condiciones combinadas (no solo igualdad)\n- Ejemplos de filtros por rango de fechas (si existen)\n- Ejemplos de consultas donde una fecha debe estar contenida dentro de un rango (si existen)\n- C\u00f3mo se env\u00edan los filtros desde Angular\n\n3. \u00bfC\u00f3mo se gestionan relaciones entre entidades?\n- Modelado en JPA\n- Ejemplos en el proyecto\n- C\u00f3mo se representan en DTOs\n- C\u00f3mo se exponen los datos relacionados\n\n4. \u00bfC\u00f3mo se implementan validaciones en backend?\n- D\u00f3nde se ubican (Service)\n- C\u00f3mo se gestionan errores\n- C\u00f3mo se propagan al frontend\n- Si existen validaciones que dependan de m\u00faltiples registros o condiciones\n- Si existen validaciones relacionadas con fechas o rangos\n- C\u00f3mo se validan restricciones basadas en datos existentes\n\n5. \u00bfC\u00f3mo se cargan datos en combos (selects) en frontend?\n- Servicios Angular utilizados\n- C\u00f3mo se obtienen los datos\n- Flujo de carga en componentes\n\nNO propongas soluciones.\nNO dise\u00f1es la funcionalidad de pr\u00e9stamos.\nNO repitas el an\u00e1lisis b\u00e1sico del sistema.\nNO incluyas c\u00f3digo completo. Resume la l\u00f3gica cuando sea necesario.\n\nC\u00e9ntrate \u00fanicamente en los aspectos necesarios para implementar la funcionalidad de gesti\u00f3n de pr\u00e9stamos.\n

    Este comando realizar\u00e1 un an\u00e1lisis exhaustivo de tu sistema que servir\u00e1 como base para definir la nueva funcionalidad en la siguiente fase.

    Sobre los permisos

    Es posible que durante el an\u00e1lisis te pida permiso para hacer ciertas tareas. Le puedes ir dando permiso una a una o darle permiso en todo el workspace, eso lo dejamos a tu elecci\u00f3n.

    En cualquier momento puedes ver el consumo de la ventana de contexto para saber si todo el conocimiento del sistema est\u00e1 en memoria o no. En el icono de la gr\u00e1fica circular que est\u00e1 situada en la parte inferior derecha del chat.

    "},{"location":"specs/loans_paid/#propose","title":"Propose","text":"

    Una vez analizado el sistema en la fase Explore, el siguiente paso es definir de forma clara y estructurada la nueva funcionalidad a implementar.

    En esta fase establecemos qu\u00e9 vamos a construir, apoy\u00e1ndonos en el conocimiento ya consolidado del sistema y en el resultado del Explore.

    Esta fase act\u00faa como puente entre el an\u00e1lisis y la implementaci\u00f3n, permitiendo dise\u00f1ar la soluci\u00f3n antes de escribir c\u00f3digo y reduciendo el riesgo de errores durante el desarrollo.

    Durante esta fase debes especificar:

    Descripci\u00f3n funcional

    • Qu\u00e9 hace la funcionalidad
    • Qu\u00e9 problema resuelve

    Reglas de negocio

    • Validaciones sobre fechas:
      • La fecha de fin no podr\u00e1 ser anterior a la fecha de inicio
    • Restricciones de duraci\u00f3n del pr\u00e9stamo:
      • El per\u00edodo de pr\u00e9stamo m\u00e1ximo solo podr\u00e1 ser de 14 d\u00edas
    • Validaciones de solapamiento de pr\u00e9stamos:
      • El mismo juego no puede estar prestado a m\u00e1s de un cliente para ninguno de los d\u00edas incluidos en el rango del pr\u00e9stamo
    • L\u00edmites de pr\u00e9stamos simult\u00e1neos por cliente:
      • Un mismo cliente no puede tener m\u00e1s de 2 pr\u00e9stamos activos para ninguno de los d\u00edas incluidos en el rango del pr\u00e9stamo

    Dise\u00f1o backend

    • Endpoints necesarios
    • Estructura del dominio (Entity, DTO, Service, Repository)
    • Tipo de operaciones (listado, creaci\u00f3n, edici\u00f3n, borrado)
    • Estrategia para filtros por fecha dentro de rangos

    Dise\u00f1o frontend

    • Componentes necesarios
    • Flujo de usuario (listado, abrir modal, guardar, borrar)
    • Servicios Angular
    • Gesti\u00f3n de combos (clientes y juegos)
    • Integraci\u00f3n de filtros y paginaci\u00f3n
    • Integraci\u00f3n de Datepicker para filtro por fecha
    • Estructura de pantalla:
      • Listado, seguir\u00e1 la estructura general de las pantallas ya existentes, reutilizando:
        • Patr\u00f3n de filtros de cat\u00e1logo. Para este caso, se permitir\u00e1 filtrar por:
          • T\u00edtulo del juego (combo)
          • Cliente (combo)
          • Fecha (Datepicker): la fecha seleccionada debe estar contenida entre la fecha de inicio y la fecha de fin del pr\u00e9stamo
        • Patr\u00f3n de paginaci\u00f3n del listado de autores
        • El orden de las columnas del listado ser\u00e1:
          • Identificador
          • Nombre del juego
          • Nombre del cliente
          • Fecha de pr\u00e9stamo
          • Fecha de devoluci\u00f3n
        • Las fechas se mostrar\u00e1n siempre en formato DD/MM/YYYY
      • Alta/edici\u00f3n:
        • El identificador aparecer\u00e1 vac\u00edo en creaci\u00f3n y en modo solo lectura
        • Debajo se mostrar\u00e1 el campo de nombre de cliente (combo seleccionable)
        • Debajo se mostrar\u00e1 el campo de nombre de juego (combo seleccionable)
        • Debajo se mostrar\u00e1 la secci\u00f3n de fechas de pr\u00e9stamo: la fecha de inicio y la fecha de fin estar\u00e1n en la misma fila
        • Todos los campos, salvo el identificador, ser\u00e1n obligatorios

    Decisiones t\u00e9cnicas

    • Qu\u00e9 patrones existentes se reutilizan
    • Qu\u00e9 partes deben extenderse
    • C\u00f3mo se gestionar\u00e1n los filtros de fecha y condiciones combinadas
    • C\u00f3mo se implementar\u00e1n validaciones basadas en m\u00faltiples registros (solapamientos y l\u00edmites)

    Plan de implementaci\u00f3n

    • Tareas ordenadas
    • Separaci\u00f3n backend / frontend
    • Prioridad de desarrollo (listado \u2192 filtros \u2192 validaciones)

    Aqu\u00ed dejamos claro:

    • Qu\u00e9 funcionalidad se va a a\u00f1adir
    • Qu\u00e9 reglas de negocio existen
    • Qu\u00e9 piezas del sistema se ven afectadas
    • Qu\u00e9 tareas habr\u00e1 que ejecutar

    \u26a0\ufe0f En esta fase:

    • NO se implementa c\u00f3digo
    • NO se redefine el sistema

    \ud83d\udcdc Prompt

    Recuerda que seguimos trabajando en modo Agent, con las carpetas del proyecto frontend y backend a\u00f1adidas al contexto.

    Para nuestro ejemplo, lo que haremos ser\u00e1 escribir en el chat de Visual Studio Code el siguiente prompt:

    /opsx:propose loan\n\nDefine la funcionalidad de gesti\u00f3n de pr\u00e9stamos bas\u00e1ndote en el sistema actual (Angular 17 + Spring Boot), en los patrones identificados en la fase Explore y en los requisitos funcionales indicados.\n\nRequisitos funcionales:\n- Se necesita una funcionalidad de gesti\u00f3n de pr\u00e9stamos\n\n- Un pr\u00e9stamo relaciona un cliente y un juego\n\n- El listado ser\u00e1 paginado\n\n- Existir\u00e1 una zona de filtros en la parte superior del listado\n\n- Se podr\u00e1 filtrar por:\n  - Juego (combo seleccionable)\n  - Cliente (combo seleccionable)\n  - Fecha (Datepicker)\n\n- La fecha seleccionada deber\u00e1 estar contenida entre la fecha de inicio y la fecha de fin del pr\u00e9stamo para que el registro aparezca en el listado\n\n- El listado deber\u00e1 mostrar:\n  - Identificador\n  - Nombre del juego\n  - Nombre del cliente\n  - Fecha de pr\u00e9stamo\n  - Fecha de devoluci\u00f3n\n\n- Las fechas se mostrar\u00e1n en formato DD/MM/YYYY\n\n- Existir\u00e1 una pantalla modal de alta / edici\u00f3n\n\n- En alta / edici\u00f3n:\n  - El identificador aparecer\u00e1 vac\u00edo en creaci\u00f3n y en modo solo lectura\n  - Se seleccionar\u00e1 cliente mediante combo\n  - Se seleccionar\u00e1 juego mediante combo\n  - Se introducir\u00e1n fecha de inicio y fecha de fin en la misma fila\n  - Todos los campos, salvo el identificador, ser\u00e1n obligatorios\n\nReglas de negocio:\n- La fecha de fin no podr\u00e1 ser anterior a la fecha de inicio\n- El per\u00edodo m\u00e1ximo del pr\u00e9stamo ser\u00e1 de 14 d\u00edas\n- El mismo juego no podr\u00e1 estar prestado a m\u00e1s de un cliente para ninguno de los d\u00edas incluidos en el rango del pr\u00e9stamo\n- Un mismo cliente no podr\u00e1 tener m\u00e1s de 2 pr\u00e9stamos activos para ninguno de los d\u00edas incluidos en el rango del pr\u00e9stamo\n- Las mismas validaciones aplican tanto en creaci\u00f3n como en edici\u00f3n\n- El backend deber\u00e1 validar siempre, aunque el frontend tambi\u00e9n realice validaciones\n\nDefine:\n\n1. Descripci\u00f3n de la funcionalidad\n\n2. Reglas de negocio\n\n3. Dise\u00f1o backend:\n- Endpoints necesarios\n- Estructura del dominio (Entity, DTO, Service, Repository)\n- Tipo de operaciones (listado, creaci\u00f3n, edici\u00f3n, borrado)\n- Estrategia para filtros por fecha dentro de rangos\n\n4. Dise\u00f1o frontend:\n- Componentes necesarios\n- Flujo de interacci\u00f3n (listado, abrir modal, guardar, borrar)\n- Servicios Angular\n- Gesti\u00f3n de combos (clientes y juegos)\n- Integraci\u00f3n de filtros y paginaci\u00f3n\n- Integraci\u00f3n de Datepicker para filtro por fecha\n- Estructura funcional del listado y del formulario de alta/edici\u00f3n\n\n5. Decisiones t\u00e9cnicas:\n- Qu\u00e9 patrones del sistema actual se reutilizan\n- Qu\u00e9 partes deben extenderse\n- C\u00f3mo se gestionar\u00e1n los filtros de fecha y condiciones combinadas\n- C\u00f3mo se implementar\u00e1n validaciones basadas en m\u00faltiples registros (solapamientos y l\u00edmites)\n\nNO implementes c\u00f3digo.\nNO analices de nuevo el proyecto.\nBasa la propuesta en los patrones detectados en la fase Explore.\n

    Igual que en la gesti\u00f3n de clientes, este comando genera dentro del directorio changes la propuesta correspondiente, que incluye los siguientes ficheros: proposal.md, design.md, spec.md, tasks.md.

    Estos artefactos est\u00e1n adaptados a la funcionalidad de gesti\u00f3n de pr\u00e9stamos, incorporando las reglas de negocio, filtros y validaciones espec\u00edficas de este caso de uso.

    Constituyen la base para la siguiente fase: Apply, donde se ejecutar\u00e1 la implementaci\u00f3n siguiendo las tareas definidas.

    Responsabilidades como developer IA

    En este punto la IA te ha hecho una propuesta que puede ser correcta o no, recordemos que se trata de un modelo matem\u00e1tico-probabil\u00edstico. Si hay algo de lo propuesto que no te encaja o es err\u00f3neo deber\u00edas comentarlo mediante el chat o corregirlo de forma manual en el fichero que corresponda. Por ejemplo si quieres a\u00f1adir una tarea porqu\u00e9 se te ha olvidado incluirla en el prompt original, deber\u00edas decirle al modelo que te incluya la nueva tarea.

    Una vez estemos de acuerdo con la propuesta que nos ha hecho la IA, podemos pasar al siguiente punto.

    "},{"location":"specs/loans_paid/#apply","title":"Apply","text":"

    Una vez validada la propuesta, ejecutamos la implementaci\u00f3n:

    El objetivo de esta fase es transformar los artefactos generados (proposal.md, design.md, spec.md, tasks.md) en c\u00f3digo funcional, asegurando que:

    • Se respetan los requisitos funcionales definidos en spec.md
    • Se siguen las decisiones t\u00e9cnicas establecidas en design.md
    • Se ejecutan las tareas en el orden definido en tasks.md

    \ud83d\udcdc Prompt

    Esto es tan f\u00e1cil como escribir en el chat de Visual Studio Code el siguiente prompt:

    /opsx:apply\n

    El agente empezar\u00e1 a realizar un mont\u00f3n de tareas y pedirnos permisos. Es posible que algunas de esas tareas fallen y \u00e9l mismo lo reintente de otra forma. El resultado deber\u00eda ser el c\u00f3digo generado e implementado tanto en la carpeta backend como en la carpeta frontend y un resumen de todas las tareas realizadas y checkeadas por la IA.

    "},{"location":"specs/loans_paid/#verificacion","title":"Verificaci\u00f3n","text":"

    Un paso que no pertenece a OpenSpec pero que es altamente recomendable es probar los cambios realizados.

    Arranca el backend y el frontend y verifica:

    • La aplicaci\u00f3n levanta correctamente
    • Las nuevas funcionalidades a\u00f1adidas est\u00e1n accesibles
    • Los flujos principales definidos en spec.md funcionan como se espera

    Ojo no te fies

    Ojo no te fies de todo lo que construya la IA. Tu est\u00e1s al mando, tu debes decidir si el sistema est\u00e1 correctamente implementado o no. Es tu responsabilidad.

    Si NO est\u00e1s a gusto con la implementaci\u00f3n o se ha dejado algo por hacer, es el momento de escribirlo por el chat indic\u00e1ndole exactamente que es lo que falta. Cuanto m\u00e1s preciso y conciso seas, mejor implementar\u00e1 la IA.

    "},{"location":"specs/loans_paid/#archive","title":"Archive","text":"

    Y llegamos a la \u00faltima etapa que nos define OpenSpec, donde se archiva el cambio y se da por finalizada la funcionalidad.

    El objetivo de esta fase es marcar la funcionalidad como completada, consolidar todos los artefactos generados durante el proceso y dejar el sistema en un estado estable, coherente y preparado para nuevas evoluciones.

    \ud83d\udcdc Prompt

    De nuevo nos vamos al chat de Visual Studio Code el siguiente prompt:

    /opsx:archive\n

    Durante el proceso de Archive, el sistema solicitar\u00e1 confirmaci\u00f3n para sincronizar los requisitos antes de archivar el cambio.

    Recuerda que al sincronizar, los requisitos definidos en spec.md pasan de ser un cambio temporal a formar parte permanente del sistema.

    Si no se sincroniza, el c\u00f3digo queda implementado, pero los requisitos no se registran en los specs principales afectando a la trazabilidad y futuras evoluciones del sistema.

    "},{"location":"specs/prepare/","title":"Preparaci\u00f3n del entorno","text":"

    Warning

    Esta secci\u00f3n se encuentra en desarrollo \ud83d\udea7. NO se recomienda realizarla a menos que te lo hayan indicado expresamente.

    En esta secci\u00f3n asumimos que ya completaste el tutorial base y que el entorno de Angular y Spring Boot est\u00e1 configurado.

    Tambi\u00e9n es recomendable haber hecho el ejercicio Ahora hazlo tu! para que el contexto funcional te resulte familiar.

    Partimos, por tanto, de un entorno con las herramientas b\u00e1sicas ya instaladas.

    Daremos por hecho que ya dispones de:

    • Visual Studio Code
    • Node.js
    • Angular CLI
    • Java (17 o superior)

    Estas herramientas son prerrequisitos y aqu\u00ed no repetiremos su instalaci\u00f3n en detalle.

    Info

    Si alguna de estas herramientas no est\u00e1 instalada o necesitas revisar el proceso completo de configuraci\u00f3n, puedes consultar los siguientes apartados del tutorial:

    • Entorno de desarrollo \u2013 Angular
    • Entorno de desarrollo \u2013 Spring Boot
    "},{"location":"specs/prepare/#prerrequisitos-tecnicos","title":"Prerrequisitos t\u00e9cnicos","text":"

    Vamos a preparar el entorno para trabajar con Spec-Driven Development usando OpenSpec desde Visual Studio Code, en un \u00fanico workspace con frontend, backend y especificaciones.

    "},{"location":"specs/prepare/#verificacion-de-nodejs","title":"Verificaci\u00f3n de Node.js","text":"

    OpenSpec se distribuye como una herramienta basada en Node.js, por lo que es necesario tener instalado Node.js 20.19.0 o superior.

    Para comprobar la versi\u00f3n instalada, ejecuta en una terminal:

    node --version\n

    Si no tienes Node.js instalado o tu versi\u00f3n es inferior, puedes descargarlo desde su web oficial.

    Se recomienda instalar la versi\u00f3n LTS m\u00e1s reciente.

    Si tienes restricciones de permisos en el port\u00e1til, tambi\u00e9n es posible instalar Node.js a trav\u00e9s del Portal de Empresa, siguiendo el mismo procedimiento utilizado durante la configuraci\u00f3n del Entorno de desarrollo para el tutorial:

    1. Accede al Portal de Empresa
    2. Entra en el cat\u00e1logo de aplicaciones pre\u2011aprobadas
    3. Busca Node.js
    4. Inst\u00e1lalo desde ah\u00ed

    Una vez finalizada la instalaci\u00f3n, vuelve a ejecutar el comando node --version para verificar que Node.js est\u00e1 correctamente instalado.

    "},{"location":"specs/prepare/#instalacion-de-openspec","title":"Instalaci\u00f3n de OpenSpec","text":"

    OpenSpec se puede instalar de forma global con cualquier gestor compatible con Node.js.

    Si utilizas npm, ejecuta el siguiente comando:

    npm install -g @fission-ai/openspec@latest\n

    Info

    OpenSpec tambi\u00e9n es compatible con pnpm, yarn o bun. En esta gu\u00eda usaremos npm por simplicidad.

    Una vez finalizada la instalaci\u00f3n, verifica que OpenSpec est\u00e1 correctamente instalado ejecutando:

    openspec --version\n

    Si el comando responde correctamente mostrando la versi\u00f3n instalada, el entorno ya est\u00e1 preparado para trabajar con Spec\u2011Driven Development utilizando OpenSpec.

    "},{"location":"specs/prepare/#convenciones-de-trabajo-aplican-a-todos-los-ejercicios","title":"Convenciones de trabajo (aplican a todos los ejercicios)","text":""},{"location":"specs/prepare/#github-copilot","title":"GitHub Copilot","text":"

    Necesitas una cuenta de GitHub con Copilot (gratuita o premium) y haber iniciado sesi\u00f3n en Visual Studio Code para usar el chat.

    "},{"location":"specs/prepare/#estructura-inicial-del-proyecto","title":"Estructura inicial del proyecto","text":"

    A partir de aqu\u00ed necesitas los proyectos base (sin el ejercicio hecho). Si no los tienes, puedes descargarlos en https://github.com/ccsw-csd/tutorial-proyectos.

    En esta gu\u00eda vamos a usar server-springboot y client-angular17. Ambos deben estar en el mismo directorio ra\u00edz. Para simplificar, durante todo el documento, los llamaremos:

    • backend
    • frontend

    La estructura deber\u00eda ser similar a esta:

    "},{"location":"specs/prepare/#reglas-generales-y-de-ejecucion","title":"Reglas generales y de ejecuci\u00f3n","text":"

    Durante todos los ejercicios:

    • Empieza cada cambio relevante en un chat nuevo para no arrastrar contexto innecesario.
    • Revisa siempre la propuesta antes de ejecutar la fase de Apply.
    • Valida manualmente los resultados funcionales despu\u00e9s de aplicar.
    "},{"location":"specs/prepare/#y-ahora-que","title":"\u00bfY ahora qu\u00e9?","text":"

    A partir de aqu\u00ed eliges ruta:

    • Con licencia de pago \ud83d\udcb0 tendr\u00e1s m\u00e1s contexto y menos fragmentaci\u00f3n.
    • Con licencia gratuita \ud83c\udd93 tendr\u00e1s menos contexto y m\u00e1s iteraciones. Adem\u00e1s es posible que superes las limitaciones diarias o de hora y tengas que esperar al d\u00eda siguiente para continuar con el tutorial.

    El flujo funcional es el mismo en ambos casos.

    Elige tu camino:

    • \ud83c\udd93 Gesti\u00f3n de clientes
    • \ud83c\udd93 Gesti\u00f3n de pr\u00e9stamos
    • \ud83d\udcb0 Gesti\u00f3n de clientes
    • \ud83d\udcb0 Gesti\u00f3n de pr\u00e9stamos
    "}]} \ No newline at end of file diff --git a/site/sitemap.xml.gz b/site/sitemap.xml.gz index 49396f3..50b2cbd 100644 Binary files a/site/sitemap.xml.gz and b/site/sitemap.xml.gz differ