sirius-framework-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +160 -0
  3. package/dist/grammars/tree-sitter-java.wasm +0 -0
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +247 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/java-parser.d.ts +25 -0
  8. package/dist/java-parser.js +281 -0
  9. package/dist/java-parser.js.map +1 -0
  10. package/dist/prompts/index.d.ts +9 -0
  11. package/dist/prompts/index.js +15 -0
  12. package/dist/prompts/index.js.map +1 -0
  13. package/dist/prompts/workflows.d.ts +27 -0
  14. package/dist/prompts/workflows.js +317 -0
  15. package/dist/prompts/workflows.js.map +1 -0
  16. package/dist/resources/biz/analytics.md +157 -0
  17. package/dist/resources/biz/biz-controller.md +151 -0
  18. package/dist/resources/biz/codelists.md +154 -0
  19. package/dist/resources/biz/entity-triple.md +142 -0
  20. package/dist/resources/biz/importer.md +153 -0
  21. package/dist/resources/biz/isenguard.md +156 -0
  22. package/dist/resources/biz/jobs.md +145 -0
  23. package/dist/resources/biz/processes.md +155 -0
  24. package/dist/resources/biz/storage.md +149 -0
  25. package/dist/resources/biz/tenants.md +159 -0
  26. package/dist/resources/biz/testing.md +127 -0
  27. package/dist/resources/db/composites.md +145 -0
  28. package/dist/resources/db/entities.md +156 -0
  29. package/dist/resources/db/mixing.md +176 -0
  30. package/dist/resources/db/queries.md +178 -0
  31. package/dist/resources/db/refs.md +135 -0
  32. package/dist/resources/index.d.ts +27 -0
  33. package/dist/resources/index.js +68 -0
  34. package/dist/resources/index.js.map +1 -0
  35. package/dist/resources/kernel/async.md +189 -0
  36. package/dist/resources/kernel/commons.md +203 -0
  37. package/dist/resources/kernel/config.md +155 -0
  38. package/dist/resources/kernel/di.md +138 -0
  39. package/dist/resources/kernel/lifecycle.md +146 -0
  40. package/dist/resources/loader.d.ts +9 -0
  41. package/dist/resources/loader.js +17 -0
  42. package/dist/resources/loader.js.map +1 -0
  43. package/dist/resources/web/controllers.md +151 -0
  44. package/dist/resources/web/services.md +136 -0
  45. package/dist/resources/web/templates.md +162 -0
  46. package/dist/tools/index.d.ts +4 -0
  47. package/dist/tools/index.js +3 -0
  48. package/dist/tools/index.js.map +1 -0
  49. package/dist/tools/introspection.d.ts +55 -0
  50. package/dist/tools/introspection.js +233 -0
  51. package/dist/tools/introspection.js.map +1 -0
  52. package/dist/tools/scaffold.d.ts +64 -0
  53. package/dist/tools/scaffold.js +505 -0
  54. package/dist/tools/scaffold.js.map +1 -0
  55. package/dist/workspace.d.ts +37 -0
  56. package/dist/workspace.js +185 -0
  57. package/dist/workspace.js.map +1 -0
  58. package/package.json +41 -0
@@ -0,0 +1,154 @@
1
+ # Code Lists and Lookup Tables
2
+
3
+ Code lists and lookup tables provide managed enumerations for master data. They map
4
+ codes to human-readable names and additional metadata, with support for
5
+ internationalization and multiple data sources.
6
+
7
+ ## CodeList — The Classic Approach
8
+
9
+ `CodeList` is the original database-backed enumeration. It follows the entity triple
10
+ pattern with `CodeListData` as the shared composite:
11
+
12
+ ```java
13
+ public interface CodeList extends TenantAware, Traced {
14
+ Mapping CODE_LIST_DATA = Mapping.named("codeListData");
15
+ CodeListData getCodeListData();
16
+ }
17
+ ```
18
+
19
+ Implementations: `SQLCodeList` (JDBC) and `MongoCodeList` (MongoDB). Each code list
20
+ belongs to a tenant and contains `CodeListEntry` items (code + value + description).
21
+
22
+ ### CodeLists Service
23
+
24
+ ```java
25
+ @Part
26
+ private CodeLists codeLists;
27
+
28
+ // Look up a value
29
+ String countryName = codeLists.getValue("countries", "DE").orElse("Unknown");
30
+
31
+ // Check if a code exists
32
+ boolean valid = codeLists.hasValue("countries", "DE");
33
+ ```
34
+
35
+ ## LookupTable — The Modern Approach
36
+
37
+ `LookupTable` is an abstraction layer over multiple data sources. A lookup table can
38
+ be backed by:
39
+
40
+ - **CodeList** — tenant-specific data from the database (`CodeListLookupTable`)
41
+ - **Jupiter IDB tables** — high-performance Redis-like key-value store (`IDBLookupTable`)
42
+ - **Config files** — static data from HOCON configuration (`ConfigLookupTable`)
43
+ - **Custom implementations** — via `CustomLookupTable`
44
+
45
+ The data source is selected via configuration:
46
+
47
+ ```hocon
48
+ lookup-tables {
49
+ countries {
50
+ type = "code-list" # or "idb", "config", "custom"
51
+ codeList = "countries"
52
+ }
53
+ currencies {
54
+ type = "idb"
55
+ table = "currencies"
56
+ }
57
+ }
58
+ ```
59
+
60
+ ### Key Operations
61
+
62
+ ```java
63
+ @Part
64
+ private LookupTables lookupTables;
65
+
66
+ LookupTable table = lookupTables.fetchTable("countries");
67
+
68
+ // Normalize a code (resolve aliases)
69
+ Optional<String> code = table.normalize("DEU"); // -> "DE"
70
+
71
+ // Resolve a display name
72
+ Optional<String> name = table.resolveName("DE"); // -> "Germany"
73
+
74
+ // Fetch additional fields
75
+ Optional<String> iso3 = table.fetchField("DE", "iso3");
76
+
77
+ // Search/suggest
78
+ List<LookupTableEntry> matches = table.suggest("Germ", new Limit(0, 10));
79
+ ```
80
+
81
+ ## LookupValue — Entity Field Type
82
+
83
+ `LookupValue` is a field type for embedding a lookup table reference directly in
84
+ an entity. It replaces raw string fields with a typed, validated reference:
85
+
86
+ ```java
87
+ private final LookupValue salutation = new LookupValue(
88
+ "salutations", // lookup table name
89
+ LookupValue.Display.NAME, // show name in UI
90
+ LookupValue.Display.CODE_AND_NAME, // extended display
91
+ LookupValue.Export.CODE, // export the code
92
+ LookupValue.CustomValues.REJECT // reject unknown values
93
+ );
94
+ ```
95
+
96
+ ### Display and Export Modes
97
+
98
+ **Display** controls what users see in the UI:
99
+ - `Display.CODE` — show the raw code (e.g., "DE")
100
+ - `Display.NAME` — show the resolved name (e.g., "Germany")
101
+ - `Display.CODE_AND_NAME` — show both (e.g., "Germany (DE)")
102
+
103
+ **Export** controls what appears in CSV/Excel exports:
104
+ - `Export.CODE` — export the raw code
105
+ - `Export.NAME` — export the resolved name
106
+
107
+ **CustomValues** controls validation:
108
+ - `CustomValues.ACCEPT` — allow values not in the lookup table
109
+ - `CustomValues.REJECT` — reject unknown values with a validation error
110
+
111
+ The framework uses `LookupValue` internally, e.g., `PersonData.salutation` is a
112
+ `LookupValue` backed by the `"salutations"` table.
113
+
114
+ ## LookupValues — Multi-Value Field
115
+
116
+ `LookupValues` stores multiple codes from the same lookup table as a list:
117
+
118
+ ```java
119
+ private final LookupValues tags = new LookupValues("product-tags");
120
+ ```
121
+
122
+ ## CustomLookupTable
123
+
124
+ For data sources that do not fit the built-in types, implement `CustomLookupTable`:
125
+
126
+ ```java
127
+ @Register
128
+ public class MyCustomTable extends CustomLookupTable {
129
+ @Override
130
+ public String getName() { return "my-table"; }
131
+
132
+ @Override
133
+ public Optional<String> normalize(String code) { ... }
134
+
135
+ @Override
136
+ public Optional<String> resolveName(String code, String language) { ... }
137
+ }
138
+ ```
139
+
140
+ ## Common Mistakes
141
+
142
+ 1. **Using raw strings instead of `LookupValue`** — Storing a code as a plain
143
+ `String` field loses validation, display formatting, and autocomplete support.
144
+
145
+ 2. **Wrong `CustomValues` mode** — Using `REJECT` on a table where users need to
146
+ enter free-form values causes validation failures. Use `ACCEPT` when the table
147
+ is advisory rather than authoritative.
148
+
149
+ 3. **Confusing CodeList and LookupTable** — `CodeList` is the raw database entity.
150
+ `LookupTable` is the abstraction layer. Always program against `LookupTable`
151
+ unless you need direct CRUD on the code list entries.
152
+
153
+ 4. **Missing lookup table configuration** — A `LookupValue` referencing a table
154
+ name that has no configuration in `lookup-tables { }` will fail at runtime.
@@ -0,0 +1,142 @@
1
+ # Entity Triple Pattern
2
+
3
+ The entity triple is the standard pattern for database-portable entities in sirius-biz.
4
+ Every entity exists as three artifacts: an interface, a JDBC implementation, and a
5
+ MongoDB implementation. This lets application code program against the interface while
6
+ the framework routes persistence to whichever database is active.
7
+
8
+ ## Step 1 — Interface (Base Package)
9
+
10
+ Define the interface in the main package (e.g., `sirius.biz.tenants`). It extends
11
+ `Entity` plus any mixins and declares `Mapping` constants and accessor methods:
12
+
13
+ ```java
14
+ @SuppressWarnings("squid:S1214")
15
+ @Explain("We rather keep the constants here, as this emulates the behaviour and layout of a real entity.")
16
+ public interface Tenant<I extends Serializable>
17
+ extends Entity, Transformable, Traced, Journaled, RateLimitedEntity, PerformanceFlagged {
18
+
19
+ String PERMISSION_SYSTEM_TENANT = "flag-system-tenant";
20
+
21
+ Mapping PARENT = Mapping.named("parent");
22
+ Mapping TENANT_DATA = Mapping.named("tenantData");
23
+
24
+ BaseEntityRef<I, ? extends Tenant<I>> getParent();
25
+ TenantData getTenantData();
26
+ boolean hasPermission(String permission);
27
+ }
28
+ ```
29
+
30
+ Key rules:
31
+ - Use `@Explain` to justify constants in the interface (SonarQube rule S1214).
32
+ - Parameterize with `<I extends Serializable>` for the database ID type.
33
+ - Keep all field data in a `Composite` (e.g., `TenantData`) so that both
34
+ implementations share the same field definitions.
35
+
36
+ ## Step 2 — JDBC Implementation (`jdbc/` Subpackage)
37
+
38
+ Place the SQL entity in a `jdbc/` subpackage. It extends `BizEntity` (which gives
39
+ you `TraceData`) or `SQLTenantAware` (if tenant-scoped) and implements the interface:
40
+
41
+ ```java
42
+ @Framework(SQLTenants.FRAMEWORK_TENANTS_JDBC)
43
+ @TranslationSource(Tenant.class)
44
+ public class SQLTenant extends BizEntity implements Tenant<Long> {
45
+
46
+ @Autoloaded
47
+ @AutoImport
48
+ @NullAllowed
49
+ private final SQLEntityRef<SQLTenant> parent =
50
+ SQLEntityRef.on(SQLTenant.class, SQLEntityRef.OnDelete.SET_NULL);
51
+
52
+ public static final Mapping TENANT_DATA = Mapping.named("tenantData");
53
+ private final TenantData tenantData = new TenantData(this);
54
+
55
+ private final SQLPerformanceData performanceData = new SQLPerformanceData(this);
56
+
57
+ // ... implement interface methods
58
+ }
59
+ ```
60
+
61
+ Key annotations:
62
+ - **`@Framework("biz.tenants-jdbc")`** — gates this entity behind a framework flag.
63
+ Only loaded when `sirius.frameworks { biz.tenants-jdbc = true }` is set.
64
+ - **`@TranslationSource(Tenant.class)`** — tells the i18n system to look up property
65
+ labels from the interface, not this class. Without this, you need duplicate `.properties`.
66
+ - Use `SQLEntityRef<T>` for references to other SQL entities.
67
+ - The ID type is `Long` (auto-increment).
68
+
69
+ ## Step 3 — MongoDB Implementation (`mongo/` Subpackage)
70
+
71
+ Place the Mongo entity in a `mongo/` subpackage. It extends `MongoBizEntity` (which
72
+ gives `TraceData` and prefix search) or `MongoTenantAware` (if tenant-scoped):
73
+
74
+ ```java
75
+ @Framework(MongoTenants.FRAMEWORK_TENANTS_MONGO)
76
+ @TranslationSource(Tenant.class)
77
+ public class MongoTenant extends MongoBizEntity implements Tenant<String> {
78
+
79
+ @Autoloaded
80
+ @NullAllowed
81
+ private final MongoRef<MongoTenant> parent =
82
+ MongoRef.on(MongoTenant.class, MongoRef.OnDelete.SET_NULL);
83
+
84
+ public static final Mapping TENANT_DATA = Mapping.named("tenantData");
85
+ private final TenantData tenantData = new TenantData(this);
86
+
87
+ private final MongoPerformanceData performanceData = new MongoPerformanceData(this);
88
+
89
+ // ... implement interface methods
90
+ }
91
+ ```
92
+
93
+ Key differences from SQL:
94
+ - Use `MongoRef<T>` instead of `SQLEntityRef<T>`.
95
+ - The ID type is `String` (MongoDB ObjectId).
96
+ - `MongoBizEntity` extends `PrefixSearchableEntity` for built-in text search.
97
+
98
+ ## Base Class Selection
99
+
100
+ | Scenario | JDBC Base Class | Mongo Base Class |
101
+ |---------------------------------|--------------------|----------------------|
102
+ | Standalone entity | `BizEntity` | `MongoBizEntity` |
103
+ | Tenant-scoped entity | `SQLTenantAware` | `MongoTenantAware` |
104
+ | No tracing needed (rare) | `SQLEntity` | `MongoEntity` |
105
+
106
+ ## @Framework vs @Register(framework = ...)
107
+
108
+ This distinction is critical and a frequent source of bugs:
109
+
110
+ - **`@Framework`** — used on **entity classes**. Controls whether the entity is
111
+ registered in the schema and ORM. Without it, the entity loads unconditionally.
112
+ - **`@Register(framework = "...")`** — used on **services, controllers, and other
113
+ components**. Controls whether the class participates in dependency injection.
114
+
115
+ ```java
116
+ // Entity — use @Framework
117
+ @Framework("biz.tenants-jdbc")
118
+ public class SQLTenant extends BizEntity { ... }
119
+
120
+ // Service — use @Register(framework = ...)
121
+ @Register(classes = Processes.class, framework = "biz.processes")
122
+ public class Processes { ... }
123
+ ```
124
+
125
+ ## Common Mistakes
126
+
127
+ 1. **Wrong base class** — Using `SQLEntity` instead of `BizEntity` loses `TraceData`.
128
+ Using `BizEntity` for a tenant-scoped entity misses the automatic tenant reference.
129
+
130
+ 2. **Missing `@Framework`** — The entity loads in all configurations and creates
131
+ database tables even when the feature is disabled.
132
+
133
+ 3. **Missing `@TranslationSource`** — Property labels defined for the interface
134
+ (e.g., `Tenant.tenantData.name`) are not found by the SQL/Mongo implementation.
135
+ You end up with raw property names in the UI.
136
+
137
+ 4. **Forgetting the Composite** — Putting fields directly in both implementations
138
+ instead of using a shared `Composite` leads to drift and duplication.
139
+
140
+ 5. **ID type mismatch** — JDBC entities use `Long`, Mongo entities use `String`.
141
+ The interface must be parameterized with `<I extends Serializable>` to abstract
142
+ over this difference.
@@ -0,0 +1,153 @@
1
+ # Importer Framework
2
+
3
+ The import framework provides a structured way to import data into entities from
4
+ external sources (CSV, Excel, XML, JSON). It handles field mapping, entity lookup,
5
+ create-or-update logic, batch operations, and extensibility through events.
6
+
7
+ ## Import Handler Hierarchy
8
+
9
+ Every entity type that can be imported needs an `ImportHandler`. The hierarchy:
10
+
11
+ - `BaseImportHandler<E>` — abstract base with convenience methods
12
+ - `SQLEntityImportHandler<E>` — for JDBC/SQL entities, uses batch queries
13
+ - `MongoEntityImportHandler<E>` — for MongoDB entities
14
+
15
+ Create a handler by extending the appropriate base class:
16
+
17
+ ```java
18
+ @Register(classes = ImportHandler.class, framework = "biz.tenants-jdbc")
19
+ public class SQLTenantImportHandler extends SQLEntityImportHandler<SQLTenant> {
20
+
21
+ @Override
22
+ protected Class<SQLTenant> getType() {
23
+ return SQLTenant.class;
24
+ }
25
+
26
+ @Override
27
+ protected void collectFindQueries(
28
+ Consumer<Tuple<Predicate<SQLTenant>, Supplier<FindQuery<SQLTenant>>>> queryConsumer) {
29
+ queryConsumer.accept(Tuple.create(
30
+ tenant -> Strings.isFilled(tenant.getTenantData().getAccountNumber()),
31
+ () -> insertQuery.newFindQuery()
32
+ .where(SQLTenant.TENANT_DATA.inner(TenantData.ACCOUNT_NUMBER))
33
+ ));
34
+ }
35
+ }
36
+ ```
37
+
38
+ ## @AutoImport Annotation
39
+
40
+ Mark entity fields with `@AutoImport` to include them in automatic import mapping.
41
+ When `BaseImportHandler.getAutoImportMappings()` is called, all `@AutoImport` fields
42
+ are collected and can be mapped from import columns:
43
+
44
+ ```java
45
+ @AutoImport
46
+ @Length(150)
47
+ @Trim
48
+ private String name;
49
+
50
+ @AutoImport
51
+ @NullAllowed
52
+ private String accountNumber;
53
+ ```
54
+
55
+ This works together with the `ImportDictionary` which maps column headers to entity
56
+ properties.
57
+
58
+ ## Event System
59
+
60
+ The importer fires events at each stage of the import lifecycle. Register handlers
61
+ to customize behavior:
62
+
63
+ | Event | When Fired |
64
+ |-----------------------------|-----------------------------------------------|
65
+ | `BeforeLoadEvent` | Before populating entity fields from input |
66
+ | `AfterLoadEvent` | After populating fields, before find/match |
67
+ | `BeforeFindEvent` | Before attempting to find an existing entity |
68
+ | `BeforeCreateOrUpdateEvent` | Before persisting (create or update) |
69
+ | `AfterCreateOrUpdateEvent` | After entity is persisted |
70
+ | `BeforeDeleteEvent` | Before deleting an entity |
71
+
72
+ Events are dispatched to `EntityImportHandlerExtender` implementations, which are
73
+ collected via `@Parts(EntityImportHandlerExtender.class)` and can hook into any
74
+ stage of the import lifecycle.
75
+
76
+ ## Find Queries — Matching Existing Records
77
+
78
+ `collectFindQueries()` defines how the importer matches incoming data to existing
79
+ entities. Each query is a pair of (predicate, query supplier):
80
+
81
+ ```java
82
+ @Override
83
+ protected void collectFindQueries(
84
+ Consumer<Tuple<Predicate<SQLProduct>, Supplier<FindQuery<SQLProduct>>>> queryConsumer) {
85
+ // Match by SKU if present
86
+ queryConsumer.accept(Tuple.create(
87
+ product -> Strings.isFilled(product.getSku()),
88
+ () -> insertQuery.newFindQuery().where(SQLProduct.SKU)
89
+ ));
90
+ // Fallback: match by name + tenant
91
+ queryConsumer.accept(Tuple.create(
92
+ product -> Strings.isFilled(product.getName()),
93
+ () -> insertQuery.newFindQuery()
94
+ .where(SQLProduct.NAME)
95
+ .where(SQLProduct.TENANT)
96
+ ));
97
+ }
98
+ ```
99
+
100
+ The predicate determines if the query applies (e.g., only if SKU is filled).
101
+ Queries are tried in order; the first match wins.
102
+
103
+ ## JDBC Batch Operations
104
+
105
+ `SQLEntityImportHandler` uses JDBC batch queries for performance:
106
+
107
+ - **`InsertQuery<E>`** — batched INSERT statements
108
+ - **`UpdateQuery<E>`** — batched UPDATE statements
109
+ - **`DeleteQuery<E>`** — batched DELETE statements
110
+ - **`FindQuery<E>`** — SELECT for matching existing records
111
+
112
+ These are created lazily and reused across the import run. The batch context
113
+ flushes automatically at configurable intervals.
114
+
115
+ ```java
116
+ protected UpdateQuery<E> updateQuery;
117
+ protected InsertQuery<E> insertQuery;
118
+ protected DeleteQuery<E> deleteQuery;
119
+ ```
120
+
121
+ ## Importer — The Entry Point
122
+
123
+ The `Importer` class orchestrates the full import:
124
+
125
+ ```java
126
+ Importer importer = new Importer("product-import");
127
+ try {
128
+ SQLTenantImportHandler handler = importer.findHandler(SQLProduct.class);
129
+ for (Context row : rows) {
130
+ handler.tryCreateOrUpdate(row);
131
+ }
132
+ } finally {
133
+ importer.close(); // flushes batches, fires completion events
134
+ }
135
+ ```
136
+
137
+ ## Common Mistakes
138
+
139
+ 1. **Forgetting `collectFindQueries()`** — Without find queries, the importer
140
+ always creates new entities instead of updating existing ones.
141
+
142
+ 2. **Not closing the Importer** — Always close in a `finally` block or
143
+ try-with-resources. Unclosed importers leave batch queries unflushed.
144
+
145
+ 3. **Modifying entity state in the wrong event** — Use `BeforeCreateOrUpdateEvent`
146
+ for validation/enrichment. Using `AfterLoadEvent` may be too early since the
147
+ entity has not been matched to an existing record yet.
148
+
149
+ 4. **Missing `@AutoImport` on fields** — Fields without `@AutoImport` are invisible
150
+ to the automatic import mapping and must be handled manually.
151
+
152
+ 5. **Not handling `MissingEntityMode`** — Decide what to do when a referenced entity
153
+ (e.g., a foreign key) is not found: skip, create, or fail.
@@ -0,0 +1,156 @@
1
+ # Isenguard
2
+
3
+ Isenguard is the built-in rate limiting and firewall facility. It tracks request
4
+ counts per scope (IP, user, tenant) and realm, blocking requests that exceed
5
+ configured limits. It is typically backed by Redis for distributed rate limiting.
6
+
7
+ ## Enabling Isenguard
8
+
9
+ Isenguard is gated behind a framework flag:
10
+
11
+ ```hocon
12
+ sirius.frameworks {
13
+ biz.isenguard = true
14
+ }
15
+ ```
16
+
17
+ The `Isenguard` service is registered unconditionally but only enforces limits
18
+ when the framework is enabled and a `Limiter` backend is available.
19
+
20
+ ## Limiter Backends
21
+
22
+ The limiter strategy is configured via:
23
+
24
+ ```hocon
25
+ isenguard {
26
+ limiter = "smart" # default — uses Redis if available, else NOOP
27
+ }
28
+ ```
29
+
30
+ Available strategies:
31
+ - **`smart`** (default) — uses `RedisLimiter` if Redis is available, otherwise
32
+ falls back to `NOOPLimiter` (no rate limiting).
33
+ - **`redis`** — always use Redis. Fails if Redis is unavailable.
34
+ - **`noop`** — disables rate limiting entirely.
35
+
36
+ ## Rate Limit Scopes
37
+
38
+ Each rate limit check requires a **scope** (who is being limited) and a **realm**
39
+ (what operation is being limited):
40
+
41
+ | Scope Type | Constant | Example Value |
42
+ |--------------|---------------------------------|------------------|
43
+ | Per IP | `Isenguard.REALM_TYPE_IP` | `"192.168.1.1"` |
44
+ | Per Tenant | `Isenguard.REALM_TYPE_TENANT` | `"tenant-42"` |
45
+ | Per User | `Isenguard.REALM_TYPE_USER` | `"user-123"` |
46
+
47
+ The scope is a free-form string — you decide what to use as the key.
48
+
49
+ ## Checking Rate Limits
50
+
51
+ ### Programmatic API
52
+
53
+ ```java
54
+ @Part
55
+ private Isenguard isenguard;
56
+
57
+ // Register a call and check if the limit is reached
58
+ boolean blocked = isenguard.registerCallAndCheckRateLimitReached(
59
+ clientIp, // scope
60
+ "api-calls", // realm
61
+ Isenguard.USE_LIMIT_FROM_CONFIG, // use configured limit
62
+ () -> new RateLimitingInfo(tenantId, tenantName, userId) // info for audit
63
+ );
64
+
65
+ if (blocked) {
66
+ throw Exceptions.createHandled()
67
+ .withNLSKey("Isenguard.rateLimitReached")
68
+ .handle();
69
+ }
70
+ ```
71
+
72
+ An overload accepts a `Runnable` callback that fires once when the limit is first
73
+ reached (useful for audit logging).
74
+
75
+ ### Check Without Counting
76
+
77
+ ```java
78
+ // Does not count as a call — only checks the current state
79
+ boolean alreadyBlocked = isenguard.checkRateLimitReached(clientIp, "api-calls");
80
+ ```
81
+
82
+ ## Configuring Limits
83
+
84
+ Limits are defined per realm in HOCON:
85
+
86
+ ```hocon
87
+ isenguard {
88
+ limit {
89
+ api-calls {
90
+ limit = 100 # max calls per interval
91
+ intervalSeconds = 60 # check interval in seconds
92
+ }
93
+ login-attempts {
94
+ limit = 5
95
+ intervalSeconds = 300
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ You can also pass an explicit limit programmatically by providing a non-zero value
102
+ instead of `Isenguard.USE_LIMIT_FROM_CONFIG`.
103
+
104
+ ## RateLimitedEntity
105
+
106
+ The `RateLimitedEntity` interface is a marker for entities that have rate limits
107
+ applied to them. Implementing it enables the `RateLimitEventsReportJobFactory`
108
+ to offer itself as a matching job for the entity:
109
+
110
+ ```java
111
+ public interface RateLimitedEntity {
112
+ String getRateLimitScope();
113
+ }
114
+ ```
115
+
116
+ `Tenant` implements `RateLimitedEntity`, so tenants automatically get rate limit
117
+ reporting. Any custom entity can implement this interface:
118
+
119
+ ```java
120
+ public class APIClient extends BizEntity implements RateLimitedEntity {
121
+ @Override
122
+ public String getRateLimitScope() {
123
+ return "api-client-" + getIdAsString();
124
+ }
125
+ }
126
+ ```
127
+
128
+ ## IP Blocking
129
+
130
+ Isenguard also functions as a firewall. IPs can be blocked permanently or
131
+ temporarily:
132
+
133
+ ```java
134
+ isenguard.blockIP("192.168.1.100");
135
+ ```
136
+
137
+ Blocked IPs receive HTTP 429 (Too Many Requests) responses. The
138
+ `BlockedIPsReportJobFactory` provides a UI for viewing and managing blocked IPs.
139
+
140
+ ## Common Mistakes
141
+
142
+ 1. **Forgetting `USE_LIMIT_FROM_CONFIG`** — Passing `0` as the explicit limit
143
+ constant means "use config." Passing a non-zero value overrides the config.
144
+ Accidentally passing `0` when you mean "no limit" does the wrong thing.
145
+
146
+ 2. **Wrong scope granularity** — Rate limiting by IP is too coarse for shared
147
+ networks (NAT). Rate limiting by user is too fine if you want to limit a
148
+ tenant's total API usage. Choose the right scope for your use case.
149
+
150
+ 3. **Not providing `RateLimitingInfo`** — The info supplier is called when the
151
+ limit is first reached to create an audit log entry. Returning null values
152
+ makes it hard to diagnose issues.
153
+
154
+ 4. **Missing Redis** — With the `smart` limiter (default), rate limiting silently
155
+ degrades to no-op when Redis is unavailable. In production, ensure Redis is
156
+ running or use the `redis` limiter to fail fast.