Skip to content

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:

  1. Backend: Extend AbstractCrudService and implement 5 abstract methods
  2. Frontend: Create a HandlerConfig object 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
type CrudMode = "LIST" | "VIEW" | "EDIT" | "CREATE"

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, or filterValues change
  • Auto-loads stats when statsEnabled: true and 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):

  1. Repository — extends ICrudRepository
  2. Service — extends AbstractCrudService, implement 5 methods + hooks
  3. Controller — @RestController with standard CRUD endpoints

Frontend (1 file):

  1. Handler config — define crud, list, form, filter sections

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   │
└─────────────────────────────────────────────┘