create-forgeon 0.1.23 → 0.1.25
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.mjs +12 -9
- package/src/core/docs.test.mjs +26 -20
- package/src/modules/executor.test.mjs +8 -0
- package/src/modules/i18n.mjs +19 -1
- package/src/presets/i18n.mjs +12 -11
- 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/main.ts +6 -8
- package/templates/base/docs/AI/ARCHITECTURE.md +48 -32
- package/templates/base/docs/AI/PROJECT.md +27 -16
- package/templates/base/docs/AI/VALIDATION.md +6 -1
- 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/docs-fragments/AI_ARCHITECTURE/20_env_base.md +1 -1
- package/templates/docs-fragments/AI_ARCHITECTURE/23_error_handling.md +11 -0
- 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/docs-fragments/AI_PROJECT/34_error_handling.md +7 -0
- package/templates/docs-fragments/README/41_error_handling.md +27 -0
- package/templates/base/apps/api/src/prisma/prisma.module.ts +0 -9
package/package.json
CHANGED
package/src/core/docs.mjs
CHANGED
|
@@ -64,11 +64,12 @@ export function generateDocs(targetRoot, options, packageRoot) {
|
|
|
64
64
|
} else {
|
|
65
65
|
readmeFragments.push('31_proxy_preset_none');
|
|
66
66
|
}
|
|
67
|
-
readmeFragments.push('32_prisma_container_start');
|
|
68
|
-
if (options.i18nEnabled) {
|
|
69
|
-
readmeFragments.push('40_i18n');
|
|
70
|
-
}
|
|
71
|
-
readmeFragments.push('
|
|
67
|
+
readmeFragments.push('32_prisma_container_start');
|
|
68
|
+
if (options.i18nEnabled) {
|
|
69
|
+
readmeFragments.push('40_i18n');
|
|
70
|
+
}
|
|
71
|
+
readmeFragments.push('41_error_handling');
|
|
72
|
+
readmeFragments.push('90_next_steps');
|
|
72
73
|
|
|
73
74
|
const aiProjectFragments = ['00_title', '10_what_is', '20_structure_base'];
|
|
74
75
|
if (options.i18nEnabled) {
|
|
@@ -80,10 +81,11 @@ export function generateDocs(targetRoot, options, packageRoot) {
|
|
|
80
81
|
} else {
|
|
81
82
|
aiProjectFragments.push('32_proxy_notes');
|
|
82
83
|
}
|
|
83
|
-
if (options.i18nEnabled) {
|
|
84
|
-
aiProjectFragments.push('33_i18n_notes');
|
|
85
|
-
}
|
|
86
|
-
aiProjectFragments.push('
|
|
84
|
+
if (options.i18nEnabled) {
|
|
85
|
+
aiProjectFragments.push('33_i18n_notes');
|
|
86
|
+
}
|
|
87
|
+
aiProjectFragments.push('34_error_handling');
|
|
88
|
+
aiProjectFragments.push('40_change_boundaries_base');
|
|
87
89
|
if (options.proxy !== 'none') {
|
|
88
90
|
aiProjectFragments.push('41_change_boundaries_docker');
|
|
89
91
|
}
|
|
@@ -97,6 +99,7 @@ export function generateDocs(targetRoot, options, packageRoot) {
|
|
|
97
99
|
aiArchitectureFragments.push('21_env_i18n');
|
|
98
100
|
}
|
|
99
101
|
aiArchitectureFragments.push('22_ts_module_policy');
|
|
102
|
+
aiArchitectureFragments.push('23_error_handling');
|
|
100
103
|
aiArchitectureFragments.push('30_default_db', '31_docker_runtime', '32_scope_freeze');
|
|
101
104
|
aiArchitectureFragments.push('40_docs_generation', '50_extension_points');
|
|
102
105
|
|
package/src/core/docs.test.mjs
CHANGED
|
@@ -38,14 +38,16 @@ describe('generateDocs', () => {
|
|
|
38
38
|
const projectDoc = readFile(path.join(targetRoot, 'docs', 'AI', 'PROJECT.md'));
|
|
39
39
|
const architectureDoc = readFile(path.join(targetRoot, 'docs', 'AI', 'ARCHITECTURE.md'));
|
|
40
40
|
|
|
41
|
-
assert.match(readme, /Docker\/infra: `enabled`/);
|
|
42
|
-
assert.match(readme, /Quick Start \(Docker\)/);
|
|
43
|
-
assert.match(readme, /Proxy Preset: none/);
|
|
44
|
-
assert.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
assert.match(projectDoc,
|
|
48
|
-
assert.
|
|
41
|
+
assert.match(readme, /Docker\/infra: `enabled`/);
|
|
42
|
+
assert.match(readme, /Quick Start \(Docker\)/);
|
|
43
|
+
assert.match(readme, /Proxy Preset: none/);
|
|
44
|
+
assert.match(readme, /Error Handling \(`core-errors`\)/);
|
|
45
|
+
assert.doesNotMatch(readme, /i18n Configuration/);
|
|
46
|
+
|
|
47
|
+
assert.match(projectDoc, /### Docker mode/);
|
|
48
|
+
assert.match(projectDoc, /Active proxy preset: `none`/);
|
|
49
|
+
assert.match(projectDoc, /CoreErrorsModule/);
|
|
50
|
+
assert.doesNotMatch(projectDoc, /packages\/i18n/);
|
|
49
51
|
|
|
50
52
|
assert.match(architectureDoc, /infra\/\*/);
|
|
51
53
|
assert.doesNotMatch(architectureDoc, /I18N_ENABLED/);
|
|
@@ -53,9 +55,10 @@ describe('generateDocs', () => {
|
|
|
53
55
|
assert.match(architectureDoc, /Config Strategy/);
|
|
54
56
|
assert.match(architectureDoc, /TypeScript Module Policy/);
|
|
55
57
|
assert.match(architectureDoc, /tsconfig\.base\.esm\.json/);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
assert.match(architectureDoc, /DbPrismaModule/);
|
|
59
|
+
} finally {
|
|
60
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
61
|
+
}
|
|
59
62
|
});
|
|
60
63
|
|
|
61
64
|
it('generates docker and caddy notes when enabled', () => {
|
|
@@ -78,12 +81,14 @@ describe('generateDocs', () => {
|
|
|
78
81
|
const projectDoc = readFile(path.join(targetRoot, 'docs', 'AI', 'PROJECT.md'));
|
|
79
82
|
const architectureDoc = readFile(path.join(targetRoot, 'docs', 'AI', 'ARCHITECTURE.md'));
|
|
80
83
|
|
|
81
|
-
assert.match(readme, /Quick Start \(Docker\)/);
|
|
82
|
-
assert.match(readme, /Proxy Preset: Caddy/);
|
|
83
|
-
assert.match(readme, /i18n Configuration/);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
assert.match(projectDoc,
|
|
84
|
+
assert.match(readme, /Quick Start \(Docker\)/);
|
|
85
|
+
assert.match(readme, /Proxy Preset: Caddy/);
|
|
86
|
+
assert.match(readme, /i18n Configuration/);
|
|
87
|
+
assert.match(readme, /Error Handling \(`core-errors`\)/);
|
|
88
|
+
|
|
89
|
+
assert.match(projectDoc, /`infra` - Docker Compose \(always\) \+ proxy preset \(`caddy`\)/);
|
|
90
|
+
assert.match(projectDoc, /Main proxy config: `infra\/caddy\/Caddyfile`/);
|
|
91
|
+
assert.match(projectDoc, /CoreExceptionFilter/);
|
|
87
92
|
|
|
88
93
|
assert.match(architectureDoc, /infra\/\*/);
|
|
89
94
|
assert.match(architectureDoc, /I18N_DEFAULT_LANG/);
|
|
@@ -91,8 +96,9 @@ describe('generateDocs', () => {
|
|
|
91
96
|
assert.match(architectureDoc, /Active reverse proxy preset: `caddy`/);
|
|
92
97
|
assert.match(architectureDoc, /TypeScript Module Policy/);
|
|
93
98
|
assert.match(architectureDoc, /tsconfig\.base\.node\.json/);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
assert.match(architectureDoc, /DbPrismaModule/);
|
|
100
|
+
} finally {
|
|
101
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
102
|
+
}
|
|
97
103
|
});
|
|
98
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
|
})
|
|
@@ -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,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,11 +1,15 @@
|
|
|
1
|
-
# ARCHITECTURE
|
|
2
|
-
|
|
3
|
-
## Monorepo Layout
|
|
4
|
-
|
|
5
|
-
- `apps/*` - deployable apps
|
|
6
|
-
- `packages/*` - reusable modules/presets
|
|
7
|
-
- `infra/*` - runtime infrastructure
|
|
8
|
-
- `resources/*` - static assets (translations)
|
|
1
|
+
# ARCHITECTURE
|
|
2
|
+
|
|
3
|
+
## Monorepo Layout
|
|
4
|
+
|
|
5
|
+
- `apps/*` - deployable apps
|
|
6
|
+
- `packages/*` - reusable modules/presets
|
|
7
|
+
- `infra/*` - runtime infrastructure
|
|
8
|
+
- `resources/*` - static assets (translations)
|
|
9
|
+
|
|
10
|
+
Canonical stack is fixed in this stage:
|
|
11
|
+
- NestJS + React + Prisma/Postgres + Docker
|
|
12
|
+
- Proxy preset can be `caddy`, `nginx`, or `none`
|
|
9
13
|
|
|
10
14
|
## Environment Flags
|
|
11
15
|
|
|
@@ -17,36 +21,48 @@
|
|
|
17
21
|
|
|
18
22
|
## Config Strategy
|
|
19
23
|
|
|
20
|
-
- `@forgeon/core` owns base runtime config
|
|
24
|
+
- `@forgeon/core` owns base runtime config, global error envelope/filter, and validation pipe defaults.
|
|
21
25
|
- Core config is validated with Zod and exposed through typed accessors.
|
|
22
26
|
- Add-modules own and validate only their module-specific env keys.
|
|
23
27
|
- i18n is an add-module; when installed, it uses its own env keys.
|
|
24
|
-
|
|
25
|
-
## Default DB Stack
|
|
26
|
-
|
|
27
|
-
Current default is Prisma + Postgres.
|
|
28
|
-
|
|
29
|
-
- Prisma schema and migrations live in `apps/api/prisma`
|
|
30
|
-
- DB access is encapsulated via `PrismaModule` (`apps/api/src/prisma`)
|
|
31
|
-
|
|
32
|
-
## Future DB Presets (Not Implemented Yet)
|
|
33
|
-
|
|
34
|
-
A future preset can switch DB by:
|
|
35
|
-
1. Replacing `PrismaModule` with another DB module package (for example Mongo package).
|
|
36
|
-
2. Updating `infra/docker/compose.yml` DB service.
|
|
37
|
-
3. Updating `DATABASE_URL` and related env keys.
|
|
38
|
-
4. Keeping app-level services dependent only on repository/data-access abstractions.
|
|
39
|
-
|
|
40
|
-
## Future Feature Modules
|
|
41
28
|
|
|
42
|
-
|
|
29
|
+
## Default DB Stack
|
|
30
|
+
|
|
31
|
+
Current default is Prisma + Postgres.
|
|
32
|
+
|
|
33
|
+
- Prisma schema and migrations live in `apps/api/prisma`
|
|
34
|
+
- DB access is encapsulated via `DbPrismaModule` in `@forgeon/db-prisma`
|
|
35
|
+
- `db-prisma` is treated as default-applied behavior in scaffold generation.
|
|
36
|
+
- Future direction: this default DB layer may be extracted to an explicit add-module/preset and optionally controlled by a CLI flag.
|
|
37
|
+
- Additional DB presets are out of scope for the current milestone.
|
|
38
|
+
|
|
39
|
+
## Module Strategy
|
|
43
40
|
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
Reusable features should be added as fullstack add-modules:
|
|
42
|
+
|
|
43
|
+
- `contracts` package (shared DTO/routes/errors)
|
|
44
|
+
- `api` package (NestJS integration)
|
|
45
|
+
- `web` package (React integration)
|
|
46
|
+
|
|
47
|
+
Reference: `docs/AI/MODULE_SPEC.md`.
|
|
46
48
|
|
|
47
49
|
## TypeScript Module Format Policy
|
|
48
50
|
|
|
49
|
-
- `apps/api`, `packages/core`, and backend runtime packages use
|
|
50
|
-
-
|
|
51
|
-
-
|
|
51
|
+
- `apps/api`, `packages/core`, and backend runtime packages use Node-oriented config:
|
|
52
|
+
- `tsconfig.base.node.json`
|
|
53
|
+
- Frontend-consumed shared packages (especially contracts/web helpers) use ESM config:
|
|
54
|
+
- `tsconfig.base.esm.json`
|
|
55
|
+
- Contracts packages are ESM-first and imported via package entrypoints only.
|
|
52
56
|
- Cross-package imports from `/src/*` are disallowed.
|
|
57
|
+
|
|
58
|
+
## Error Handling Strategy
|
|
59
|
+
|
|
60
|
+
- `@forgeon/core` owns the global HTTP error envelope and filter.
|
|
61
|
+
- API apps import `CoreErrorsModule` and register `CoreExceptionFilter` globally.
|
|
62
|
+
- Envelope fields:
|
|
63
|
+
- `error.code`
|
|
64
|
+
- `error.message`
|
|
65
|
+
- `error.status`
|
|
66
|
+
- `error.details` (optional)
|
|
67
|
+
- `error.requestId` (optional)
|
|
68
|
+
- `error.timestamp`
|
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
A canonical fullstack monorepo scaffold intended to be reused as a project starter.
|
|
6
6
|
|
|
7
|
-
## Structure
|
|
8
|
-
|
|
9
|
-
- `apps/api` - NestJS backend
|
|
10
|
-
- `apps/web` - frontend
|
|
11
|
-
- `packages/core` - shared backend core package
|
|
12
|
-
- `packages/
|
|
13
|
-
- `
|
|
14
|
-
- `
|
|
15
|
-
- `
|
|
7
|
+
## Structure
|
|
8
|
+
|
|
9
|
+
- `apps/api` - NestJS backend
|
|
10
|
+
- `apps/web` - React frontend (fixed stack)
|
|
11
|
+
- `packages/core` - shared backend core package (`core-config`, `core-errors`, `core-validation`)
|
|
12
|
+
- `packages/db-prisma` - reusable Prisma/Postgres module (`DbPrismaModule`, `PrismaService`, config)
|
|
13
|
+
- `packages/i18n` - reusable nestjs-i18n integration package
|
|
14
|
+
- `infra` - Docker Compose + proxy preset (`caddy|nginx|none`)
|
|
15
|
+
- `resources/i18n` - translation dictionaries
|
|
16
|
+
- `docs` - documentation, AI prompts, and module contracts
|
|
16
17
|
|
|
17
18
|
## Run Modes
|
|
18
19
|
|
|
@@ -23,10 +24,20 @@ pnpm install
|
|
|
23
24
|
pnpm dev
|
|
24
25
|
```
|
|
25
26
|
|
|
26
|
-
### Docker mode
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
docker compose --env-file infra/docker/.env.example -f infra/docker/compose.yml up --build
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
The API uses Prisma and expects `DATABASE_URL` from env.
|
|
27
|
+
### Docker mode
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
docker compose --env-file infra/docker/.env.example -f infra/docker/compose.yml up --build
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The API uses Prisma and expects `DATABASE_URL` from env.
|
|
34
|
+
|
|
35
|
+
If proxy preset is `none`, API is directly available on `localhost:3000`.
|
|
36
|
+
|
|
37
|
+
## Error Handling
|
|
38
|
+
|
|
39
|
+
`core-errors` is enabled by default.
|
|
40
|
+
|
|
41
|
+
- `CoreErrorsModule` is imported in `apps/api/src/app.module.ts`.
|
|
42
|
+
- `CoreExceptionFilter` is registered globally in `apps/api/src/main.ts`.
|
|
43
|
+
- Throw standard Nest exceptions from controllers/services; the filter converts them to a stable envelope.
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
## Backend DTO Validation Standard
|
|
4
4
|
|
|
5
5
|
- Use `class-validator` decorators on DTO classes.
|
|
6
|
-
- Global validation is
|
|
6
|
+
- Global validation is centralized in `@forgeon/core` via `createValidationPipe()`.
|
|
7
|
+
- Current defaults:
|
|
7
8
|
- `whitelist: true`
|
|
8
9
|
- `transform: true`
|
|
10
|
+
- `validationError.target: false`
|
|
11
|
+
- `validationError.value: false`
|
|
9
12
|
- Keep DTO validation messages stable and explicit.
|
|
10
13
|
- For required values, use a consistent key or message pattern.
|
|
11
14
|
|
|
@@ -24,3 +27,5 @@
|
|
|
24
27
|
- `error.status`
|
|
25
28
|
- optional `error.details`
|
|
26
29
|
- Validation details should be structured (not `any`).
|
|
30
|
+
- `core-validation` formats validation details as:
|
|
31
|
+
- `{ field?: string, message: string }[]`
|
|
@@ -88,15 +88,23 @@ export class CoreExceptionFilter implements ExceptionFilter {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
private resolveDetails(payload: unknown, status: number): AppErrorDetails | undefined {
|
|
91
|
-
if (
|
|
91
|
+
if (typeof payload !== 'object' || payload === null) {
|
|
92
92
|
return undefined;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
const obj = payload as { message?: unknown; details?: unknown };
|
|
96
|
+
|
|
97
|
+
if (Array.isArray(obj.details)) {
|
|
98
|
+
return obj.details as AppErrorDetails;
|
|
99
|
+
}
|
|
100
|
+
if (obj.details && typeof obj.details === 'object') {
|
|
101
|
+
return obj.details as AppErrorDetails;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (status !== HttpStatus.BAD_REQUEST) {
|
|
96
105
|
return undefined;
|
|
97
106
|
}
|
|
98
107
|
|
|
99
|
-
const obj = payload as { message?: unknown };
|
|
100
108
|
const messages = Array.isArray(obj.message)
|
|
101
109
|
? obj.message.filter((item): item is string => typeof item === 'string')
|
|
102
110
|
: typeof obj.message === 'string'
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { BadRequestException, ValidationPipe } from '@nestjs/common';
|
|
2
|
+
import type { ValidationErrorDetail } from '../errors';
|
|
3
|
+
|
|
4
|
+
type ValidationNode = {
|
|
5
|
+
property?: string;
|
|
6
|
+
constraints?: Record<string, string>;
|
|
7
|
+
children?: ValidationNode[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function toPath(parentPath: string, property: string | undefined): string {
|
|
11
|
+
if (!property || property.length === 0) {
|
|
12
|
+
return parentPath;
|
|
13
|
+
}
|
|
14
|
+
return parentPath.length > 0 ? `${parentPath}.${property}` : property;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function collectValidationDetails(
|
|
18
|
+
errors: ValidationNode[],
|
|
19
|
+
parentPath = '',
|
|
20
|
+
): ValidationErrorDetail[] {
|
|
21
|
+
const details: ValidationErrorDetail[] = [];
|
|
22
|
+
|
|
23
|
+
for (const error of errors) {
|
|
24
|
+
const field = toPath(parentPath, error.property);
|
|
25
|
+
const constraints = error.constraints ? Object.values(error.constraints) : [];
|
|
26
|
+
|
|
27
|
+
for (const message of constraints) {
|
|
28
|
+
details.push(field.length > 0 ? { field, message } : { message });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (Array.isArray(error.children) && error.children.length > 0) {
|
|
32
|
+
details.push(...collectValidationDetails(error.children, field));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return details;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createValidationPipe(): ValidationPipe {
|
|
40
|
+
return new ValidationPipe({
|
|
41
|
+
whitelist: true,
|
|
42
|
+
transform: true,
|
|
43
|
+
validationError: {
|
|
44
|
+
target: false,
|
|
45
|
+
value: false,
|
|
46
|
+
},
|
|
47
|
+
exceptionFactory: (errors) => {
|
|
48
|
+
const details = collectValidationDetails(errors as ValidationNode[]);
|
|
49
|
+
const firstMessage = details[0]?.message ?? 'Validation failed';
|
|
50
|
+
return new BadRequestException({
|
|
51
|
+
message: firstMessage,
|
|
52
|
+
details,
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './core-validation.pipe';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forgeon/db-prisma",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc -p tsconfig.json"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@nestjs/common": "^11.0.1",
|
|
12
|
+
"@nestjs/config": "^4.0.2",
|
|
13
|
+
"@prisma/client": "^6.18.0",
|
|
14
|
+
"zod": "^3.23.8"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^22.10.7",
|
|
18
|
+
"typescript": "^5.7.3"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { registerAs } from '@nestjs/config';
|
|
2
|
+
import { parseDbPrismaEnv } from './db-prisma-env.schema';
|
|
3
|
+
|
|
4
|
+
export const DB_PRISMA_CONFIG_NAMESPACE = 'dbPrisma';
|
|
5
|
+
|
|
6
|
+
export interface DbPrismaConfigValues {
|
|
7
|
+
databaseUrl: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const dbPrismaConfig = registerAs(
|
|
11
|
+
DB_PRISMA_CONFIG_NAMESPACE,
|
|
12
|
+
(): DbPrismaConfigValues => {
|
|
13
|
+
const env = parseDbPrismaEnv(process.env);
|
|
14
|
+
return {
|
|
15
|
+
databaseUrl: env.DATABASE_URL,
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { ConfigService } from '@nestjs/config';
|
|
3
|
+
import { DB_PRISMA_CONFIG_NAMESPACE } from './db-prisma-config.loader';
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class DbPrismaConfigService {
|
|
7
|
+
constructor(private readonly configService: ConfigService) {}
|
|
8
|
+
|
|
9
|
+
get databaseUrl(): string {
|
|
10
|
+
return this.configService.getOrThrow<string>(`${DB_PRISMA_CONFIG_NAMESPACE}.databaseUrl`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const dbPrismaEnvSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
DATABASE_URL: z
|
|
6
|
+
.string()
|
|
7
|
+
.trim()
|
|
8
|
+
.min(1)
|
|
9
|
+
.default('postgresql://postgres:postgres@localhost:5432/app?schema=public'),
|
|
10
|
+
})
|
|
11
|
+
.passthrough();
|
|
12
|
+
|
|
13
|
+
export type DbPrismaEnv = z.infer<typeof dbPrismaEnvSchema>;
|
|
14
|
+
|
|
15
|
+
export function parseDbPrismaEnv(input: Record<string, unknown>): DbPrismaEnv {
|
|
16
|
+
return dbPrismaEnvSchema.parse(input);
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Global, Module } from '@nestjs/common';
|
|
2
|
+
import { ConfigModule } from '@nestjs/config';
|
|
3
|
+
import { dbPrismaConfig } from './db-prisma-config.loader';
|
|
4
|
+
import { DbPrismaConfigService } from './db-prisma-config.service';
|
|
5
|
+
import { PrismaService } from './prisma.service';
|
|
6
|
+
|
|
7
|
+
@Global()
|
|
8
|
+
@Module({
|
|
9
|
+
imports: [ConfigModule.forFeature(dbPrismaConfig)],
|
|
10
|
+
providers: [DbPrismaConfigService, PrismaService],
|
|
11
|
+
exports: [DbPrismaConfigService, PrismaService],
|
|
12
|
+
})
|
|
13
|
+
export class DbPrismaModule {}
|
|
@@ -1,27 +1,24 @@
|
|
|
1
|
-
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
|
2
|
-
import { PrismaClient } from '@prisma/client';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
await this.$disconnect();
|
|
26
|
-
}
|
|
27
|
-
}
|
|
1
|
+
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { PrismaClient } from '@prisma/client';
|
|
3
|
+
import { DbPrismaConfigService } from './db-prisma-config.service';
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
|
7
|
+
constructor(configService: DbPrismaConfigService) {
|
|
8
|
+
super({
|
|
9
|
+
datasources: {
|
|
10
|
+
db: {
|
|
11
|
+
url: configService.databaseUrl,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async onModuleInit(): Promise<void> {
|
|
18
|
+
await this.$connect();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async onModuleDestroy(): Promise<void> {
|
|
22
|
+
await this.$disconnect();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
## Config Strategy
|
|
8
8
|
|
|
9
|
-
- `@forgeon/core` owns base runtime config
|
|
9
|
+
- `@forgeon/core` owns base runtime config, global error envelope/filter, and validation pipe defaults.
|
|
10
10
|
- Core config is validated with Zod and exposed through typed accessors.
|
|
11
11
|
- Add-modules own and validate only their module-specific env keys.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
## Error Handling Strategy
|
|
2
|
+
|
|
3
|
+
- `@forgeon/core` owns the global HTTP error envelope.
|
|
4
|
+
- API apps import `CoreErrorsModule` and register `CoreExceptionFilter` as a global filter.
|
|
5
|
+
- Envelope fields:
|
|
6
|
+
- `error.code`
|
|
7
|
+
- `error.message`
|
|
8
|
+
- `error.status`
|
|
9
|
+
- `error.details` (optional, mainly validation context)
|
|
10
|
+
- `error.requestId` (optional, derived from `x-request-id`)
|
|
11
|
+
- `error.timestamp`
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
## Default DB Stack
|
|
2
|
-
|
|
3
|
-
Current default stack is `{{DB_LABEL}}`.
|
|
4
|
-
|
|
5
|
-
- Prisma schema and migrations live in `apps/api/prisma`
|
|
6
|
-
- DB access is encapsulated via `
|
|
7
|
-
-
|
|
1
|
+
## Default DB Stack
|
|
2
|
+
|
|
3
|
+
Current default stack is `{{DB_LABEL}}`.
|
|
4
|
+
|
|
5
|
+
- Prisma schema and migrations live in `apps/api/prisma`
|
|
6
|
+
- DB access is encapsulated via `DbPrismaModule` in `@forgeon/db-prisma`
|
|
7
|
+
- `db-prisma` is currently default-applied during scaffold generation.
|
|
8
|
+
- Future direction: this DB layer may be extracted into an explicit add-module/preset and optionally exposed via CLI flag(s).
|
|
9
|
+
- Additional DB presets are intentionally out of scope for the current milestone.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
## Scope Freeze (Current)
|
|
2
|
-
|
|
3
|
-
- Frontend preset selection is disabled (React is fixed).
|
|
4
|
-
- DB preset selection is disabled (Prisma/Postgres is fixed).
|
|
5
|
-
- Docker is always generated; runtime proxy is selectable (`caddy|nginx|none`).
|
|
1
|
+
## Scope Freeze (Current)
|
|
2
|
+
|
|
3
|
+
- Frontend preset selection is disabled (React is fixed).
|
|
4
|
+
- DB preset selection is disabled (Prisma/Postgres is fixed).
|
|
5
|
+
- Docker is always generated; runtime proxy is selectable (`caddy|nginx|none`).
|
|
6
|
+
- DB preset flags may return in a future milestone after `db-prisma` is separated into an explicit preset/module flow.
|
|
@@ -2,4 +2,5 @@
|
|
|
2
2
|
|
|
3
3
|
- `apps/api` - NestJS backend
|
|
4
4
|
- `apps/web` - React frontend (`{{FRONTEND_LABEL}}`, fixed)
|
|
5
|
-
- `packages/core` - shared backend core package (`core-config`
|
|
5
|
+
- `packages/core` - shared backend core package (`core-config`, `core-errors`, `core-validation`)
|
|
6
|
+
- `packages/db-prisma` - default DB module (`DbPrismaModule`, Prisma service + config)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
### Error Handling
|
|
2
|
+
|
|
3
|
+
`core-errors` is enabled by default.
|
|
4
|
+
|
|
5
|
+
- `CoreErrorsModule` is imported in `apps/api/src/app.module.ts`.
|
|
6
|
+
- `CoreExceptionFilter` is registered globally in `apps/api/src/main.ts`.
|
|
7
|
+
- Controllers and services should throw standard Nest exceptions; envelope formatting is handled centrally.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
## Error Handling (`core-errors`)
|
|
2
|
+
|
|
3
|
+
`@forgeon/core` includes a default global exception filter (`CoreExceptionFilter`).
|
|
4
|
+
|
|
5
|
+
Wiring:
|
|
6
|
+
- module import: `apps/api/src/app.module.ts` (`CoreErrorsModule`)
|
|
7
|
+
- global registration: `apps/api/src/main.ts` (`app.useGlobalFilters(app.get(CoreExceptionFilter))`)
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
- throw standard Nest exceptions in services/controllers:
|
|
11
|
+
- `throw new ConflictException('Email already exists')`
|
|
12
|
+
- `throw new NotFoundException('Resource not found')`
|
|
13
|
+
|
|
14
|
+
Response envelope:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"error": {
|
|
19
|
+
"code": "conflict",
|
|
20
|
+
"message": "Email already exists",
|
|
21
|
+
"status": 409,
|
|
22
|
+
"details": [],
|
|
23
|
+
"requestId": "optional",
|
|
24
|
+
"timestamp": "2026-02-25T12:00:00.000Z"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|