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,145 @@
|
|
|
1
|
+
# Jobs Framework
|
|
2
|
+
|
|
3
|
+
The jobs framework provides a system for defining, parameterizing, and executing
|
|
4
|
+
background tasks. Jobs are stateless factories (`JobFactory`) that produce work
|
|
5
|
+
which runs either interactively (in the browser) or as batch processes on
|
|
6
|
+
distributed worker nodes.
|
|
7
|
+
|
|
8
|
+
## JobFactory Interface
|
|
9
|
+
|
|
10
|
+
`JobFactory` is the top-level interface, annotated with `@AutoRegister` so that
|
|
11
|
+
implementations are automatically discovered. In practice, always subclass
|
|
12
|
+
`BasicJobFactory` which handles parameter collection, validation, and execution
|
|
13
|
+
setup.
|
|
14
|
+
|
|
15
|
+
```java
|
|
16
|
+
@AutoRegister
|
|
17
|
+
public interface JobFactory extends Named, Priorized {
|
|
18
|
+
String getLabel();
|
|
19
|
+
String getIcon();
|
|
20
|
+
String getDescription();
|
|
21
|
+
List<Parameter<?, ?>> getParameters();
|
|
22
|
+
// ...
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Class Hierarchy — Choose the Right Base Class
|
|
27
|
+
|
|
28
|
+
The hierarchy branches into two paths: interactive and batch.
|
|
29
|
+
|
|
30
|
+
**Interactive jobs** run synchronously while the user waits:
|
|
31
|
+
|
|
32
|
+
- `BasicJobFactory`
|
|
33
|
+
- `InteractiveJobFactory` — base for all interactive jobs
|
|
34
|
+
- `ReportJobFactory` — produces tabular HTML reports
|
|
35
|
+
- `DoughnutChartJobFactory` — renders a doughnut chart
|
|
36
|
+
- `LinearChartJobFactory` — renders a line chart
|
|
37
|
+
- `TimeseriesChartJobFactory` — renders a time-series chart
|
|
38
|
+
- `PolarAreaChartJobFactory` — renders a polar area chart
|
|
39
|
+
|
|
40
|
+
**Batch jobs** run asynchronously in a distributed process:
|
|
41
|
+
|
|
42
|
+
- `BasicJobFactory`
|
|
43
|
+
- `BatchProcessJobFactory` — creates a `Process` and dispatches work via `DistributedTasks`
|
|
44
|
+
- `SimpleBatchProcessJobFactory` — single `execute(ProcessContext)` method
|
|
45
|
+
- `ImportBatchProcessFactory` — file-based import workflows
|
|
46
|
+
- `ExportBatchProcessFactory` — file-based export workflows
|
|
47
|
+
- `CheckBatchProcessFactory` — data validation/checking workflows
|
|
48
|
+
- `ReportBatchProcessFactory` — long-running report generation
|
|
49
|
+
|
|
50
|
+
**Key rule:** Pick the most specific base class. If your job imports data from a
|
|
51
|
+
file, extend `ImportBatchProcessFactory`, not `SimpleBatchProcessJobFactory`.
|
|
52
|
+
|
|
53
|
+
## Defining Parameters
|
|
54
|
+
|
|
55
|
+
Override `collectParameters()` to declare the job's inputs:
|
|
56
|
+
|
|
57
|
+
```java
|
|
58
|
+
@Override
|
|
59
|
+
protected void collectParameters(Consumer<Parameter<?, ?>> parameterCollector) {
|
|
60
|
+
parameterCollector.accept(DATE_RANGE_PARAMETER);
|
|
61
|
+
parameterCollector.accept(ACTIVE_ONLY_PARAMETER);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Common parameter types (all in `sirius.biz.jobs.params`):
|
|
66
|
+
|
|
67
|
+
| Type | Description |
|
|
68
|
+
|------------------------|--------------------------------------------|
|
|
69
|
+
| `StringParameter` | Free-text input |
|
|
70
|
+
| `BooleanParameter` | Checkbox toggle |
|
|
71
|
+
| `EnumParameter` | Dropdown from a Java enum |
|
|
72
|
+
| `EntityParameter` | Autocomplete for a database entity |
|
|
73
|
+
| `LocalDateParameter` | Date picker |
|
|
74
|
+
| `DateRangeParameter` | Date range selector |
|
|
75
|
+
| `CodeListParameter` | Selection from a code list |
|
|
76
|
+
| `FileParameter` | File upload (references Layer 2 blobs) |
|
|
77
|
+
| `IntParameter` | Integer input |
|
|
78
|
+
| `SelectStringParameter`| Dropdown from a fixed list of strings |
|
|
79
|
+
|
|
80
|
+
Parameters are built using a fluent API:
|
|
81
|
+
|
|
82
|
+
```java
|
|
83
|
+
private static final Parameter<String, StringParameter> NAME_PARAM =
|
|
84
|
+
new StringParameter("name", "$MyJob.name")
|
|
85
|
+
.withDescription("$MyJob.name.help")
|
|
86
|
+
.markRequired()
|
|
87
|
+
.build();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## SimpleBatchProcessJobFactory
|
|
91
|
+
|
|
92
|
+
The simplest way to create a batch job. Override `execute(ProcessContext)`:
|
|
93
|
+
|
|
94
|
+
```java
|
|
95
|
+
@Register
|
|
96
|
+
public class MyCleanupJob extends SimpleBatchProcessJobFactory {
|
|
97
|
+
|
|
98
|
+
@Override
|
|
99
|
+
protected String getLabel() { return "Cleanup old records"; }
|
|
100
|
+
|
|
101
|
+
@Override
|
|
102
|
+
protected void collectParameters(Consumer<Parameter<?, ?>> parameterCollector) {
|
|
103
|
+
parameterCollector.accept(MAX_AGE_PARAMETER);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@Override
|
|
107
|
+
protected void execute(ProcessContext process) throws Exception {
|
|
108
|
+
int maxAge = process.getParameter(MAX_AGE_PARAMETER).orElse(30);
|
|
109
|
+
// do work, use process.log() to report progress
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@Override
|
|
113
|
+
public String getCategory() {
|
|
114
|
+
return StandardCategories.SYSTEM_ADMINISTRATION;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Important:** Override `execute(ProcessContext)`, not `executeInBackground()`.
|
|
120
|
+
The process context is already set up for you.
|
|
121
|
+
|
|
122
|
+
## Categories
|
|
123
|
+
|
|
124
|
+
Group jobs in the UI using `StandardCategories`:
|
|
125
|
+
|
|
126
|
+
- `StandardCategories.MISC` — miscellaneous
|
|
127
|
+
- `StandardCategories.SYSTEM_ADMINISTRATION` — system admin tasks
|
|
128
|
+
- `StandardCategories.MONITORING` — monitoring jobs
|
|
129
|
+
- `StandardCategories.USERS_AND_TENANTS` — user/tenant management
|
|
130
|
+
|
|
131
|
+
Or define your own category string constant (prefix with `$` for NLS lookup).
|
|
132
|
+
|
|
133
|
+
## Common Mistakes
|
|
134
|
+
|
|
135
|
+
1. **Storing state in the factory** — `JobFactory` is a singleton. Never store
|
|
136
|
+
per-execution state in fields. Use `ProcessContext` or a `BatchJob` subclass.
|
|
137
|
+
|
|
138
|
+
2. **Wrong base class** — Using `SimpleBatchProcessJobFactory` for an import that
|
|
139
|
+
should extend `ImportBatchProcessFactory` loses file handling, progress tracking,
|
|
140
|
+
and error reporting that the more specific class provides.
|
|
141
|
+
|
|
142
|
+
3. **Missing `getCategory()`** — Jobs without a category are hard to find in the UI.
|
|
143
|
+
|
|
144
|
+
4. **Forgetting required permissions** — Override `getRequiredPermissions()` to
|
|
145
|
+
restrict who can run the job. Without it, any logged-in user can trigger it.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Processes
|
|
2
|
+
|
|
3
|
+
The process framework provides a way to track, log, and monitor long-running
|
|
4
|
+
background operations. Processes are stored in Elasticsearch and provide a
|
|
5
|
+
real-time UI for viewing progress, logs, and outputs.
|
|
6
|
+
|
|
7
|
+
## The Processes Service
|
|
8
|
+
|
|
9
|
+
`Processes` is the central service, registered with framework flag `biz.processes`:
|
|
10
|
+
|
|
11
|
+
```java
|
|
12
|
+
@Register(classes = Processes.class, framework = Processes.FRAMEWORK_PROCESSES)
|
|
13
|
+
public class Processes { ... }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Enable it in your configuration:
|
|
17
|
+
|
|
18
|
+
```hocon
|
|
19
|
+
sirius.frameworks {
|
|
20
|
+
biz.processes = true
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Two Types of Processes
|
|
25
|
+
|
|
26
|
+
### Normal Processes
|
|
27
|
+
|
|
28
|
+
Created for a finite task, run to completion, then terminate:
|
|
29
|
+
|
|
30
|
+
```java
|
|
31
|
+
@Part
|
|
32
|
+
private Processes processes;
|
|
33
|
+
|
|
34
|
+
public void runExport() {
|
|
35
|
+
String processId = processes.createProcess(
|
|
36
|
+
"export", // type (for filtering)
|
|
37
|
+
"Export Products", // title shown in UI
|
|
38
|
+
"fa-solid fa-download", // icon
|
|
39
|
+
UserContext.getCurrentUser(), // owning user
|
|
40
|
+
PersistencePeriod.THREE_MONTHS, // how long to keep
|
|
41
|
+
Map.of("format", "csv") // context data
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
processes.execute(processId, process -> {
|
|
45
|
+
// process is a ProcessContext
|
|
46
|
+
process.log(ProcessLog.info().withMessage("Starting export..."));
|
|
47
|
+
// do work
|
|
48
|
+
process.addTiming("products", watchElapsed);
|
|
49
|
+
process.forceUpdateState("Exported 500 products");
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- `execute(processId, consumer)` — runs the consumer and marks the process as
|
|
55
|
+
completed (or errored) when done.
|
|
56
|
+
- `partiallyExecute(processId, consumer)` — runs the consumer but leaves the
|
|
57
|
+
process open for further work (even from other nodes).
|
|
58
|
+
|
|
59
|
+
### Standby Processes
|
|
60
|
+
|
|
61
|
+
Long-lived processes for recurring background activity (e.g., a webhook receiver
|
|
62
|
+
that logs errors):
|
|
63
|
+
|
|
64
|
+
```java
|
|
65
|
+
processes.executeInStandbyProcess(
|
|
66
|
+
"webhook-receiver", // type
|
|
67
|
+
() -> "Webhook Receiver", // title supplier
|
|
68
|
+
"fa-solid fa-satellite-dish", // icon
|
|
69
|
+
() -> "Processing incoming webhooks...", // state supplier
|
|
70
|
+
process -> {
|
|
71
|
+
// This runs each time the standby process is invoked
|
|
72
|
+
process.log(ProcessLog.warn().withMessage("Received invalid payload"));
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Standby processes are created on demand and reused. The system periodically cleans
|
|
78
|
+
up old log entries to prevent unbounded growth.
|
|
79
|
+
|
|
80
|
+
## ProcessContext — The Client API
|
|
81
|
+
|
|
82
|
+
Inside `execute()` or `partiallyExecute()`, you interact with a `ProcessContext`:
|
|
83
|
+
|
|
84
|
+
| Method | Description |
|
|
85
|
+
|-----------------------------------|----------------------------------------------|
|
|
86
|
+
| `log(ProcessLog)` | Writes a log entry (info, warn, error) |
|
|
87
|
+
| `addTiming(counter, millis)` | Increments a named performance counter |
|
|
88
|
+
| `addDebugTiming(counter, millis)` | Counter visible only when debugging enabled |
|
|
89
|
+
| `incCounter(counter)` | Increment a counter by one |
|
|
90
|
+
| `forceUpdateState(text)` | Updates the process state text immediately |
|
|
91
|
+
| `updateTitle(text)` | Changes the process title |
|
|
92
|
+
| `addOutput(ProcessOutput)` | Adds a structured output (table, chart) |
|
|
93
|
+
| `getProcessId()` | Returns the process ID |
|
|
94
|
+
| `isActive()` | Returns false if the process was cancelled |
|
|
95
|
+
|
|
96
|
+
### Structured Outputs
|
|
97
|
+
|
|
98
|
+
Processes can produce tables and charts, not just log lines:
|
|
99
|
+
|
|
100
|
+
```java
|
|
101
|
+
TableOutput table = process.addTable("results", "Results", columns);
|
|
102
|
+
table.addRow("product-1", "Widget", "42");
|
|
103
|
+
|
|
104
|
+
ChartOutput chart = process.addChart("timeline", "Timeline");
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Process Entity
|
|
108
|
+
|
|
109
|
+
`Process` is an Elasticsearch entity gated by `@Framework(Processes.FRAMEWORK_PROCESSES)`.
|
|
110
|
+
It stores:
|
|
111
|
+
- Process metadata (type, title, icon, user, tenant)
|
|
112
|
+
- State (RUNNING, STANDBY, TERMINATED, CANCELED, ERRORED)
|
|
113
|
+
- Counters and timings
|
|
114
|
+
- Context map (arbitrary key-value pairs)
|
|
115
|
+
- Persistence period (how long to retain)
|
|
116
|
+
|
|
117
|
+
`ProcessLog` is a separate Elasticsearch entity for individual log entries,
|
|
118
|
+
also gated by `@Framework(Processes.FRAMEWORK_PROCESSES)`.
|
|
119
|
+
|
|
120
|
+
## Layered Cache Architecture
|
|
121
|
+
|
|
122
|
+
Because Elasticsearch has a ~1-second write-visibility delay, `Processes` uses a
|
|
123
|
+
two-level cache:
|
|
124
|
+
|
|
125
|
+
1. **First-level cache** — very short-lived, for direct modifications on the same node.
|
|
126
|
+
2. **Second-level cache** — coherent cache for reading "static" process data
|
|
127
|
+
(context, user, title) across nodes.
|
|
128
|
+
|
|
129
|
+
Standby processes have their own coherent cache since they are long-lived and few
|
|
130
|
+
in number.
|
|
131
|
+
|
|
132
|
+
## PersistencePeriod
|
|
133
|
+
|
|
134
|
+
Controls how long a completed process is retained before automatic cleanup:
|
|
135
|
+
|
|
136
|
+
- `PersistencePeriod.ONE_DAY`
|
|
137
|
+
- `PersistencePeriod.TWO_WEEKS`
|
|
138
|
+
- `PersistencePeriod.THREE_MONTHS`
|
|
139
|
+
- `PersistencePeriod.SIX_MONTHS`
|
|
140
|
+
- `PersistencePeriod.ONE_YEAR`
|
|
141
|
+
- `PersistencePeriod.FOREVER`
|
|
142
|
+
|
|
143
|
+
## Common Mistakes
|
|
144
|
+
|
|
145
|
+
1. **Using `execute()` when `partiallyExecute()` is needed** — If multiple nodes
|
|
146
|
+
or tasks contribute to one process, `execute()` will prematurely mark it as done.
|
|
147
|
+
|
|
148
|
+
2. **Logging too much** — Every `ProcessLog` is an Elasticsearch document.
|
|
149
|
+
Use `addTiming()` for high-frequency counters instead of logging each item.
|
|
150
|
+
|
|
151
|
+
3. **Ignoring `isActive()`** — Always check `process.isActive()` in long loops.
|
|
152
|
+
If the user cancels, you should stop promptly.
|
|
153
|
+
|
|
154
|
+
4. **Forgetting the framework flag** — Without `biz.processes = true` in the
|
|
155
|
+
config, the `Processes` service will not load and `@Part Processes` will be null.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Storage
|
|
2
|
+
|
|
3
|
+
The storage module provides a three-layer architecture for managing files and
|
|
4
|
+
binary content. Each layer serves a different purpose and can be configured
|
|
5
|
+
independently.
|
|
6
|
+
|
|
7
|
+
## Layer 1 — Physical Storage
|
|
8
|
+
|
|
9
|
+
Layer 1 handles the raw byte storage. It abstracts over different storage backends:
|
|
10
|
+
|
|
11
|
+
- **`FSObjectStorageSpace`** — local filesystem storage (development, small deployments)
|
|
12
|
+
- **`S3ObjectStorageSpace`** — S3-compatible object storage (production)
|
|
13
|
+
|
|
14
|
+
Configuration is done per "space" in HOCON:
|
|
15
|
+
|
|
16
|
+
```hocon
|
|
17
|
+
storage.layer1 {
|
|
18
|
+
spaces {
|
|
19
|
+
default {
|
|
20
|
+
engine = "fs"
|
|
21
|
+
basePath = "/data/storage"
|
|
22
|
+
}
|
|
23
|
+
documents {
|
|
24
|
+
engine = "s3"
|
|
25
|
+
bucketName = "my-bucket"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Key classes:
|
|
32
|
+
- `ObjectStorage` — service for accessing storage spaces
|
|
33
|
+
- `ObjectStorageSpace` — a configured storage area
|
|
34
|
+
- `FileHandle` — represents a downloaded file with metadata
|
|
35
|
+
|
|
36
|
+
## Layer 2 — Metadata and Blobs
|
|
37
|
+
|
|
38
|
+
Layer 2 adds metadata, versioning, and variant management on top of Layer 1.
|
|
39
|
+
This is the primary API for working with files in application code.
|
|
40
|
+
|
|
41
|
+
### Core Concepts
|
|
42
|
+
|
|
43
|
+
- **`BlobStorageSpace`** — a logical container for blobs (configured per space)
|
|
44
|
+
- **`Blob`** — a file with metadata (name, size, content type, tenant, etc.)
|
|
45
|
+
- **`BlobContainer`** — groups related blobs (e.g., attachments for an entity)
|
|
46
|
+
- **`Directory`** — optional directory structure within a space
|
|
47
|
+
|
|
48
|
+
### JDBC vs MongoDB
|
|
49
|
+
|
|
50
|
+
Layer 2 metadata can be stored in either database:
|
|
51
|
+
|
|
52
|
+
| Component | JDBC | MongoDB |
|
|
53
|
+
|-----------------|-------------------------|--------------------------|
|
|
54
|
+
| Storage class | `SQLBlobStorage` | `MongoBlobStorage` |
|
|
55
|
+
| Blob entity | `SQLBlob` | `MongoBlob` |
|
|
56
|
+
| Directory | `SQLDirectory` | `MongoDirectory` |
|
|
57
|
+
| Framework flag | `biz.storage-blob-jdbc` | `biz.storage-blob-mongo` |
|
|
58
|
+
|
|
59
|
+
### Referencing Blobs from Entities
|
|
60
|
+
|
|
61
|
+
Use `BlobHardRef` or `BlobSoftRef` in entity fields:
|
|
62
|
+
|
|
63
|
+
```java
|
|
64
|
+
// Hard ref — blob is deleted when the entity is deleted
|
|
65
|
+
private final BlobHardRef logo =
|
|
66
|
+
new BlobHardRef(this, "logo", "tenant-logos");
|
|
67
|
+
|
|
68
|
+
// Soft ref — blob survives entity deletion
|
|
69
|
+
private final BlobSoftRef attachment =
|
|
70
|
+
new BlobSoftRef(this, "attachment", "documents");
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Working with Blobs
|
|
74
|
+
|
|
75
|
+
```java
|
|
76
|
+
@Part
|
|
77
|
+
private BlobStorage blobStorage;
|
|
78
|
+
|
|
79
|
+
// Upload
|
|
80
|
+
BlobStorageSpace space = blobStorage.getSpace("documents");
|
|
81
|
+
Blob blob = space.createBlob("report.pdf", "application/pdf", tenantId);
|
|
82
|
+
try (OutputStream out = blob.createOutputStream()) {
|
|
83
|
+
// write content
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Download
|
|
87
|
+
try (InputStream in = blob.createInputStream()) {
|
|
88
|
+
// read content
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Layer 3 — Virtual File System (VFS)
|
|
93
|
+
|
|
94
|
+
Layer 3 provides a unified directory/file tree view over Layer 2 spaces and other
|
|
95
|
+
sources. It powers the file manager UI and FTP/WebDAV access.
|
|
96
|
+
|
|
97
|
+
### VirtualFile
|
|
98
|
+
|
|
99
|
+
`VirtualFile` is the abstraction for both files and directories:
|
|
100
|
+
|
|
101
|
+
```java
|
|
102
|
+
@Part
|
|
103
|
+
private VirtualFileSystem vfs;
|
|
104
|
+
|
|
105
|
+
VirtualFile root = vfs.root();
|
|
106
|
+
VirtualFile docs = root.resolve("documents");
|
|
107
|
+
for (VirtualFile child : docs.children()) {
|
|
108
|
+
if (child.isDirectory()) {
|
|
109
|
+
// traverse
|
|
110
|
+
} else {
|
|
111
|
+
// access file content via child.createInputStream()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### VFS Roots
|
|
117
|
+
|
|
118
|
+
The VFS tree is assembled from `VFSRoot` implementations. Each root contributes
|
|
119
|
+
a subtree (e.g., one root for tenant documents, another for shared templates).
|
|
120
|
+
Custom roots are registered via `@Register(classes = VFSRoot.class)`.
|
|
121
|
+
|
|
122
|
+
## Framework Flags
|
|
123
|
+
|
|
124
|
+
Storage requires multiple framework flags depending on which layers and backends
|
|
125
|
+
you use:
|
|
126
|
+
|
|
127
|
+
```hocon
|
|
128
|
+
sirius.frameworks {
|
|
129
|
+
biz.storage = true # Core storage (required)
|
|
130
|
+
biz.storage-blob-jdbc = true # Layer 2 metadata in JDBC
|
|
131
|
+
biz.storage-blob-mongo = false # Layer 2 metadata in MongoDB
|
|
132
|
+
biz.storage-replication-jdbc = false # Replication tasks in JDBC
|
|
133
|
+
biz.storage-replication-mongo = false # Replication tasks in MongoDB
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Common Mistakes
|
|
138
|
+
|
|
139
|
+
1. **Wrong ref type** — Using `BlobHardRef` when the blob should outlive the entity
|
|
140
|
+
(e.g., shared documents). Use `BlobSoftRef` if the blob has independent lifecycle.
|
|
141
|
+
|
|
142
|
+
2. **Missing framework flags** — `biz.storage` alone is not enough. You also need
|
|
143
|
+
`biz.storage-blob-jdbc` or `biz.storage-blob-mongo` for Layer 2 to work.
|
|
144
|
+
|
|
145
|
+
3. **Not closing streams** — Always close `InputStream` and `OutputStream` from blob
|
|
146
|
+
operations. Unclosed streams leak file handles and may corrupt uploads.
|
|
147
|
+
|
|
148
|
+
4. **Ignoring variants** — Layer 2 supports automatic variant generation (e.g.,
|
|
149
|
+
thumbnails). Configure variants per space rather than generating them manually.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Tenants
|
|
2
|
+
|
|
3
|
+
The tenants module provides multi-tenant user management. Every piece of business
|
|
4
|
+
data belongs to a tenant, and every user belongs to a tenant. The framework enforces
|
|
5
|
+
tenant isolation automatically.
|
|
6
|
+
|
|
7
|
+
## Core Interfaces
|
|
8
|
+
|
|
9
|
+
### Tenant<I>
|
|
10
|
+
|
|
11
|
+
The database-independent interface for a tenant (company/organization):
|
|
12
|
+
|
|
13
|
+
```java
|
|
14
|
+
public interface Tenant<I extends Serializable>
|
|
15
|
+
extends Entity, Transformable, Traced, Journaled, RateLimitedEntity, PerformanceFlagged {
|
|
16
|
+
|
|
17
|
+
Mapping TENANT_DATA = Mapping.named("tenantData");
|
|
18
|
+
TenantData getTenantData();
|
|
19
|
+
boolean hasPermission(String permission);
|
|
20
|
+
Set<String> getPermissions();
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### UserAccount<I, T>
|
|
25
|
+
|
|
26
|
+
The database-independent interface for a user account within a tenant:
|
|
27
|
+
|
|
28
|
+
```java
|
|
29
|
+
public interface UserAccount<I extends Serializable, T extends Tenant<I>>
|
|
30
|
+
extends Entity, Transformable, Traced, Journaled {
|
|
31
|
+
|
|
32
|
+
Mapping USER_ACCOUNT_DATA = Mapping.named("userAccountData");
|
|
33
|
+
UserAccountData getUserAccountData();
|
|
34
|
+
BaseEntityRef<I, T> getTenant();
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## TenantData and UserAccountData
|
|
39
|
+
|
|
40
|
+
All tenant fields live in the `TenantData` composite, shared between SQL and Mongo
|
|
41
|
+
implementations. Similarly, user fields live in `UserAccountData`. This avoids
|
|
42
|
+
field duplication across implementations.
|
|
43
|
+
|
|
44
|
+
Key fields in `TenantData`:
|
|
45
|
+
- `name` — display name of the tenant
|
|
46
|
+
- `accountNumber` — unique business identifier
|
|
47
|
+
- `address` — embedded `InternationalAddressData` composite
|
|
48
|
+
- `packageData` — permissions/features assigned to this tenant
|
|
49
|
+
|
|
50
|
+
Key fields in `UserAccountData`:
|
|
51
|
+
- `person` — embedded `PersonData` composite (name, salutation, email)
|
|
52
|
+
- `login` — embedded `LoginData` composite (username, password hash)
|
|
53
|
+
- `permissions` — assigned permissions/roles
|
|
54
|
+
|
|
55
|
+
## JDBC and MongoDB Implementations
|
|
56
|
+
|
|
57
|
+
| Interface | JDBC Implementation | Mongo Implementation |
|
|
58
|
+
|------------------|------------------------|--------------------------|
|
|
59
|
+
| `Tenant<I>` | `SQLTenant` | `MongoTenant` |
|
|
60
|
+
| `UserAccount` | `SQLUserAccount` | `MongoUserAccount` |
|
|
61
|
+
|
|
62
|
+
These follow the entity triple pattern (see entity-triple resource). Both are
|
|
63
|
+
gated behind framework flags:
|
|
64
|
+
- `biz.tenants-jdbc` for SQL implementations
|
|
65
|
+
- `biz.tenants-mongo` for MongoDB implementations
|
|
66
|
+
|
|
67
|
+
## Tenant-Aware Entities
|
|
68
|
+
|
|
69
|
+
Entities that belong to a tenant extend `SQLTenantAware` or `MongoTenantAware`:
|
|
70
|
+
|
|
71
|
+
```java
|
|
72
|
+
public class SQLProduct extends SQLTenantAware {
|
|
73
|
+
// Automatically gets a tenant reference field
|
|
74
|
+
// Tenant is auto-filled on create via fillWithCurrentTenant()
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`SQLTenantAware` extends `BizEntity` and adds:
|
|
79
|
+
- A `tenant` field (`SQLEntityRef<SQLTenant>`) — write-once, reject-on-delete
|
|
80
|
+
- `fillWithCurrentTenant()` — fills the tenant from the current session
|
|
81
|
+
- `assertSameTenant()` — validates that a referenced entity belongs to the same tenant
|
|
82
|
+
|
|
83
|
+
The Mongo equivalent (`MongoTenantAware`) works identically with `MongoRef`.
|
|
84
|
+
|
|
85
|
+
## Tenants Service
|
|
86
|
+
|
|
87
|
+
The `Tenants<I, T, U>` service provides tenant operations:
|
|
88
|
+
|
|
89
|
+
```java
|
|
90
|
+
@Part
|
|
91
|
+
private Tenants<?, ?, ?> tenants;
|
|
92
|
+
|
|
93
|
+
// Get current tenant
|
|
94
|
+
Tenant<?> currentTenant = tenants.getRequiredTenant();
|
|
95
|
+
|
|
96
|
+
// Check if current user is in the system tenant
|
|
97
|
+
boolean isSystem = tenants.isCurrentTenantSystemTenant();
|
|
98
|
+
|
|
99
|
+
// Filter a query to the current tenant
|
|
100
|
+
SmartQuery<SQLProduct> query = tenants.forCurrentTenant(oma.select(SQLProduct.class));
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Concrete implementations: `SQLTenants` (JDBC) and `MongoTenants` (MongoDB).
|
|
104
|
+
|
|
105
|
+
## TenantUserManager
|
|
106
|
+
|
|
107
|
+
`TenantUserManager` is the `UserManager` implementation for the tenants module.
|
|
108
|
+
It bridges the web security framework with the tenant/user database:
|
|
109
|
+
|
|
110
|
+
- Authenticates users via login credentials
|
|
111
|
+
- Resolves permissions from user roles + tenant package
|
|
112
|
+
- Provides session management
|
|
113
|
+
- Supports "spy" mode (system tenant admins can act as another tenant)
|
|
114
|
+
|
|
115
|
+
Key permissions:
|
|
116
|
+
- `TenantUserManager.PERMISSION_SYSTEM_TENANT_MEMBER` — user belongs to the system tenant
|
|
117
|
+
- `TenantUserManager.PERMISSION_SYSTEM_ADMINISTRATOR` — user is a system admin
|
|
118
|
+
- `Tenant.PERMISSION_SYSTEM_TENANT` — flag on the tenant entity itself
|
|
119
|
+
|
|
120
|
+
## Permission Model
|
|
121
|
+
|
|
122
|
+
Permissions flow from multiple sources:
|
|
123
|
+
|
|
124
|
+
1. **Tenant package** — `PackageData` on the tenant defines which features are enabled
|
|
125
|
+
2. **User roles** — roles assigned to the user, each granting a set of permissions
|
|
126
|
+
3. **Role mapping** — configured in HOCON, maps role names to permission sets
|
|
127
|
+
4. **System tenant flag** — system tenant users get additional administrative permissions
|
|
128
|
+
|
|
129
|
+
```hocon
|
|
130
|
+
security.tenantPermissions {
|
|
131
|
+
admin = ["permission-manage-users", "permission-manage-products"]
|
|
132
|
+
viewer = ["permission-view-products"]
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## System Tenant
|
|
137
|
+
|
|
138
|
+
One tenant is designated as the "system tenant" (configured by ID). Users in the
|
|
139
|
+
system tenant can:
|
|
140
|
+
- Manage all other tenants
|
|
141
|
+
- "Spy" on other tenants (act as if they belong to another tenant)
|
|
142
|
+
- Access system administration features
|
|
143
|
+
|
|
144
|
+
The system tenant flag is checked via `Tenant.hasPermission(Tenant.PERMISSION_SYSTEM_TENANT)`.
|
|
145
|
+
|
|
146
|
+
## Common Mistakes
|
|
147
|
+
|
|
148
|
+
1. **Not using `findForTenant()`** — Using `find()` in controllers allows access
|
|
149
|
+
to entities from other tenants. Always use `findForTenant()` for tenant-scoped data.
|
|
150
|
+
|
|
151
|
+
2. **Forgetting to enable the framework** — Without `biz.tenants-jdbc = true` or
|
|
152
|
+
`biz.tenants-mongo = true`, tenant entities are not registered.
|
|
153
|
+
|
|
154
|
+
3. **Manual tenant assignment** — Do not set the tenant reference manually.
|
|
155
|
+
`SQLTenantAware.fillWithCurrentTenant()` is called automatically during entity
|
|
156
|
+
creation. Manual assignment can cause tenant mismatch errors.
|
|
157
|
+
|
|
158
|
+
4. **Confusing tenant permissions with user permissions** — `Tenant.hasPermission()`
|
|
159
|
+
checks tenant-level features. User permissions are checked via `UserInfo.hasPermission()`.
|