create-forgeon 0.1.25 → 0.1.26

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-forgeon",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "Forgeon project generator CLI",
5
5
  "license": "MIT",
6
6
  "author": "Forgeon",
@@ -142,29 +142,70 @@ export class AppModule {}
142
142
  'health',
143
143
  'health.controller.ts',
144
144
  );
145
- fs.writeFileSync(
146
- healthControllerPath,
147
- `import { Controller, Get, Query } from '@nestjs/common';
148
- import { EchoQueryDto } from '../common/dto/echo-query.dto';
149
-
150
- @Controller('health')
151
- export class HealthController {
152
- @Get()
153
- getHealth(@Query('lang') _lang?: string) {
154
- return {
155
- status: 'ok',
156
- message: 'OK',
157
- };
158
- }
159
-
160
- @Get('echo')
161
- getEcho(@Query() query: EchoQueryDto) {
162
- return { value: query.value };
163
- }
164
- }
165
- `,
166
- 'utf8',
167
- );
145
+ fs.writeFileSync(
146
+ healthControllerPath,
147
+ `import { ConflictException, Controller, Get, Post, Query } from '@nestjs/common';
148
+ import { PrismaService } from '@forgeon/db-prisma';
149
+ import { EchoQueryDto } from '../common/dto/echo-query.dto';
150
+
151
+ @Controller('health')
152
+ export class HealthController {
153
+ constructor(private readonly prisma: PrismaService) {}
154
+
155
+ @Get()
156
+ getHealth(@Query('lang') lang?: string) {
157
+ const locale = this.resolveLocale(lang);
158
+ return {
159
+ status: 'ok',
160
+ message: 'OK',
161
+ i18n: locale === 'uk' ? 'Ukrainian' : 'English',
162
+ };
163
+ }
164
+
165
+ @Get('error')
166
+ getErrorProbe() {
167
+ throw new ConflictException({
168
+ message: 'Email already exists',
169
+ details: {
170
+ feature: 'core-errors',
171
+ probe: 'health.error',
172
+ },
173
+ });
174
+ }
175
+
176
+ @Get('validation')
177
+ getValidationProbe(@Query() query: EchoQueryDto) {
178
+ return {
179
+ status: 'ok',
180
+ validated: true,
181
+ value: query.value,
182
+ };
183
+ }
184
+
185
+ @Post('db')
186
+ async getDbProbe() {
187
+ const token = \`\${Date.now()}-\${Math.floor(Math.random() * 1_000_000)}\`;
188
+ const email = \`health-probe-\${token}@example.local\`;
189
+ const user = await this.prisma.user.create({
190
+ data: { email },
191
+ select: { id: true, email: true, createdAt: true },
192
+ });
193
+
194
+ return {
195
+ status: 'ok',
196
+ feature: 'db-prisma',
197
+ user,
198
+ };
199
+ }
200
+
201
+ private resolveLocale(lang?: string): 'en' | 'uk' {
202
+ const normalized = (lang ?? '').toLowerCase();
203
+ return normalized.startsWith('uk') ? 'uk' : 'en';
204
+ }
205
+ }
206
+ `,
207
+ 'utf8',
208
+ );
168
209
 
169
210
  removeIfExists(
170
211
  path.join(targetRoot, 'apps', 'api', 'src', 'common', 'filters', 'app-exception.filter.ts'),
@@ -1,10 +1,14 @@
1
- import { Controller, Get, Optional, Query } from '@nestjs/common';
1
+ import { ConflictException, Controller, Get, Optional, Post, Query } from '@nestjs/common';
2
+ import { PrismaService } from '@forgeon/db-prisma';
2
3
  import { I18nService } from 'nestjs-i18n';
3
4
  import { EchoQueryDto } from '../common/dto/echo-query.dto';
4
5
 
5
6
  @Controller('health')
6
7
  export class HealthController {
7
- constructor(@Optional() private readonly i18n?: I18nService) {}
8
+ constructor(
9
+ private readonly prisma: PrismaService,
10
+ @Optional() private readonly i18n?: I18nService,
11
+ ) {}
8
12
 
9
13
  @Get()
10
14
  getHealth(@Query('lang') lang?: string) {
@@ -16,16 +20,45 @@ export class HealthController {
16
20
  };
17
21
  }
18
22
 
19
- @Get('echo')
20
- getEcho(@Query() query: EchoQueryDto) {
21
- return { value: query.value };
23
+ @Get('error')
24
+ getErrorProbe() {
25
+ throw new ConflictException({
26
+ message: 'Email already exists',
27
+ details: {
28
+ feature: 'core-errors',
29
+ probe: 'health.error',
30
+ },
31
+ });
22
32
  }
23
33
 
24
- private translate(key: string, lang?: string): string {
34
+ @Get('validation')
35
+ getValidationProbe(@Query() query: EchoQueryDto) {
36
+ return {
37
+ status: 'ok',
38
+ validated: true,
39
+ value: query.value,
40
+ };
41
+ }
42
+
43
+ @Post('db')
44
+ async getDbProbe() {
45
+ const token = `${Date.now()}-${Math.floor(Math.random() * 1_000_000)}`;
46
+ const email = `health-probe-${token}@example.local`;
47
+ const user = await this.prisma.user.create({
48
+ data: { email },
49
+ select: { id: true, email: true, createdAt: true },
50
+ });
51
+
52
+ return {
53
+ status: 'ok',
54
+ feature: 'db-prisma',
55
+ user,
56
+ };
57
+ }
58
+
59
+ private translate(key: string, lang?: string): string {
25
60
  if (!this.i18n) {
26
61
  if (key === 'common.ok') return 'OK';
27
- if (key === 'common.checkApiHealth') return 'Check API health';
28
- if (key === 'common.language') return 'Language';
29
62
  if (key === 'common.languages.english') return 'English';
30
63
  if (key === 'common.languages.ukrainian') return 'Ukrainian';
31
64
  return key;
@@ -1,37 +1,78 @@
1
- import { useState } from 'react';
2
- import './styles.css';
1
+ import { useState } from 'react';
2
+ import './styles.css';
3
+
4
+ type ProbeResult = {
5
+ statusCode: number;
6
+ body: unknown;
7
+ };
8
+
9
+ export default function App() {
10
+ const [healthResult, setHealthResult] = useState<ProbeResult | null>(null);
11
+ const [errorProbeResult, setErrorProbeResult] = useState<ProbeResult | null>(null);
12
+ const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);
13
+ const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);
14
+ const [networkError, setNetworkError] = useState<string | null>(null);
15
+
16
+ const requestProbe = async (url: string, init?: RequestInit): Promise<ProbeResult> => {
17
+ const response = await fetch(url, init);
18
+ let body: unknown = null;
19
+
20
+ try {
21
+ body = await response.json();
22
+ } catch {
23
+ body = { message: 'Non-JSON response' };
24
+ }
25
+
26
+ return {
27
+ statusCode: response.status,
28
+ body,
29
+ };
30
+ };
31
+
32
+ const runProbe = async (
33
+ setter: (value: ProbeResult | null) => void,
34
+ url: string,
35
+ init?: RequestInit,
36
+ ) => {
37
+ setNetworkError(null);
38
+ try {
39
+ const result = await requestProbe(url, init);
40
+ setter(result);
41
+ } catch (err) {
42
+ setNetworkError(err instanceof Error ? err.message : 'Unknown error');
43
+ }
44
+ };
45
+
46
+ const renderResult = (title: string, result: ProbeResult | null) => (
47
+ <section>
48
+ <h3>{title}</h3>
49
+ {result ? <pre>{JSON.stringify(result, null, 2)}</pre> : null}
50
+ </section>
51
+ );
52
+
53
+ return (
54
+ <main className="page">
55
+ <h1>Forgeon Fullstack Scaffold</h1>
56
+ <p>Default frontend preset: React + Vite + TypeScript.</p>
57
+ <div className="actions">
58
+ <button onClick={() => runProbe(setHealthResult, '/api/health')}>Check API health</button>
59
+ <button onClick={() => runProbe(setErrorProbeResult, '/api/health/error')}>
60
+ Check error envelope
61
+ </button>
62
+ <button onClick={() => runProbe(setValidationProbeResult, '/api/health/validation')}>
63
+ Check validation (expect 400)
64
+ </button>
65
+ <button onClick={() => runProbe(setDbProbeResult, '/api/health/db', { method: 'POST' })}>
66
+ Check database (create user)
67
+ </button>
68
+ </div>
69
+ {renderResult('Health response', healthResult)}
70
+ {renderResult('Error probe response', errorProbeResult)}
71
+ {renderResult('Validation probe response', validationProbeResult)}
72
+ {renderResult('DB probe response', dbProbeResult)}
73
+ {networkError ? <p className="error">{networkError}</p> : null}
74
+ </main>
75
+ );
76
+ }
3
77
 
4
- type HealthResponse = {
5
- status: string;
6
- message: string;
7
- };
8
-
9
- export default function App() {
10
- const [data, setData] = useState<HealthResponse | null>(null);
11
- const [error, setError] = useState<string | null>(null);
12
-
13
- const checkApi = async () => {
14
- setError(null);
15
- try {
16
- const response = await fetch('/api/health');
17
- if (!response.ok) {
18
- throw new Error(`HTTP ${response.status}`);
19
- }
20
- const payload = (await response.json()) as HealthResponse;
21
- setData(payload);
22
- } catch (err) {
23
- setError(err instanceof Error ? err.message : 'Unknown error');
24
- }
25
- };
26
-
27
- return (
28
- <main className="page">
29
- <h1>Forgeon Fullstack Scaffold</h1>
30
- <p>Default frontend preset: React + Vite + TypeScript.</p>
31
- <button onClick={checkApi}>Check API health</button>
32
- {data ? <pre>{JSON.stringify(data, null, 2)}</pre> : null}
33
- {error ? <p className="error">{error}</p> : null}
34
- </main>
35
- );
36
- }
37
78
 
@@ -8,23 +8,35 @@ body {
8
8
  color: #0f172a;
9
9
  }
10
10
 
11
- .page {
12
- max-width: 720px;
13
- margin: 3rem auto;
14
- padding: 0 1rem;
15
- }
16
-
17
- button {
18
- padding: 0.6rem 1rem;
19
- border: 0;
20
- border-radius: 0.5rem;
21
- cursor: pointer;
22
- }
23
-
24
- pre {
25
- background: #e2e8f0;
26
- padding: 1rem;
27
- border-radius: 0.5rem;
11
+ .page {
12
+ max-width: 720px;
13
+ margin: 3rem auto;
14
+ padding: 0 1rem;
15
+ }
16
+
17
+ .actions {
18
+ display: grid;
19
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
20
+ gap: 0.6rem;
21
+ margin: 1rem 0 1.25rem;
22
+ }
23
+
24
+ button {
25
+ padding: 0.6rem 1rem;
26
+ border: 0;
27
+ border-radius: 0.5rem;
28
+ cursor: pointer;
29
+ }
30
+
31
+ h3 {
32
+ margin: 1rem 0 0.5rem;
33
+ font-size: 1rem;
34
+ }
35
+
36
+ pre {
37
+ background: #e2e8f0;
38
+ padding: 1rem;
39
+ border-radius: 0.5rem;
28
40
  overflow: auto;
29
41
  }
30
42
 
@@ -0,0 +1,25 @@
1
+ # MODULE CHECKS
2
+
3
+ ## Purpose
4
+
5
+ Define mandatory runtime verification hooks for Forgeon modules.
6
+
7
+ If a module can be validated through a safe API call, it must provide:
8
+
9
+ 1. A probe endpoint in API (`/api/health/*`).
10
+ 2. A probe trigger on default web page (`apps/web/src/App.tsx`).
11
+ 3. A visible result block in UI with HTTP status and JSON body.
12
+
13
+ ## Current Baseline Probes
14
+
15
+ - `core-errors`: `GET /api/health/error` (returns error envelope, expected `409`)
16
+ - `core-validation`: `GET /api/health/validation` without `value` (expected `400`)
17
+ - `db-prisma`: `POST /api/health/db` (creates probe user and returns it, expected `201`)
18
+
19
+ ## Rules For Future Modules
20
+
21
+ - Probe path should be explicit and feature-scoped (`/api/health/<feature>`).
22
+ - Probe must be deterministic and documented (expected status + payload shape).
23
+ - If probe writes data, it must use clearly marked probe/test records.
24
+ - Probe should not require hidden setup beyond documented env/dependencies.
25
+ - `create-forgeon add <module>` must wire both API probe and web probe UI when feasible.
@@ -63,3 +63,4 @@ Must contain:
63
63
  - Contracts package can be imported from both sides without circular dependencies.
64
64
  - Contracts package exports are stable from `dist/index` entrypoint.
65
65
  - Module has docs under `docs/AI/MODULES/<module-id>.md`.
66
+ - If module behavior can be runtime-checked, it also includes API+Web probe hooks (see `docs/AI/MODULE_CHECKS.md`).
@@ -48,11 +48,12 @@ Create or update create-forgeon preset flow:
48
48
  ## Add Fullstack Module
49
49
 
50
50
  ```text
51
- Implement `create-forgeon add <module-id>` for a fullstack feature.
52
- Requirements:
53
- - split module into contracts/api/web packages
54
- - contracts is source of truth for routes, DTOs, errors
55
- - add docs note under docs/AI/MODULES/<module-id>.md
56
- - keep backward compatibility
57
- ```
51
+ Implement `create-forgeon add <module-id>` for a fullstack feature.
52
+ Requirements:
53
+ - split module into contracts/api/web packages
54
+ - contracts is source of truth for routes, DTOs, errors
55
+ - if feasible, add module probe hooks in API (`/api/health/*`) and web diagnostics UI
56
+ - add docs note under docs/AI/MODULES/<module-id>.md
57
+ - keep backward compatibility
58
+ ```
58
59
 
@@ -3,5 +3,6 @@
3
3
  - `AI/PROJECT.md` - project overview and run modes
4
4
  - `AI/ARCHITECTURE.md` - monorepo design and extension model
5
5
  - `AI/MODULE_SPEC.md` - fullstack module contract (`contracts/api/web`)
6
+ - `AI/MODULE_CHECKS.md` - required runtime probe hooks for modules
6
7
  - `AI/VALIDATION.md` - DTO/env validation standards
7
8
  - `AI/TASKS.md` - ready-to-use Codex prompts
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "ok": "OK",
3
3
  "checkApiHealth": "Check API health",
4
+ "checkErrorEnvelope": "Check error envelope",
5
+ "checkValidation": "Check validation (expect 400)",
6
+ "checkDatabase": "Check database (create user)",
4
7
  "language": "Language",
5
8
  "languages": {
6
9
  "english": "English",
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "ok": "OK",
3
3
  "checkApiHealth": "Перевірити API health",
4
+ "checkErrorEnvelope": "Перевірити error envelope",
5
+ "checkValidation": "Перевірити валідацію (очікуємо 400)",
6
+ "checkDatabase": "Перевірити базу даних (створити користувача)",
4
7
  "language": "Мова",
5
8
  "languages": {
6
9
  "english": "Англійська",
@@ -5,10 +5,9 @@ import * as i18nWeb from '@forgeon/i18n-web';
5
5
  import type { I18nLocale } from '@forgeon/i18n-web';
6
6
  import './styles.css';
7
7
 
8
- type HealthResponse = {
9
- status: string;
10
- message: string;
11
- i18n: string;
8
+ type ProbeResult = {
9
+ statusCode: number;
10
+ body: unknown;
12
11
  };
13
12
 
14
13
  function localeLabelKey(locale: I18nLocale): string {
@@ -19,8 +18,11 @@ export default function App() {
19
18
  const { t } = useTranslation(['common']);
20
19
  const { I18N_LOCALES, getInitialLocale, persistLocale, toLangQuery } = i18nWeb;
21
20
  const [locale, setLocale] = useState<I18nLocale>(getInitialLocale);
22
- const [data, setData] = useState<HealthResponse | null>(null);
23
- const [error, setError] = useState<string | null>(null);
21
+ const [healthResult, setHealthResult] = useState<ProbeResult | null>(null);
22
+ const [errorProbeResult, setErrorProbeResult] = useState<ProbeResult | null>(null);
23
+ const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);
24
+ const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);
25
+ const [networkError, setNetworkError] = useState<string | null>(null);
24
26
 
25
27
  const changeLocale = (nextLocale: I18nLocale) => {
26
28
  setLocale(nextLocale);
@@ -28,24 +30,49 @@ export default function App() {
28
30
  void i18n.changeLanguage(nextLocale);
29
31
  };
30
32
 
31
- const checkApi = async () => {
32
- setError(null);
33
+ const requestProbe = async (path: string, init?: RequestInit): Promise<ProbeResult> => {
34
+ const response = await fetch(`/api${path}${toLangQuery(locale)}`, {
35
+ ...init,
36
+ headers: {
37
+ ...(init?.headers ?? {}),
38
+ 'Accept-Language': locale,
39
+ },
40
+ });
41
+
42
+ let body: unknown = null;
43
+ try {
44
+ body = await response.json();
45
+ } catch {
46
+ body = { message: 'Non-JSON response' };
47
+ }
48
+
49
+ return {
50
+ statusCode: response.status,
51
+ body,
52
+ };
53
+ };
54
+
55
+ const runProbe = async (
56
+ setter: (value: ProbeResult | null) => void,
57
+ path: string,
58
+ init?: RequestInit,
59
+ ) => {
60
+ setNetworkError(null);
33
61
  try {
34
- const response = await fetch(`/api/health${toLangQuery(locale)}`, {
35
- headers: {
36
- 'Accept-Language': locale,
37
- },
38
- });
39
- if (!response.ok) {
40
- throw new Error(`HTTP ${response.status}`);
41
- }
42
- const payload = (await response.json()) as HealthResponse;
43
- setData(payload);
62
+ const result = await requestProbe(path, init);
63
+ setter(result);
44
64
  } catch (err) {
45
- setError(err instanceof Error ? err.message : 'Unknown error');
65
+ setNetworkError(err instanceof Error ? err.message : 'Unknown error');
46
66
  }
47
67
  };
48
68
 
69
+ const renderResult = (title: string, result: ProbeResult | null) => (
70
+ <section>
71
+ <h3>{title}</h3>
72
+ {result ? <pre>{JSON.stringify(result, null, 2)}</pre> : null}
73
+ </section>
74
+ );
75
+
49
76
  return (
50
77
  <main className="page">
51
78
  <h1>Forgeon Fullstack Scaffold</h1>
@@ -62,9 +89,23 @@ export default function App() {
62
89
  </option>
63
90
  ))}
64
91
  </select>
65
- <button onClick={checkApi}>{t('common:checkApiHealth')}</button>
66
- {data ? <pre>{JSON.stringify(data, null, 2)}</pre> : null}
67
- {error ? <p className="error">{error}</p> : null}
92
+ <div className="actions">
93
+ <button onClick={() => runProbe(setHealthResult, '/health')}>{t('common:checkApiHealth')}</button>
94
+ <button onClick={() => runProbe(setErrorProbeResult, '/health/error')}>
95
+ {t('common:checkErrorEnvelope')}
96
+ </button>
97
+ <button onClick={() => runProbe(setValidationProbeResult, '/health/validation')}>
98
+ {t('common:checkValidation')}
99
+ </button>
100
+ <button onClick={() => runProbe(setDbProbeResult, '/health/db', { method: 'POST' })}>
101
+ {t('common:checkDatabase')}
102
+ </button>
103
+ </div>
104
+ {renderResult('Health response', healthResult)}
105
+ {renderResult('Error probe response', errorProbeResult)}
106
+ {renderResult('Validation probe response', validationProbeResult)}
107
+ {renderResult('DB probe response', dbProbeResult)}
108
+ {networkError ? <p className="error">{networkError}</p> : null}
68
109
  </main>
69
110
  );
70
111
  }