Skip to content

Commit d8335d2

Browse files
author
Siziy Ivan
committed
Merge remote-tracking branch 'origin/history-actions' into next
2 parents 2704f54 + 57d6a60 commit d8335d2

87 files changed

Lines changed: 2560 additions & 776 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
dist/*
1+
dist
22
node_modules
33
.idea
44
TODO.md

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"typescript.tsdk": "node_modules\\typescript\\lib",
3+
}

docs/HistoryActions.md

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# History System Documentation
2+
3+
## Overview
4+
5+
The History system in Adminizer provides a flexible and extensible way to track changes to model instances, store historical records, and expose them securely to users through an API. It supports multiple adapters, access control, model-specific configurations, and smart data formatting.
6+
7+
The core idea is to **capture state snapshots** of any model change (create, update, delete), assign metadata (user, timestamp, model name, ID), and allow retrieval filtered by user permissions, time range, or model type.
8+
9+
---
10+
11+
## Key Components
12+
13+
### 1. `HistoryHandler`
14+
15+
- Central registry for managing one or more history adapters.
16+
- Aggregates functionality from registered adapters.
17+
- Ensures proper routing of requests based on adapter availability.
18+
19+
### 2. `AbstractHistoryAdapter`
20+
21+
An abstract base class defining the contract for all history adapters. It includes:
22+
- Access rights management (`history-${id}`, `users-history-${id}`).
23+
- Built-in filtering based on user permissions.
24+
- Model access checks (via `getModels()`).
25+
- Data enhancement (e.g., `displayName` resolution).
26+
- Media manager and association field handling.
27+
- Protection against internal/admin models via `excludedModels`.
28+
29+
---
30+
31+
## Default Adapter: `DefaultHistoryAdapter`
32+
33+
This built-in adapter uses the `HistoryActionsAP` model to persist history records in the database.
34+
35+
### How It Works
36+
37+
1. **Storage**
38+
On every tracked change:
39+
- Existing current record (`isCurrent: true`) for the same `(modelName, modelId)` is marked as outdated.
40+
- A new record is created with `isCurrent: true`, containing:
41+
- `modelName`, `modelId`
42+
- `user` (ID or login)
43+
- `action` (e.g., "update")
44+
- `data` — full snapshot of the model's fields at that time
45+
- Timestamps (`createdAt`, etc.)
46+
47+
2. **Retrieval**
48+
- Filters results based on:
49+
- User's read access to the model.
50+
- Whether the user has `users-history-default` permission (to view others’ actions).
51+
- Optional filters: `modelName`, `forUserName`, `from`, `to`, pagination.
52+
53+
3. **Data Formatting**
54+
- Resolves `displayName` using model config:
55+
```ts
56+
displayName: string | ((record: any) => string)
57+
```
58+
If not definedfalls back to `modelId`.
59+
60+
4. **Security**
61+
- Respects RBAC (Role-Based Access Control).
62+
- Internal models (like `UserAP`, `MediaManagerAP`, etc.) are excluded by default.
63+
- Configurable extra exclusions via `config.history.excludeModels`.
64+
65+
---
66+
67+
## Configuration (Type Definition)
68+
69+
```ts
70+
interface HistoryConfig {
71+
enabled?: boolean;
72+
adapter?: string; // 'default' | custom adapter ID
73+
excludeModels?: string[]; // additional models to exclude from tracking
74+
}
75+
```
76+
77+
**Example:**
78+
79+
```ts
80+
config: {
81+
history: {
82+
enabled: true,
83+
adapter: "default", // optional, default if not specified
84+
excludeModels: ["post", "category"]
85+
}
86+
}
87+
```
88+
If `enabled: false` or not set, no history will be recorded or served.
89+
90+
## Using a Custom Adapter
91+
You can replace or extend the default behavior by implementing your own adapter.
92+
93+
**Step 1:** Implement `AbstractHistoryAdapter`
94+
95+
```ts
96+
import { AbstractHistoryAdapter } from '../lib/history-actions/AbstractHistoryAdapter';
97+
import { HistoryActionsAP, UserAP } from '../models';
98+
99+
export class MyCustomHistoryAdapter extends AbstractHistoryAdapter {
100+
public id = 'myadapter'; // must be unique
101+
public model = 'custom_history'; // optional, depends on your storage
102+
103+
constructor(adminizer) {
104+
super(adminizer);
105+
// Your initialization
106+
}
107+
108+
async getAllHistory(...) { ... }
109+
async getAllModelHistory(...) { ... }
110+
async setHistory(...) { ... }
111+
async getModelFieldsHistory(...) { ... }
112+
}
113+
```
114+
115+
You must implement all abstract methods.
116+
117+
**Step 2:** Register Your Adapter
118+
119+
```ts
120+
adminizer.historyHandler = new HistoryHandler();
121+
adminizer.historyHandler.add(new MyCustomHistoryAdapter(adminizer));
122+
```
123+
Use Cases for Custom Adapters
124+
125+
Logging to external systems (e.g., Kafka, ELK).
126+
Immutable storage (e.g., blockchain-like ledger).
127+
Lightweight logging without full snapshots.
128+
Different DB (e.g., MongoDB, Redis for recent activity).
129+
130+
## Access Control Tokens
131+
132+
Each adapter registers its own permissions:
133+
134+
| Token | Purpose |
135+
|-------|---------|
136+
| `history-${id}` | General access to view history |
137+
| `users-history-${id}` | View history of any user (otherwise only own) |
138+
139+
These are auto-registered with the Adminizer access rights system.
140+
141+
## Summary Flow
142+
143+
| Step | Description |
144+
|------|-------------|
145+
| **1** | **Model Update** |
146+
| **2** | Adminizer calls `.setHistory(data)` |
147+
| **3** | Adapter saves snapshot with: `user`, `model`, `id`, `data`, `isCurrent = true` |
148+
| **4** | Older records for same `(model, id)``isCurrent = false` |
149+
| **5** | **On GET historyfilter by:**<br>• User permissions<br>Model access<br>Time range<br>User scope (own vs all) |
150+
| **6** | **Format output:**<br>• Add `displayName`<br>Resolve media/associations |
151+
| **7** | **Return to frontend** |
152+
153+
## Notes
154+
155+
The system is opt-out: all models are tracked unless listed in excludeModels.
156+
Future improvements may include diff-only storage and compression.

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
* [Localization](Configuration/Localization.md)
1717
* [Icons](Icons.md)
1818
* [Notifications](Notifications.md)
19+
* [History-Actions](HistoryActions.md)
1920
## 3. Frontend Integration
2021

2122
* [Inertia Adapter & Flash](InertiaAdapter.md)

fixture/adminizerConfig.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const models: AdminpanelConfig["models"] = {
66
Test: {
77
title: 'Test model',
88
model: 'test',
9+
displayName: 'title',
910
userAccessRelation: 'owner',
1011
fields: {
1112
createdAt: false,
@@ -44,6 +45,7 @@ const models: AdminpanelConfig["models"] = {
4445
Example: {
4546
title: 'All controls',
4647
model: 'example',
48+
displayName: 'description',
4749
userAccessRelation: 'owner',
4850
tools: [
4951
{
@@ -230,14 +232,14 @@ const models: AdminpanelConfig["models"] = {
230232
displayModifier: function (data) {
231233
return data?.title;
232234
},
233-
disabled: true
235+
disabled: false
234236
},
235237
tests: {
236238
title: 'One to many association',
237239
displayModifier: function (data) {
238240
return data?.title;
239241
},
240-
disabled: true
242+
disabled: false
241243
},
242244
},
243245
list: {
@@ -388,11 +390,16 @@ const models: AdminpanelConfig["models"] = {
388390
icon: 'pets'
389391
},
390392
Category: {
391-
title: 'Category',
393+
title: 'Категории',
392394
model: 'category',
393395
icon: 'category',
396+
displayName: (data: any) => {
397+
return data?.slug ?? 'no data'
398+
},
394399
fields: {
395-
createdAt: false,
400+
createdAt: {
401+
title: 'Created at',
402+
},
396403
updatedAt: false,
397404
mediamanager_one: {
398405
title: 'Images 1',
@@ -434,7 +441,7 @@ const models: AdminpanelConfig["models"] = {
434441
add: true,
435442
edit: true,
436443
view: true,
437-
remove: false
444+
remove: true
438445
},
439446
TestCatalog: {
440447
title: '',
@@ -471,13 +478,19 @@ const config: AdminpanelConfig = {
471478
enableGeneral: true,
472479
initTab: 'general',
473480
},
474-
cors: {
481+
history: {
475482
enabled: true,
483+
adapter: "default",
484+
excludeModels: ["TestCatalog"]
485+
},
486+
cors: {
487+
enabled: false,
476488
origin: 'http://localhost:3000',
477489
path: 'api/*'
478490
},
479491
aiAssistant: {
480-
enabled: (process.env.ENABLE_AI_ASSISTANT ?? "true") === 'true',
492+
// enabled: (process.env.ENABLE_AI_ASSISTANT ?? "true") === 'true',
493+
enabled: false,
481494
defaultModel: 'openai-data',
482495
models: ['openai-data', 'dummy'],
483496
},
@@ -705,13 +718,6 @@ const config: AdminpanelConfig = {
705718
link: `${routePrefix}/catalog/test-catalog`,
706719
title: 'Test Catalog',
707720
icon: 'bug_report'
708-
},
709-
{
710-
id: '6',
711-
link: '/',
712-
type: 'blank',
713-
title: 'Main Site',
714-
icon: 'home'
715721
}
716722
]
717723
},

0 commit comments

Comments
 (0)