Skip to content

Commit bf29503

Browse files
authored
Merge pull request #565 from devforth/next
Next
2 parents 8ffbf0a + 8e917b0 commit bf29503

49 files changed

Lines changed: 3248 additions & 445 deletions

Some content is hidden

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

AGENTS.md

Lines changed: 212 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,212 @@
1-
Follow SOLID principles
2-
Use DRY and avoid duplication
3-
Keep the code KISS unless complexity is required
4-
Avoid unnecessary abstractions.
5-
Cover edge cases and clear error handling
1+
2+
# AGENTS.md
3+
4+
## General engineering rules
5+
6+
Write code as if the system contracts are already defined and trusted.
7+
8+
Do not add defensive checks “just in case” when the contract, type, schema, backend response, or controlled internal API already guarantees the shape of the data.
9+
10+
Prefer simplicity, clarity, and consistency over paranoia.
11+
12+
Follow these principles strictly:
13+
14+
- DRY
15+
- SOLID
16+
- YAGNI
17+
- Keep code minimal
18+
- Trust typed contracts
19+
- Avoid duplicate validation
20+
- Avoid speculative fallback logic
21+
- Avoid inline regex literals when reused or non-trivial
22+
23+
---
24+
25+
## Trust the contract
26+
27+
If backend and frontend are part of the same system, and the backend explicitly guarantees a response shape, do **not** re-validate every field on the frontend.
28+
29+
Bad:
30+
- backend returns a strict object
31+
- frontend then checks every field for `undefined`, wrong type, empty string, wrong enum, etc.
32+
- frontend adds fallback branches for impossible states
33+
34+
Good:
35+
- backend owns validation
36+
- shared types or schemas define the contract
37+
- frontend consumes the contract directly
38+
- frontend only handles real UI states, not imaginary protocol corruption
39+
40+
Do not treat trusted internal responses as untrusted external input.
41+
42+
Only add extra runtime validation when:
43+
- data comes from a truly external/untrusted source
44+
- the contract is unknown or unstable
45+
- there is an explicit requirement for hardening
46+
- security boundaries require validation
47+
- corrupted legacy data is known to exist in production
48+
49+
If none of the above is true, do not add redundant guards.
50+
51+
---
52+
53+
## No duplicate validation
54+
55+
Validation must live in one clear place.
56+
57+
Prefer this order:
58+
1. Boundary validation
59+
2. Domain validation
60+
3. UI rendering without re-checking the same invariants
61+
62+
Examples:
63+
- Validate request payload on the backend boundary
64+
- Validate forms at form/input level
65+
- Validate DB input before persistence if needed
66+
- Do not re-check the same rule again deeper in the stack unless there is a real new boundary
67+
68+
If a field is already guaranteed by type/schema/backend, do not validate it again in consumers.
69+
70+
---
71+
72+
## Frontend rules
73+
74+
When rendering data from a trusted backend:
75+
- do not check every property manually
76+
- do not write `if (!obj || !obj.a || !obj.b || !obj.c)` chains for guaranteed objects
77+
- do not silently swallow impossible states
78+
- do not invent fallback values for required fields unless product requirements explicitly ask for fallback UI
79+
80+
Prefer:
81+
- strong TypeScript types
82+
- narrow, explicit UI states
83+
- a small number of meaningful guards at actual boundaries
84+
85+
Allowed:
86+
- loading state
87+
- not-found state
88+
- permission-denied state
89+
- explicitly documented optional fields
90+
91+
Not allowed:
92+
- repetitive property-by-property paranoia checks for required response fields
93+
94+
---
95+
96+
## Backend rules
97+
98+
Backend should enforce the contract once, clearly and centrally.
99+
100+
- Validate incoming external input at the boundary
101+
- Normalize data once
102+
- Return stable, typed responses
103+
- Do not scatter the same checks across controllers, services, and callers
104+
- Do not add branches for impossible states without evidence
105+
106+
When a response format is defined, keep it strict and predictable so consumers do not need defensive coding.
107+
108+
---
109+
110+
## Regex rules
111+
112+
Do not inline non-trivial regexes directly inside business logic.
113+
114+
Bad:
115+
- inline regex literals scattered through the codebase
116+
- repeating the same regex multiple times
117+
- unreadable regex embedded inside conditions
118+
119+
Good:
120+
- define regex once as a named constant
121+
- place reusable regexes in a dedicated constants/module file
122+
- give regexes semantic names
123+
- compile once when appropriate
124+
125+
Example:
126+
- `const EMAIL_RE = /.../`
127+
- `const SLUG_RE = /.../`
128+
- `const STRIP_HTML_RE = /.../g`
129+
130+
If regex is used more than once or is not instantly obvious, extract it.
131+
132+
---
133+
134+
## No speculative coding
135+
136+
Do not add code for hypothetical scenarios unless they are documented requirements or known production realities.
137+
138+
Avoid:
139+
- “in case backend changes”
140+
- “in case this field comes null someday”
141+
- “in case this enum gets other values”
142+
- “in case this internal method returns something unexpected”
143+
144+
If such a risk is real, document it and solve it at the right boundary, not everywhere.
145+
146+
---
147+
148+
## Error handling
149+
150+
Error handling must be intentional, not reflexive.
151+
152+
Handle:
153+
- real network failures
154+
- documented optionality
155+
- known domain errors
156+
- permission/auth errors
157+
- user-caused invalid input
158+
- external system failures
159+
160+
Do not handle:
161+
- impossible states already excluded by the contract
162+
- contradictions to compile-time types without evidence
163+
- redundant field-level checks for trusted internal data
164+
165+
For impossible states, prefer failing loudly in development rather than hiding the issue with fallback logic.
166+
167+
---
168+
169+
## Code review rules for agents
170+
171+
Before writing extra guards, ask:
172+
173+
1. Is this input external and untrusted?
174+
2. Is this invariant already guaranteed by types/schema/backend?
175+
3. Am I repeating validation that already exists elsewhere?
176+
4. Is this a real production case or just imagination?
177+
5. Would this code make the system clearer, or only noisier?
178+
179+
If the invariant is already guaranteed, do not add the guard.
180+
181+
---
182+
183+
## Preferred style
184+
185+
Prefer:
186+
- shared types
187+
- schema-driven boundaries
188+
- single-source-of-truth validation
189+
- concise functions
190+
- explicit contracts
191+
- named helpers/constants
192+
- reusable compiled regex constants
193+
194+
Avoid:
195+
- defensive clutter
196+
- repeated null/undefined chains for required fields
197+
- duplicated business rules
198+
- inline complicated regex
199+
- fallback-on-fallback logic
200+
- broad “safety” code without a concrete reason
201+
202+
---
203+
204+
## Decision rule
205+
206+
When choosing between:
207+
- trusting a well-defined internal contract
208+
- or adding more defensive checks
209+
210+
choose trusting the contract, unless there is a clear boundary, documented risk, or real evidence that extra validation is needed.
211+
212+
Less code, fewer duplicate checks, clearer ownership.

adminforth/basePlugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export default class AdminForthPlugin implements IAdminForthPlugin {
4747

4848
const seed = `af_pl_${this.constructor.name}_${resourceConfig.resourceId}_${uniqueness}`;
4949
this.pluginInstanceId = md5hash(seed);
50-
afLogger.trace(`🪲 AdminForthPlugin.modifyResourceConfig, ${seed}, 'id', ${this.pluginInstanceId}`);
50+
afLogger.trace({seed, pluginInstanceId: this.pluginInstanceId}, `🪲 AdminForthPlugin.modifyResourceConfig`);
5151
this.adminforth = adminforth;
5252
}
5353

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
1-
import { Express, Request, Response } from "express";
2-
import { IAdminForth } from "adminforth";
1+
import { Express, Response } from "express";
2+
import { IAdminForth, IAdminUserExpressRequest } from "adminforth";
3+
import * as z from "zod";
4+
35
export function initApi(app: Express, admin: IAdminForth) {
46
app.get(`${admin.config.baseUrl}/api/hello/`,
57

6-
// you can use data API to work with your database https://adminforth.dev/docs/tutorial/Customization/dataApi/
7-
async (req: Request, res: Response) => {
8-
// req.adminUser to get info about the admin users
9-
const allUsers = await admin.resource("adminuser").list([]);
10-
res.json({
11-
message: "List of admin users from AdminForth API",
12-
users: allUsers,
13-
});
14-
},
8+
admin.express.withSchema(
9+
{
10+
description: "Returns example data from a custom Express API together with the current authenticated AdminForth user.",
11+
response: z.object({
12+
message: z.string(),
13+
users: z.array(z.record(z.string(), z.unknown())),
14+
adminUser: z.record(z.string(), z.unknown()),
15+
}),
16+
},
1517

16-
// you can use admin.express.authorize to get info about the current user
17-
admin.express.authorize(
18-
async (req: Request, res: Response) => {
19-
res.json({ message: "Current adminuser from AdminForth API", adminUser: req.adminUser });
20-
}
18+
// you can use data API to work with your database https://adminforth.dev/docs/tutorial/Customization/dataApi/
19+
// and admin.express.authorize to inject req.adminUser
20+
admin.express.authorize(
21+
async (req: IAdminUserExpressRequest, res: Response) => {
22+
const allUsers = await admin.resource("adminuser").list([]);
23+
res.json({
24+
message: "Hello from AdminForth API!",
25+
users: allUsers,
26+
adminUser: req.adminUser,
27+
});
28+
}
29+
)
2130
)
2231
);
2332
}

adminforth/commands/createApp/templates/package.json.hbs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
"dependencies": {
3636
"@dotenvx/dotenvx": "^1.34.0",
3737
"adminforth": "{{adminforthVersion}}",
38-
"express": "latest-4"
38+
"express": "latest-4",
39+
"zod": "^4.3.6"
3940
},
4041
"devDependencies": {
4142
"typescript": "5.4.5",

adminforth/documentation/blog/2025-04-10-how-to-translate-dynamic-strings/index.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,17 @@ export default {
4545
You might have this page and return it in your API for nuxt:
4646

4747
```ts
48+
import * as z from "zod";
49+
4850
app.get(`${admin.config.baseUrl}/api/get_page`,
51+
admin.express.withSchema(
52+
{
53+
description: 'Returns translated SEO metadata for the page specified by the pageUrl query parameter.',
54+
response: z.object({
55+
meta_title: z.string(),
56+
meta_desc: z.string(),
57+
}),
58+
},
4959
async (req:any, res: Response): Promise<void> => {
5060
const pageUrl = req.query.pageUrl;
5161
if (!pageUrl) {
@@ -62,18 +72,29 @@ app.get(`${admin.config.baseUrl}/api/get_page`,
6272
meta_desc: page.meta_desc,
6373
});
6474
}
65-
)
75+
)
6676
);
6777
```
6878

79+
Install and import Zod before using this pattern: `pnpm add zod` or `npm install zod`, then `import * as z from 'zod';`. `admin.express.withSchema(...)` will convert the Zod schema to OpenAPI for you.
80+
6981
Now you want to translate page meta title and meta description. You can do this by using `i18n` plugin for AdminForth.
7082

7183
```ts
7284
import { AdminForth } from "adminforth";
85+
import * as z from "zod";
7386

7487
export const SEO_PAGE_CATEGORY = "seo_page_config";
7588

7689
app.get(`${admin.config.baseUrl}/api/get_page`,\
90+
admin.express.withSchema(
91+
{
92+
description: 'Returns translated SEO metadata for the page specified by the pageUrl query parameter.',
93+
response: z.object({
94+
meta_title: z.string(),
95+
meta_desc: z.string(),
96+
}),
97+
},
7798
//diff-add
7899
admin.express.translatable(
79100
async (req:any, res: Response): Promise<void> => {
@@ -107,6 +128,7 @@ app.get(`${admin.config.baseUrl}/api/get_page`,\
107128
meta_desc,
108129
});
109130
}
131+
)
110132
//diff-add
111133
)
112134
);

adminforth/documentation/blog/tags.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,13 @@ ansible:
6767
label: Ansible
6868
permalink: /ansible
6969
description: Ansible is an open-source automation tool used for configuration management, application deployment, and task automation.
70+
71+
context7:
72+
label: Context7
73+
permalink: /context7
74+
description: Context7 provides MCP-powered library and API documentation access for coding assistants.
75+
76+
MCP:
77+
label: MCP
78+
permalink: /mcp
79+
description: Model Context Protocol is a standard way for tools and assistants to connect to external capabilities and data sources.

0 commit comments

Comments
 (0)