The purpose of this document is to provide guidelines for proceeding efficiently and consistently with implementation work in this project.

This book is aimed at developers participating in this project. Developers aim to efficiently develop high-quality systems by complying with the processes shown in this document and the standards shown in the architecture description.

How to continue reading the guide

We recommend that you read this book in the following order. This order is designed so that developers can check the main points of this document and architecture description while referring to the actual working application and source code at hand.

  1. Building an environment
    Development environment construction procedure In accordance with the instructions, an execution/development environment is built on the PC used for development.

  2. Check the application
    Run the application in the built environment. Architectural description's use cases Operate the application against and check the implemented functions.

  3. Review the application architecture

    1. logical configuration
      Architectural description's application configuration , component configuration See and review the components that make up the application and their responsibilities.

    2. physical implementation
      Implementation steps See the source code for each component described there and check how the component is implemented.

Development environment construction procedure

Installing required software

The following software is required for development.

  • Docker desktop

  • JDK v21

  • Node.js v22

  • pnpm

  • Git

  • Visual Studio Code

Follow the instructions for each OS below to install the software.

Windows

On Windows, use Chocolatey to install software.

Start Powershell with administrative rights. (See below for startup operations)

  1. keyboard shortcuts Windows + R Run it.

  2. displayed ファイル名を選択して実行 In dialogs powershell Type

  3. keyboard shortcuts Ctrl + shift + Enter Run it.

Run the following command in the Powershell window that opens.

Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))

Then run the following command to install the required software.

choco install -y docker-desktop
choco install -y corretto21jdk
choco install -y nodejs-lts
choco install -y pnpm
choco install -y git
choco install -y vscode

macOS

On macOS, use Homebrew to install software.

Run the following command in a terminal to install Homebrew.

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Then run the following command to install the required software.

brew install --cask docker
brew install --cask corretto@21
brew install node@22
brew install pnpm
brew install git
brew install --cask visual-studio-code

Get project materials and set up

Execute the following command in the command prompt (Windows) /terminal (macOS) to obtain project materials and set up the development environment.

(1)
git clone https://github.com/project-au-lait/svqk.git
cd svqk

(2)
chcp 65001
mvnw install -T 1C -P setup,browse-e2etest

(3)
./mvnw install -T 1C -P setup,browse-e2etest
1 Acquisition of project materials
2 Setting up a development environment (Command Prompt (Windows))
chcp 65001 is a command to set the command prompt character code to UTF-8
3 Setting up a development environment (for terminal (macOS))

Processing details of the above setup command and implementation method Build Configurations See.

How to use the development environment

Open a project

Run the following command to open the project as a VSCode workspace.

code svqk.code-workspace

Once the VSCode workspace opens, use the following dialog displayed at the bottom right of the window Install Click the button to install the extension.

recommended extension

Once you've opened a VSCode workspace, you can open it by following the steps below.

  1. Start VSCode

  2. File menu > Open Recent > Select svqk (Workspace)

How to use projects

Table 1. How to use projects
How to use operation behavior

Start the DBMS

VSCode Task: start-db
Command: mvnw -f svqk-container compile -P setup

DBMS (PostgreSQL) starts.
This VSCode task is also automatically executed when the VSCode workspace is opened.

Start Backend

VSCode Task: start-back
Command: mvnw -f svqk-back quarkus:dev

The backend server (Quarkus) starts.
Once the Backend server is started, the web API application implemented within the BACK project can be used.

Run Backend debugging

VSCode debug configuration: Attach Quarkus

VSCode attaches to the Backend server's debug port.
This enables VSCode debug execution (step execution with breakpoints, etc.).
This operation can be performed with Backend running.

Start Frontend

VSCode Task: start-front
Command: pnpm dev --open
(at svqk-front directory)

The Frontend server (Vite) starts.
When the Frontend server starts, the browser starts and the web application implemented within the FRONT project is displayed.

Run DB migrations

VSCode Task: migrage
Command: mvnw -f svqk-migration compile -P setup

The DS migration tool (Flyway) is executed.
When execution is completed, the migration materials (sql, java) in the migration project are reflected in the DB.
This operation can be performed while the DBMS is running.

Entity generation

VSCode Task: gen-entity

jpa-entity-generator is executed.
When execution is finished, a JPA Entity Java file is generated within the Entity project.
This operation can be performed while the DBMS is running.

Generating an API Client for Front

VSCode Task: gen-api-client
Command: pnpm gen-api-client
(at svqk-front directory)

swagger-typescript-api is executed. Once the execution is complete, an API client will be generated within the FRONT project and the E2eTest project.
This operation can be performed with Backend running.

How to run VSCode Tasks

  1. keyboard shortcuts Ctrl + shift + P execute

  2. In the displayed command palette task and enter

  3. Tasks: Run Task select

access information

Table 2. access information
Connect to Connection information/URL

DBMS

  • Port: 5431

  • db:postgres

  • schema: public

  • user: svqk

  • password: svqk

Quaruks Developer UI

http://localhost:8081/q/dev-ui

Backend Web APIs (Swagger UI)

http://localhost:8081/q/dev-ui/io.quarkus.quarkus-smallrye-openapi/swagger-ui

Frontend Web App

Implementation steps

The implementation steps guide how to implement the source code, where to place the files, and the order in which they are implemented.

The implementation method explains the role of each line of source code using a reference implementation as an example, and developers can implement source code that conforms to the architecture description by referring to this and implementing their own scope of responsibility.

The implementation order is structured so that the implementation results can be immediately run and checked as an application.

Implementation procedures are categorized by process, and developers can refer to processes that are classified in the same category as their area of responsibility.

Registration screen

Here, I will explain the procedure for implementing the registration screen. The explanation takes the ticket registration screen of the reference implementation as an example.

input page
Figure 1. Example of registration screen Ticket registration screen

Items for entering registration details and a button to execute registration are placed on the registration screen. Also, there are the following two processes on the registration screen.

  1. Initialization process when the screen transitions

  2. Registration process for data entered on the screen

The implementation for each process is explained below.

Initialization process

The initialization process is a process that is executed from when a transition event to the screen occurs until the screen is displayed. The following processes are executed during the initialization process on the registration screen.

  • Initializing input items
    Set the value of the input item when displayed on the screen.

  • Input field validation settings
    Set a single item check to be performed on the client side for the input field.

  • drawing screen elements

The sequence of initialization processing on the registration screen is as follows.

Table 3. Processing sequence when the screen is initially displayed
Diagram
  1. The user is transferred to the registration screen.

  2. PageLoader's load function is called.

  3. PageLoader creates an instance of Model that stores screen input values. Set initial values for screen elements in instance properties.

  4. Page constructs the HTML for the screen based on the model obtained from PageLoader. Also, set validation for input items.

Hereafter, we will implement each element of the processing sequence described above in the following order.

  1. Frontend: Implementing screen element placement and validation definitions

Frontend

In the implementation procedure for the registration screen, we will first implement the appearance part of Frontend. Also, an input form will be implemented as a UIComponent and will be standardized on the registration screen and update screen.

Frontend is implemented with the Frontend server running. This makes it possible to proceed with implementation while checking the screen appearance and validation behavior in a browser. How to start the Frontend server How to use projects See.

page

Architecture description Follow the instructions to create/update the page Svelte file.

svqk-front/src/routes/issues/new/+page.svelte script section
  import { goto } from '$app/navigation';
  import { pageStore } from '$lib/arch/global/PageStore';
  import IssueForm from '$lib/domain/issue/IssueForm.svelte';
  import { t } from '$lib/translations';

  const issue = $state({ (1)
    subject: ''
    // ...
  });

  async function handleAfterSave(id?: number) { (2)
    await goto(`/issues/${id}`);
  }
1 Define an object that stores input values for input items. You can store input values by binding this object's properties to input fields in the markup section.
2 Implement a callback function after registration processing.
svqk-front/src/routes/issues/new/+page.svelte markup section
<IssueForm {issue} {handleAfterSave} actionBtnLabel={$t('msg.register')} /> (1)
1 Arrange the UIComponent of the input form and set the properties.
UIComponent

Create/update UIComponent's Svelte files.

svqk-front/src/lib/domain/issue/issueform.svelte script section
  import FormValidator from '$lib/arch/form/FormValidator';
  import InputField from '$lib/arch/form/InputField.svelte';
  import { issueStatuses } from '$lib/domain/issue/IssueStatusMasterStore';
  import { t } from '$lib/translations';
  import * as yup from 'yup';

  (1)
  interface Props {
    issue: IssueModel;
    handleAfterSave: (id?: number) => Promise<void>;
    actionBtnLabel: string;
  }

  (2)
  let { issue = $bindable(), handleAfterSave, actionBtnLabel }: Props = $props();

  (3)
  const spec = {
    subject: yup.string().required().label($t('msg.label.issue.subject'))
    // ...
  };

  const form = FormValidator.createForm(spec, save); (4)

  (5)
  async function save() {
    console.log('saved')
  }
1 The type of property that UIComponent receives from the outside is defined as interface.
2 The above interface is defined as a property of UIComponent.
3 yup.string() etc. yup Define input form validation specifications using the functions provided by. A specification is defined as an object defining properties for each input item and property validation. Here, subject It defines a required input item of the string type named.
4 FormValidator.createForm Use functions to define objects for applying validation to html elements.
5 Define a function to be called when validation finishes without an error. At this point, only log output for operation confirmation will be implemented.
svqk-front/src/lib/domain/issue/issueform.svelte markup section
<form use:form>  (1)
  <div>
    <InputField id="subject" label="Subject" bind:value={issue.subject} />  (2)
  </div>

  ...

    <div>
      <SelectBox
        id="status"
        label={$t('msg.status')}
        options={$issueStatuses}   (3)
        bind:value={issue.issueStatus.id}
      />
    </div>

  ...

  <div>
    <button id="save" type="submit">{actionBtnLabel}</button> (4)
  </div>
</form>
1 It becomes an input form form Place the tags. form The tag was generated in the script section form Set up an object.
2 Arrange the input items. The example above places a text box for entering the ticket title.
3 If master data is required for select box options, etc., MasterStore is used. MasterStore is separate Master data load It is necessary to implement it according to
4 Arrange the screen elements that serve as the starting point of the registration process. What is a screen element button type="submit" It will be implemented with

Perform screen operation with the Frontend server, backend server, and DB all running, and confirm that the behavior is as expected, and that the Web API processing log etc. are output to the Backend server log.

Registration process

The processing sequence for registration of the registration screen is as follows.

Diagram
  1. The user performs the registration operation.

  2. Page calls the Web API.

  3. ControlELR converts the posted DTO into an Entity.

  4. The Controller invokes the Service.

  5. Service calls the Repository.

  6. The repository performs an insert or update depending on the entity's state.

  7. Repository returns the entity.

  8. Service returns the Entity.

  9. The controller returns the entity's ID and version properties.

DB

Implement a CREATE statement to create a table and an INSERT statement for data in the migration script.

svqk-migration/src/main/resources/db/migration/V001__init.sql
(1)
CREATE TABLE issue (
  id SERIAL PRIMARY KEY,
  subject VARCHAR(128) NOT NULL,
  due_date DATE,
  status_id CHAR(1) NOT NULL REFERENCES issue_status,
  tracker_id CHAR(1) NOT NULL REFERENCES tracker,
  description VARCHAR(8192),
  --${commonColumns}
);
1 Implement the CREATE statement to create a table.
Backend
Entity

Add settings for generating entities to the JEG configuration file (jeg-config.yml).

svqk-entity/src/tool/resources/jeg-config.yml
packages:
  ${project.groupId}.domain.issue:  (1)
    - issue
  1. Add the package and table name where the entity was generated to the packages attribute.

    • “Entity generation destination package”: ["table name"]

After updating the jeg configuration file VSCode Task: gen-entity Run and confirm that an entity Java file has been generated under the entity project.

svqk-entity/src/main/java/dev/aulait/svqk/domain/issue/IssueEntity.java
package dev.aulait.svqk.domain.issue;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.HashSet;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "issue")
public class IssueEntity extends dev.aulait.svqk.arch.jpa.BaseEntity
    implements java.io.Serializable {

  @Id
  @Column(name = "id")
  @jakarta.persistence.GeneratedValue(strategy = jakarta.persistence.GenerationType.IDENTITY)
  private Integer id;

  @Column(name = "subject")
  private String subject;

  @Column(name = "due_date")
  private java.time.LocalDate dueDate;

  @Column(name = "description")
  private String description;

  @Builder.Default
  @OneToMany(fetch = FetchType.LAZY)
  @JoinColumn(name = "issue_id", insertable = false, updatable = false)
  private Set<JournalEntity> journals = new HashSet<>();

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "tracker_id")
  private TrackerEntity tracker;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "status_id")
  private IssueStatusEntity issueStatus;
}
Repository

Architecture description Follow the instructions to create/update the repository Java file.

svqk-back/src/main/java/dev/aulait/svqk/domain/issue/IssueRepository.java
package dev.aulait.svqk.domain.issue;

import static dev.aulait.svqk.arch.jpa.JpaUtils.findWithFetch;

import jakarta.persistence.EntityManager;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

public interface IssueRepository extends JpaRepository<IssueEntity, Integer> {
}
service

Architecture description Follow the instructions to create/update the Service Java file.

svqk-back/src/main/java/dev/aulait/svqk/domain/issue/IssueService.java
package dev.aulait.svqk.domain.issue;

@ApplicationScoped
@RequiredArgsConstructor
public class IssueService {

  private final IssueRepository repository;

  @Transactional
  public IssueEntity save(IssueEntity entity) { (1)
    return repository.save(entity); (2)
  }
}
1 Define a method to store entities in the DB. Methods include @Transactional Set it up.
2 Implement calling the Repository method to store entities in the DB. abovementioned save Is IssueRepository Will inherit JpaRepository The method inserts the specified entity into the `issue` table.
DTO

Architecture description Follow the instructions to create/update the DTO Java file.

svqk-back/src/main/java/dev/aulait/svqk/interfaces/issue/IssueDto.java
package dev.aulait.svqk.interfaces.issue;

import jakarta.validation.constraints.NotBlank;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.SortedSet;
import lombok.Data;
import org.eclipse.microprofile.openapi.annotations.media.Schema;

@Data
public class IssueDto {

  @Schema(required = true, readOnly = true)
  private Integer id;

  @Schema(required = true)
  @NotBlank
  private String subject;

  private String description;

  private LocalDate dueDate;

  @Schema(required = true)
  private IssueStatusDto issueStatus;

  @Schema(required = true)
  private TrackerDto tracker;

  @Schema(required = true)
  private Long version;

  @Schema(required = true, readOnly = true)
  private LocalDateTime updatedAt;

  @Schema(required = true, readOnly = true)
  private SortedSet<JournalDto> journals;
}
Controllers

Architecture description Follow the instructions to create/update the controller's Java file.

svqk-back/src/main/java/dev/aulait/svqk/interfaces/issue/IssueController.java
package dev.aulait.svqk.interfaces.issue;

@Path(IssueController.ISSUES_PATH)
@RequiredArgsConstructor
public class IssueController {

  private final IssueService service;

  @POST
  public int create(@Valid IssueDto dto) { (1)

    (2)
    IssueEntity entity = BeanUtils.map(dto, IssueEntity.class);
    IssueEntity savedEntity = service.save(entity);

    return savedEntity.getId();
  }
}
1 Define a method that is an endpoint for the Web API that stores screen input values. Set the method to `@POST`.
2 Implement the following processing inside the method.
  1. BeanUtils Converting from DTO to Entity using

  2. Save entities to DB using Service

  3. BeanUtils Convert saved entities to DTO using

  4. Return DTO as response

Once the Backend implementation is complete, start the Backend server and check the operation using the Swagger UI. How to start the Backend server How to use projects Where can I access Swagger UI access information See each one.

Frontend

Create an API client and add web API call processing.

Frontend is implemented with the Frontend server running. This makes it possible to proceed with implementation while checking the screen appearance and validation behavior in a browser. How to start the Frontend server How to use projects See.

API Client

VSCode Task: gen-api-client Run to generate an API Client. The generated API client is output to the svqk-front/src/lib/arch/api/api.ts file.

UIComponent
svqk-front/src/lib/domain/issue/issueform.svelte script section
  ...
  import type { IssueModel, IssueStatusModel } from '$lib/arch/api/Api';
  import ApiHandler from '$lib/arch/api/ApiHandler';
  import { messageStore } from '$lib/arch/global/MessageStore';

  interface Props {
    issue: IssueModel; (1)
    handleAfterSave: (id?: number) => Promise<void>;
    actionBtnLabel: string;
  }

  ...

  async function save() {
    (2)
    const response = await ApiHandler.handle<IdModel>(fetch, (api) =>
      api.issues.issuesCreate(issue)
    );

    (3)
    if (response) {
      await handleAfterSave(response.id);
      messageStore.show($t('msg.saved'));
    }
  }
1 The Model type is defined as an object that stores input values for input items.
2 ApiHandler.handle Use a function to call a web API that stores screen input values.
3 ApiHandler.handle Errors are determined by the return value of the function. If it is not an error, the following processing of the message is performed.
  • Execute callback function outside UIComponent

  • View global messages

page
src/routes/issues/new/+page.svelte script section
  import type { IssueModel, IssueStatusModel } from '$lib/arch/api/Api';

  let issue = $state({
    issueStatus: {} as IssueStatusModel,
    tracker: {} as TrackerModel
  } as IssueModel); (1)
1 The Model type is defined as an object that stores input values for input items. If the property is an object, the Model type is defined for that property as well.

List screen

Here, I will explain the implementation procedure for the list screen. The explanation takes the ticket list screen of the reference implementation as an example.

list page
Figure 2. Example of a list screen Ticket list screen

Items for entering search conditions, buttons to perform the search, and a table displaying search results are arranged on the list screen. Also, there are the following two processes on the list screen.

  1. Initialization process when the screen transitions

  2. Data search processing based on search conditions entered on the screen

The implementation for each process is explained below.

Initialization process

The initialization process is a process that is executed from when a transition event to the screen occurs until the screen is displayed. The following processes are executed during the initialization process on the list screen.

  • searching
    Retrieve search results from Backend using default search conditions.

  • drawing screen elements

The sequence of initialization processing on the list screen is as follows.

Table 4. Processing sequence when the screen is initially displayed
Diagram
  1. The user transitions to the list screen.

  2. PageLoader's load function is called.

  3. PageLoader calls the search web API with empty search conditions. The obtained results are returned to page as a model.

  4. Controller uses factroy to convert search conditions from DTO to VO.

  5. The Controller invokes the Service.

  6. Service calls searchUtils.

  7. SearchUtils performs SELECT on the DB and retrieves the number of search results.

  8. If the number of search results is greater than 0, searchUtils performs SELECT on the DB and retrieves search results.

  9. Controller uses Factory to convert search results from VO to DTO.

Backend
service

Architecture description Follow the instructions to create/update the Service Java file.

svqk-back/src/main/java/dev/aulait/svqk/domain/issue/IssueService.java
package dev.aulait.svqk.domain.issue;

import dev.aulait.svqk.arch.jpa.SearchUtils;
import dev.aulait.svqk.arch.search.SearchCriteriaVo;
import dev.aulait.svqk.arch.search.SearchResultVo;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import java.util.List;
import lombok.RequiredArgsConstructor;

@ApplicationScoped
@RequiredArgsConstructor
public class IssueService {

  private final IssueRepository repository;
  private final EntityManager em;

  public SearchResultVo<IssueEntity> search(SearchCriteriaVo criteria) { (1)
    return SearchUtils.search(em, criteria); (2)
  }
1 Define a method to perform a search. The search condition Vo is specified as an argument, and the search result Vo is specified as the return value.
2 Perform the search process SearchUtils.search Implement method calls.
DTO

Architecture description Follow the instructions to create/update the DTO Java file.

svqk-back/src/main/java/dev/aulait/svqk/interfaces/issue/IssueSearchCriteriaDto.java
package dev.aulait.svqk.interfaces.issue;

import dev.aulait.svqk.arch.search.SearchCriteriaDto;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper = true) (1)
public class IssueSearchCriteriaDto extends SearchCriteriaDto { (2)
  (3)
  private String text;
  private List<IssueStatusDto> issueStatuses = new ArrayList<>();
  private LocalDate dueDate;
  private boolean subjectOnly;
}
1 @EqualsAndHashCode Set it up.
2 It inherits the common base search condition DTO.
3 Define fields to be used as search criteria.
Factory

Create/update a factory Java file that constructs VO from the DTO of the search conditions.

svqk-back/src/main/java/dev/aulait/svqk/interfaces/issue/IssueFactory.java
package dev.aulait.svqk.interfaces.issue;

import static dev.aulait.svqk.arch.search.ArithmeticOperatorCd.*;
import static dev.aulait.svqk.arch.search.LogicalOperatorCd.*;

import dev.aulait.svqk.arch.search.SearchCriteriaBuilder;
import dev.aulait.svqk.arch.search.SearchCriteriaVo;
import dev.aulait.svqk.arch.search.SearchResultVo;
import dev.aulait.svqk.arch.util.BeanUtils;
import dev.aulait.svqk.arch.util.BeanUtils.MappingConfig;
import dev.aulait.svqk.domain.issue.IssueEntity;
import dev.aulait.svqk.domain.issue.IssueStatusEntity;
import dev.aulait.svqk.domain.issue.IssueTrackingRs;
import dev.aulait.svqk.interfaces.issue.IssueController.IssueSearchResultDto;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;

@ApplicationScoped
public class IssueFactory {

  private MappingConfig<IssueEntity, IssueDto> searchResultConfig =
      BeanUtils.buildConfig(IssueEntity.class, IssueDto.class)
          .skip(IssueDto::setJournals)
          .build(); (1)

  public SearchCriteriaVo build(IssueSearchCriteriaDto criteria) { (2)
    SearchCriteriaBuilder builder =
        new SearchCriteriaBuilder()
            .select("SELECT i FROM IssueEntity i")
            .select("JOIN FETCH i.issueStatus s")
            .select("JOIN FETCH i.tracker t")
            .where("i.subject", LIKE, criteria.getText()); (3)

    if (!criteria.isSubjectOnly()) {
      builder.where(OR, "i.description", LIKE, criteria.getText());
    }

    var statuses = BeanUtils.mapAll(criteria.getIssueStatuses(), IssueStatusEntity.class);

    return builder
        .where("i.issueStatus", IN, statuses)
        .where("i.dueDate", criteria.getDueDate())
        .defaultOrderBy("i.id", false)
        .build(criteria); (4)
  }

  public IssueSearchResultDto build(SearchResultVo<IssueEntity> vo) { (5)
    return BeanUtils.map(searchResultConfig, vo, IssueSearchResultDto.class); (6)
  }
}
1 Define mapping settings from search result entity to dTO. The example above excludes the issuedTo.journals mapping in the mapping from IssueEntity to IssuedTo.
2 Define a method to construct VO from the search condition dTO.
3 SearchConditionBuilder Set extraction conditions using the entity to be searched and the item of the search condition dTO.
4 SearchConditionBuilder The search condition Vo is constructed based on the set results to and returned.
5 Define a method to construct DTO from search results VO.
6 Construct and return DTOs from search results VO using mapping settings.
Controllers

Architecture description Follow the instructions to create/update the controller's Java file.

svqk-back/src/main/java/dev/aulait/svqk/interfaces/issue/IssueController.java
package dev.aulait.svqk.interfaces.issue;

import dev.aulait.svqk.arch.search.SearchCriteriaVo;
import dev.aulait.svqk.arch.search.SearchResultDto;
import dev.aulait.svqk.arch.search.SearchResultVo;
import dev.aulait.svqk.arch.util.BeanUtils;
import dev.aulait.svqk.domain.issue.IssueEntity;
import dev.aulait.svqk.domain.issue.IssueService;
import dev.aulait.svqk.domain.issue.JournalEntity;
import jakarta.validation.Valid;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import lombok.RequiredArgsConstructor;

@Path(IssueController.ISSUES_PATH)
@RequiredArgsConstructor
public class IssueController {

  private final IssueService service;

  private final IssueFactory factory;

  static final String ISSUES_PATH = "issues";

  static final String ISSUES_SEARCH_PATH = "search";

  public static class IssueSearchResultDto extends SearchResultDto<IssueDto> {} (1)

  @POST
  @Path(ISSUES_SEARCH_PATH)
  public IssueSearchResultDto search(IssueSearchCriteriaDto dto) { (2)
    (3)
    SearchCriteriaVo vo = factory.build(dto);
    SearchResultVo<IssueEntity> result = service.search(vo);

    return factory.build(result);
  }

}
1 Define a DTO type for storing search results. The common base search result DTO is inherited, and DTO representing one search result is specified as the type parameter.
2 Define search methods. Specify the search condition DTO as the argument, and the search result DTO as the return value. Also, @POST , @Path Set it up.
3 Implement the following processing inside the method.
  1. IssueFactory Convert search conditions from DTO to VO using

  2. Use Service to perform searches

  3. SearchResultFactory Convert search results from VO to DTO using

  4. Return DTO as response

Once the Backend implementation is complete, start the Backend server and check the operation using the Swagger UI. How to start the Backend server How to use projects Where can I access Swagger UI access information See each one.

Frontend

Frontend is implemented with the Frontend server running. This makes it possible to proceed with implementation while checking the screen appearance and validation behavior in a browser. How to start the Frontend server How to use projects See.

API Client

VSCode Task: gen-api-client Run to generate an API Client. The generated API client is output to the svqk-front/src/lib/arch/api/api.ts file.

svqk-front/src/lib/arch/api/api.ts
export interface IssueSearchCriteriaModel {
  /** @format int32 */
  pageNumber?: number;
  /** @format int32 */
  pageSize?: number;
  /** @format int32 */
  pageNumsRange?: number;
  sortOrders?: SortOrderModel[];
  text?: string;
  issueStatuses?: IssueStatusModel[];
  dueDate?: LocalDate;
  subjectOnly?: boolean;
}

export interface IssueSearchResultModel {
  list: IssueModel[];
  pageCtrl: PageControlModel;
}

export interface PageControlModel {
  /** @format int64 */
  count: number;
  /** @format int32 */
  pageSize: number;
  /** @format int32 */
  start: number;
  /** @format int32 */
  end: number;
  /** @format int32 */
  lastPage: number;
  pageNums: number[];
}

export interface SortOrderModel {
  asc?: boolean;
  field?: string;
}

export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDataType> {
  issues = {
    /**
     * No description
     *
     * @tags Issue Controller
     * @name IssuesSearch
     * @request POST:/api/issues/search
     */
    issuesSearch: (data: IssueSearchCriteriaModel, params: RequestParams = {}) =>
      this.request<IssueSearchResultModel, any>({
        path: `/api/issues/search`,
        method: 'POST',
        body: data,
        type: ContentType.Json,
        format: 'json',
        ...params
      }),
  };
}
pageLoader

Architecture description Follow the instructions to create/update the PageLoader TS file.

svqk-front/src/routes/issues/+page.ts
import type { IssueSearchCriteriaModel, IssueSearchResultModel } from '$lib/arch/api/Api';
import ApiHandler from '$lib/arch/api/ApiHandler';
import { t } from '$lib/translations';
import type { PageLoad } from './$types';

export const load: PageLoad = async ({ fetch }) => {
  const condition = { issueStatuses: [], pageNumber: 1 } as IssueSearchCriteriaModel; (1)

  const result =
    (await ApiHandler.handle<IssueSearchResultModel>(fetch, (api) =>
      api.issues.issuesSearch(condition)
    )) || ({} as IssueSearchResultModel); (2)

  return {
    title: t.get('msg.issue'),
    condition,
    result
  };
};
1 Define a Model to store search conditions.
2 ApiHandler.handle Use functions to implement search web API calls.
page

Architecture description Follow the instructions to create/update the page Svelte file.

svqk-front/src/routes/issues/+page.svelte script section
  import type { IssueModel, IssueSearchResultModel } from '$lib/arch/api/Api';
  import ApiHandler from '$lib/arch/api/ApiHandler';
  import CheckBox from '$lib/arch/form/CheckBox.svelte';
  import FormValidator from '$lib/arch/form/FormValidator';
  import InputField from '$lib/arch/form/InputField.svelte';
  import SelectBox from '$lib/arch/form/SelectBox.svelte';
  import ListTable, { ColumnsBuilder } from '$lib/arch/search/ListTable.svelte';
  import DateUtils from '$lib/arch/util/DateUtils';
  import { issueStatuses } from '$lib/domain/issue/IssueStatusMasterStore';
  import { t } from '$lib/translations';
  import type { PageData } from '../issues/$types';

  let { data }: { data: PageData } = $props();
  let { result, condition } = $state(data);

  const form = FormValidator.createForm({}, search); (1)

  const columns = new ColumnsBuilder<IssueModel>()
    .add('#', 'i.id', () => issueIdAnchor)
    .add($t('msg.tracker'), 'i.tracker', (issue) => issue.tracker.name)
    .add($t('msg.status'), 'i.issueStatus', (issue) => issue.issueStatus.name)
    .add($t('msg.subject'), 'i.subject', (issue) => issue.subject, ['align-left'])
    .add($t('msg.dueDate'), 'i.dueDate', (issue) => DateUtils.date(issue.dueDate))
    .add($t('msg.updatedAt'), 'i.updatedAt', (issue) => DateUtils.datetime(issue.updatedAt))
    .build(); (2)

  (3)
  async function search() {
  }
1 FormValidator.createForm Use a function to define an object to submit a form.
2 Define each column (header name, cell display content, sort key) in the search results list. If you want to use your own markup for cell display content, use the markup section #snippet define and specify it. (IssueIdAnchor is set in the example above)
3 Define a function that handles search events in the screen. At this point, let's say it's an empty function.
svqk-front/src/routes/issues/+page.svelte markup section
<section>
  <form use:form>  (1)
    <fieldset role="search">  (2)
      <input type="search" bind:value={condition.text} />
      <input type="submit" value="Search" />
    </fieldset>

    :
  </form>
</section>

<section>
  <ListTable {result} {columns} bind:condition {search} />  (3)
</section>

<!-- for ListTable issueId Column -->
{#snippet issueIdAnchor(issue: IssueModel)}
  <a href={`/issues/${issue.id}`}>{issue.id}</a>
{/snippet}
1 For arranging input items for search conditions form Place the tags. form The tag was generated in the script section form Set up an object.
2 Arrange the input items for the search conditions.
3 Arrange a list of search results using common UICPomonent.

search process

The search process is a process that is executed from when a search event occurs due to user screen operation until the screen is displayed. The following processes are executed in the search process on the list screen.

  • searching
    Retrieve search results from Backend using the search conditions entered on the screen.

  • drawing screen elements

The sequence of the search process on the list screen is as follows.

Table 5. Processing sequence when the screen is initially displayed
Diagram
  1. The user manipulates the screen.

  2. Page calls the search web API with search conditions. The obtained results are returned to page as a model. The search web API is the same as the initialization process. The description is omitted in the image on the left.

  3. The HTML for the screen is constructed based on the model obtained by page.

Frontend

Frontend is implemented with the Frontend server running. This makes it possible to proceed with implementation while checking the screen appearance and validation behavior in a browser. How to start the Frontend server How to use projects See.

page

Architecture description Follow the instructions to create/update the page Svelte file.

svqk-front/src/routes/issues/+page.svelte script section
  async function search() {
    (1)
    const r = await ApiHandler.handle<IssueSearchResultModel>(fetch, (api) =>
      api.issues.issuesSearch(condition)
    );

    (2)
    if (r) {
      result = r;
    }
  }
1 ApiHandler.handle Use a function to call the web API.
2 If the Web API call result can be obtained successfully, the search result object will be overwritten.

Master data load

The implementation procedure for master data load is explained here. The explanation takes the reference implementation's ticket status as an example.

  • Master data retrieval
    Master data used on the screen is retrieved from Backend.

  • Maintaining master data
    Master data obtained from Backend is kept as a store.

The master data load process sequence is as follows.

Table 6. Master data load processing sequence
Diagram
  1. When a user accesses Frontend, the LayoutLoader load function is called upon initial access.

  2. loadExecutor performs the MasterStore load process.

  3. MasterStore calls a web API to obtain code values, etc. used on the screen.

  4. The Controller invokes the Service.

  5. Service calls the Repository.

  6. The repository performs SELECT against the DB.

  7. The controller converts the entity obtained from the service to DTO.

  8. MasterStore holds the results obtained as a STORE.

Here, LayoutLoader is SvelteKit's layout.ts It indicates, and is placed below src/route in the reference implementation. Also, loadExecutor is a common function provided by Arch, and is placed in src/lib/arch/master in the reference implementation.

Hereafter, we will implement each element of the processing sequence described above in the following order.

  1. DB: Implementing a migration script to create master tables and load master data

  2. Backend: Implementing a web API to retrieve master data

  3. Frontend: Generating and Embedding Web API Clients

DB

Migration Script - DDL

Implement a CREATE statement to create a table in the migration script (SQL).

svqk-migration/src/main/resources/db/migration/V001__init.sql
(1)
CREATE TABLE issue_status (
  id CHAR(1) PRIMARY KEY,
  name VARCHAR(128) NOT NULL,
  --${commonColumns}  (2)
);
1 Implement the CREATE statement to create a table.
2 Add common columns (author, creation date, etc.) to the table to be created.
Migration Script - Data

Implement a migration script (Java).

svqk-migration/src/main/java/db/migration/V002__AddRecords.java
package db.migration;

import dev.aulait.csvloader.flyway.BaseJavaCsvMigration;

@SuppressWarnings("squid:S101")
public class V002__AddRecords extends BaseJavaCsvMigration {}

Arrange the table list files to be used for migration.

svqk-migration/src/main/resources/db/migration/V002__AddRecords/table-list.txt
issue_status

Arrange the master data files (csv).

svqk-migration/src/main/resources/db/migration/V002__AddRecords/issue_status.csv
id,name
"1","New"
"2","In Progress"
"3","Closed"

Once you've implemented the migration script, VSCode Task: migration Run it.

Backend

Implement a web API to obtain master data used on the screen from the DB in Backend.

Entity

Add settings for generating entities to the JEG configuration file (jeg-config.yml).

svqk-entity/src/tool/resources/jeg-config.yml
packages:
  ${project.groupId}.domain.issue:  (1)
    - issue_status
1 Add the package and table name where the entity was generated to the packages attribute.
  • “Entity generation destination package”: ["table name"]

After updating the jeg configuration file VSCode Task: gen-entity Run and confirm that an entity Java file has been generated under the entity project.

svqk-entity/src/main/java/dev/aulait/svqk/domain/issue/IssueStatusEntity.java
package dev.aulait.svqk.domain.issue;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "issue_status")
public class IssueStatusEntity extends dev.aulait.svqk.arch.jpa.BaseEntity
    implements java.io.Serializable {

  @Id
  @Column(name = "id")
  private String id;

  @Column(name = "name")
  private String name;
}
Repository

Architecture description Follow the instructions to create/update the repository Java file.

svqk-back/src/main/java/dev/aulait/svqk/domain/issue/IssueStatusRepository.java
package dev.aulait.svqk.domain.issue;

import org.springframework.data.jpa.repository.JpaRepository;

public interface IssueStatusRepository extends JpaRepository<IssueStatusEntity, String> {}
service

Architecture description Follow the instructions to create/update the Service Java file.

svqk-back/src/main/java/dev/aulait/svqk/domain/issue/IssueStatusService.java
package dev.aulait.svqk.domain.issue;

import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import lombok.RequiredArgsConstructor;

@ApplicationScoped
@RequiredArgsConstructor
public class IssueStatusService {

  private final IssueStatusRepository statusRepository;

  public List<IssueStatusEntity> findAll() { (1)
    return statusRepository.findAll(); (2)
  }
}
1 Define a method for retrieving master data from DB.
2 Implement calling the Repository method to retrieve master data from the DB. abovementioned findAll Is IssueStatusRepository Will inherit JpaRepository With the method of issue_status SELECT all records in the table.
DTO

Architecture description Follow the instructions to create/update the DTO Java file.

svqk-back/src/main/java/dev/aulait/svqk/interfaces/issue/IssueStatusDto.java
package dev.aulait.svqk.interfaces.issue;

import lombok.Data;
import org.eclipse.microprofile.openapi.annotations.media.Schema;

@Data
public class IssueStatusDto implements Comparable<IssueStatusDto> {

  @Schema(required = true, readOnly = true)
  private String id;

  @Schema(required = true)
  private String name;

  @Schema(required = true, readOnly = true)
  private long version;

  @Override
  public int compareTo(IssueStatusDto o) {
    return id.compareTo(o.id);
  }
}
Controllers

Architecture description Follow the instructions to create/update the controller's Java file.

svqk-back/src/main/java/dev/aulait/svqk/interfaces/issue/IssueStatusController.java
package dev.aulait.svqk.interfaces.issue;

import dev.aulait.svqk.arch.util.BeanUtils;
import dev.aulait.svqk.domain.issue.IssueStatusService;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import java.util.List;
import lombok.RequiredArgsConstructor;

@Path(IssueStatusController.ISSUE_STATUSES_PATH)
@RequiredArgsConstructor
public class IssueStatusController {

  private final IssueStatusService service;

  static final String ISSUE_STATUSES_PATH = "issue-statuses";

  @GET
  public List<IssueStatusDto> get() { (1)
    return BeanUtils.mapAll(service.findAll(), IssueStatusDto.class); (2)
  }
}
1 Define a method that is an endpoint for the Web API to obtain master data used on the screen. Set the method to `@GET`.
2 Retrieving an Entity from a Service, BeanUtils Implement conversion to DTO using and return as a response.

Once the Backend implementation is complete, start the Backend server and check the operation using the Swagger UI. How to start the Backend server How to use projects Where can I access Swagger UI access information See each one.

Frontend

Generate an API client and implement masterstore and web API call processing.

API Client

VSCode Task: gen-api-client Run to generate an API Client. The generated API client is output to the svqk-front/src/lib/arch/api/api.ts file.

svqk-front/src/lib/arch/api/api.ts
export interface IssueStatusModel {
  id: string;
  name: string;
  /** @format int64 */
  version: number;
}

export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDataType> {
  issueStatuses = {
    /**
     * No description
     *
     * @tags Issue Status Controller
     * @name IssueStatusesList
     * @request GET:/api/issue-statuses
     */
    issueStatusesList: (params: RequestParams = {}) =>
      this.request<IssueStatusModel[], any>({
        path: `/api/issue-statuses`,
        method: 'GET',
        format: 'json',
        ...params
      })
  };
}
MasterStore
svqk-front/src/lib/domain/issue/issuestatusmasterstore.ts
import { readable } from 'svelte/store';
import { provide } from 'inversify-binding-decorators';
import { TYPES } from '$lib/arch/di/Types';
import { MasterStoreBase } from '$lib/arch/master/MasterStoreBase';
import type { IssueStatusModel } from '$lib/arch/api/Api';

type Fetch = typeof fetch;

export let issueStatuses = readable([] as IssueStatusModel[]); (1)

@provide(TYPES.MasterStore) (2)
export class IssueStatusMasterStore extends MasterStoreBase<IssueStatusModel[]> {
  constructor() {
    super((api) => api.issueStatuses.issueStatusesList()); (3)
  }

  (4)
  override async load(fetch: Fetch) {
    await super.load(fetch);
    issueStatuses = this.store;
  }
}
1 Declare an array of Models representing master data as a Readable Store and export it. This allows master data to be used as Svelte's Store.
2 Set @provide to MasterStore. This allows MasterStore to be used via the DI container. Also, MasterStore inherits MasterStoreBase and implements it. This allows the master data load process to be executed the first time the user accesses it.
3 Implement a call to the Web API Client.
4 Implement a process to set data to the Readable Store.
svqk-front/src/hooks.ts
import '$lib/domain/issue/IssueStatusMasterStore'; (1)
1 Implement MasterStore import.

When using master data on screens, etc., MasterStore is used as follows.

Using MasterStore
  import { issueStatuses } from '$lib/domain/issue/IssueStatusMasterStore';

  $issueStatuses // IssueStatusModel[]