Saturday, July 1, 2023

Spring Data Reactive With MongoDB and GrahpQL

Spring Boot Reactive is a framework that makes it easy to build reactive applications using Spring Boot. It provides a number of features that make it easier to develop, test, and deploy reactive applications, including:

  • Reactive web support: Spring Boot Reactive provides support for building reactive web applications using Spring WebFlux.
  • Reactive messaging support: Spring Boot Reactive provides support for building reactive messaging applications using Spring Cloud Stream.
  • Reactive testing support: Spring Boot Reactive provides support for testing reactive applications using Spring Boot Test.
  • Reactive deployment support: Spring Boot Reactive provides support for deploying reactive applications to a variety of environments, including cloud environments.
  • Flux and Mono are two reactive types that are used in Spring Boot Reactive applications. They are both implementations of the Publisher interface, which means that they can emit a sequence of elements to subscribers. The main difference between Flux and Mono is that Flux can emit zero or more elements, while Mono can only emit zero or one element. This means that Flux is more general-purpose than Mono, but it can also be more complex to use.
  


GraphQL is an open-source query language and runtime for APIs (Application Programming Interfaces). It was developed by Facebook and released in 2015. GraphQL provides a flexible and efficient way to define, query, and manipulate data in APIs.


Technology

  • Spring Boot 3.1.1
  • GraphQL
  • MongoDB
  • Docker
  • Maven 
  • IntelliJ IDEA

Configuration Spring Boot project  :

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.henry</groupId>
<artifactId>spring-data-mongodb-graphql-reactive</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-data-reactive-mongodb</name>
<description>Demo project for Spring Boot with MongoDB</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/de.flapdoodle.embed/de.flapdoodle.embed.mongo
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo.spring30x</artifactId>
<version>4.7.0</version>
</dependency>-->

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>

<!-- https://mvnrepository.com/artifact/com.graphql-java/graphql-java-tools -->
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>5.2.4</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>


application.yml:


# graphql Configuration
graphql:
graphiql:
enabled: true

# mongodb Configuration
spring:
data:
mongodb:
uri: mongodb://test:test@localhost:27017/tech_notes?authSource=admin


Schema

Spring for GraphQL application, create a directory src/main/resources/graphql. Add a new file schema.graphqls to this folder with the following content:


type Query {
getAllCategories: [Category]
getCategoryById(id: ID): Category
}

type Mutation {
createCategory(input: CategoryInput): Category
updateCategory(id: ID, input: CategoryInput): Category
deleteCategory(id: ID): Boolean
deleteAllCategories: Boolean
}

type Category {
id: ID
title: String
posts: [String]
}

input CategoryInput {
id: Int
title: String
posts: [String]
}


Spring Data Reactive GraphQL Application


Config


package com.henry.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.config.CorsRegistry;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.config.WebFluxConfigurer;

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addCorsMappings(CorsRegistry corsRegistry) {
corsRegistry.addMapping("/**")
.allowedOrigins("*")
//.allowedMethods("POST")
.maxAge(3600);
}
}


Model


package com.henry.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

import java.util.List;

@AllArgsConstructor
@NoArgsConstructor
@Data
@Document(collection = "categories")
public class Category {

@Id
private long id;
private String title;
private List<String> posts;
}

package com.henry.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@AllArgsConstructor
@NoArgsConstructor
@Data
@Document(collection = "cat_sequences")
public class Sequence {
@Id
private String id;
private long value;
}


Repository


package com.henry.repository;

import com.henry.model.Category;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;

public interface CategoryRepository extends ReactiveMongoRepository<Category, Long> {
}

package com.henry.repository;

import com.henry.model.Sequence;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;

public interface SequenceRepository extends ReactiveCrudRepository<Sequence, String> {
}


Service


package com.henry.service;

import com.henry.model.Category;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

public interface CategoryService {
public Flux<Category> getAllCategories();
public Mono<Category> getCategoryById(Long id);
public Mono<Category> createCategory(Category category);
public Mono<Category> updateCategory(Long id, Category updatedCategory);

public Mono<Void> deleteCategory(Long id);

public Mono<Void> deleteAllCategories();
}

Resolver


package com.henry.resolver;

import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import com.coxautodev.graphql.tools.GraphQLQueryResolver;
import com.henry.model.Category;
import com.henry.model.Sequence;
import com.henry.repository.CategoryRepository;
import com.henry.repository.SequenceRepository;
import com.henry.service.CategoryService;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Component
public class CategoryResolver implements CategoryService, GraphQLQueryResolver, GraphQLMutationResolver {

private final SequenceRepository sequenceRepository;
private final CategoryRepository categoryRepository;

public CategoryResolver(SequenceRepository sequenceRepository, CategoryRepository categoryRepository) {
this.sequenceRepository = sequenceRepository;
this.categoryRepository = categoryRepository;
}

@Override
public Flux<Category> getAllCategories() {
return categoryRepository.findAll();
}

@Override
public Mono<Category> getCategoryById(Long id) {
return categoryRepository.findById(id);
}

@Override
public Mono<Category> createCategory(Category category) {
return getNextSequenceId().map(id -> {
category.setId(id);
return category;
}).flatMap(categoryRepository::save);
}

@Override
public Mono<Category> updateCategory(Long id, Category updatedCategory) {
return categoryRepository.findById(id)
.flatMap(category -> {
category.setTitle(updatedCategory.getTitle());
category.setPosts(updatedCategory.getPosts());
return categoryRepository.save(category);
});
}

@Override
public Mono<Void> deleteCategory(Long id) {
return categoryRepository.deleteById(id);
}

@Override
public Mono<Void> deleteAllCategories() {
return categoryRepository.deleteAll();
}

private Mono<Long> getNextSequenceId() {
return sequenceRepository.findById("categoryId")
.map(sequence -> {
long nextValue = sequence.getValue() + 1;
sequence.setValue(nextValue);
return sequence;
})
.defaultIfEmpty(new Sequence("categoryId", 1))
.flatMap(sequenceRepository::save)
.map(Sequence::getValue);
}

}



Controller

package com.henry.controller;

import com.henry.model.Category;
import com.henry.resolver.CategoryResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Controller
public class CategoryController {

private final CategoryResolver categoryResolver;

@Autowired
public CategoryController(CategoryResolver categoryResolver) {
this.categoryResolver = categoryResolver;
}

@QueryMapping
public Flux<Category> getAllCategories() {
return categoryResolver.getAllCategories();
}

@QueryMapping
public Mono<Category> getCategoryById(@Argument Long id) {
return categoryResolver.getCategoryById(id);
}

@SchemaMapping(typeName = "Mutation", field = "createCategory")
public Mono<Category> createCategory(@Argument(name = "input") Category category) {
return categoryResolver.createCategory(category);
}

@SchemaMapping(typeName = "Mutation", field = "updateCategory")
public Mono<Category> updateCategory(@Argument Long id, @Argument(name = "input") Category updatedCategory) {
return categoryResolver.updateCategory(id, updatedCategory);
}

@SchemaMapping(typeName = "Mutation", field = "deleteCategory")
public Mono<Void> deleteCategory(@Argument Long id) {
return categoryResolver.deleteCategory(id);
}

@SchemaMapping(typeName = "Mutation", field = "deleteAllCategories")
public Mono<Void> deleteAllCategories() {
return categoryResolver.deleteAllCategories();
}
}



Downloading an installing MongoDB in Ubuntu


Create a docker-compose-mongodb.yml file with the following contents:


version: "2.0"
services:
  mongodb_container:
    image: mongo:latest
    environment:
      MONGO_INITDB_ROOT_USERNAME: test
      MONGO_INITDB_ROOT_PASSWORD: test
      MONGO_INITDB_DATABASE: tech_notes
    ports:
      - 27017:27017
    volumes:
      - mongodb_data_container:/data/db

volumes:
  mongodb_data_container:


Run the following command to start the MongoDB container:
 docker-compose -f docker-compose-mongodb.yml up -d


Run & Test

Run Spring Boot application with command: mvn spring-boot:run. by console, IntelliJ etc.


Postman

POST
http://localhost:8080/graphql


createCategory query:

mutation {
  createCategory(
    input: {
      title"Java"
      posts: [
        "Spring Data Reactive With MongoDB and GrahpQL"
        "Angular 16  Spring Boot 3 Spring Data GraphQL CRUD"
        "Spring Boot 3 Spring Data GraphQL CRUD"
        "Spring Boot 3 Spring Data  Elasticsearch "
      ]
    }
  ) {
    id
    title
    posts
  }
}









updateCategory query:

mutation {
  updateCategory(
    id2
    input: {
      title"Java"
      posts: [
        "Spring Data Reactive With MongoDB and GrahpQL"
        "Spring Security 6 Custom Login, OAuth2 Login with Google and Basic Auth"
        "Spring Boot 3 Spring Data GraphQL CRUD"
        "Spring Boot 3 Spring Data  Elasticsearch "
      ]
    }
  ) {
    id
    title
    posts
  }
}





deleteCategory query:

mutation {
  deleteCategory
    id2
   ) 
}





deleteAllCategories query:

mutation {
  deleteAllCategories
}





getCategoryById query:

query  {
  getCategoryById(id3) {
      title
    posts
  }
}





getAllCategories query:

query {
  getAllCategories
  {
    id
    title
    posts
  }
}









Source Code


Here on GitHub.


















            

Friday, June 16, 2023

Angular 16 Spring Boot 3 Spring Data GraphQL CRUD

Angular 16 was released in April 2022, and it includes a number of new features and improvements. Some of the key features of Angular 16 include:

  • Standalone components: Angular 16 introduces the concepts of standalone components, which are components that can be used independently of any other angular code. this makes it easier to reuse components and to create more modular applications.
  • Signals:  Signals are a new way of handling reactivity in Angular. Signals provide a simpler and more efficient way to notify the framework when data changes.
  • Non-destructive hydration: Angular 16 introduces a new approach to hydration called no-destructive hydration. This approach allows Angular applications to be loaded more efficiently and to perform better in terms of performance.
  • Improved security: Angular 16 includes a number of security improvements, including support for native Trusted Types and CSP.
  • For more.





Creating  Angular Application

In this example I integration Angular, GraphQL, and Spring Boot 3.

  • Configuration Angular 
  • Components
  • Services

Angular GraphQL API 

Replace with your GraphQL API URL

MethodsURLs
POSThttp://localhost:8080/graphql

Technology

  • Visual Studio Code 
  • Node 16
  • Npm
  • Angular Cli 16
  • Angular Material 16

Project Structure






















Install Angular Material

ng add @angular/material


app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AuthorsComponent } from './components/authors/authors.component';
import { MatTableModule } from '@angular/material/table';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatDialogModule } from '@angular/material/dialog';

import {MatIconModule} from '@angular/material/icon';
import {MatDividerModule} from '@angular/material/divider';
import {MatButtonModule} from '@angular/material/button';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';

import { HttpClientModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms';

import { AuthorsService } from './services/authors.service';
import { DialogAuthorComponent } from './components/dialog/dialog-author/dialog-author.component';

@NgModule({
  declarations: [
    AppComponent,
    AuthorsComponent,
    DialogAuthorComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    BrowserAnimationsModule,
    MatTableModule,
    MatPaginatorModule,
    MatSortModule,
    MatIconModule,
    MatDividerModule,
    MatButtonModule,
    MatFormFieldModule,
    MatDialogModule,
    MatInputModule,
    HttpClientModule,
    FormsModule
  ],
  providers: [AuthorsService],
  bootstrap: [AppComponent]
})
export class AppModule { }


Components

authors.component.ts

import { AfterViewInit, Component, ViewChild, OnInit } from '@angular/core';
import { MatTable } from '@angular/material/table';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatDialog } from '@angular/material/dialog';
import { DialogAuthorComponent } from '../dialog/dialog-author/dialog-author.component';
import { MatTableDataSource } from '@angular/material/table';
import { Subscription } from 'rxjs';

import { AuthorsService } from '../../services/authors.service';

// TODO: Replace this with your own data model type
export interface AuthorsItem {
  id: number;
  firstName: string;
  lastName: string;
  bookRecords: BookItem[];
}

export interface BookItem {
  id: number;
  title: string;
  author: AuthorsItem;
}

@Component({
  selector: 'app-authors',
  templateUrl: './authors.component.html',
  styleUrls: ['./authors.component.scss']
})
export class AuthorsComponent implements AfterViewInit, OnInit {
  @ViewChild(MatPaginator) paginator!: MatPaginator;
  @ViewChild(MatSort) sort!: MatSort;
  @ViewChild(MatTable) table!: MatTable<AuthorsItem>;
  dataSource: any;
  private subscription: Subscription = new Subscription;

  /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
  displayedColumns = ['id', 'firstName', 'lastName', 'update', 'delete'];

  constructor(public dialog: MatDialog, private authorService: AuthorsService) { }

  ngOnInit(): void { }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }

  ngAfterViewInit(): void {
    this.getAllAuthors();
  }

  getAllAuthors(): void {

    this.subscription = this.authorService.getAllAuthors().subscribe({
      next: (response: any) => {
        // Handle successful response, if needed
        this.dataSource = new MatTableDataSource(response.data.getAllAuthors);
        this.table.dataSource = this.dataSource;
        this.dataSource.sort = this.sort;
        this.dataSource.paginator = this.paginator;
      },
      error: (error: any) => {
        console.error('Error updating author:', error);
      },    // errorHandler
    });

  }

  deleteAuthor(id: number): void {

    this.authorService.deleteAuthor(id).subscribe({
      complete: () => {
        console.log('Author deleted');
        this.getAllAuthors();

      }, // completeHandler
      error: (error: any) => {
        console.error('Error deleting author:', error);
      },    // errorHandler
    });

  }

  openDialog(enterAnimationDuration: string, exitAnimationDuration: string, mode: string, row?: any): void {
    const dialogRef = this.dialog.open(DialogAuthorComponent, {
      width: '250px',
      enterAnimationDuration,
      exitAnimationDuration,
      data: {
        mode: mode,
        id: (mode === 'create' ? null : row.id),
        firstName: (mode === 'create' ? '' : row.firstName),
        lastName: (mode === 'create' ? '' : row.lastName)
      }
    });

    dialogRef.afterClosed().subscribe(
      data => {
        if (data === 1) {
          this.getAllAuthors();
        }
      }
    );

  }

  applyFilter(event: Event) {
    const filterValue = (event.target as HTMLInputElement).value;
    this.dataSource.filter = filterValue.trim().toLowerCase();
  }

}




authors.component.scss


.mat-mdc-form-field {
  margin-bottom: 16px; /* Adjust the margin-bottom as desired */
  width: 100%; /* Adjust the width as desired */
}

.mat-mdc-form-field .mat-mdc-input-element {
  border-radius: 4px; /* Adjust the border radius as desired */
  background-color: #f1f1f1; /* Set the background color for the input field */
  padding: 8px; /* Adjust the padding as desired */
}

.mat-mdc-form-field .mat-mdc-input-element:focus {
  outline: none; /* Remove the focus outline */
  background-color: #ffffff; /* Set the background color when the input field is focused */
  box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); /* Add a box shadow when the input field is focused */
}


.mat-mdc-row:nth-child(even) {
  background-color: #f1f1f1; /* Set the background color for even rows */
}

.mat-mdc-row:nth-child(odd) {
  background-color: #ffffff; /* Set the background color for odd rows */
}

.mat-mdc-table .mdc-data-table__header-row {
  height: 35px;
}

.mat-mdc-table .mdc-data-table__row{
  height: 35px;
}

.mdc-fab {
  position: relative;
  display: inline-flex;
  position: relative;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  width: 35px;
  height: 35px;
  padding: 0;
  border: none;
  fill: currentColor;
  text-decoration: none;
  cursor: pointer;
  user-select: none;
  overflow: visible;
  transition: box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1),opacity 15ms linear 30ms,transform 270ms 0ms cubic-bezier(0, 0, 0.2, 1);
}

.header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 20px;
  background-color: #f1f1f1;
}

.graphql-header {
  font-size: 24px;
  font-weight: bold;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-grow: 1;
  color: #444;
}

.button-container {
  display: flex;
  align-items: center;
  justify-content: flex-end;
}

.content-container {
  display: flex;
  align-items: center;
}

.text {
  margin-right: 8px; /* Adjust the margin as desired */
}

.table-container {
  margin: 20px;
}

@media (max-width: 600px) {
  .header {
    flex-direction: column;
    align-items: flex-start;
  }

  .button-container {
    margin-top: 10px;
  }

  .mdc-fab {
    width: 20px;
    height: 20px;
  }

  .mat-mdc-table .mdc-data-table__header-row {
    height: 20px;
  }
 
  .mat-mdc-table .mdc-data-table__row{
    height: 20px;
  }

}


authors.component.html

<div class="header">
  <div class="graphql-header">
    Author CRUD - GraphQL with Spring Boot 3
  </div>
  <div class="button-container">
    <button mat-raised-button color="primary" (click)="openDialog('0ms', '0ms', 'create')">
      <span class="content-container">
        <span class="text">Create Author</span>
        <span class="icon-container">
          <mat-icon>add</mat-icon>
        </span>
      </span>
    </button>
  </div>
</div>



<div class="table-container">
 
  <mat-form-field>
    <input matInput (keyup)="applyFilter($event)" placeholder="Filter">
  </mat-form-field>

  <table mat-table  matSort aria-label="Elements">
    <!-- Id Column -->
   <ng-container matColumnDef="id">
     <th mat-header-cell *matHeaderCellDef mat-sort-header>Id</th>
     <td mat-cell *matCellDef="let row">{{row.id}}</td>
   </ng-container>
 
  <!-- Name Column -->
   <ng-container matColumnDef="firstName">
     <th mat-header-cell *matHeaderCellDef mat-sort-header>Name</th>
     <td mat-cell *matCellDef="let row">{{row.firstName}}</td>
   </ng-container>


    <!-- lastName Column -->
    <ng-container matColumnDef="lastName">
      <th mat-header-cell *matHeaderCellDef mat-sort-header>Last name</th>
      <td mat-cell *matCellDef="let row">{{row.lastName}}</td>
    </ng-container>
 
    <ng-container matColumnDef="update">
     <th mat-header-cell *matHeaderCellDef mat-sort-header></th>
     <td mat-cell *matCellDef="let row">
       <button mat-fab color="primary" aria-label="Example icon button with a update icon" (click)="openDialog('0ms', '0ms', 'edit', row)">
         <mat-icon>edit</mat-icon>
       </button>
     </td>
   </ng-container>
    <ng-container matColumnDef="delete">
     <th mat-header-cell *matHeaderCellDef mat-sort-header></th>
     <td mat-cell *matCellDef="let row">
       <button mat-fab color="warn" aria-label="Example icon button with a delete icon" (click)="deleteAuthor(row.id)">
         <mat-icon>delete</mat-icon>
       </button>
     </td>
   </ng-container>

   <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
   <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
 </table>


 
  <mat-paginator #paginator [pageIndex]="0" [pageSize]="10"
    [pageSizeOptions]="[5, 10, 20]" aria-label="Select page">
  </mat-paginator>


</div>


dialog-author.component.ts

import { Component, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { AuthorsService } from '../../../services/authors.service';

@Component({
  selector: 'app-dialog-author',
  templateUrl: './dialog-author.component.html',
  styleUrls: ['./dialog-author.component.scss']
})
export class DialogAuthorComponent {

  firstName: string;
  lastName: string;
  id: number;
  mode: string;

  constructor(public dialogRef: MatDialogRef<DialogAuthorComponent>,
    @Inject(MAT_DIALOG_DATA) public data: any,
    private authorService: AuthorsService) {

    this.mode = data.mode;
    this.firstName = data.firstName || '';
    this.lastName = data.lastName || '';
    this.id = data.id || 0;
  }

  onCancel(): void {
    this.dialogRef.close();
  }


  updateAuthor(): void {

    this.authorService.updateAuthor(this.id, this.firstName, this.lastName).subscribe({
      next: (response: any) => {
        // Handle successful response, if needed
        console.log('Author updated:', response.data.updateAuthor);
        this.dialogRef.close(1);
      }, // completeHandler
      error: (error: any) => {
        console.error('Error updating author:', error);
        this.dialogRef.close(0);
      },    // errorHandler
    });

  }

  createAuthor(): void {

    this.authorService.createAuthor(this.firstName, this.lastName).subscribe({
      next: (response: any) => {
        // Handle successful response, if needed
        console.log('Author created:', response.data.createAuthor);
        this.dialogRef.close(1);
      }, // completeHandler
      error: (error: any) => {
        this.dialogRef.close(0);
        console.error('Error creating author:', error);
      },    // errorHandler
    });

  }

}


dialog-author.component.scss

.dialog-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 16px;
  }
 
  .dialog-title {
    display: flex;
    align-items: center;
    font-weight: bold;
  }
 
  .dialog-title mat-icon {
    margin-right: 4px;
  }
 
  .dialog-title span {
    font-size: 20px;
    font-weight: bold;
    margin: 0;
  }
 
  .close-button {
    margin-left: 8px;
  }

.mdc-dialog__title {
    font-size: 20px;
    font-weight: bold;
    margin-bottom: 16px;
}

.mdc-dialog__content {
    display: flex;
    flex-direction: column;
    margin-bottom: 16px;
}

.mdc-dialog__content mat-form-field {
    width: 100%;
    margin-bottom: 8px;
}

.mdc-dialog__actions {
    display: flex;
    justify-content: flex-end;
}

.mdc-dialog__actions button {
    margin-left: 8px;
}

/* Responsive styles */
@media screen and (max-width: 600px) {
    .dialog-header {
      flex-direction: column;
      align-items: flex-start;
    }
   
    .dialog-title {
      margin-bottom: 8px;
    }
   
    .mdc-dialog__actions {
      flex-direction: column;
    }
   
    .mdc-dialog__actions button {
      margin-top: 8px;
    }
  }


dialog-author.component.html

<div class="dialog-header">
  <div class="dialog-title">
    <mat-icon>person_add</mat-icon>
    <span>{{ mode === 'create' ? 'Create Author' : 'Edit Author' }}</span>
  </div>
  <button mat-icon-button class="close-button" (click)="onCancel()">
    <mat-icon>close</mat-icon>
  </button>
</div>

<div mat-dialog-content>
  <ng-container *ngIf="mode === 'edit'">
    <mat-form-field>
      <input matInput [(ngModel)]="id" [disabled]="true" placeholder="ID">
    </mat-form-field>
  </ng-container>
  <mat-form-field>
    <input matInput [(ngModel)]="firstName" placeholder="First Name">
  </mat-form-field>
  <mat-form-field>
    <input matInput [(ngModel)]="lastName" placeholder="Last Name">
  </mat-form-field>
</div>
<div mat-dialog-actions>
  <button mat-button (click)="onCancel()">Cancel</button>
  <button mat-button color="primary" (click)="mode === 'create' ? createAuthor() : updateAuthor()">
    {{ mode === 'create' ? 'Create' : 'Update' }}
  </button>
</div>


Services

authors.service.ts: We can find GraphQL Queries and Mutations.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class AuthorsService {

  constructor(private http: HttpClient) { }

  getAllAuthors(): Observable<any> {
    const query = `
      query {
        getAllAuthors {
          id
          firstName
          lastName
          bookRecords {
            id
          }
        }
      }
    `;
    return this.http.post<any>(environment.apiUrl, { query });
  }

  getAuthorById(id: number): Observable<any> {
    const query = `
      query {
        getAuthorById(id: ${id}) {
          firstName
          lastName
          bookRecords {
            id
          }
        }
      }
    `;
    return this.http.post<any>(environment.apiUrl, { query });
  }

  createAuthor(firstName: string, lastName: string): Observable<any> {
    const mutation = `
      mutation {
        createAuthor(firstName: "${firstName}", lastName: "${lastName}") {
          id
          firstName
          lastName
        }
      }
    `;
    return this.http.post<any>(environment.apiUrl, { query: mutation });
  }

  updateAuthor(id: number, firstName: string, lastName: string): Observable<any> {
    const mutation = `
      mutation {
        updateAuthor(id: ${id}, firstName: "${firstName}", lastName: "${lastName}") {
          id
          firstName
          lastName
        }
      }
    `;
    return this.http.post<any>(environment.apiUrl, { query: mutation });
  }

  deleteAuthor(id: number): Observable<any> {
    const mutation = `
      mutation {
        deleteAuthor(id: ${id})
      }
    `;
    return this.http.post<any>(environment.apiUrl, { query: mutation });
  }

}


Environments

environment.ts

// This file can be replaced during build by using the `fileReplacements` array.
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.

export const environment = {
    production: false,
    apiUrl: "http://localhost:8080/graphql" // Replace with your GraphQL API URL
  };
 


Run & Test


Run Angular application with command: ng serve





And Create, Read, Update and Delete.


Creating a Spring Boot Application


Integration Spring boot 3, Spring Data JPA, GraphQL, HyperSQL and Java 17

Detailed description and steps for run backend project found here in this post:












🚀 Spring Boot 3.5 → 4.0.3 Migration Summary

Import Changes, API Adjustments & Compatibility Results Migration summary from Spring Boot 3.5 → 4.0, including breaking changes, depend...