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.
- package/LICENSE +191 -0
- package/README.md +160 -0
- package/dist/grammars/tree-sitter-java.wasm +0 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +247 -0
- package/dist/index.js.map +1 -0
- package/dist/java-parser.d.ts +25 -0
- package/dist/java-parser.js +281 -0
- package/dist/java-parser.js.map +1 -0
- package/dist/prompts/index.d.ts +9 -0
- package/dist/prompts/index.js +15 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/workflows.d.ts +27 -0
- package/dist/prompts/workflows.js +317 -0
- package/dist/prompts/workflows.js.map +1 -0
- package/dist/resources/biz/analytics.md +157 -0
- package/dist/resources/biz/biz-controller.md +151 -0
- package/dist/resources/biz/codelists.md +154 -0
- package/dist/resources/biz/entity-triple.md +142 -0
- package/dist/resources/biz/importer.md +153 -0
- package/dist/resources/biz/isenguard.md +156 -0
- package/dist/resources/biz/jobs.md +145 -0
- package/dist/resources/biz/processes.md +155 -0
- package/dist/resources/biz/storage.md +149 -0
- package/dist/resources/biz/tenants.md +159 -0
- package/dist/resources/biz/testing.md +127 -0
- package/dist/resources/db/composites.md +145 -0
- package/dist/resources/db/entities.md +156 -0
- package/dist/resources/db/mixing.md +176 -0
- package/dist/resources/db/queries.md +178 -0
- package/dist/resources/db/refs.md +135 -0
- package/dist/resources/index.d.ts +27 -0
- package/dist/resources/index.js +68 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/kernel/async.md +189 -0
- package/dist/resources/kernel/commons.md +203 -0
- package/dist/resources/kernel/config.md +155 -0
- package/dist/resources/kernel/di.md +138 -0
- package/dist/resources/kernel/lifecycle.md +146 -0
- package/dist/resources/loader.d.ts +9 -0
- package/dist/resources/loader.js +17 -0
- package/dist/resources/loader.js.map +1 -0
- package/dist/resources/web/controllers.md +151 -0
- package/dist/resources/web/services.md +136 -0
- package/dist/resources/web/templates.md +162 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/introspection.d.ts +55 -0
- package/dist/tools/introspection.js +233 -0
- package/dist/tools/introspection.js.map +1 -0
- package/dist/tools/scaffold.d.ts +64 -0
- package/dist/tools/scaffold.js +505 -0
- package/dist/tools/scaffold.js.map +1 -0
- package/dist/workspace.d.ts +37 -0
- package/dist/workspace.js +185 -0
- package/dist/workspace.js.map +1 -0
- 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.
|