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.
-
Building an environment
Development environment construction procedure In accordance with the instructions, an execution/development environment is built on the PC used for development. -
Check the application
Run the application in the built environment. Architectural description's use cases Operate the application against and check the implemented functions. -
Review the application architecture
-
logical configuration
Architectural description's application configuration , component configuration See and review the components that make up the application and their responsibilities. -
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)
-
keyboard shortcuts Windows + R Run it.
-
displayed
ファイル名を選択して実行
In dialogspowershell
Type -
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
|
Once you've opened a VSCode workspace, you can open it by following the steps below.
|
How to use projects
How to use | operation | behavior |
---|---|---|
Start the DBMS |
VSCode Task:
|
DBMS (PostgreSQL) starts.
|
Start Backend |
VSCode Task:
|
The backend server (Quarkus) starts.
|
Run Backend debugging |
VSCode debug configuration:
|
VSCode attaches to the Backend server's debug port.
|
Start Frontend |
VSCode Task:
|
The Frontend server (Vite) starts.
|
Run DB migrations |
VSCode Task:
|
The DS migration tool (Flyway) is executed.
|
Entity generation |
VSCode Task:
|
jpa-entity-generator is executed.
|
Generating an API Client for Front |
VSCode Task:
|
swagger-typescript-api is executed.
Once the execution is complete, an API client will be generated within the FRONT project and the E2eTest project.
|
How to run VSCode Tasks
-
keyboard shortcuts Ctrl + shift + P execute
-
In the displayed command palette
task
and enter -
Tasks: Run Task
select
access information
Connect to | Connection information/URL |
---|---|
DBMS |
|
Quaruks Developer 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.
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.
-
Initialization process when the screen transitions
-
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.
|
Hereafter, we will implement each element of the processing sequence described above in the following order.
-
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.
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. |
<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.
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. |
<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.
|
DB
Implement a CREATE statement to create a table and an INSERT statement for data in the migration script.
(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).
packages:
${project.groupId}.domain.issue: (1)
- issue
-
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.
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.
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.
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.
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.
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.
|
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
...
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.
|
page
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.
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.
-
Initialization process when the screen transitions
-
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.
|
Backend
service
Architecture description Follow the instructions to create/update the Service Java file.
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.
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.
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.
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.
|
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.
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.
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.
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. |
<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.
|
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.
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.
|
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.
-
DB: Implementing a migration script to create master tables and load master data
-
Backend: Implementing a web API to retrieve master data
-
Frontend: Generating and Embedding Web API Clients
DB
Migration Script - DDL
Implement a CREATE statement to create a table in the migration script (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).
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.
issue_status
Arrange the master data files (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).
packages:
${project.groupId}.domain.issue: (1)
- issue_status
1 |
Add the package and table name where the entity was generated to the packages attribute.
|
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.
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.
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.
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.
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.
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.
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
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. |
import '$lib/domain/issue/IssueStatusMasterStore'; (1)
1 | Implement MasterStore import. |
When using master data on screens, etc., MasterStore is used as follows.
import { issueStatuses } from '$lib/domain/issue/IssueStatusMasterStore';
$issueStatuses // IssueStatusModel[]