CRUD Engine¶
Türkçe
CRUD Engine, ICOSYS'un kalbidir. Hem backend'de (AbstractCrudService) hem frontend'de
(CrudPage + useCrudHandler) standart CRUD operasyonlarını sıfır tekrar ile yönetir.
Yeni bir entity eklediğinizde sadece config yazarsınız — CRUD kodu yazmak gerekmez.
Overview¶
The ICOSYS CRUD Engine eliminates boilerplate by providing a complete CRUD lifecycle on both backend and frontend. Developers only need to:
- Backend: Extend
AbstractCrudServiceand implement 5 abstract methods - Frontend: Create a
HandlerConfigobject describing fields, filters, and layout
Everything else — pagination, sorting, filtering, validation, error handling, mode switching — is handled automatically.
Frontend Backend
┌──────────────┐ REST/JSON ┌──────────────────────┐
│ CrudPage │ ◄────────────► │ Controller │
│ ├ Renderer │ │ ├ @GetMapping │
│ ├ useCrud │ │ ├ @PostMapping │
│ └ FieldConf │ │ └ ... │
│ │ ├──────────────────────┤
│ HandlerConf │ │ AbstractCrudService │
│ (config) │ │ ├ lifecycle hooks │
│ │ │ ├ validation │
│ │ │ └ pagination │
└──────────────┘ ├──────────────────────┤
│ Repository + Spec │
└──────────────────────┘
Backend — AbstractCrudService¶
Type Parameters¶
public abstract class AbstractCrudService<
E extends Serializable, // Entity type (e.g., Branch)
I extends Serializable, // ID type (e.g., Long)
D extends Serializable, // Edit DTO (e.g., BranchEditDto)
L extends Serializable, // List DTO (e.g., BranchListDto)
F extends Serializable // Filter DTO (e.g., BranchFilterDto)
>
5 Required Abstract Methods¶
Every service must implement these:
@Service
@Transactional
@Slf4j
public class BranchService extends AbstractCrudService<
Branch, Long, BranchEditDto, BranchListDto, BranchFilterDto> {
@Override
protected ICrudRepository<Branch, Long> getRepository() {
return repository;
}
@Override
protected IEditDtoConverter<Branch, BranchEditDto> getEditConverter() {
return editConverter;
}
@Override
protected IListDtoConverter<Branch, BranchListDto> getListConverter() {
return listConverter;
}
@Override
protected String getEntityName() {
return "Branch";
}
@Override
protected Long extractId(Branch entity) {
return entity.getId();
}
}
| Method | Returns | Purpose |
|---|---|---|
getRepository() |
ICrudRepository<E, I> |
JPA repository for data access |
getEditConverter() |
IEditDtoConverter<E, D> |
Entity ↔ Edit DTO converter |
getListConverter() |
IListDtoConverter<E, L> |
Entity → List DTO converter |
getEntityName() |
String |
Entity name for logs and error messages |
extractId(E) |
I |
Extracts ID from entity instance |
CRUD Operation Flows¶
Create¶
1. validateForCreate(dto) ← Validation hook
↓
2. entity = editConverter.to(dto) ← DTO → new Entity
↓
3. beforeCreate(entity, dto) ← Set crProcess, defaults
↓
4. saved = repository.save(entity)
↓
5. afterCreate(saved) ← Audit, notifications
↓
6. return editConverter.from(saved) ← Entity → DTO
Update¶
1. entity = findEntityById(id) ← Fetch managed entity
↓
2. validateForUpdate(dto, id) ← Validation hook
↓
3. beforeUpdate(entity, dto) ← Pre-merge customization
↓
4. editConverter.to(dto, entity) ← Merge DTO into entity
↓
5. saved = repository.save(entity)
↓
6. afterUpdate(saved) ← Audit, cache invalidation
↓
7. return editConverter.from(saved)
Delete (Safe Deletion)¶
1. entity = findEntityById(id)
↓
2. validateDeletion(entity) ← Business rule checks
↓
3. refs = checkReferences(id) ← FK reference checking
↓
4. if (!refs.isEmpty()) ← Reject if referenced
throw ReferenceExistsException
↓
5. beforeDelete(entity) ← Cascade cleanup
↓
6. performDeletion(entity) ← repository.delete (or custom)
↓
7. afterDelete(entity) ← Post-delete cleanup
Find Paged¶
1. spec = buildSpecification(filter, searchTerm)
↓
2. page = repository.findAll(spec, pageable)
↓
3. return pagingSupport.toPageResponse(page, listConverter::from)
Lifecycle Hooks¶
Override these methods to customize behavior at each stage:
| Hook | When Called | Common Use Case |
|---|---|---|
validateForCreate(dto) |
Before create conversion | Required field checks, uniqueness |
beforeCreate(entity, dto) |
After conversion, before save | Set crProcess, set defaults |
afterCreate(entity) |
After save | Audit logging, notifications |
validateForUpdate(dto, id) |
Before update conversion | Status checks, uniqueness |
beforeUpdate(entity, dto) |
After fetch, before merge | Computed fields |
afterUpdate(entity) |
After save | Audit logging, cache invalidation |
validateDeletion(entity) |
Before delete | Business rule checks |
checkReferences(id) |
Before delete | FK reference checking |
beforeDelete(entity) |
Before delete | Cascade cleanup |
afterDelete(entity) |
After delete | Cleanup external resources |
performDeletion(entity) |
Delete step | Custom deletion logic |
buildSpecification(filter, searchTerm) |
Find paged | Dynamic query building |
Türkçe
En sık kullanılan hook'lar beforeCreate (crProcess set etmek için) ve
validateForCreate (zorunlu alan kontrolü). Diğerleri ihtiyaç oldukça override edilir.
Example — Using hooks:
@Override
protected void validateForCreate(BranchEditDto dto) {
requireNonBlank(dto.getName(), "name");
requireNonNull(dto.getBusinessPartnerId(), "businessPartnerId");
}
@Override
protected void beforeCreate(Branch entity, BranchEditDto dto) {
if (dto.getCrProcessId() != null) {
CrProcess crProcess = entityManager.getReference(
CrProcess.class, dto.getCrProcessId());
entity.setCrProcess(crProcess);
}
}
@Override
protected List<ReferenceInfo> checkReferences(Long id) {
List<ReferenceInfo> refs = new ArrayList<>();
if (contractRepository.existsByBranch_Id(id)) {
refs.add(new ReferenceInfo("Contract", "branch"));
}
return refs;
}
Validation Helpers¶
Inherited utility methods for common validations:
// Throws ServiceException if value is null
requireNonNull(dto.getType(), "type");
// Throws ServiceException if value is null or blank
requireNonBlank(dto.getCode(), "code");
Read Operations¶
// Returns Optional<EditDto> — no exception if not found
Optional<D> findById(I id)
// Returns EditDto or throws NotFound exception
D findByIdOrThrow(I id)
// Internal: returns managed entity or throws
protected E findEntityById(I id)
Frontend — CrudPage¶
URL → Mode Mapping¶
CrudPage reads the URL to determine the current mode:
| URL Pattern | Mode | Description |
|---|---|---|
/:entity |
LIST | Paginated table |
/:entity/new |
CREATE | Empty form |
/:entity/:id |
VIEW | Read-only form |
/:entity/:id/edit |
EDIT | Editable form |
Mode determination logic:
let initialMode: CrudMode = "LIST"
if (isNew) initialMode = "CREATE"
else if (hasEntityId && isEdit) initialMode = "EDIT"
else if (hasEntityId) initialMode = "VIEW"
// Read-only handlers force downgrade
if (isReadOnlyHandler) {
if (initialMode === "CREATE") initialMode = "LIST"
else if (initialMode === "EDIT") initialMode = "VIEW"
}
Component Architecture¶
CrudPage ← URL parsing, auth guard, config loading
└── CrudRenderer ← Mode router
├── CrudListView ← LIST mode (table + filter + pagination)
│ ├── FilterPanel ← Dynamic filter fields
│ ├── DataTable ← TanStack Table with sorting
│ └── Pagination ← Page controls
└── CrudFormView ← VIEW / EDIT / CREATE modes
├── FormSections ← Grouped field rendering
│ └── DynamicField[] ← Per-field rendering by type
├── SubHandlerPanel ← Drill-down tabs (if configured)
└── Sidebar ← Related counts, summary (if configured)
useCrudHandler Hook¶
Central state management for all CRUD operations:
const {
// State
mode, setMode,
listData, stats, loading, loadingEntity,
currentEntity, entityError,
page, pageSize, sortField, sortOrder,
searchTerm, filterValues, formDirty,
// Operations
loadList, loadEntity,
createEntity, updateEntity, deleteEntity,
loadStats,
setSort, setSearchTerm, setFilterValues, resetFilter,
} = useCrudHandler(config, serviceClient)
Key behaviors:
- Auto-loads list when
mode,page,sortField,searchTerm, orfilterValueschange - Auto-loads stats when
statsEnabled: trueand mode is LIST - Normalizes filter values (trims strings, converts datetime formats)
- Sanitizes DTOs before save (removes extra fields, handles nested objects)
ID Encoding¶
Numeric primary keys are encoded in URLs using a Feistel cipher (7-char base36):
| Raw ID | Encoded | URL |
|---|---|---|
123 |
a7x9k2m |
/branches/a7x9k2m |
"ACC001" |
ACC001 |
/account/ACC001 (string IDs pass through) |
Türkçe
ID encoding güvenlik için değil, URL'lerin tahmin edilmesini zorlaştırmak içindir.
idField değeri id, userId gibi sayısal alanlar encode edilir;
accountNo gibi string alanlar olduğu gibi kullanılır.
Pagination & Sorting¶
// Request sent to backend
interface PageRequest<F> {
page: number // 0-based page index
size: number // rows per page
sort?: string // field name
sortOrder?: "asc" | "desc"
searchTerm?: string // free-text search
filter?: F // typed filter object
}
// Response from backend
interface PageResponse<T> {
content: T[]
totalElements: number
totalPages: number
page: number
size: number
}
Putting It Together¶
To add a new entity to the system:
Backend (3 files + hooks):
- Repository — extends
ICrudRepository - Service — extends
AbstractCrudService, implement 5 methods + hooks - Controller —
@RestControllerwith standard CRUD endpoints
Frontend (1 file):
- Handler config — define
crud,list,form,filtersections
That's it. The CRUD engine handles everything else.
┌─────────────────────────────────────────────┐
│ What you write (per entity) │
├──────────────────┬──────────────────────────┤
│ Backend │ Frontend │
│ • Repository │ • HandlerConfig │
│ • Service │ (1 TypeScript file) │
│ • Controller │ │
│ • Converters │ │
│ • Specification │ │
├──────────────────┴──────────────────────────┤
│ What the engine provides │
│ • Pagination, sorting, filtering │
│ • Mode switching (LIST/VIEW/EDIT/CREATE) │
│ • Form rendering with validation │
│ • Table rendering with decorators │
│ • Error handling & toast notifications │
│ • RBAC authorization guard │
│ • URL routing & ID encoding │
│ • Dirty form detection & unsaved warning │
└─────────────────────────────────────────────┘