What this is about
A CDMS project has two kinds of code. The generated code is created by the build from the model in the hub, see Code generation in the build. The custom code is written by you: business logic that cannot be modeled.
The two live in different places and have different owners. If you keep them apart, you can regenerate at any time without losing anything.
What lives where
flowchart TB
subgraph T["target/generated-sources/ · generated, recreated on every build"]
A["OrderApi"] --> S["OrderSystem"] --> D["OrderDatabase"]
S --> AL["OrderAuthorizationLayer"]
M["OrderEntity, OrderDto, Payloads, Mapper, MetaService"]
end
subgraph SRC["src/main/java/ · yours, in the repository"]
H["OrderHook<br/>(Hook)"]
F["OrderFilter<br/>(Filter)"]
C["custom controller<br/>or service"]
ST["Start.java<br/>(created once)"]
end
subgraph R["src/main/resources/ · yours"]
Y["application.yaml"]
end
H -. "is called by" .-> S
F -. "is asked by" .-> S
C -- "calls" --> S
| Location | Owner | in the repository? | what happens during the build |
|---|---|---|---|
target/generated-sources/annotations/ | Generator | no | is recreated |
src/main/java/ | you | yes | is compiled, never overwritten |
src/main/resources/ | you | yes | is packaged, never overwritten |
pom.xml between the cdms-managed markers | Hub | yes | is kept up to date, see Project scaffold |
Where does my logic go?
| What do you want to do? | Extension point |
|---|---|
| add a field, a rule, an endpoint, a role | change the model in the hub and build again, no code |
| check, calculate or trigger something before or after saving | Hook on the model |
| hide rows depending on a user attribute | access filter in the hub, a custom filter class if needed |
| restrict every search on a model with your own logic | custom filter |
| offer a business endpoint that CDMS does not know | custom controller that calls the system layer |
| adjust the behavior of CDMS | configuration in application.yaml or the environment |
| change a generated class | not possible, the change is gone after the next build |
Hooks
A hook is a class that CDMS calls on every operation on a model. You write it for the model’s entity:
@Service
@Order(100)
public class OrderHook implements HookServiceInterface<OrderEntity> {
@Override
public void beforeDatabaseChange(OrderEntity item, CmsMethods method)
throws AbstractCodamaiException {
if (method == CmsMethods.CREATE && item.getOrderNr() == null) {
item.setOrderNr(nextNumber());
}
}
@Override
public void afterDatabaseChange(OrderEntity item, CmsMethods method)
throws AbstractCodamaiException {
// e.g. publish an event
}
@Override
public void fieldBeforeDatabaseChange(OrderEntity item, String field, CmsMethods method) { }
@Override
public void fieldOnDatabaseRead(OrderEntity item, String field, CmsMethods method) { }
}
-
1Developerwrites a class that implements
HookServiceInterface<OrderEntity> -
2Developermakes it a Spring bean with
@Serviceand gives it an@Order@Ordersets the order when there are several hooks on the same model. Smaller number first. Put it directly on the class. -
3CDMSfinds all hook beans for
OrderEntityand sorts them by@Order -
4Hookis called on every operation, with the operation as
CmsMethodsCREATE,READ,UPDATE,PATCH,DELETE,ROLLBACK
The work is done by beforeDatabaseChange (before writing, inside the transaction) and afterDatabaseChange (after writing, and when reading after loading). If a hook throws an AbstractCodamaiException, the whole operation stops and the transaction is rolled back. Exactly when which hook runs is described in Hooks: kinds and timing and Order of hooks.
Custom filters
A filter restricts every search on a model before it reaches the database. It works with the model’s DTO. The most common case is an attribute filter: each user sees only the rows whose field matches an attribute in their token. For this there is the ready-made base class AbstractAttributeFilter:
@Service
public class OrderFilter extends AbstractAttributeFilter<OrderDto> {
@Getter
private final String dataAttribute = "companyId"; // field on the model
@Getter
private final String profileAttribute = "company"; // attribute in the user's token
}
| Value of the attribute in the token | Condition in the search |
|---|---|
one value, e.g. c-17 | companyId = 'c-17' |
| several values | companyId IN (…) |
* | no filter, all rows |
| missing or empty | request is rejected |
If you need different logic, you implement CdmsFilterInterface<OrderDto> directly and return your own search filter in get(). If you create an access filter in the hub, the generator creates the filter class itself. More in Attribute filter and Custom data filters.
Custom controllers and services
For business processes that go beyond create, read, update and delete, you write a normal Spring @RestController. It calls the generated system layers. This way its calls also go through permission checks, validation and hooks.
The generated OrderApi, on the other hand, you do not extend or replace. The generated beans have fixed names. A second bean with the same name stops the application from starting.
Configuration
What you can adjust in CDMS is set in application.yaml or comes from the environment:
| Property | what for |
|---|---|
codamai.cdms.api.createReadMode | how strict reading back after a create is (STRICT, LENIENT) |
codamai.cdms.cias.reader-roles | roles that may read the role declaration |
codamai.cdms.persistence.file.basePath | storage location for files |
codamai.debug | stack trace in error responses (false in production) |
CODAMAI_PERSISTENCE_TENANT_MODE | SINGLE or MULTI |
CODAMAI_PERSISTENCE_DATABASE_* | driver, URL, user, password of the database |