@webpieces/dev-config 0.2.79 → 0.2.80
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/architecture/executors/validate-code/executor.d.ts +7 -0
- package/architecture/executors/validate-code/executor.js +13 -2
- package/architecture/executors/validate-code/executor.js.map +1 -1
- package/architecture/executors/validate-code/executor.ts +23 -2
- package/architecture/executors/validate-code/schema.json +21 -0
- package/architecture/executors/validate-dtos/executor.d.ts +38 -0
- package/architecture/executors/validate-dtos/executor.js +370 -0
- package/architecture/executors/validate-dtos/executor.js.map +1 -0
- package/architecture/executors/validate-dtos/executor.ts +471 -0
- package/architecture/executors/validate-dtos/schema.json +24 -0
- package/executors.json +5 -0
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { ExecutorContext } from '@nx/devkit';
|
|
|
2
2
|
import { ReturnTypeMode } from '../validate-return-types/executor';
|
|
3
3
|
import { NoInlineTypesMode } from '../validate-no-inline-types/executor';
|
|
4
4
|
import { NoAnyUnknownMode } from '../validate-no-any-unknown/executor';
|
|
5
|
+
import { ValidateDtosMode } from '../validate-dtos/executor';
|
|
5
6
|
export type MethodMaxLimitMode = 'OFF' | 'NEW_METHODS' | 'NEW_AND_MODIFIED_METHODS' | 'MODIFIED_FILES';
|
|
6
7
|
export type FileMaxLimitMode = 'OFF' | 'MODIFIED_FILES';
|
|
7
8
|
export interface MethodMaxLimitConfig {
|
|
@@ -16,12 +17,18 @@ export interface FileMaxLimitConfig {
|
|
|
16
17
|
disableAllowed?: boolean;
|
|
17
18
|
ignoreModifiedUntilEpoch?: number;
|
|
18
19
|
}
|
|
20
|
+
export interface ValidateDtosConfig {
|
|
21
|
+
mode?: ValidateDtosMode;
|
|
22
|
+
prismaSchemaPath?: string;
|
|
23
|
+
dtoSourcePaths?: string[];
|
|
24
|
+
}
|
|
19
25
|
export interface ValidateCodeOptions {
|
|
20
26
|
methodMaxLimit?: MethodMaxLimitConfig;
|
|
21
27
|
fileMaxLimit?: FileMaxLimitConfig;
|
|
22
28
|
requireReturnTypeMode?: ReturnTypeMode;
|
|
23
29
|
noInlineTypeLiteralsMode?: NoInlineTypesMode;
|
|
24
30
|
noAnyUnknownMode?: NoAnyUnknownMode;
|
|
31
|
+
validateDtos?: ValidateDtosConfig;
|
|
25
32
|
}
|
|
26
33
|
export interface ExecutorResult {
|
|
27
34
|
success: boolean;
|
|
@@ -8,6 +8,7 @@ const executor_3 = tslib_1.__importDefault(require("../validate-modified-files/e
|
|
|
8
8
|
const executor_4 = tslib_1.__importDefault(require("../validate-return-types/executor"));
|
|
9
9
|
const executor_5 = tslib_1.__importDefault(require("../validate-no-inline-types/executor"));
|
|
10
10
|
const executor_6 = tslib_1.__importDefault(require("../validate-no-any-unknown/executor"));
|
|
11
|
+
const executor_7 = tslib_1.__importDefault(require("../validate-dtos/executor"));
|
|
11
12
|
function formatEpochDate(epoch) {
|
|
12
13
|
return new Date(epoch * 1000).toISOString().split('T')[0];
|
|
13
14
|
}
|
|
@@ -63,6 +64,9 @@ function parseConfig(options) {
|
|
|
63
64
|
returnTypeMode: options.requireReturnTypeMode ?? 'OFF',
|
|
64
65
|
noInlineTypesMode: options.noInlineTypeLiteralsMode ?? 'OFF',
|
|
65
66
|
noAnyUnknownMode: options.noAnyUnknownMode ?? 'OFF',
|
|
67
|
+
validateDtosMode: options.validateDtos?.mode ?? 'OFF',
|
|
68
|
+
validateDtosPrismaPath: options.validateDtos?.prismaSchemaPath,
|
|
69
|
+
validateDtosSrcPaths: options.validateDtos?.dtoSourcePaths ?? [],
|
|
66
70
|
};
|
|
67
71
|
}
|
|
68
72
|
function formatOverride(override) {
|
|
@@ -78,12 +82,13 @@ function logConfig(config) {
|
|
|
78
82
|
console.log(` Require return types: ${config.returnTypeMode}`);
|
|
79
83
|
console.log(` No inline type literals: ${config.noInlineTypesMode}`);
|
|
80
84
|
console.log(` No any/unknown: ${config.noAnyUnknownMode}`);
|
|
85
|
+
console.log(` Validate DTOs: ${config.validateDtosMode}`);
|
|
81
86
|
console.log('');
|
|
82
87
|
}
|
|
83
88
|
function isAllOff(config) {
|
|
84
89
|
return config.methodMode === 'OFF' && config.fileMode === 'OFF' &&
|
|
85
90
|
config.returnTypeMode === 'OFF' && config.noInlineTypesMode === 'OFF' &&
|
|
86
|
-
config.noAnyUnknownMode === 'OFF';
|
|
91
|
+
config.noAnyUnknownMode === 'OFF' && config.validateDtosMode === 'OFF';
|
|
87
92
|
}
|
|
88
93
|
async function runMethodValidators(config, context) {
|
|
89
94
|
const results = [];
|
|
@@ -116,9 +121,15 @@ async function runExecutor(options, context) {
|
|
|
116
121
|
const returnTypesResult = await (0, executor_4.default)({ mode: config.returnTypeMode }, context);
|
|
117
122
|
const noInlineTypesResult = await (0, executor_5.default)({ mode: config.noInlineTypesMode }, context);
|
|
118
123
|
const noAnyUnknownResult = await (0, executor_6.default)({ mode: config.noAnyUnknownMode }, context);
|
|
124
|
+
const validateDtosResult = await (0, executor_7.default)({
|
|
125
|
+
mode: config.validateDtosMode,
|
|
126
|
+
prismaSchemaPath: config.validateDtosPrismaPath,
|
|
127
|
+
dtoSourcePaths: config.validateDtosSrcPaths,
|
|
128
|
+
}, context);
|
|
119
129
|
const allSuccess = methodResults.every((r) => r.success) &&
|
|
120
130
|
fileResult.success && returnTypesResult.success &&
|
|
121
|
-
noInlineTypesResult.success && noAnyUnknownResult.success
|
|
131
|
+
noInlineTypesResult.success && noAnyUnknownResult.success &&
|
|
132
|
+
validateDtosResult.success;
|
|
122
133
|
console.log(allSuccess ? '\n\u2705 All code validations passed\n' : '\n\u274c Some code validations failed\n');
|
|
123
134
|
return { success: allSuccess };
|
|
124
135
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/dev-config/architecture/executors/validate-code/executor.ts"],"names":[],"mappings":"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","sourcesContent":["import { ExecutorContext } from '@nx/devkit';\nimport runNewMethodsExecutor from '../validate-new-methods/executor';\nimport runModifiedMethodsExecutor from '../validate-modified-methods/executor';\nimport runModifiedFilesExecutor from '../validate-modified-files/executor';\nimport runReturnTypesExecutor, { ReturnTypeMode } from '../validate-return-types/executor';\nimport runNoInlineTypesExecutor, { NoInlineTypesMode } from '../validate-no-inline-types/executor';\nimport runNoAnyUnknownExecutor, { NoAnyUnknownMode } from '../validate-no-any-unknown/executor';\n\nexport type MethodMaxLimitMode = 'OFF' | 'NEW_METHODS' | 'NEW_AND_MODIFIED_METHODS' | 'MODIFIED_FILES';\nexport type FileMaxLimitMode = 'OFF' | 'MODIFIED_FILES';\n\nexport interface MethodMaxLimitConfig {\n limit?: number;\n mode?: MethodMaxLimitMode;\n disableAllowed?: boolean;\n ignoreModifiedUntilEpoch?: number;\n}\n\nexport interface FileMaxLimitConfig {\n limit?: number;\n mode?: FileMaxLimitMode;\n disableAllowed?: boolean;\n ignoreModifiedUntilEpoch?: number;\n}\n\nexport interface ValidateCodeOptions {\n methodMaxLimit?: MethodMaxLimitConfig;\n fileMaxLimit?: FileMaxLimitConfig;\n requireReturnTypeMode?: ReturnTypeMode;\n noInlineTypeLiteralsMode?: NoInlineTypesMode;\n noAnyUnknownMode?: NoAnyUnknownMode;\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\ninterface OverrideInfo {\n active: boolean;\n normalMode: string;\n expiresDate: string;\n}\n\ninterface ParsedConfig {\n methodLimit: number;\n methodMode: MethodMaxLimitMode;\n methodDisableAllowed: boolean;\n methodOverride: OverrideInfo | undefined;\n fileLimit: number;\n fileMode: FileMaxLimitMode;\n fileDisableAllowed: boolean;\n fileOverride: OverrideInfo | undefined;\n returnTypeMode: ReturnTypeMode;\n noInlineTypesMode: NoInlineTypesMode;\n noAnyUnknownMode: NoAnyUnknownMode;\n}\n\ninterface ResolvedMethodMode {\n mode: MethodMaxLimitMode;\n override: OverrideInfo | undefined;\n}\n\ninterface ResolvedFileMode {\n mode: FileMaxLimitMode;\n override: OverrideInfo | undefined;\n}\n\nfunction formatEpochDate(epoch: number): string {\n return new Date(epoch * 1000).toISOString().split('T')[0];\n}\n\nfunction resolveMethodMode(\n normalMode: MethodMaxLimitMode, epoch: number | undefined\n): ResolvedMethodMode {\n if (epoch === undefined) {\n return { mode: normalMode, override: undefined };\n }\n const nowSeconds = Date.now() / 1000;\n if (nowSeconds < epoch) {\n // Active: downgrade to skip modified checking\n const downgraded: MethodMaxLimitMode =\n normalMode === 'OFF' ? 'OFF' : 'NEW_METHODS';\n return {\n mode: downgraded,\n override: { active: true, normalMode, expiresDate: formatEpochDate(epoch) },\n };\n }\n // Expired\n console.log(`\\n\\u26a0\\ufe0f methodMaxLimit.ignoreModifiedUntilEpoch (${epoch}) has expired (${formatEpochDate(epoch)}). Remove it from nx.json. Using normal mode: ${normalMode}\\n`);\n return { mode: normalMode, override: undefined };\n}\n\nfunction resolveFileMode(\n normalMode: FileMaxLimitMode, epoch: number | undefined\n): ResolvedFileMode {\n if (epoch === undefined) {\n return { mode: normalMode, override: undefined };\n }\n const nowSeconds = Date.now() / 1000;\n if (nowSeconds < epoch) {\n // Active: file checking is inherently about modified files, so skip entirely\n return {\n mode: 'OFF',\n override: { active: true, normalMode, expiresDate: formatEpochDate(epoch) },\n };\n }\n // Expired\n console.log(`\\n\\u26a0\\ufe0f fileMaxLimit.ignoreModifiedUntilEpoch (${epoch}) has expired (${formatEpochDate(epoch)}). Remove it from nx.json. Using normal mode: ${normalMode}\\n`);\n return { mode: normalMode, override: undefined };\n}\n\nfunction parseConfig(options: ValidateCodeOptions): ParsedConfig {\n const methodConfig: MethodMaxLimitConfig = options.methodMaxLimit ?? {};\n const fileConfig: FileMaxLimitConfig = options.fileMaxLimit ?? {};\n\n const normalMethodMode = methodConfig.mode ?? 'NEW_AND_MODIFIED_METHODS';\n const normalFileMode = fileConfig.mode ?? 'MODIFIED_FILES';\n\n const methodResolved = resolveMethodMode(normalMethodMode, methodConfig.ignoreModifiedUntilEpoch);\n const fileResolved = resolveFileMode(normalFileMode, fileConfig.ignoreModifiedUntilEpoch);\n\n return {\n methodLimit: methodConfig.limit ?? 80,\n methodMode: methodResolved.mode,\n methodDisableAllowed: methodConfig.disableAllowed ?? true,\n methodOverride: methodResolved.override,\n fileLimit: fileConfig.limit ?? 900,\n fileMode: fileResolved.mode,\n fileDisableAllowed: fileConfig.disableAllowed ?? true,\n fileOverride: fileResolved.override,\n returnTypeMode: options.requireReturnTypeMode ?? 'OFF',\n noInlineTypesMode: options.noInlineTypeLiteralsMode ?? 'OFF',\n noAnyUnknownMode: options.noAnyUnknownMode ?? 'OFF',\n };\n}\n\nfunction formatOverride(override: OverrideInfo | undefined): string {\n if (!override) {\n return '';\n }\n return ` (override active, normal: ${override.normalMode}, expires: ${override.expiresDate})`;\n}\n\nfunction logConfig(config: ParsedConfig): void {\n console.log('\\n\\ud83d\\udccf Running Code Validations\\n');\n console.log(` Method limits: mode=${config.methodMode}${formatOverride(config.methodOverride)}, limit=${config.methodLimit}, disableAllowed=${config.methodDisableAllowed}`);\n console.log(` File limits: mode=${config.fileMode}${formatOverride(config.fileOverride)}, limit=${config.fileLimit}, disableAllowed=${config.fileDisableAllowed}`);\n console.log(` Require return types: ${config.returnTypeMode}`);\n console.log(` No inline type literals: ${config.noInlineTypesMode}`);\n console.log(` No any/unknown: ${config.noAnyUnknownMode}`);\n console.log('');\n}\n\nfunction isAllOff(config: ParsedConfig): boolean {\n return config.methodMode === 'OFF' && config.fileMode === 'OFF' &&\n config.returnTypeMode === 'OFF' && config.noInlineTypesMode === 'OFF' &&\n config.noAnyUnknownMode === 'OFF';\n}\n\nasync function runMethodValidators(config: ParsedConfig, context: ExecutorContext): Promise<ExecutorResult[]> {\n const results: ExecutorResult[] = [];\n const runNew = config.methodMode === 'NEW_METHODS' || config.methodMode === 'NEW_AND_MODIFIED_METHODS';\n const runModified = config.methodMode === 'NEW_AND_MODIFIED_METHODS' || config.methodMode === 'MODIFIED_FILES';\n\n if (runNew) {\n results.push(await runNewMethodsExecutor({\n limit: config.methodLimit,\n mode: config.methodMode, disableAllowed: config.methodDisableAllowed,\n }, context));\n }\n if (runModified) {\n results.push(await runModifiedMethodsExecutor({\n limit: config.methodLimit, mode: config.methodMode, disableAllowed: config.methodDisableAllowed,\n }, context));\n }\n return results;\n}\n\nexport default async function runExecutor(\n options: ValidateCodeOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const config = parseConfig(options);\n\n if (isAllOff(config)) {\n console.log('\\n\\u23ed\\ufe0f Skipping all code validations (all modes: OFF)\\n');\n return { success: true };\n }\n\n logConfig(config);\n\n const methodResults = await runMethodValidators(config, context);\n const fileResult = await runModifiedFilesExecutor({\n limit: config.fileLimit, mode: config.fileMode, disableAllowed: config.fileDisableAllowed,\n }, context);\n const returnTypesResult = await runReturnTypesExecutor({ mode: config.returnTypeMode }, context);\n const noInlineTypesResult = await runNoInlineTypesExecutor({ mode: config.noInlineTypesMode }, context);\n const noAnyUnknownResult = await runNoAnyUnknownExecutor({ mode: config.noAnyUnknownMode }, context);\n\n const allSuccess = methodResults.every((r) => r.success) &&\n fileResult.success && returnTypesResult.success &&\n noInlineTypesResult.success && noAnyUnknownResult.success;\n\n console.log(allSuccess ? '\\n\\u2705 All code validations passed\\n' : '\\n\\u274c Some code validations failed\\n');\n return { success: allSuccess };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/dev-config/architecture/executors/validate-code/executor.ts"],"names":[],"mappings":"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","sourcesContent":["import { ExecutorContext } from '@nx/devkit';\nimport runNewMethodsExecutor from '../validate-new-methods/executor';\nimport runModifiedMethodsExecutor from '../validate-modified-methods/executor';\nimport runModifiedFilesExecutor from '../validate-modified-files/executor';\nimport runReturnTypesExecutor, { ReturnTypeMode } from '../validate-return-types/executor';\nimport runNoInlineTypesExecutor, { NoInlineTypesMode } from '../validate-no-inline-types/executor';\nimport runNoAnyUnknownExecutor, { NoAnyUnknownMode } from '../validate-no-any-unknown/executor';\nimport runValidateDtosExecutor, { ValidateDtosMode } from '../validate-dtos/executor';\n\nexport type MethodMaxLimitMode = 'OFF' | 'NEW_METHODS' | 'NEW_AND_MODIFIED_METHODS' | 'MODIFIED_FILES';\nexport type FileMaxLimitMode = 'OFF' | 'MODIFIED_FILES';\n\nexport interface MethodMaxLimitConfig {\n limit?: number;\n mode?: MethodMaxLimitMode;\n disableAllowed?: boolean;\n ignoreModifiedUntilEpoch?: number;\n}\n\nexport interface FileMaxLimitConfig {\n limit?: number;\n mode?: FileMaxLimitMode;\n disableAllowed?: boolean;\n ignoreModifiedUntilEpoch?: number;\n}\n\nexport interface ValidateDtosConfig {\n mode?: ValidateDtosMode;\n prismaSchemaPath?: string;\n dtoSourcePaths?: string[];\n}\n\nexport interface ValidateCodeOptions {\n methodMaxLimit?: MethodMaxLimitConfig;\n fileMaxLimit?: FileMaxLimitConfig;\n requireReturnTypeMode?: ReturnTypeMode;\n noInlineTypeLiteralsMode?: NoInlineTypesMode;\n noAnyUnknownMode?: NoAnyUnknownMode;\n validateDtos?: ValidateDtosConfig;\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\ninterface OverrideInfo {\n active: boolean;\n normalMode: string;\n expiresDate: string;\n}\n\ninterface ParsedConfig {\n methodLimit: number;\n methodMode: MethodMaxLimitMode;\n methodDisableAllowed: boolean;\n methodOverride: OverrideInfo | undefined;\n fileLimit: number;\n fileMode: FileMaxLimitMode;\n fileDisableAllowed: boolean;\n fileOverride: OverrideInfo | undefined;\n returnTypeMode: ReturnTypeMode;\n noInlineTypesMode: NoInlineTypesMode;\n noAnyUnknownMode: NoAnyUnknownMode;\n validateDtosMode: ValidateDtosMode;\n validateDtosPrismaPath: string | undefined;\n validateDtosSrcPaths: string[];\n}\n\ninterface ResolvedMethodMode {\n mode: MethodMaxLimitMode;\n override: OverrideInfo | undefined;\n}\n\ninterface ResolvedFileMode {\n mode: FileMaxLimitMode;\n override: OverrideInfo | undefined;\n}\n\nfunction formatEpochDate(epoch: number): string {\n return new Date(epoch * 1000).toISOString().split('T')[0];\n}\n\nfunction resolveMethodMode(\n normalMode: MethodMaxLimitMode, epoch: number | undefined\n): ResolvedMethodMode {\n if (epoch === undefined) {\n return { mode: normalMode, override: undefined };\n }\n const nowSeconds = Date.now() / 1000;\n if (nowSeconds < epoch) {\n // Active: downgrade to skip modified checking\n const downgraded: MethodMaxLimitMode =\n normalMode === 'OFF' ? 'OFF' : 'NEW_METHODS';\n return {\n mode: downgraded,\n override: { active: true, normalMode, expiresDate: formatEpochDate(epoch) },\n };\n }\n // Expired\n console.log(`\\n\\u26a0\\ufe0f methodMaxLimit.ignoreModifiedUntilEpoch (${epoch}) has expired (${formatEpochDate(epoch)}). Remove it from nx.json. Using normal mode: ${normalMode}\\n`);\n return { mode: normalMode, override: undefined };\n}\n\nfunction resolveFileMode(\n normalMode: FileMaxLimitMode, epoch: number | undefined\n): ResolvedFileMode {\n if (epoch === undefined) {\n return { mode: normalMode, override: undefined };\n }\n const nowSeconds = Date.now() / 1000;\n if (nowSeconds < epoch) {\n // Active: file checking is inherently about modified files, so skip entirely\n return {\n mode: 'OFF',\n override: { active: true, normalMode, expiresDate: formatEpochDate(epoch) },\n };\n }\n // Expired\n console.log(`\\n\\u26a0\\ufe0f fileMaxLimit.ignoreModifiedUntilEpoch (${epoch}) has expired (${formatEpochDate(epoch)}). Remove it from nx.json. Using normal mode: ${normalMode}\\n`);\n return { mode: normalMode, override: undefined };\n}\n\nfunction parseConfig(options: ValidateCodeOptions): ParsedConfig {\n const methodConfig: MethodMaxLimitConfig = options.methodMaxLimit ?? {};\n const fileConfig: FileMaxLimitConfig = options.fileMaxLimit ?? {};\n\n const normalMethodMode = methodConfig.mode ?? 'NEW_AND_MODIFIED_METHODS';\n const normalFileMode = fileConfig.mode ?? 'MODIFIED_FILES';\n\n const methodResolved = resolveMethodMode(normalMethodMode, methodConfig.ignoreModifiedUntilEpoch);\n const fileResolved = resolveFileMode(normalFileMode, fileConfig.ignoreModifiedUntilEpoch);\n\n return {\n methodLimit: methodConfig.limit ?? 80,\n methodMode: methodResolved.mode,\n methodDisableAllowed: methodConfig.disableAllowed ?? true,\n methodOverride: methodResolved.override,\n fileLimit: fileConfig.limit ?? 900,\n fileMode: fileResolved.mode,\n fileDisableAllowed: fileConfig.disableAllowed ?? true,\n fileOverride: fileResolved.override,\n returnTypeMode: options.requireReturnTypeMode ?? 'OFF',\n noInlineTypesMode: options.noInlineTypeLiteralsMode ?? 'OFF',\n noAnyUnknownMode: options.noAnyUnknownMode ?? 'OFF',\n validateDtosMode: options.validateDtos?.mode ?? 'OFF',\n validateDtosPrismaPath: options.validateDtos?.prismaSchemaPath,\n validateDtosSrcPaths: options.validateDtos?.dtoSourcePaths ?? [],\n };\n}\n\nfunction formatOverride(override: OverrideInfo | undefined): string {\n if (!override) {\n return '';\n }\n return ` (override active, normal: ${override.normalMode}, expires: ${override.expiresDate})`;\n}\n\nfunction logConfig(config: ParsedConfig): void {\n console.log('\\n\\ud83d\\udccf Running Code Validations\\n');\n console.log(` Method limits: mode=${config.methodMode}${formatOverride(config.methodOverride)}, limit=${config.methodLimit}, disableAllowed=${config.methodDisableAllowed}`);\n console.log(` File limits: mode=${config.fileMode}${formatOverride(config.fileOverride)}, limit=${config.fileLimit}, disableAllowed=${config.fileDisableAllowed}`);\n console.log(` Require return types: ${config.returnTypeMode}`);\n console.log(` No inline type literals: ${config.noInlineTypesMode}`);\n console.log(` No any/unknown: ${config.noAnyUnknownMode}`);\n console.log(` Validate DTOs: ${config.validateDtosMode}`);\n console.log('');\n}\n\nfunction isAllOff(config: ParsedConfig): boolean {\n return config.methodMode === 'OFF' && config.fileMode === 'OFF' &&\n config.returnTypeMode === 'OFF' && config.noInlineTypesMode === 'OFF' &&\n config.noAnyUnknownMode === 'OFF' && config.validateDtosMode === 'OFF';\n}\n\nasync function runMethodValidators(config: ParsedConfig, context: ExecutorContext): Promise<ExecutorResult[]> {\n const results: ExecutorResult[] = [];\n const runNew = config.methodMode === 'NEW_METHODS' || config.methodMode === 'NEW_AND_MODIFIED_METHODS';\n const runModified = config.methodMode === 'NEW_AND_MODIFIED_METHODS' || config.methodMode === 'MODIFIED_FILES';\n\n if (runNew) {\n results.push(await runNewMethodsExecutor({\n limit: config.methodLimit,\n mode: config.methodMode, disableAllowed: config.methodDisableAllowed,\n }, context));\n }\n if (runModified) {\n results.push(await runModifiedMethodsExecutor({\n limit: config.methodLimit, mode: config.methodMode, disableAllowed: config.methodDisableAllowed,\n }, context));\n }\n return results;\n}\n\nexport default async function runExecutor(\n options: ValidateCodeOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const config = parseConfig(options);\n\n if (isAllOff(config)) {\n console.log('\\n\\u23ed\\ufe0f Skipping all code validations (all modes: OFF)\\n');\n return { success: true };\n }\n\n logConfig(config);\n\n const methodResults = await runMethodValidators(config, context);\n const fileResult = await runModifiedFilesExecutor({\n limit: config.fileLimit, mode: config.fileMode, disableAllowed: config.fileDisableAllowed,\n }, context);\n const returnTypesResult = await runReturnTypesExecutor({ mode: config.returnTypeMode }, context);\n const noInlineTypesResult = await runNoInlineTypesExecutor({ mode: config.noInlineTypesMode }, context);\n const noAnyUnknownResult = await runNoAnyUnknownExecutor({ mode: config.noAnyUnknownMode }, context);\n const validateDtosResult = await runValidateDtosExecutor({\n mode: config.validateDtosMode,\n prismaSchemaPath: config.validateDtosPrismaPath,\n dtoSourcePaths: config.validateDtosSrcPaths,\n }, context);\n\n const allSuccess = methodResults.every((r) => r.success) &&\n fileResult.success && returnTypesResult.success &&\n noInlineTypesResult.success && noAnyUnknownResult.success &&\n validateDtosResult.success;\n\n console.log(allSuccess ? '\\n\\u2705 All code validations passed\\n' : '\\n\\u274c Some code validations failed\\n');\n return { success: allSuccess };\n}\n"]}
|
|
@@ -5,6 +5,7 @@ import runModifiedFilesExecutor from '../validate-modified-files/executor';
|
|
|
5
5
|
import runReturnTypesExecutor, { ReturnTypeMode } from '../validate-return-types/executor';
|
|
6
6
|
import runNoInlineTypesExecutor, { NoInlineTypesMode } from '../validate-no-inline-types/executor';
|
|
7
7
|
import runNoAnyUnknownExecutor, { NoAnyUnknownMode } from '../validate-no-any-unknown/executor';
|
|
8
|
+
import runValidateDtosExecutor, { ValidateDtosMode } from '../validate-dtos/executor';
|
|
8
9
|
|
|
9
10
|
export type MethodMaxLimitMode = 'OFF' | 'NEW_METHODS' | 'NEW_AND_MODIFIED_METHODS' | 'MODIFIED_FILES';
|
|
10
11
|
export type FileMaxLimitMode = 'OFF' | 'MODIFIED_FILES';
|
|
@@ -23,12 +24,19 @@ export interface FileMaxLimitConfig {
|
|
|
23
24
|
ignoreModifiedUntilEpoch?: number;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
export interface ValidateDtosConfig {
|
|
28
|
+
mode?: ValidateDtosMode;
|
|
29
|
+
prismaSchemaPath?: string;
|
|
30
|
+
dtoSourcePaths?: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
export interface ValidateCodeOptions {
|
|
27
34
|
methodMaxLimit?: MethodMaxLimitConfig;
|
|
28
35
|
fileMaxLimit?: FileMaxLimitConfig;
|
|
29
36
|
requireReturnTypeMode?: ReturnTypeMode;
|
|
30
37
|
noInlineTypeLiteralsMode?: NoInlineTypesMode;
|
|
31
38
|
noAnyUnknownMode?: NoAnyUnknownMode;
|
|
39
|
+
validateDtos?: ValidateDtosConfig;
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
export interface ExecutorResult {
|
|
@@ -53,6 +61,9 @@ interface ParsedConfig {
|
|
|
53
61
|
returnTypeMode: ReturnTypeMode;
|
|
54
62
|
noInlineTypesMode: NoInlineTypesMode;
|
|
55
63
|
noAnyUnknownMode: NoAnyUnknownMode;
|
|
64
|
+
validateDtosMode: ValidateDtosMode;
|
|
65
|
+
validateDtosPrismaPath: string | undefined;
|
|
66
|
+
validateDtosSrcPaths: string[];
|
|
56
67
|
}
|
|
57
68
|
|
|
58
69
|
interface ResolvedMethodMode {
|
|
@@ -131,6 +142,9 @@ function parseConfig(options: ValidateCodeOptions): ParsedConfig {
|
|
|
131
142
|
returnTypeMode: options.requireReturnTypeMode ?? 'OFF',
|
|
132
143
|
noInlineTypesMode: options.noInlineTypeLiteralsMode ?? 'OFF',
|
|
133
144
|
noAnyUnknownMode: options.noAnyUnknownMode ?? 'OFF',
|
|
145
|
+
validateDtosMode: options.validateDtos?.mode ?? 'OFF',
|
|
146
|
+
validateDtosPrismaPath: options.validateDtos?.prismaSchemaPath,
|
|
147
|
+
validateDtosSrcPaths: options.validateDtos?.dtoSourcePaths ?? [],
|
|
134
148
|
};
|
|
135
149
|
}
|
|
136
150
|
|
|
@@ -148,13 +162,14 @@ function logConfig(config: ParsedConfig): void {
|
|
|
148
162
|
console.log(` Require return types: ${config.returnTypeMode}`);
|
|
149
163
|
console.log(` No inline type literals: ${config.noInlineTypesMode}`);
|
|
150
164
|
console.log(` No any/unknown: ${config.noAnyUnknownMode}`);
|
|
165
|
+
console.log(` Validate DTOs: ${config.validateDtosMode}`);
|
|
151
166
|
console.log('');
|
|
152
167
|
}
|
|
153
168
|
|
|
154
169
|
function isAllOff(config: ParsedConfig): boolean {
|
|
155
170
|
return config.methodMode === 'OFF' && config.fileMode === 'OFF' &&
|
|
156
171
|
config.returnTypeMode === 'OFF' && config.noInlineTypesMode === 'OFF' &&
|
|
157
|
-
config.noAnyUnknownMode === 'OFF';
|
|
172
|
+
config.noAnyUnknownMode === 'OFF' && config.validateDtosMode === 'OFF';
|
|
158
173
|
}
|
|
159
174
|
|
|
160
175
|
async function runMethodValidators(config: ParsedConfig, context: ExecutorContext): Promise<ExecutorResult[]> {
|
|
@@ -196,10 +211,16 @@ export default async function runExecutor(
|
|
|
196
211
|
const returnTypesResult = await runReturnTypesExecutor({ mode: config.returnTypeMode }, context);
|
|
197
212
|
const noInlineTypesResult = await runNoInlineTypesExecutor({ mode: config.noInlineTypesMode }, context);
|
|
198
213
|
const noAnyUnknownResult = await runNoAnyUnknownExecutor({ mode: config.noAnyUnknownMode }, context);
|
|
214
|
+
const validateDtosResult = await runValidateDtosExecutor({
|
|
215
|
+
mode: config.validateDtosMode,
|
|
216
|
+
prismaSchemaPath: config.validateDtosPrismaPath,
|
|
217
|
+
dtoSourcePaths: config.validateDtosSrcPaths,
|
|
218
|
+
}, context);
|
|
199
219
|
|
|
200
220
|
const allSuccess = methodResults.every((r) => r.success) &&
|
|
201
221
|
fileResult.success && returnTypesResult.success &&
|
|
202
|
-
noInlineTypesResult.success && noAnyUnknownResult.success
|
|
222
|
+
noInlineTypesResult.success && noAnyUnknownResult.success &&
|
|
223
|
+
validateDtosResult.success;
|
|
203
224
|
|
|
204
225
|
console.log(allSuccess ? '\n\u2705 All code validations passed\n' : '\n\u274c Some code validations failed\n');
|
|
205
226
|
return { success: allSuccess };
|
|
@@ -73,6 +73,27 @@
|
|
|
73
73
|
"enum": ["OFF", "MODIFIED_CODE", "MODIFIED_FILES"],
|
|
74
74
|
"description": "OFF: skip validation. MODIFIED_CODE: only changed lines in diff. MODIFIED_FILES: all in modified files. Disallows `any` and `unknown` TypeScript keywords.",
|
|
75
75
|
"default": "OFF"
|
|
76
|
+
},
|
|
77
|
+
"validateDtos": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"description": "Validate DTO fields match Prisma Dbo model fields. Prevents AI from inventing field names.",
|
|
80
|
+
"properties": {
|
|
81
|
+
"mode": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"enum": ["OFF", "MODIFIED_FILES"],
|
|
84
|
+
"description": "OFF: skip validation. MODIFIED_FILES: validate Dto files that were modified in the diff.",
|
|
85
|
+
"default": "OFF"
|
|
86
|
+
},
|
|
87
|
+
"prismaSchemaPath": {
|
|
88
|
+
"type": "string",
|
|
89
|
+
"description": "Relative path from workspace root to schema.prisma"
|
|
90
|
+
},
|
|
91
|
+
"dtoSourcePaths": {
|
|
92
|
+
"type": "array",
|
|
93
|
+
"items": { "type": "string" },
|
|
94
|
+
"description": "Array of directories (relative to workspace root) containing Dto files"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
76
97
|
}
|
|
77
98
|
},
|
|
78
99
|
"required": []
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate DTOs Executor
|
|
3
|
+
*
|
|
4
|
+
* Validates that every non-deprecated field in a XxxDto class/interface exists
|
|
5
|
+
* in the corresponding XxxDbo Prisma model. This catches AI agents inventing
|
|
6
|
+
* field names that don't match the database schema.
|
|
7
|
+
*
|
|
8
|
+
* ============================================================================
|
|
9
|
+
* MODES
|
|
10
|
+
* ============================================================================
|
|
11
|
+
* - OFF: Skip validation entirely
|
|
12
|
+
* - MODIFIED_FILES: Validate Dto files that were modified in the diff
|
|
13
|
+
*
|
|
14
|
+
* ============================================================================
|
|
15
|
+
* SKIP CONDITIONS
|
|
16
|
+
* ============================================================================
|
|
17
|
+
* - If schema.prisma itself is modified, validation is skipped (schema in flux)
|
|
18
|
+
* - Dto classes ending with "JoinDto" are skipped (they compose other Dtos)
|
|
19
|
+
* - Fields marked @deprecated in a comment are exempt
|
|
20
|
+
*
|
|
21
|
+
* ============================================================================
|
|
22
|
+
* MATCHING
|
|
23
|
+
* ============================================================================
|
|
24
|
+
* - UserDto matches UserDbo by case-insensitive prefix ("user")
|
|
25
|
+
* - Dto fields must be a subset of Dbo fields
|
|
26
|
+
* - Extra Dbo fields are allowed (e.g., password)
|
|
27
|
+
*/
|
|
28
|
+
import type { ExecutorContext } from '@nx/devkit';
|
|
29
|
+
export type ValidateDtosMode = 'OFF' | 'MODIFIED_FILES';
|
|
30
|
+
export interface ValidateDtosOptions {
|
|
31
|
+
mode?: ValidateDtosMode;
|
|
32
|
+
prismaSchemaPath?: string;
|
|
33
|
+
dtoSourcePaths?: string[];
|
|
34
|
+
}
|
|
35
|
+
export interface ExecutorResult {
|
|
36
|
+
success: boolean;
|
|
37
|
+
}
|
|
38
|
+
export default function runExecutor(options: ValidateDtosOptions, context: ExecutorContext): Promise<ExecutorResult>;
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Validate DTOs Executor
|
|
4
|
+
*
|
|
5
|
+
* Validates that every non-deprecated field in a XxxDto class/interface exists
|
|
6
|
+
* in the corresponding XxxDbo Prisma model. This catches AI agents inventing
|
|
7
|
+
* field names that don't match the database schema.
|
|
8
|
+
*
|
|
9
|
+
* ============================================================================
|
|
10
|
+
* MODES
|
|
11
|
+
* ============================================================================
|
|
12
|
+
* - OFF: Skip validation entirely
|
|
13
|
+
* - MODIFIED_FILES: Validate Dto files that were modified in the diff
|
|
14
|
+
*
|
|
15
|
+
* ============================================================================
|
|
16
|
+
* SKIP CONDITIONS
|
|
17
|
+
* ============================================================================
|
|
18
|
+
* - If schema.prisma itself is modified, validation is skipped (schema in flux)
|
|
19
|
+
* - Dto classes ending with "JoinDto" are skipped (they compose other Dtos)
|
|
20
|
+
* - Fields marked @deprecated in a comment are exempt
|
|
21
|
+
*
|
|
22
|
+
* ============================================================================
|
|
23
|
+
* MATCHING
|
|
24
|
+
* ============================================================================
|
|
25
|
+
* - UserDto matches UserDbo by case-insensitive prefix ("user")
|
|
26
|
+
* - Dto fields must be a subset of Dbo fields
|
|
27
|
+
* - Extra Dbo fields are allowed (e.g., password)
|
|
28
|
+
*/
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.default = runExecutor;
|
|
31
|
+
const tslib_1 = require("tslib");
|
|
32
|
+
const child_process_1 = require("child_process");
|
|
33
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
34
|
+
const path = tslib_1.__importStar(require("path"));
|
|
35
|
+
const ts = tslib_1.__importStar(require("typescript"));
|
|
36
|
+
/**
|
|
37
|
+
* Auto-detect the base branch by finding the merge-base with origin/main.
|
|
38
|
+
*/
|
|
39
|
+
function detectBase(workspaceRoot) {
|
|
40
|
+
try {
|
|
41
|
+
const mergeBase = (0, child_process_1.execSync)('git merge-base HEAD origin/main', {
|
|
42
|
+
cwd: workspaceRoot,
|
|
43
|
+
encoding: 'utf-8',
|
|
44
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
45
|
+
}).trim();
|
|
46
|
+
if (mergeBase) {
|
|
47
|
+
return mergeBase;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
try {
|
|
52
|
+
const mergeBase = (0, child_process_1.execSync)('git merge-base HEAD main', {
|
|
53
|
+
cwd: workspaceRoot,
|
|
54
|
+
encoding: 'utf-8',
|
|
55
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
56
|
+
}).trim();
|
|
57
|
+
if (mergeBase) {
|
|
58
|
+
return mergeBase;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Ignore
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get changed files between base and head (or working tree if head not specified).
|
|
69
|
+
*/
|
|
70
|
+
// webpieces-disable max-lines-new-methods -- Git command handling with untracked files requires multiple code paths
|
|
71
|
+
function getChangedFiles(workspaceRoot, base, head) {
|
|
72
|
+
try {
|
|
73
|
+
const diffTarget = head ? `${base} ${head}` : base;
|
|
74
|
+
const output = (0, child_process_1.execSync)(`git diff --name-only ${diffTarget}`, {
|
|
75
|
+
cwd: workspaceRoot,
|
|
76
|
+
encoding: 'utf-8',
|
|
77
|
+
});
|
|
78
|
+
const changedFiles = output
|
|
79
|
+
.trim()
|
|
80
|
+
.split('\n')
|
|
81
|
+
.filter((f) => f.length > 0);
|
|
82
|
+
if (!head) {
|
|
83
|
+
try {
|
|
84
|
+
const untrackedOutput = (0, child_process_1.execSync)('git ls-files --others --exclude-standard', {
|
|
85
|
+
cwd: workspaceRoot,
|
|
86
|
+
encoding: 'utf-8',
|
|
87
|
+
});
|
|
88
|
+
const untrackedFiles = untrackedOutput
|
|
89
|
+
.trim()
|
|
90
|
+
.split('\n')
|
|
91
|
+
.filter((f) => f.length > 0);
|
|
92
|
+
const allFiles = new Set([...changedFiles, ...untrackedFiles]);
|
|
93
|
+
return Array.from(allFiles);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return changedFiles;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return changedFiles;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Parse schema.prisma to build a map of Dbo model name -> set of field names.
|
|
107
|
+
* Only models whose name ends with "Dbo" are included.
|
|
108
|
+
*/
|
|
109
|
+
function parsePrismaSchema(schemaPath) {
|
|
110
|
+
const models = new Map();
|
|
111
|
+
if (!fs.existsSync(schemaPath)) {
|
|
112
|
+
return models;
|
|
113
|
+
}
|
|
114
|
+
const content = fs.readFileSync(schemaPath, 'utf-8');
|
|
115
|
+
const lines = content.split('\n');
|
|
116
|
+
let currentModel = null;
|
|
117
|
+
let currentFields = null;
|
|
118
|
+
for (const line of lines) {
|
|
119
|
+
const trimmed = line.trim();
|
|
120
|
+
// Match model declaration: model XxxDbo {
|
|
121
|
+
const modelMatch = trimmed.match(/^model\s+(\w+Dbo)\s*\{/);
|
|
122
|
+
if (modelMatch) {
|
|
123
|
+
currentModel = modelMatch[1];
|
|
124
|
+
currentFields = new Set();
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// End of model block
|
|
128
|
+
if (currentModel && trimmed === '}') {
|
|
129
|
+
models.set(currentModel, currentFields);
|
|
130
|
+
currentModel = null;
|
|
131
|
+
currentFields = null;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
// Inside a model block - extract field names
|
|
135
|
+
if (currentModel && currentFields) {
|
|
136
|
+
// Skip empty lines, comments, and model-level attributes (@@)
|
|
137
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@')) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
// Field name is the first word on the line
|
|
141
|
+
const fieldMatch = trimmed.match(/^(\w+)\s/);
|
|
142
|
+
if (fieldMatch) {
|
|
143
|
+
currentFields.add(fieldMatch[1]);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return models;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Check if a field has @deprecated in a comment above it (within 3 lines).
|
|
151
|
+
*/
|
|
152
|
+
function isFieldDeprecated(fileLines, fieldLine) {
|
|
153
|
+
const start = Math.max(0, fieldLine - 4);
|
|
154
|
+
for (let i = start; i <= fieldLine - 1; i++) {
|
|
155
|
+
const line = fileLines[i]?.trim() ?? '';
|
|
156
|
+
if (line.includes('@deprecated'))
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Parse a TypeScript file to find Dto class/interface declarations and their fields.
|
|
163
|
+
* Skips classes ending with "JoinDto" since they compose other Dtos.
|
|
164
|
+
*/
|
|
165
|
+
// webpieces-disable max-lines-new-methods -- AST traversal for both class and interface Dto detection with field extraction
|
|
166
|
+
function findDtosInFile(filePath, workspaceRoot) {
|
|
167
|
+
const fullPath = path.join(workspaceRoot, filePath);
|
|
168
|
+
if (!fs.existsSync(fullPath))
|
|
169
|
+
return [];
|
|
170
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
171
|
+
const fileLines = content.split('\n');
|
|
172
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
173
|
+
const dtos = [];
|
|
174
|
+
function visit(node) {
|
|
175
|
+
const isClass = ts.isClassDeclaration(node);
|
|
176
|
+
const isInterface = ts.isInterfaceDeclaration(node);
|
|
177
|
+
if ((isClass || isInterface) && node.name) {
|
|
178
|
+
const name = node.name.text;
|
|
179
|
+
// Must end with Dto but NOT with JoinDto
|
|
180
|
+
if (name.endsWith('Dto') && !name.endsWith('JoinDto')) {
|
|
181
|
+
const fields = [];
|
|
182
|
+
for (const member of node.members) {
|
|
183
|
+
if (ts.isPropertyDeclaration(member) || ts.isPropertySignature(member)) {
|
|
184
|
+
if (member.name && ts.isIdentifier(member.name)) {
|
|
185
|
+
const fieldName = member.name.text;
|
|
186
|
+
const startPos = member.getStart(sourceFile);
|
|
187
|
+
const pos = sourceFile.getLineAndCharacterOfPosition(startPos);
|
|
188
|
+
const line = pos.line + 1;
|
|
189
|
+
const deprecated = isFieldDeprecated(fileLines, line);
|
|
190
|
+
fields.push({ name: fieldName, line, deprecated });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
dtos.push({ name, file: filePath, fields });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
ts.forEachChild(node, visit);
|
|
198
|
+
}
|
|
199
|
+
visit(sourceFile);
|
|
200
|
+
return dtos;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Extract the prefix from a Dto/Dbo name by removing the suffix.
|
|
204
|
+
* e.g., "UserDto" -> "user", "UserDbo" -> "user"
|
|
205
|
+
*/
|
|
206
|
+
function extractPrefix(name, suffix) {
|
|
207
|
+
return name.slice(0, -suffix.length).toLowerCase();
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Find violations: Dto fields that don't exist in the corresponding Dbo.
|
|
211
|
+
*/
|
|
212
|
+
function findViolations(dtos, dboModels) {
|
|
213
|
+
const violations = [];
|
|
214
|
+
// Build a lowercase prefix -> Dbo info map
|
|
215
|
+
const dboByPrefix = new Map();
|
|
216
|
+
for (const [dboName, fields] of dboModels) {
|
|
217
|
+
const prefix = extractPrefix(dboName, 'Dbo');
|
|
218
|
+
dboByPrefix.set(prefix, { name: dboName, fields });
|
|
219
|
+
}
|
|
220
|
+
for (const dto of dtos) {
|
|
221
|
+
const prefix = extractPrefix(dto.name, 'Dto');
|
|
222
|
+
const dbo = dboByPrefix.get(prefix);
|
|
223
|
+
if (!dbo) {
|
|
224
|
+
// No matching Dbo found - skip (might be a Dto without a DB table)
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
for (const field of dto.fields) {
|
|
228
|
+
if (field.deprecated)
|
|
229
|
+
continue;
|
|
230
|
+
if (!dbo.fields.has(field.name)) {
|
|
231
|
+
violations.push({
|
|
232
|
+
file: dto.file,
|
|
233
|
+
line: field.line,
|
|
234
|
+
dtoName: dto.name,
|
|
235
|
+
fieldName: field.name,
|
|
236
|
+
dboName: dbo.name,
|
|
237
|
+
availableFields: Array.from(dbo.fields).sort(),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return violations;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Report violations to console.
|
|
246
|
+
*/
|
|
247
|
+
function reportViolations(violations) {
|
|
248
|
+
console.error('');
|
|
249
|
+
console.error('❌ DTO fields don\'t match Prisma Dbo models!');
|
|
250
|
+
console.error('');
|
|
251
|
+
console.error('📚 Every non-deprecated field in a Dto must exist in the corresponding Dbo.');
|
|
252
|
+
console.error(' This prevents AI from inventing field names that don\'t match the database schema.');
|
|
253
|
+
console.error(' Dbo can have extra fields (e.g., password) - Dto cannot.');
|
|
254
|
+
console.error('');
|
|
255
|
+
for (const v of violations) {
|
|
256
|
+
console.error(` ❌ ${v.file}:${v.line}`);
|
|
257
|
+
console.error(` ${v.dtoName}.${v.fieldName} does not exist in ${v.dboName}`);
|
|
258
|
+
console.error(` Available Dbo fields: ${v.availableFields.join(', ')}`);
|
|
259
|
+
}
|
|
260
|
+
console.error('');
|
|
261
|
+
console.error(' Dto fields must be a subset of Dbo fields (matching TypeScript field names from schema.prisma).');
|
|
262
|
+
console.error(' Fields marked @deprecated in the Dto are exempt from this check.');
|
|
263
|
+
console.error('');
|
|
264
|
+
console.error(' When needing fields from multiple tables (e.g., a join), use a XxxJoinDto that');
|
|
265
|
+
console.error(' contains YYDto and ZZDto fields from the other tables instead of flattening.');
|
|
266
|
+
console.error('');
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Filter changed files to only TypeScript Dto source files within configured paths.
|
|
270
|
+
*/
|
|
271
|
+
function filterDtoFiles(changedFiles, dtoSourcePaths) {
|
|
272
|
+
return changedFiles.filter((f) => {
|
|
273
|
+
if (!f.endsWith('.ts') && !f.endsWith('.tsx'))
|
|
274
|
+
return false;
|
|
275
|
+
if (f.includes('.spec.ts') || f.includes('.test.ts'))
|
|
276
|
+
return false;
|
|
277
|
+
return dtoSourcePaths.some((srcPath) => f.startsWith(srcPath));
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Collect all Dto definitions from the given files.
|
|
282
|
+
*/
|
|
283
|
+
function collectDtos(dtoFiles, workspaceRoot) {
|
|
284
|
+
const allDtos = [];
|
|
285
|
+
for (const file of dtoFiles) {
|
|
286
|
+
const dtos = findDtosInFile(file, workspaceRoot);
|
|
287
|
+
allDtos.push(...dtos);
|
|
288
|
+
}
|
|
289
|
+
return allDtos;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Resolve git base ref from env vars or auto-detection.
|
|
293
|
+
*/
|
|
294
|
+
function resolveBase(workspaceRoot) {
|
|
295
|
+
const envBase = process.env['NX_BASE'];
|
|
296
|
+
if (envBase)
|
|
297
|
+
return envBase;
|
|
298
|
+
return detectBase(workspaceRoot) ?? undefined;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Run the core validation after early-exit checks have passed.
|
|
302
|
+
*/
|
|
303
|
+
function validateDtoFiles(workspaceRoot, prismaSchemaPath, changedFiles, dtoSourcePaths) {
|
|
304
|
+
if (changedFiles.some((f) => f.endsWith(prismaSchemaPath))) {
|
|
305
|
+
console.log('⏭️ Skipping validate-dtos (schema.prisma is modified - schema in flux)');
|
|
306
|
+
console.log('');
|
|
307
|
+
return { success: true };
|
|
308
|
+
}
|
|
309
|
+
const dtoFiles = filterDtoFiles(changedFiles, dtoSourcePaths);
|
|
310
|
+
if (dtoFiles.length === 0) {
|
|
311
|
+
console.log('✅ No Dto files changed');
|
|
312
|
+
return { success: true };
|
|
313
|
+
}
|
|
314
|
+
console.log(`📂 Checking ${dtoFiles.length} changed file(s) for Dto definitions...`);
|
|
315
|
+
const fullSchemaPath = path.join(workspaceRoot, prismaSchemaPath);
|
|
316
|
+
const dboModels = parsePrismaSchema(fullSchemaPath);
|
|
317
|
+
if (dboModels.size === 0) {
|
|
318
|
+
console.log('⏭️ No Dbo models found in schema.prisma');
|
|
319
|
+
console.log('');
|
|
320
|
+
return { success: true };
|
|
321
|
+
}
|
|
322
|
+
console.log(` Found ${dboModels.size} Dbo model(s) in schema.prisma`);
|
|
323
|
+
const allDtos = collectDtos(dtoFiles, workspaceRoot);
|
|
324
|
+
if (allDtos.length === 0) {
|
|
325
|
+
console.log('✅ No Dto definitions found in changed files');
|
|
326
|
+
return { success: true };
|
|
327
|
+
}
|
|
328
|
+
console.log(` Found ${allDtos.length} Dto definition(s) in changed files`);
|
|
329
|
+
const violations = findViolations(allDtos, dboModels);
|
|
330
|
+
if (violations.length === 0) {
|
|
331
|
+
console.log('✅ All Dto fields match their Dbo models');
|
|
332
|
+
return { success: true };
|
|
333
|
+
}
|
|
334
|
+
reportViolations(violations);
|
|
335
|
+
return { success: false };
|
|
336
|
+
}
|
|
337
|
+
async function runExecutor(options, context) {
|
|
338
|
+
const workspaceRoot = context.root;
|
|
339
|
+
const mode = options.mode ?? 'OFF';
|
|
340
|
+
if (mode === 'OFF') {
|
|
341
|
+
console.log('\n⏭️ Skipping validate-dtos (mode: OFF)');
|
|
342
|
+
console.log('');
|
|
343
|
+
return { success: true };
|
|
344
|
+
}
|
|
345
|
+
const prismaSchemaPath = options.prismaSchemaPath;
|
|
346
|
+
const dtoSourcePaths = options.dtoSourcePaths ?? [];
|
|
347
|
+
if (!prismaSchemaPath || dtoSourcePaths.length === 0) {
|
|
348
|
+
const reason = !prismaSchemaPath ? 'no prismaSchemaPath configured' : 'no dtoSourcePaths configured';
|
|
349
|
+
console.log(`\n⏭️ Skipping validate-dtos (${reason})`);
|
|
350
|
+
console.log('');
|
|
351
|
+
return { success: true };
|
|
352
|
+
}
|
|
353
|
+
console.log('\n📏 Validating DTOs match Prisma Dbo models\n');
|
|
354
|
+
console.log(` Mode: ${mode}`);
|
|
355
|
+
console.log(` Schema: ${prismaSchemaPath}`);
|
|
356
|
+
console.log(` Dto paths: ${dtoSourcePaths.join(', ')}`);
|
|
357
|
+
const base = resolveBase(workspaceRoot);
|
|
358
|
+
const head = process.env['NX_HEAD'];
|
|
359
|
+
if (!base) {
|
|
360
|
+
console.log('\n⏭️ Skipping validate-dtos (could not detect base branch)');
|
|
361
|
+
console.log('');
|
|
362
|
+
return { success: true };
|
|
363
|
+
}
|
|
364
|
+
console.log(` Base: ${base}`);
|
|
365
|
+
console.log(` Head: ${head ?? 'working tree (includes uncommitted changes)'}`);
|
|
366
|
+
console.log('');
|
|
367
|
+
const changedFiles = getChangedFiles(workspaceRoot, base, head);
|
|
368
|
+
return validateDtoFiles(workspaceRoot, prismaSchemaPath, changedFiles, dtoSourcePaths);
|
|
369
|
+
}
|
|
370
|
+
//# sourceMappingURL=executor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/dev-config/architecture/executors/validate-dtos/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Validate DTOs Executor\n *\n * Validates that every non-deprecated field in a XxxDto class/interface exists\n * in the corresponding XxxDbo Prisma model. This catches AI agents inventing\n * field names that don't match the database schema.\n *\n * ============================================================================\n * MODES\n * ============================================================================\n * - OFF: Skip validation entirely\n * - MODIFIED_FILES: Validate Dto files that were modified in the diff\n *\n * ============================================================================\n * SKIP CONDITIONS\n * ============================================================================\n * - If schema.prisma itself is modified, validation is skipped (schema in flux)\n * - Dto classes ending with \"JoinDto\" are skipped (they compose other Dtos)\n * - Fields marked @deprecated in a comment are exempt\n *\n * ============================================================================\n * MATCHING\n * ============================================================================\n * - UserDto matches UserDbo by case-insensitive prefix (\"user\")\n * - Dto fields must be a subset of Dbo fields\n * - Extra Dbo fields are allowed (e.g., password)\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { execSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as ts from 'typescript';\n\nexport type ValidateDtosMode = 'OFF' | 'MODIFIED_FILES';\n\nexport interface ValidateDtosOptions {\n mode?: ValidateDtosMode;\n prismaSchemaPath?: string;\n dtoSourcePaths?: string[];\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\ninterface DtoFieldInfo {\n name: string;\n line: number;\n deprecated: boolean;\n}\n\ninterface DtoInfo {\n name: string;\n file: string;\n fields: DtoFieldInfo[];\n}\n\ninterface DtoViolation {\n file: string;\n line: number;\n dtoName: string;\n fieldName: string;\n dboName: string;\n availableFields: string[];\n}\n\ninterface DboEntry {\n name: string;\n fields: Set<string>;\n}\n\n/**\n * Auto-detect the base branch by finding the merge-base with origin/main.\n */\nfunction detectBase(workspaceRoot: string): string | null {\n try {\n const mergeBase = execSync('git merge-base HEAD origin/main', {\n cwd: workspaceRoot,\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n\n if (mergeBase) {\n return mergeBase;\n }\n } catch {\n try {\n const mergeBase = execSync('git merge-base HEAD main', {\n cwd: workspaceRoot,\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n\n if (mergeBase) {\n return mergeBase;\n }\n } catch {\n // Ignore\n }\n }\n return null;\n}\n\n/**\n * Get changed files between base and head (or working tree if head not specified).\n */\n// webpieces-disable max-lines-new-methods -- Git command handling with untracked files requires multiple code paths\nfunction getChangedFiles(workspaceRoot: string, base: string, head?: string): string[] {\n try {\n const diffTarget = head ? `${base} ${head}` : base;\n const output = execSync(`git diff --name-only ${diffTarget}`, {\n cwd: workspaceRoot,\n encoding: 'utf-8',\n });\n const changedFiles = output\n .trim()\n .split('\\n')\n .filter((f) => f.length > 0);\n\n if (!head) {\n try {\n const untrackedOutput = execSync('git ls-files --others --exclude-standard', {\n cwd: workspaceRoot,\n encoding: 'utf-8',\n });\n const untrackedFiles = untrackedOutput\n .trim()\n .split('\\n')\n .filter((f) => f.length > 0);\n const allFiles = new Set([...changedFiles, ...untrackedFiles]);\n return Array.from(allFiles);\n } catch {\n return changedFiles;\n }\n }\n\n return changedFiles;\n } catch {\n return [];\n }\n}\n\n/**\n * Parse schema.prisma to build a map of Dbo model name -> set of field names.\n * Only models whose name ends with \"Dbo\" are included.\n */\nfunction parsePrismaSchema(schemaPath: string): Map<string, Set<string>> {\n const models = new Map<string, Set<string>>();\n\n if (!fs.existsSync(schemaPath)) {\n return models;\n }\n\n const content = fs.readFileSync(schemaPath, 'utf-8');\n const lines = content.split('\\n');\n\n let currentModel: string | null = null;\n let currentFields: Set<string> | null = null;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Match model declaration: model XxxDbo {\n const modelMatch = trimmed.match(/^model\\s+(\\w+Dbo)\\s*\\{/);\n if (modelMatch) {\n currentModel = modelMatch[1];\n currentFields = new Set<string>();\n continue;\n }\n\n // End of model block\n if (currentModel && trimmed === '}') {\n models.set(currentModel, currentFields!);\n currentModel = null;\n currentFields = null;\n continue;\n }\n\n // Inside a model block - extract field names\n if (currentModel && currentFields) {\n // Skip empty lines, comments, and model-level attributes (@@)\n if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@')) {\n continue;\n }\n\n // Field name is the first word on the line\n const fieldMatch = trimmed.match(/^(\\w+)\\s/);\n if (fieldMatch) {\n currentFields.add(fieldMatch[1]);\n }\n }\n }\n\n return models;\n}\n\n/**\n * Check if a field has @deprecated in a comment above it (within 3 lines).\n */\nfunction isFieldDeprecated(fileLines: string[], fieldLine: number): boolean {\n const start = Math.max(0, fieldLine - 4);\n for (let i = start; i <= fieldLine - 1; i++) {\n const line = fileLines[i]?.trim() ?? '';\n if (line.includes('@deprecated')) return true;\n }\n return false;\n}\n\n/**\n * Parse a TypeScript file to find Dto class/interface declarations and their fields.\n * Skips classes ending with \"JoinDto\" since they compose other Dtos.\n */\n// webpieces-disable max-lines-new-methods -- AST traversal for both class and interface Dto detection with field extraction\nfunction findDtosInFile(filePath: string, workspaceRoot: string): DtoInfo[] {\n const fullPath = path.join(workspaceRoot, filePath);\n if (!fs.existsSync(fullPath)) return [];\n\n const content = fs.readFileSync(fullPath, 'utf-8');\n const fileLines = content.split('\\n');\n const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);\n\n const dtos: DtoInfo[] = [];\n\n function visit(node: ts.Node): void {\n const isClass = ts.isClassDeclaration(node);\n const isInterface = ts.isInterfaceDeclaration(node);\n\n if ((isClass || isInterface) && node.name) {\n const name = node.name.text;\n\n // Must end with Dto but NOT with JoinDto\n if (name.endsWith('Dto') && !name.endsWith('JoinDto')) {\n const fields: DtoFieldInfo[] = [];\n\n for (const member of node.members) {\n if (ts.isPropertyDeclaration(member) || ts.isPropertySignature(member)) {\n if (member.name && ts.isIdentifier(member.name)) {\n const fieldName = member.name.text;\n const startPos = member.getStart(sourceFile);\n const pos = sourceFile.getLineAndCharacterOfPosition(startPos);\n const line = pos.line + 1;\n const deprecated = isFieldDeprecated(fileLines, line);\n\n fields.push({ name: fieldName, line, deprecated });\n }\n }\n }\n\n dtos.push({ name, file: filePath, fields });\n }\n }\n\n ts.forEachChild(node, visit);\n }\n\n visit(sourceFile);\n return dtos;\n}\n\n/**\n * Extract the prefix from a Dto/Dbo name by removing the suffix.\n * e.g., \"UserDto\" -> \"user\", \"UserDbo\" -> \"user\"\n */\nfunction extractPrefix(name: string, suffix: string): string {\n return name.slice(0, -suffix.length).toLowerCase();\n}\n\n/**\n * Find violations: Dto fields that don't exist in the corresponding Dbo.\n */\nfunction findViolations(\n dtos: DtoInfo[],\n dboModels: Map<string, Set<string>>\n): DtoViolation[] {\n const violations: DtoViolation[] = [];\n\n // Build a lowercase prefix -> Dbo info map\n const dboByPrefix = new Map<string, DboEntry>();\n for (const [dboName, fields] of dboModels) {\n const prefix = extractPrefix(dboName, 'Dbo');\n dboByPrefix.set(prefix, { name: dboName, fields });\n }\n\n for (const dto of dtos) {\n const prefix = extractPrefix(dto.name, 'Dto');\n const dbo = dboByPrefix.get(prefix);\n\n if (!dbo) {\n // No matching Dbo found - skip (might be a Dto without a DB table)\n continue;\n }\n\n for (const field of dto.fields) {\n if (field.deprecated) continue;\n\n if (!dbo.fields.has(field.name)) {\n violations.push({\n file: dto.file,\n line: field.line,\n dtoName: dto.name,\n fieldName: field.name,\n dboName: dbo.name,\n availableFields: Array.from(dbo.fields).sort(),\n });\n }\n }\n }\n\n return violations;\n}\n\n/**\n * Report violations to console.\n */\nfunction reportViolations(violations: DtoViolation[]): void {\n console.error('');\n console.error('❌ DTO fields don\\'t match Prisma Dbo models!');\n console.error('');\n console.error('📚 Every non-deprecated field in a Dto must exist in the corresponding Dbo.');\n console.error(' This prevents AI from inventing field names that don\\'t match the database schema.');\n console.error(' Dbo can have extra fields (e.g., password) - Dto cannot.');\n console.error('');\n\n for (const v of violations) {\n console.error(` ❌ ${v.file}:${v.line}`);\n console.error(` ${v.dtoName}.${v.fieldName} does not exist in ${v.dboName}`);\n console.error(` Available Dbo fields: ${v.availableFields.join(', ')}`);\n }\n console.error('');\n\n console.error(' Dto fields must be a subset of Dbo fields (matching TypeScript field names from schema.prisma).');\n console.error(' Fields marked @deprecated in the Dto are exempt from this check.');\n console.error('');\n console.error(' When needing fields from multiple tables (e.g., a join), use a XxxJoinDto that');\n console.error(' contains YYDto and ZZDto fields from the other tables instead of flattening.');\n console.error('');\n}\n\n/**\n * Filter changed files to only TypeScript Dto source files within configured paths.\n */\nfunction filterDtoFiles(changedFiles: string[], dtoSourcePaths: string[]): string[] {\n return changedFiles.filter((f) => {\n if (!f.endsWith('.ts') && !f.endsWith('.tsx')) return false;\n if (f.includes('.spec.ts') || f.includes('.test.ts')) return false;\n return dtoSourcePaths.some((srcPath) => f.startsWith(srcPath));\n });\n}\n\n/**\n * Collect all Dto definitions from the given files.\n */\nfunction collectDtos(dtoFiles: string[], workspaceRoot: string): DtoInfo[] {\n const allDtos: DtoInfo[] = [];\n for (const file of dtoFiles) {\n const dtos = findDtosInFile(file, workspaceRoot);\n allDtos.push(...dtos);\n }\n return allDtos;\n}\n\n/**\n * Resolve git base ref from env vars or auto-detection.\n */\nfunction resolveBase(workspaceRoot: string): string | undefined {\n const envBase = process.env['NX_BASE'];\n if (envBase) return envBase;\n return detectBase(workspaceRoot) ?? undefined;\n}\n\n/**\n * Run the core validation after early-exit checks have passed.\n */\nfunction validateDtoFiles(\n workspaceRoot: string,\n prismaSchemaPath: string,\n changedFiles: string[],\n dtoSourcePaths: string[]\n): ExecutorResult {\n if (changedFiles.some((f) => f.endsWith(prismaSchemaPath))) {\n console.log('⏭️ Skipping validate-dtos (schema.prisma is modified - schema in flux)');\n console.log('');\n return { success: true };\n }\n\n const dtoFiles = filterDtoFiles(changedFiles, dtoSourcePaths);\n\n if (dtoFiles.length === 0) {\n console.log('✅ No Dto files changed');\n return { success: true };\n }\n\n console.log(`📂 Checking ${dtoFiles.length} changed file(s) for Dto definitions...`);\n\n const fullSchemaPath = path.join(workspaceRoot, prismaSchemaPath);\n const dboModels = parsePrismaSchema(fullSchemaPath);\n\n if (dboModels.size === 0) {\n console.log('⏭️ No Dbo models found in schema.prisma');\n console.log('');\n return { success: true };\n }\n\n console.log(` Found ${dboModels.size} Dbo model(s) in schema.prisma`);\n\n const allDtos = collectDtos(dtoFiles, workspaceRoot);\n\n if (allDtos.length === 0) {\n console.log('✅ No Dto definitions found in changed files');\n return { success: true };\n }\n\n console.log(` Found ${allDtos.length} Dto definition(s) in changed files`);\n\n const violations = findViolations(allDtos, dboModels);\n\n if (violations.length === 0) {\n console.log('✅ All Dto fields match their Dbo models');\n return { success: true };\n }\n\n reportViolations(violations);\n return { success: false };\n}\n\nexport default async function runExecutor(\n options: ValidateDtosOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const workspaceRoot = context.root;\n const mode: ValidateDtosMode = options.mode ?? 'OFF';\n\n if (mode === 'OFF') {\n console.log('\\n⏭️ Skipping validate-dtos (mode: OFF)');\n console.log('');\n return { success: true };\n }\n\n const prismaSchemaPath = options.prismaSchemaPath;\n const dtoSourcePaths = options.dtoSourcePaths ?? [];\n\n if (!prismaSchemaPath || dtoSourcePaths.length === 0) {\n const reason = !prismaSchemaPath ? 'no prismaSchemaPath configured' : 'no dtoSourcePaths configured';\n console.log(`\\n⏭️ Skipping validate-dtos (${reason})`);\n console.log('');\n return { success: true };\n }\n\n console.log('\\n📏 Validating DTOs match Prisma Dbo models\\n');\n console.log(` Mode: ${mode}`);\n console.log(` Schema: ${prismaSchemaPath}`);\n console.log(` Dto paths: ${dtoSourcePaths.join(', ')}`);\n\n const base = resolveBase(workspaceRoot);\n const head = process.env['NX_HEAD'];\n\n if (!base) {\n console.log('\\n⏭️ Skipping validate-dtos (could not detect base branch)');\n console.log('');\n return { success: true };\n }\n\n console.log(` Base: ${base}`);\n console.log(` Head: ${head ?? 'working tree (includes uncommitted changes)'}`);\n console.log('');\n\n const changedFiles = getChangedFiles(workspaceRoot, base, head);\n\n return validateDtoFiles(workspaceRoot, prismaSchemaPath, changedFiles, dtoSourcePaths);\n}\n"]}
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate DTOs Executor
|
|
3
|
+
*
|
|
4
|
+
* Validates that every non-deprecated field in a XxxDto class/interface exists
|
|
5
|
+
* in the corresponding XxxDbo Prisma model. This catches AI agents inventing
|
|
6
|
+
* field names that don't match the database schema.
|
|
7
|
+
*
|
|
8
|
+
* ============================================================================
|
|
9
|
+
* MODES
|
|
10
|
+
* ============================================================================
|
|
11
|
+
* - OFF: Skip validation entirely
|
|
12
|
+
* - MODIFIED_FILES: Validate Dto files that were modified in the diff
|
|
13
|
+
*
|
|
14
|
+
* ============================================================================
|
|
15
|
+
* SKIP CONDITIONS
|
|
16
|
+
* ============================================================================
|
|
17
|
+
* - If schema.prisma itself is modified, validation is skipped (schema in flux)
|
|
18
|
+
* - Dto classes ending with "JoinDto" are skipped (they compose other Dtos)
|
|
19
|
+
* - Fields marked @deprecated in a comment are exempt
|
|
20
|
+
*
|
|
21
|
+
* ============================================================================
|
|
22
|
+
* MATCHING
|
|
23
|
+
* ============================================================================
|
|
24
|
+
* - UserDto matches UserDbo by case-insensitive prefix ("user")
|
|
25
|
+
* - Dto fields must be a subset of Dbo fields
|
|
26
|
+
* - Extra Dbo fields are allowed (e.g., password)
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { ExecutorContext } from '@nx/devkit';
|
|
30
|
+
import { execSync } from 'child_process';
|
|
31
|
+
import * as fs from 'fs';
|
|
32
|
+
import * as path from 'path';
|
|
33
|
+
import * as ts from 'typescript';
|
|
34
|
+
|
|
35
|
+
export type ValidateDtosMode = 'OFF' | 'MODIFIED_FILES';
|
|
36
|
+
|
|
37
|
+
export interface ValidateDtosOptions {
|
|
38
|
+
mode?: ValidateDtosMode;
|
|
39
|
+
prismaSchemaPath?: string;
|
|
40
|
+
dtoSourcePaths?: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ExecutorResult {
|
|
44
|
+
success: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface DtoFieldInfo {
|
|
48
|
+
name: string;
|
|
49
|
+
line: number;
|
|
50
|
+
deprecated: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface DtoInfo {
|
|
54
|
+
name: string;
|
|
55
|
+
file: string;
|
|
56
|
+
fields: DtoFieldInfo[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface DtoViolation {
|
|
60
|
+
file: string;
|
|
61
|
+
line: number;
|
|
62
|
+
dtoName: string;
|
|
63
|
+
fieldName: string;
|
|
64
|
+
dboName: string;
|
|
65
|
+
availableFields: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface DboEntry {
|
|
69
|
+
name: string;
|
|
70
|
+
fields: Set<string>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Auto-detect the base branch by finding the merge-base with origin/main.
|
|
75
|
+
*/
|
|
76
|
+
function detectBase(workspaceRoot: string): string | null {
|
|
77
|
+
try {
|
|
78
|
+
const mergeBase = execSync('git merge-base HEAD origin/main', {
|
|
79
|
+
cwd: workspaceRoot,
|
|
80
|
+
encoding: 'utf-8',
|
|
81
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
82
|
+
}).trim();
|
|
83
|
+
|
|
84
|
+
if (mergeBase) {
|
|
85
|
+
return mergeBase;
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
try {
|
|
89
|
+
const mergeBase = execSync('git merge-base HEAD main', {
|
|
90
|
+
cwd: workspaceRoot,
|
|
91
|
+
encoding: 'utf-8',
|
|
92
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
93
|
+
}).trim();
|
|
94
|
+
|
|
95
|
+
if (mergeBase) {
|
|
96
|
+
return mergeBase;
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Ignore
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get changed files between base and head (or working tree if head not specified).
|
|
107
|
+
*/
|
|
108
|
+
// webpieces-disable max-lines-new-methods -- Git command handling with untracked files requires multiple code paths
|
|
109
|
+
function getChangedFiles(workspaceRoot: string, base: string, head?: string): string[] {
|
|
110
|
+
try {
|
|
111
|
+
const diffTarget = head ? `${base} ${head}` : base;
|
|
112
|
+
const output = execSync(`git diff --name-only ${diffTarget}`, {
|
|
113
|
+
cwd: workspaceRoot,
|
|
114
|
+
encoding: 'utf-8',
|
|
115
|
+
});
|
|
116
|
+
const changedFiles = output
|
|
117
|
+
.trim()
|
|
118
|
+
.split('\n')
|
|
119
|
+
.filter((f) => f.length > 0);
|
|
120
|
+
|
|
121
|
+
if (!head) {
|
|
122
|
+
try {
|
|
123
|
+
const untrackedOutput = execSync('git ls-files --others --exclude-standard', {
|
|
124
|
+
cwd: workspaceRoot,
|
|
125
|
+
encoding: 'utf-8',
|
|
126
|
+
});
|
|
127
|
+
const untrackedFiles = untrackedOutput
|
|
128
|
+
.trim()
|
|
129
|
+
.split('\n')
|
|
130
|
+
.filter((f) => f.length > 0);
|
|
131
|
+
const allFiles = new Set([...changedFiles, ...untrackedFiles]);
|
|
132
|
+
return Array.from(allFiles);
|
|
133
|
+
} catch {
|
|
134
|
+
return changedFiles;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return changedFiles;
|
|
139
|
+
} catch {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Parse schema.prisma to build a map of Dbo model name -> set of field names.
|
|
146
|
+
* Only models whose name ends with "Dbo" are included.
|
|
147
|
+
*/
|
|
148
|
+
function parsePrismaSchema(schemaPath: string): Map<string, Set<string>> {
|
|
149
|
+
const models = new Map<string, Set<string>>();
|
|
150
|
+
|
|
151
|
+
if (!fs.existsSync(schemaPath)) {
|
|
152
|
+
return models;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const content = fs.readFileSync(schemaPath, 'utf-8');
|
|
156
|
+
const lines = content.split('\n');
|
|
157
|
+
|
|
158
|
+
let currentModel: string | null = null;
|
|
159
|
+
let currentFields: Set<string> | null = null;
|
|
160
|
+
|
|
161
|
+
for (const line of lines) {
|
|
162
|
+
const trimmed = line.trim();
|
|
163
|
+
|
|
164
|
+
// Match model declaration: model XxxDbo {
|
|
165
|
+
const modelMatch = trimmed.match(/^model\s+(\w+Dbo)\s*\{/);
|
|
166
|
+
if (modelMatch) {
|
|
167
|
+
currentModel = modelMatch[1];
|
|
168
|
+
currentFields = new Set<string>();
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// End of model block
|
|
173
|
+
if (currentModel && trimmed === '}') {
|
|
174
|
+
models.set(currentModel, currentFields!);
|
|
175
|
+
currentModel = null;
|
|
176
|
+
currentFields = null;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Inside a model block - extract field names
|
|
181
|
+
if (currentModel && currentFields) {
|
|
182
|
+
// Skip empty lines, comments, and model-level attributes (@@)
|
|
183
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@')) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Field name is the first word on the line
|
|
188
|
+
const fieldMatch = trimmed.match(/^(\w+)\s/);
|
|
189
|
+
if (fieldMatch) {
|
|
190
|
+
currentFields.add(fieldMatch[1]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return models;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Check if a field has @deprecated in a comment above it (within 3 lines).
|
|
200
|
+
*/
|
|
201
|
+
function isFieldDeprecated(fileLines: string[], fieldLine: number): boolean {
|
|
202
|
+
const start = Math.max(0, fieldLine - 4);
|
|
203
|
+
for (let i = start; i <= fieldLine - 1; i++) {
|
|
204
|
+
const line = fileLines[i]?.trim() ?? '';
|
|
205
|
+
if (line.includes('@deprecated')) return true;
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Parse a TypeScript file to find Dto class/interface declarations and their fields.
|
|
212
|
+
* Skips classes ending with "JoinDto" since they compose other Dtos.
|
|
213
|
+
*/
|
|
214
|
+
// webpieces-disable max-lines-new-methods -- AST traversal for both class and interface Dto detection with field extraction
|
|
215
|
+
function findDtosInFile(filePath: string, workspaceRoot: string): DtoInfo[] {
|
|
216
|
+
const fullPath = path.join(workspaceRoot, filePath);
|
|
217
|
+
if (!fs.existsSync(fullPath)) return [];
|
|
218
|
+
|
|
219
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
220
|
+
const fileLines = content.split('\n');
|
|
221
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
222
|
+
|
|
223
|
+
const dtos: DtoInfo[] = [];
|
|
224
|
+
|
|
225
|
+
function visit(node: ts.Node): void {
|
|
226
|
+
const isClass = ts.isClassDeclaration(node);
|
|
227
|
+
const isInterface = ts.isInterfaceDeclaration(node);
|
|
228
|
+
|
|
229
|
+
if ((isClass || isInterface) && node.name) {
|
|
230
|
+
const name = node.name.text;
|
|
231
|
+
|
|
232
|
+
// Must end with Dto but NOT with JoinDto
|
|
233
|
+
if (name.endsWith('Dto') && !name.endsWith('JoinDto')) {
|
|
234
|
+
const fields: DtoFieldInfo[] = [];
|
|
235
|
+
|
|
236
|
+
for (const member of node.members) {
|
|
237
|
+
if (ts.isPropertyDeclaration(member) || ts.isPropertySignature(member)) {
|
|
238
|
+
if (member.name && ts.isIdentifier(member.name)) {
|
|
239
|
+
const fieldName = member.name.text;
|
|
240
|
+
const startPos = member.getStart(sourceFile);
|
|
241
|
+
const pos = sourceFile.getLineAndCharacterOfPosition(startPos);
|
|
242
|
+
const line = pos.line + 1;
|
|
243
|
+
const deprecated = isFieldDeprecated(fileLines, line);
|
|
244
|
+
|
|
245
|
+
fields.push({ name: fieldName, line, deprecated });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
dtos.push({ name, file: filePath, fields });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
ts.forEachChild(node, visit);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
visit(sourceFile);
|
|
258
|
+
return dtos;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Extract the prefix from a Dto/Dbo name by removing the suffix.
|
|
263
|
+
* e.g., "UserDto" -> "user", "UserDbo" -> "user"
|
|
264
|
+
*/
|
|
265
|
+
function extractPrefix(name: string, suffix: string): string {
|
|
266
|
+
return name.slice(0, -suffix.length).toLowerCase();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Find violations: Dto fields that don't exist in the corresponding Dbo.
|
|
271
|
+
*/
|
|
272
|
+
function findViolations(
|
|
273
|
+
dtos: DtoInfo[],
|
|
274
|
+
dboModels: Map<string, Set<string>>
|
|
275
|
+
): DtoViolation[] {
|
|
276
|
+
const violations: DtoViolation[] = [];
|
|
277
|
+
|
|
278
|
+
// Build a lowercase prefix -> Dbo info map
|
|
279
|
+
const dboByPrefix = new Map<string, DboEntry>();
|
|
280
|
+
for (const [dboName, fields] of dboModels) {
|
|
281
|
+
const prefix = extractPrefix(dboName, 'Dbo');
|
|
282
|
+
dboByPrefix.set(prefix, { name: dboName, fields });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
for (const dto of dtos) {
|
|
286
|
+
const prefix = extractPrefix(dto.name, 'Dto');
|
|
287
|
+
const dbo = dboByPrefix.get(prefix);
|
|
288
|
+
|
|
289
|
+
if (!dbo) {
|
|
290
|
+
// No matching Dbo found - skip (might be a Dto without a DB table)
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
for (const field of dto.fields) {
|
|
295
|
+
if (field.deprecated) continue;
|
|
296
|
+
|
|
297
|
+
if (!dbo.fields.has(field.name)) {
|
|
298
|
+
violations.push({
|
|
299
|
+
file: dto.file,
|
|
300
|
+
line: field.line,
|
|
301
|
+
dtoName: dto.name,
|
|
302
|
+
fieldName: field.name,
|
|
303
|
+
dboName: dbo.name,
|
|
304
|
+
availableFields: Array.from(dbo.fields).sort(),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return violations;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Report violations to console.
|
|
315
|
+
*/
|
|
316
|
+
function reportViolations(violations: DtoViolation[]): void {
|
|
317
|
+
console.error('');
|
|
318
|
+
console.error('❌ DTO fields don\'t match Prisma Dbo models!');
|
|
319
|
+
console.error('');
|
|
320
|
+
console.error('📚 Every non-deprecated field in a Dto must exist in the corresponding Dbo.');
|
|
321
|
+
console.error(' This prevents AI from inventing field names that don\'t match the database schema.');
|
|
322
|
+
console.error(' Dbo can have extra fields (e.g., password) - Dto cannot.');
|
|
323
|
+
console.error('');
|
|
324
|
+
|
|
325
|
+
for (const v of violations) {
|
|
326
|
+
console.error(` ❌ ${v.file}:${v.line}`);
|
|
327
|
+
console.error(` ${v.dtoName}.${v.fieldName} does not exist in ${v.dboName}`);
|
|
328
|
+
console.error(` Available Dbo fields: ${v.availableFields.join(', ')}`);
|
|
329
|
+
}
|
|
330
|
+
console.error('');
|
|
331
|
+
|
|
332
|
+
console.error(' Dto fields must be a subset of Dbo fields (matching TypeScript field names from schema.prisma).');
|
|
333
|
+
console.error(' Fields marked @deprecated in the Dto are exempt from this check.');
|
|
334
|
+
console.error('');
|
|
335
|
+
console.error(' When needing fields from multiple tables (e.g., a join), use a XxxJoinDto that');
|
|
336
|
+
console.error(' contains YYDto and ZZDto fields from the other tables instead of flattening.');
|
|
337
|
+
console.error('');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Filter changed files to only TypeScript Dto source files within configured paths.
|
|
342
|
+
*/
|
|
343
|
+
function filterDtoFiles(changedFiles: string[], dtoSourcePaths: string[]): string[] {
|
|
344
|
+
return changedFiles.filter((f) => {
|
|
345
|
+
if (!f.endsWith('.ts') && !f.endsWith('.tsx')) return false;
|
|
346
|
+
if (f.includes('.spec.ts') || f.includes('.test.ts')) return false;
|
|
347
|
+
return dtoSourcePaths.some((srcPath) => f.startsWith(srcPath));
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Collect all Dto definitions from the given files.
|
|
353
|
+
*/
|
|
354
|
+
function collectDtos(dtoFiles: string[], workspaceRoot: string): DtoInfo[] {
|
|
355
|
+
const allDtos: DtoInfo[] = [];
|
|
356
|
+
for (const file of dtoFiles) {
|
|
357
|
+
const dtos = findDtosInFile(file, workspaceRoot);
|
|
358
|
+
allDtos.push(...dtos);
|
|
359
|
+
}
|
|
360
|
+
return allDtos;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Resolve git base ref from env vars or auto-detection.
|
|
365
|
+
*/
|
|
366
|
+
function resolveBase(workspaceRoot: string): string | undefined {
|
|
367
|
+
const envBase = process.env['NX_BASE'];
|
|
368
|
+
if (envBase) return envBase;
|
|
369
|
+
return detectBase(workspaceRoot) ?? undefined;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Run the core validation after early-exit checks have passed.
|
|
374
|
+
*/
|
|
375
|
+
function validateDtoFiles(
|
|
376
|
+
workspaceRoot: string,
|
|
377
|
+
prismaSchemaPath: string,
|
|
378
|
+
changedFiles: string[],
|
|
379
|
+
dtoSourcePaths: string[]
|
|
380
|
+
): ExecutorResult {
|
|
381
|
+
if (changedFiles.some((f) => f.endsWith(prismaSchemaPath))) {
|
|
382
|
+
console.log('⏭️ Skipping validate-dtos (schema.prisma is modified - schema in flux)');
|
|
383
|
+
console.log('');
|
|
384
|
+
return { success: true };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const dtoFiles = filterDtoFiles(changedFiles, dtoSourcePaths);
|
|
388
|
+
|
|
389
|
+
if (dtoFiles.length === 0) {
|
|
390
|
+
console.log('✅ No Dto files changed');
|
|
391
|
+
return { success: true };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
console.log(`📂 Checking ${dtoFiles.length} changed file(s) for Dto definitions...`);
|
|
395
|
+
|
|
396
|
+
const fullSchemaPath = path.join(workspaceRoot, prismaSchemaPath);
|
|
397
|
+
const dboModels = parsePrismaSchema(fullSchemaPath);
|
|
398
|
+
|
|
399
|
+
if (dboModels.size === 0) {
|
|
400
|
+
console.log('⏭️ No Dbo models found in schema.prisma');
|
|
401
|
+
console.log('');
|
|
402
|
+
return { success: true };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
console.log(` Found ${dboModels.size} Dbo model(s) in schema.prisma`);
|
|
406
|
+
|
|
407
|
+
const allDtos = collectDtos(dtoFiles, workspaceRoot);
|
|
408
|
+
|
|
409
|
+
if (allDtos.length === 0) {
|
|
410
|
+
console.log('✅ No Dto definitions found in changed files');
|
|
411
|
+
return { success: true };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
console.log(` Found ${allDtos.length} Dto definition(s) in changed files`);
|
|
415
|
+
|
|
416
|
+
const violations = findViolations(allDtos, dboModels);
|
|
417
|
+
|
|
418
|
+
if (violations.length === 0) {
|
|
419
|
+
console.log('✅ All Dto fields match their Dbo models');
|
|
420
|
+
return { success: true };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
reportViolations(violations);
|
|
424
|
+
return { success: false };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export default async function runExecutor(
|
|
428
|
+
options: ValidateDtosOptions,
|
|
429
|
+
context: ExecutorContext
|
|
430
|
+
): Promise<ExecutorResult> {
|
|
431
|
+
const workspaceRoot = context.root;
|
|
432
|
+
const mode: ValidateDtosMode = options.mode ?? 'OFF';
|
|
433
|
+
|
|
434
|
+
if (mode === 'OFF') {
|
|
435
|
+
console.log('\n⏭️ Skipping validate-dtos (mode: OFF)');
|
|
436
|
+
console.log('');
|
|
437
|
+
return { success: true };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const prismaSchemaPath = options.prismaSchemaPath;
|
|
441
|
+
const dtoSourcePaths = options.dtoSourcePaths ?? [];
|
|
442
|
+
|
|
443
|
+
if (!prismaSchemaPath || dtoSourcePaths.length === 0) {
|
|
444
|
+
const reason = !prismaSchemaPath ? 'no prismaSchemaPath configured' : 'no dtoSourcePaths configured';
|
|
445
|
+
console.log(`\n⏭️ Skipping validate-dtos (${reason})`);
|
|
446
|
+
console.log('');
|
|
447
|
+
return { success: true };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
console.log('\n📏 Validating DTOs match Prisma Dbo models\n');
|
|
451
|
+
console.log(` Mode: ${mode}`);
|
|
452
|
+
console.log(` Schema: ${prismaSchemaPath}`);
|
|
453
|
+
console.log(` Dto paths: ${dtoSourcePaths.join(', ')}`);
|
|
454
|
+
|
|
455
|
+
const base = resolveBase(workspaceRoot);
|
|
456
|
+
const head = process.env['NX_HEAD'];
|
|
457
|
+
|
|
458
|
+
if (!base) {
|
|
459
|
+
console.log('\n⏭️ Skipping validate-dtos (could not detect base branch)');
|
|
460
|
+
console.log('');
|
|
461
|
+
return { success: true };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
console.log(` Base: ${base}`);
|
|
465
|
+
console.log(` Head: ${head ?? 'working tree (includes uncommitted changes)'}`);
|
|
466
|
+
console.log('');
|
|
467
|
+
|
|
468
|
+
const changedFiles = getChangedFiles(workspaceRoot, base, head);
|
|
469
|
+
|
|
470
|
+
return validateDtoFiles(workspaceRoot, prismaSchemaPath, changedFiles, dtoSourcePaths);
|
|
471
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"title": "Validate DTOs Executor",
|
|
4
|
+
"description": "Validate DTO fields match Prisma Dbo model fields. Ensures AI agents don't invent field names.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"mode": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"enum": ["OFF", "MODIFIED_FILES"],
|
|
10
|
+
"description": "OFF: skip validation. MODIFIED_FILES: validate Dto files that were modified in the diff.",
|
|
11
|
+
"default": "OFF"
|
|
12
|
+
},
|
|
13
|
+
"prismaSchemaPath": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Relative path from workspace root to schema.prisma"
|
|
16
|
+
},
|
|
17
|
+
"dtoSourcePaths": {
|
|
18
|
+
"type": "array",
|
|
19
|
+
"items": { "type": "string" },
|
|
20
|
+
"description": "Array of directories (relative to workspace root) containing Dto files"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"required": []
|
|
24
|
+
}
|
package/executors.json
CHANGED
|
@@ -79,6 +79,11 @@
|
|
|
79
79
|
"implementation": "./architecture/executors/validate-no-any-unknown/executor",
|
|
80
80
|
"schema": "./architecture/executors/validate-no-any-unknown/schema.json",
|
|
81
81
|
"description": "Validate no any/unknown keywords are used - use specific types instead"
|
|
82
|
+
},
|
|
83
|
+
"validate-dtos": {
|
|
84
|
+
"implementation": "./architecture/executors/validate-dtos/executor",
|
|
85
|
+
"schema": "./architecture/executors/validate-dtos/schema.json",
|
|
86
|
+
"description": "Validate DTO fields match Prisma Dbo model fields"
|
|
82
87
|
}
|
|
83
88
|
}
|
|
84
89
|
}
|