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. Interact with the application and check the features implemented.

  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 (automatically generated) 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. In the dialog that was displayed 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 structure 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-backend
Command: mvnw -f svqk-backend quarkus:dev

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

Run Backend debugging

VSCode debug configuration: Attach Quarkus

VSCode attaches to the Backend server 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-frontend
Command: pnpm dev --open
(at svqk-frontend directory)

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

Run DB migrations

VSCode Task: migrate-db
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 with the DBMS running.

Generate source files

VSCode Task: generate

generator starts up, You will be prompted to specify the components and tables to be generated.
This operation can be performed with the DBMS 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 (automatically generated)

The implementation procedure (automatically generated) generator It guides you through the process of automatically generating source files using.

By using Generator, it is possible to generate source files that can operate applications (Frontend, Backend) and automated tests (Integration Test, End to End Test) that conform to the architecture description.

Developers implement the details of the requirements into source files generated using Generator.

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.

After updating the migration script VSCode Task: migrate-db Run and confirm that the table has been created.

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

generator

VSCode Task: generate Run it. In the prompt that appears, enter the components to be generated and the table from which they were generated. The following are components all , on the table issue This is an example of a source file generated by Generator when entered. How to use Generator in detail Here See.

Generated source files
📁 svqk
├── 📁 svqk-entity
│   └── 📁 src
│       └── 📁 main/java/dev/aulait/svqk/domain/issue
│           └── 📄 IssueEntity.java
├── 📁 svqk-backend
│   └── 📁 src
│       ├── 📁 main/java/dev/aulait/svqk
│       │   ├── interfaces/issue
│       │   │   ├── 📄 IssueController.java
│       │   │   ├── 📄 IssueFactory.java
│       │   │   └── 📄 IssueDto.java
│       │   └── 📁 domain/issue
│       │       ├── 📄 IssueService.java
│       │       └── 📄 IssueRepository.java
│       └── 📁 integration-test/java/dev/aulait/svqk/interfaces/issue
│           ├── 📄 IssueControllerIT.java
│           ├── 📄 IssueDataFactory.java
│           └── 📄 IssueClient.java
├── 📁 svqk-frontend
│   └── 📁 src
│       ├── 📁 routes/issues
│       │   ├── 📁 [issueId]
│       │   │   ├── 📄 +page.svelte
│       │   │   └── 📄 +page.ts
│       │   ├── 📁 new
│       │   │   ├── 📄 +page.svelte
│       │   │   └── 📄 +page.ts
│       │   ├── 📄 +page.svelte
│       │   └── 📄 +page.ts
│       ├── 📁 lib
│       │   └── 📁 arch
│       │       └── 📄 Api.ts
│       └── 📁 lib/domains/issue
│           └── 📄 IssueForm.svelte
└── 📁 svqk-e2etest
    └── 📁 tests
        ├── 📁 api
        │   └── 📄 Api.ts
        ├── 📁 facades
        │   └── 📄 IssueFacade.ts
        ├── 📁 factories
        │   └── 📄 IssueDataFactory.ts
        ├── 📁 pages
        │   ├── 📁 issue-input
        │   │   ├── 📄 IssueInputPage.ts
        │   │   └── 📄 IssueInputPageElement.ts
        │   └── 📁 issue-list
        │       ├── 📄 IssueListPage.ts
        │       └── 📄 IssueListPageElement.ts
        └── 📁 specs
            └── 📄 issue.spec.ts

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. How to start each How to use projects See.

From now on, we will implement the details of the requirements into the generated source files.

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 the same classification of processes 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 single item checks to be performed on the client side as input items.

  • 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-frontend/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-frontend/src/routes/issues/new/+page.svelte markup section
<IssueForm bind:issue {handleAfterSave} /> (1)
1 Arrange the UIComponent of the input form and set the properties.
UIComponent

Create/update UIComponent's Svelte files.

svqk-frontend/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 Use the functions provided by to define input form validation specifications. 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 a function to define an object 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 is implemented.
svqk-frontend/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. How to start each How to use projects See.

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 Service.

  5. Service calls the Repository.

  6. The repository will insert or update according to 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.

After updating the migration script VSCode Task: migrate-db Run and confirm that the table has been created.

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 javax.annotation.processing.Generated;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

@Generated("dev.aulait.jeg:jpa-entity-generator")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
@Entity
@Table(name = "issue")
public class IssueEntity extends dev.aulait.svqk.arch.jpa.BaseEntity
    implements java.io.Serializable {

  @EqualsAndHashCode.Include
  @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", referencedColumnName = "id", insertable = false, updatable = false)
  private Set<JournalEntity> journals = new HashSet<>();

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

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

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

svqk-backend/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-backend/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-backend/src/main/java/dev/aulait/svqk/interfaces/issue/IssueDto.java
package dev.aulait.svqk.interfaces.issue;

import com.fasterxml.jackson.annotation.JsonProperty;
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)
  @JsonProperty(access = JsonProperty.Access.READ_ONLY)
  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-backend/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. Return the entity ID 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

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-frontend/src/lib/arch/api/api.ts file.

UIComponent
svqk-frontend/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 based on 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 the 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 Service.

  6. Service invokes searchExecutor.

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

  8. If the number of search results is greater than 0, the searchExecutor performs SELECT on the DB and retrieves the 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-backend/src/main/java/dev/aulait/svqk/domain/issue/IssueService.java
package dev.aulait.svqk.domain.issue;

import dev.aulait.sqb.SearchCriteria;
import dev.aulait.sqb.SearchResult;
import dev.aulait.sqb.jpa.JpaSearchQueryExecutor;
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;
  private final JpaSearchQueryExecutor searchExecutor;

  public SearchResult<IssueEntity> search(SearchCriteria criteria) { (1)
    return searchExecutor.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 JpaSearchQueryExecutor.search Implement method calls.
DTO

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

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

import dev.aulait.sqb.PageControl;
import dev.aulait.sqb.SortOrder;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
import org.eclipse.microprofile.openapi.annotations.media.Schema;

@Data (1)
public class IssueSearchCriteriaDto {
  (2)
  private String text;
  private List<String> issueStatuses = new ArrayList<>();
  private LocalDate dueDate;
  private boolean subjectOnly;

  @Schema(required = true)
  private PageControl pageControl = new PageControl();

  private List<SortOrder> sortOrders = new ArrayList<>();
}
1 @Data Set it up.
2 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-backend/src/main/java/dev/aulait/svqk/interfaces/issue/IssueFactory.java
package dev.aulait.svqk.interfaces.issue;

import static dev.aulait.sqb.ComparisonOperator.*;
import static dev.aulait.sqb.LogicalOperator.*;

import dev.aulait.sqb.LikePattern;
import dev.aulait.sqb.SearchCriteria;
import dev.aulait.sqb.SearchCriteriaBuilder;
import dev.aulait.sqb.SearchResult;
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.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 SearchCriteria build(IssueSearchCriteriaDto criteria) { (2)
    Object text = LikePattern.contains(criteria.getText());

    return new SearchCriteriaBuilder() (3)
        .select("SELECT i FROM IssueEntity i")
        .select("JOIN FETCH i.issueStatus")
        .select("JOIN FETCH i.tracker")
        .where("i.subject", LIKE, text)
        .where("i.issueStatus.id", IN, criteria.getIssueStatuses())
        .where("i.dueDate", criteria.getDueDate())
        .where(OR, "i.description", LIKE, criteria.isSubjectOnly() ? null : text)
        .defaultOrderBy("i.id", false)
        .orderBy(criteria.getSortOrders())
        .build(criteria.getPageControl()); (4)
  }

  public IssueSearchResultDto build(SearchResult<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 SearchCriteriaBuilder Set extraction conditions using the entity to be searched and the item of the search condition dTO.
4 SearchCriteriaBuilder Based on the setting results to SearchCriteria Build it and return it.
5 SearchResult Define a method to construct a DTO from
6 Use mapping settings SearchResult Build a DTO from and return it.
Controllers

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

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

import dev.aulait.sqb.SearchCriteria;
import dev.aulait.sqb.SearchResult;
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.DELETE;
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 SearchResult<IssueDto> {} (1)

  @POST
  @Path(ISSUES_SEARCH_PATH)
  public IssueSearchResultDto search(IssueSearchCriteriaDto dto) { (2)
    (3)
    SearchCriteria vo = factory.build(dto);
    SearchResult<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-frontend/src/lib/arch/api/api.ts file.

pageLoader

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

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

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

  const open = false; (2)

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

  return {
    title: t.get('msg.issue'),
    criteria,
    open,
    result
  };
};
1 Define a Model to store search conditions.
2 In addition to search conditions, define values that maintain state before and after the search process.
3 ApiHandler.handle Use functions to implement search web API calls.
page

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

svqk-frontend/src/routes/issues/+page.svelte script section
  import { goto } from '$app/navigation';
  import type { IssueModel } from '$lib/arch/api/Api';
  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 CriteriaUtils from '$lib/arch/search/CriteriaUtils';
  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 { criteria, open } = $state(data);
  let { result } = $derived(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)
  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-frontend/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 encodes search conditions into URL parameters, specifies those parameters, and transitions to its own screen.

  3. PageLoader decodes search conditions from URL parameters and performs search processing based on them. The rest of the process is the same as the initialization process.

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.

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

export const load: PageLoad = async ({ fetch, url }) => {
  const criteria = {
    issueStatuses: [],
    ...CriteriaUtils.decode(url) (1)
  } as IssueSearchCriteriaModel;

  const open = CriteriaUtils.decodeParam<boolean>(url, 'open') ?? false; (2)

  :
}
1 CriteriaUtils.decode Retrieve search conditions from a URL using
2 CriteriaUtils.decodeParam Retrieve parameters other than search conditions from a URL using
page

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

svqk-frontend/src/routes/issues/+page.svelte script section
(1)
  function search() {
    goto(CriteriaUtils.encode(criteria, { open }));
  }
1 CriteriaUtils.encode Use a function to encode search conditions into URL parameters, goto Specify it as a function. Thus, it transitions to its own screen along with the URL parameters, and the search process implemented by PageLoader is executed.

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 held 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. The 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 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 {}

Place the table list file 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: generate Run the prompt's component at entity Select 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 javax.annotation.processing.Generated;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

@Generated("dev.aulait.jeg:jpa-entity-generator")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
@Entity
@Table(name = "issue_status")
public class IssueStatusEntity extends dev.aulait.svqk.arch.jpa.BaseEntity
    implements java.io.Serializable {

  @EqualsAndHashCode.Include
  @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-backend/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-backend/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-backend/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-backend/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. Methods include @GET Set it up.
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-frontend/src/lib/arch/api/api.ts file.

MasterStore
svqk-frontend/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-frontend/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[]