create-forgeon 0.1.24 → 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 +1 -1
- package/src/core/docs.test.mjs +8 -6
- package/src/modules/executor.test.mjs +8 -0
- package/src/modules/i18n.mjs +19 -1
- package/src/presets/i18n.mjs +76 -34
- package/templates/base/apps/api/Dockerfile +7 -4
- package/templates/base/apps/api/package.json +3 -2
- package/templates/base/apps/api/src/app.module.ts +4 -4
- package/templates/base/apps/api/src/health/health.controller.ts +41 -8
- package/templates/base/apps/api/src/main.ts +6 -8
- package/templates/base/apps/web/src/App.tsx +76 -35
- package/templates/base/apps/web/src/styles.css +29 -17
- package/templates/base/docs/AI/ARCHITECTURE.md +48 -32
- package/templates/base/docs/AI/MODULE_CHECKS.md +25 -0
- package/templates/base/docs/AI/MODULE_SPEC.md +1 -0
- package/templates/base/docs/AI/PROJECT.md +27 -16
- package/templates/base/docs/AI/TASKS.md +8 -7
- package/templates/base/docs/AI/VALIDATION.md +6 -1
- package/templates/base/docs/README.md +1 -0
- package/templates/base/packages/core/README.md +1 -0
- package/templates/base/packages/core/src/errors/core-exception.filter.ts +11 -3
- package/templates/base/packages/core/src/index.ts +1 -0
- package/templates/base/packages/core/src/validation/core-validation.pipe.ts +56 -0
- package/templates/base/packages/core/src/validation/index.ts +1 -0
- package/templates/base/packages/db-prisma/README.md +9 -0
- package/templates/base/packages/db-prisma/package.json +20 -0
- package/templates/base/packages/db-prisma/src/db-prisma-config.loader.ts +18 -0
- package/templates/base/packages/db-prisma/src/db-prisma-config.service.ts +12 -0
- package/templates/base/packages/db-prisma/src/db-prisma-env.schema.ts +17 -0
- package/templates/base/packages/db-prisma/src/db-prisma.module.ts +13 -0
- package/templates/base/packages/db-prisma/src/index.ts +5 -0
- package/templates/base/{apps/api/src/prisma → packages/db-prisma/src}/prisma.service.ts +24 -27
- package/templates/base/packages/db-prisma/tsconfig.json +9 -0
- package/templates/base/resources/i18n/en/common.json +3 -0
- package/templates/base/resources/i18n/uk/common.json +3 -0
- package/templates/docs-fragments/AI_ARCHITECTURE/20_env_base.md +1 -1
- package/templates/docs-fragments/AI_ARCHITECTURE/30_default_db.md +9 -7
- package/templates/docs-fragments/AI_ARCHITECTURE/32_scope_freeze.md +6 -5
- package/templates/docs-fragments/AI_PROJECT/20_structure_base.md +2 -1
- package/templates/module-presets/i18n/apps/web/src/App.tsx +63 -22
- package/templates/base/apps/api/src/prisma/prisma.module.ts +0 -9
package/package.json
CHANGED
package/src/core/docs.test.mjs
CHANGED
|
@@ -55,9 +55,10 @@ describe('generateDocs', () => {
|
|
|
55
55
|
assert.match(architectureDoc, /Config Strategy/);
|
|
56
56
|
assert.match(architectureDoc, /TypeScript Module Policy/);
|
|
57
57
|
assert.match(architectureDoc, /tsconfig\.base\.esm\.json/);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
assert.match(architectureDoc, /DbPrismaModule/);
|
|
59
|
+
} finally {
|
|
60
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
61
|
+
}
|
|
61
62
|
});
|
|
62
63
|
|
|
63
64
|
it('generates docker and caddy notes when enabled', () => {
|
|
@@ -95,8 +96,9 @@ describe('generateDocs', () => {
|
|
|
95
96
|
assert.match(architectureDoc, /Active reverse proxy preset: `caddy`/);
|
|
96
97
|
assert.match(architectureDoc, /TypeScript Module Policy/);
|
|
97
98
|
assert.match(architectureDoc, /tsconfig\.base\.node\.json/);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
assert.match(architectureDoc, /DbPrismaModule/);
|
|
100
|
+
} finally {
|
|
101
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
102
|
+
}
|
|
101
103
|
});
|
|
102
104
|
});
|
|
@@ -98,6 +98,7 @@ describe('addModule', () => {
|
|
|
98
98
|
assert.equal(fs.existsSync(path.join(projectRoot, 'tsconfig.base.esm.json')), true);
|
|
99
99
|
|
|
100
100
|
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
101
|
+
assert.match(apiPackage, /@forgeon\/db-prisma/);
|
|
101
102
|
assert.match(apiPackage, /@forgeon\/i18n/);
|
|
102
103
|
assert.match(apiPackage, /@forgeon\/i18n-contracts/);
|
|
103
104
|
|
|
@@ -110,15 +111,20 @@ describe('addModule', () => {
|
|
|
110
111
|
|
|
111
112
|
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
112
113
|
assert.match(appModule, /coreConfig/);
|
|
114
|
+
assert.match(appModule, /dbPrismaConfig/);
|
|
115
|
+
assert.match(appModule, /dbPrismaEnvSchema/);
|
|
113
116
|
assert.match(appModule, /createEnvValidator/);
|
|
114
117
|
assert.match(appModule, /coreEnvSchema/);
|
|
115
118
|
assert.match(appModule, /i18nConfig/);
|
|
116
119
|
assert.match(appModule, /i18nEnvSchema/);
|
|
117
120
|
assert.match(appModule, /CoreConfigModule/);
|
|
118
121
|
assert.match(appModule, /CoreErrorsModule/);
|
|
122
|
+
assert.match(appModule, /DbPrismaModule/);
|
|
119
123
|
|
|
120
124
|
const mainTs = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'main.ts'), 'utf8');
|
|
121
125
|
assert.match(mainTs, /CoreExceptionFilter/);
|
|
126
|
+
assert.match(mainTs, /createValidationPipe/);
|
|
127
|
+
assert.doesNotMatch(mainTs, /new ValidationPipe\(/);
|
|
122
128
|
|
|
123
129
|
const forgeonI18nModule = fs.readFileSync(
|
|
124
130
|
path.join(projectRoot, 'packages', 'i18n', 'src', 'forgeon-i18n.module.ts'),
|
|
@@ -225,6 +231,8 @@ describe('addModule', () => {
|
|
|
225
231
|
'utf8',
|
|
226
232
|
);
|
|
227
233
|
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/core build/);
|
|
234
|
+
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/db-prisma build/);
|
|
235
|
+
assert.match(apiDockerfile, /COPY packages\/db-prisma\/package\.json packages\/db-prisma\/package\.json/);
|
|
228
236
|
} finally {
|
|
229
237
|
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
230
238
|
}
|
package/src/modules/i18n.mjs
CHANGED
|
@@ -102,6 +102,11 @@ function patchApiDockerfile(targetRoot) {
|
|
|
102
102
|
content = ensureLineAfter(
|
|
103
103
|
content,
|
|
104
104
|
'COPY packages/core/package.json packages/core/package.json',
|
|
105
|
+
'COPY packages/db-prisma/package.json packages/db-prisma/package.json',
|
|
106
|
+
);
|
|
107
|
+
content = ensureLineAfter(
|
|
108
|
+
content,
|
|
109
|
+
'COPY packages/db-prisma/package.json packages/db-prisma/package.json',
|
|
105
110
|
'COPY packages/i18n-contracts/package.json packages/i18n-contracts/package.json',
|
|
106
111
|
);
|
|
107
112
|
content = ensureLineAfter(
|
|
@@ -112,6 +117,11 @@ function patchApiDockerfile(targetRoot) {
|
|
|
112
117
|
content = ensureLineAfter(
|
|
113
118
|
content,
|
|
114
119
|
'COPY packages/core packages/core',
|
|
120
|
+
'COPY packages/db-prisma packages/db-prisma',
|
|
121
|
+
);
|
|
122
|
+
content = ensureLineAfter(
|
|
123
|
+
content,
|
|
124
|
+
'COPY packages/db-prisma packages/db-prisma',
|
|
115
125
|
'COPY packages/i18n-contracts packages/i18n-contracts',
|
|
116
126
|
);
|
|
117
127
|
content = ensureLineAfter(
|
|
@@ -122,6 +132,7 @@ function patchApiDockerfile(targetRoot) {
|
|
|
122
132
|
|
|
123
133
|
content = content
|
|
124
134
|
.replace(/^RUN pnpm --filter @forgeon\/core build\r?\n?/gm, '')
|
|
135
|
+
.replace(/^RUN pnpm --filter @forgeon\/db-prisma build\r?\n?/gm, '')
|
|
125
136
|
.replace(/^RUN pnpm --filter @forgeon\/i18n-contracts build\r?\n?/gm, '')
|
|
126
137
|
.replace(/^RUN pnpm --filter @forgeon\/i18n build\r?\n?/gm, '');
|
|
127
138
|
|
|
@@ -130,6 +141,11 @@ function patchApiDockerfile(targetRoot) {
|
|
|
130
141
|
'RUN pnpm --filter @forgeon/api prisma:generate',
|
|
131
142
|
'RUN pnpm --filter @forgeon/core build',
|
|
132
143
|
);
|
|
144
|
+
content = ensureLineBefore(
|
|
145
|
+
content,
|
|
146
|
+
'RUN pnpm --filter @forgeon/api prisma:generate',
|
|
147
|
+
'RUN pnpm --filter @forgeon/db-prisma build',
|
|
148
|
+
);
|
|
133
149
|
content = ensureLineBefore(
|
|
134
150
|
content,
|
|
135
151
|
'RUN pnpm --filter @forgeon/api prisma:generate',
|
|
@@ -229,10 +245,11 @@ function patchApiPackage(targetRoot) {
|
|
|
229
245
|
ensureScript(
|
|
230
246
|
packageJson,
|
|
231
247
|
'predev',
|
|
232
|
-
'pnpm --filter @forgeon/i18n-contracts build && pnpm --filter @forgeon/i18n build',
|
|
248
|
+
'pnpm --filter @forgeon/core build && pnpm --filter @forgeon/db-prisma build && pnpm --filter @forgeon/i18n-contracts build && pnpm --filter @forgeon/i18n build',
|
|
233
249
|
);
|
|
234
250
|
ensureDependency(packageJson, '@forgeon/i18n', 'workspace:*');
|
|
235
251
|
ensureDependency(packageJson, '@forgeon/i18n-contracts', 'workspace:*');
|
|
252
|
+
ensureDependency(packageJson, '@forgeon/db-prisma', 'workspace:*');
|
|
236
253
|
ensureDependency(packageJson, 'nestjs-i18n', '^10.5.1');
|
|
237
254
|
writeJson(packagePath, packageJson);
|
|
238
255
|
}
|
|
@@ -298,6 +315,7 @@ function patchRootPackage(targetRoot) {
|
|
|
298
315
|
}
|
|
299
316
|
|
|
300
317
|
export function applyI18nModule({ packageRoot, targetRoot }) {
|
|
318
|
+
copyFromBase(packageRoot, targetRoot, path.join('packages', 'db-prisma'));
|
|
301
319
|
copyFromBase(packageRoot, targetRoot, path.join('packages', 'i18n'));
|
|
302
320
|
copyFromBase(packageRoot, targetRoot, path.join('resources', 'i18n'));
|
|
303
321
|
|
package/src/presets/i18n.mjs
CHANGED
|
@@ -9,12 +9,13 @@ export function applyI18nDisabled(targetRoot) {
|
|
|
9
9
|
removeIfExists(path.join(targetRoot, 'resources', 'i18n'));
|
|
10
10
|
|
|
11
11
|
const apiPackagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
|
|
12
|
-
if (fs.existsSync(apiPackagePath)) {
|
|
13
|
-
const apiPackage = JSON.parse(fs.readFileSync(apiPackagePath, 'utf8'));
|
|
14
|
-
|
|
15
|
-
if (apiPackage.scripts) {
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
if (fs.existsSync(apiPackagePath)) {
|
|
13
|
+
const apiPackage = JSON.parse(fs.readFileSync(apiPackagePath, 'utf8'));
|
|
14
|
+
|
|
15
|
+
if (apiPackage.scripts) {
|
|
16
|
+
apiPackage.scripts.predev =
|
|
17
|
+
'pnpm --filter @forgeon/core build && pnpm --filter @forgeon/db-prisma build';
|
|
18
|
+
}
|
|
18
19
|
|
|
19
20
|
if (apiPackage.dependencies) {
|
|
20
21
|
delete apiPackage.dependencies['@forgeon/i18n'];
|
|
@@ -110,21 +111,21 @@ export function applyI18nDisabled(targetRoot) {
|
|
|
110
111
|
appModulePath,
|
|
111
112
|
`import { Module } from '@nestjs/common';
|
|
112
113
|
import { ConfigModule } from '@nestjs/config';
|
|
113
|
-
import {
|
|
114
|
+
import { dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule } from '@forgeon/db-prisma';
|
|
115
|
+
import { CoreConfigModule, CoreErrorsModule, coreConfig, coreEnvSchema, createEnvValidator } from '@forgeon/core';
|
|
114
116
|
import { HealthController } from './health/health.controller';
|
|
115
|
-
import { PrismaModule } from './prisma/prisma.module';
|
|
116
117
|
|
|
117
118
|
@Module({
|
|
118
119
|
imports: [
|
|
119
120
|
ConfigModule.forRoot({
|
|
120
121
|
isGlobal: true,
|
|
121
|
-
load: [coreConfig],
|
|
122
|
-
validate:
|
|
122
|
+
load: [coreConfig, dbPrismaConfig],
|
|
123
|
+
validate: createEnvValidator([coreEnvSchema, dbPrismaEnvSchema]),
|
|
123
124
|
envFilePath: '.env',
|
|
124
125
|
}),
|
|
125
126
|
CoreConfigModule,
|
|
126
127
|
CoreErrorsModule,
|
|
127
|
-
|
|
128
|
+
DbPrismaModule,
|
|
128
129
|
],
|
|
129
130
|
controllers: [HealthController],
|
|
130
131
|
})
|
|
@@ -141,29 +142,70 @@ export class AppModule {}
|
|
|
141
142
|
'health',
|
|
142
143
|
'health.controller.ts',
|
|
143
144
|
);
|
|
144
|
-
fs.writeFileSync(
|
|
145
|
-
healthControllerPath,
|
|
146
|
-
`import { Controller, Get, Query } from '@nestjs/common';
|
|
147
|
-
import {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
);
|
|
167
209
|
|
|
168
210
|
removeIfExists(
|
|
169
211
|
path.join(targetRoot, 'apps', 'api', 'src', 'common', 'filters', 'app-exception.filter.ts'),
|
|
@@ -4,19 +4,22 @@ WORKDIR /app
|
|
|
4
4
|
RUN corepack enable
|
|
5
5
|
|
|
6
6
|
COPY package.json pnpm-workspace.yaml tsconfig.base.json tsconfig.base.node.json tsconfig.base.esm.json ./
|
|
7
|
-
COPY apps/api/package.json apps/api/package.json
|
|
8
|
-
COPY apps/api/prisma apps/api/prisma
|
|
9
|
-
COPY packages/core/package.json packages/core/package.json
|
|
10
|
-
COPY packages/
|
|
7
|
+
COPY apps/api/package.json apps/api/package.json
|
|
8
|
+
COPY apps/api/prisma apps/api/prisma
|
|
9
|
+
COPY packages/core/package.json packages/core/package.json
|
|
10
|
+
COPY packages/db-prisma/package.json packages/db-prisma/package.json
|
|
11
|
+
COPY packages/i18n/package.json packages/i18n/package.json
|
|
11
12
|
|
|
12
13
|
RUN pnpm install --frozen-lockfile=false
|
|
13
14
|
|
|
14
15
|
COPY apps/api apps/api
|
|
15
16
|
COPY packages/core packages/core
|
|
17
|
+
COPY packages/db-prisma packages/db-prisma
|
|
16
18
|
COPY packages/i18n packages/i18n
|
|
17
19
|
COPY resources resources
|
|
18
20
|
|
|
19
21
|
RUN pnpm --filter @forgeon/core build
|
|
22
|
+
RUN pnpm --filter @forgeon/db-prisma build
|
|
20
23
|
RUN pnpm --filter @forgeon/i18n build
|
|
21
24
|
RUN pnpm --filter @forgeon/api prisma:generate
|
|
22
25
|
RUN pnpm --filter @forgeon/api build
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"name": "@forgeon/api",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"private": true,
|
|
5
|
-
"scripts": {
|
|
6
|
-
"predev": "pnpm --filter @forgeon/i18n build",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"predev": "pnpm --filter @forgeon/core build && pnpm --filter @forgeon/db-prisma build && pnpm --filter @forgeon/i18n build",
|
|
7
7
|
"build": "tsc -p tsconfig.build.json",
|
|
8
8
|
"dev": "ts-node --transpile-only src/main.ts",
|
|
9
9
|
"start": "node dist/main.js",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"prisma:seed": "ts-node --transpile-only prisma/seed.ts"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
+
"@forgeon/db-prisma": "workspace:*",
|
|
17
18
|
"@forgeon/core": "workspace:*",
|
|
18
19
|
"@forgeon/i18n": "workspace:*",
|
|
19
20
|
"@nestjs/common": "^11.0.1",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Module } from '@nestjs/common';
|
|
2
2
|
import { ConfigModule } from '@nestjs/config';
|
|
3
|
+
import { dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule } from '@forgeon/db-prisma';
|
|
3
4
|
import {
|
|
4
5
|
CoreConfigModule,
|
|
5
6
|
CoreErrorsModule,
|
|
@@ -10,7 +11,6 @@ import {
|
|
|
10
11
|
import { ForgeonI18nModule, i18nConfig, i18nEnvSchema } from '@forgeon/i18n';
|
|
11
12
|
import { join } from 'path';
|
|
12
13
|
import { HealthController } from './health/health.controller';
|
|
13
|
-
import { PrismaModule } from './prisma/prisma.module';
|
|
14
14
|
|
|
15
15
|
const i18nPath = join(__dirname, '..', '..', '..', 'resources', 'i18n');
|
|
16
16
|
|
|
@@ -18,16 +18,16 @@ const i18nPath = join(__dirname, '..', '..', '..', 'resources', 'i18n');
|
|
|
18
18
|
imports: [
|
|
19
19
|
ConfigModule.forRoot({
|
|
20
20
|
isGlobal: true,
|
|
21
|
-
load: [coreConfig, i18nConfig],
|
|
22
|
-
validate: createEnvValidator([coreEnvSchema, i18nEnvSchema]),
|
|
21
|
+
load: [coreConfig, dbPrismaConfig, i18nConfig],
|
|
22
|
+
validate: createEnvValidator([coreEnvSchema, dbPrismaEnvSchema, i18nEnvSchema]),
|
|
23
23
|
envFilePath: '.env',
|
|
24
24
|
}),
|
|
25
25
|
CoreConfigModule,
|
|
26
26
|
CoreErrorsModule,
|
|
27
|
+
DbPrismaModule,
|
|
27
28
|
ForgeonI18nModule.register({
|
|
28
29
|
path: i18nPath,
|
|
29
30
|
}),
|
|
30
|
-
PrismaModule,
|
|
31
31
|
],
|
|
32
32
|
controllers: [HealthController],
|
|
33
33
|
})
|
|
@@ -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(
|
|
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('
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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,6 +1,9 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
CoreConfigService,
|
|
4
|
+
CoreExceptionFilter,
|
|
5
|
+
createValidationPipe,
|
|
6
|
+
} from '@forgeon/core';
|
|
4
7
|
import { NestFactory } from '@nestjs/core';
|
|
5
8
|
import { AppModule } from './app.module';
|
|
6
9
|
|
|
@@ -10,12 +13,7 @@ async function bootstrap() {
|
|
|
10
13
|
const coreConfigService = app.get(CoreConfigService);
|
|
11
14
|
|
|
12
15
|
app.setGlobalPrefix(coreConfigService.apiPrefix);
|
|
13
|
-
app.useGlobalPipes(
|
|
14
|
-
new ValidationPipe({
|
|
15
|
-
whitelist: true,
|
|
16
|
-
transform: true,
|
|
17
|
-
}),
|
|
18
|
-
);
|
|
16
|
+
app.useGlobalPipes(createValidationPipe());
|
|
19
17
|
app.useGlobalFilters(app.get(CoreExceptionFilter));
|
|
20
18
|
|
|
21
19
|
await app.listen(coreConfigService.port);
|
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|