@webpieces/dev-config 0.2.80 → 0.2.82
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 +23 -2
- package/architecture/executors/validate-dtos/executor.d.ts +4 -2
- package/architecture/executors/validate-dtos/executor.js +126 -10
- package/architecture/executors/validate-dtos/executor.js.map +1 -1
- package/architecture/executors/validate-dtos/executor.ts +146 -11
- package/architecture/executors/validate-dtos/schema.json +2 -2
- package/architecture/executors/validate-prisma-converters/executor.d.ts +56 -0
- package/architecture/executors/validate-prisma-converters/executor.js +489 -0
- package/architecture/executors/validate-prisma-converters/executor.js.map +1 -0
- package/architecture/executors/validate-prisma-converters/executor.ts +615 -0
- package/architecture/executors/validate-prisma-converters/schema.json +24 -0
- package/executors.json +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Validate Prisma Converters Executor
|
|
4
|
+
*
|
|
5
|
+
* Validates that Prisma converter methods follow a scalable pattern:
|
|
6
|
+
* methods returning XxxDto (where XxxDbo exists in schema.prisma) must
|
|
7
|
+
* accept that exact XxxDbo as the first parameter. This keeps single-table
|
|
8
|
+
* converters clean and forces join converters to compose them.
|
|
9
|
+
*
|
|
10
|
+
* ============================================================================
|
|
11
|
+
* RULES
|
|
12
|
+
* ============================================================================
|
|
13
|
+
*
|
|
14
|
+
* 1. First param must be exact Dbo:
|
|
15
|
+
* If method returns XxxDto and XxxDbo exists in schema.prisma,
|
|
16
|
+
* the first parameter must be of type XxxDbo.
|
|
17
|
+
*
|
|
18
|
+
* 2. Extra params must be booleans:
|
|
19
|
+
* Additional parameters beyond the Dbo are allowed but must be boolean
|
|
20
|
+
* (used for filtering payloads / security info).
|
|
21
|
+
*
|
|
22
|
+
* 3. No async converters:
|
|
23
|
+
* Methods returning Promise<XxxDto> are flagged — converters should be
|
|
24
|
+
* pure data mapping, no async work.
|
|
25
|
+
*
|
|
26
|
+
* 4. No standalone functions:
|
|
27
|
+
* Standalone functions in converter files are flagged — must be class
|
|
28
|
+
* methods so the converter class can be injected (dependency tree tracing).
|
|
29
|
+
*
|
|
30
|
+
* 5. Dto creation outside converters directory:
|
|
31
|
+
* Files outside the configured convertersPath that create `new XxxDto(...)`
|
|
32
|
+
* where XxxDbo exists in schema.prisma are flagged — Dto instances tied to
|
|
33
|
+
* a Dbo must only be created via a converter class.
|
|
34
|
+
*
|
|
35
|
+
* ============================================================================
|
|
36
|
+
* SKIP CONDITIONS
|
|
37
|
+
* ============================================================================
|
|
38
|
+
* - Methods with @deprecated decorator or JSDoc tag
|
|
39
|
+
* - Lines with: // webpieces-disable prisma-converter -- [reason]
|
|
40
|
+
*
|
|
41
|
+
* ============================================================================
|
|
42
|
+
* MODES
|
|
43
|
+
* ============================================================================
|
|
44
|
+
* - OFF: Skip validation entirely
|
|
45
|
+
* - MODIFIED_FILES: Validate converter files that were modified in the diff
|
|
46
|
+
*/
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.default = runExecutor;
|
|
49
|
+
const tslib_1 = require("tslib");
|
|
50
|
+
const child_process_1 = require("child_process");
|
|
51
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
52
|
+
const path = tslib_1.__importStar(require("path"));
|
|
53
|
+
const ts = tslib_1.__importStar(require("typescript"));
|
|
54
|
+
/**
|
|
55
|
+
* Auto-detect the base branch by finding the merge-base with origin/main.
|
|
56
|
+
*/
|
|
57
|
+
function detectBase(workspaceRoot) {
|
|
58
|
+
try {
|
|
59
|
+
const mergeBase = (0, child_process_1.execSync)('git merge-base HEAD origin/main', {
|
|
60
|
+
cwd: workspaceRoot,
|
|
61
|
+
encoding: 'utf-8',
|
|
62
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
63
|
+
}).trim();
|
|
64
|
+
if (mergeBase) {
|
|
65
|
+
return mergeBase;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
try {
|
|
70
|
+
const mergeBase = (0, child_process_1.execSync)('git merge-base HEAD main', {
|
|
71
|
+
cwd: workspaceRoot,
|
|
72
|
+
encoding: 'utf-8',
|
|
73
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
74
|
+
}).trim();
|
|
75
|
+
if (mergeBase) {
|
|
76
|
+
return mergeBase;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Ignore
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get changed TypeScript files between base and head (or working tree if head not specified).
|
|
87
|
+
*/
|
|
88
|
+
// webpieces-disable max-lines-new-methods -- Git command handling with untracked files requires multiple code paths
|
|
89
|
+
function getChangedTypeScriptFiles(workspaceRoot, base, head) {
|
|
90
|
+
try {
|
|
91
|
+
const diffTarget = head ? `${base} ${head}` : base;
|
|
92
|
+
const output = (0, child_process_1.execSync)(`git diff --name-only ${diffTarget} -- '*.ts' '*.tsx'`, {
|
|
93
|
+
cwd: workspaceRoot,
|
|
94
|
+
encoding: 'utf-8',
|
|
95
|
+
});
|
|
96
|
+
const changedFiles = output
|
|
97
|
+
.trim()
|
|
98
|
+
.split('\n')
|
|
99
|
+
.filter((f) => f && !f.includes('.spec.ts') && !f.includes('.test.ts'));
|
|
100
|
+
if (!head) {
|
|
101
|
+
try {
|
|
102
|
+
const untrackedOutput = (0, child_process_1.execSync)(`git ls-files --others --exclude-standard '*.ts' '*.tsx'`, {
|
|
103
|
+
cwd: workspaceRoot,
|
|
104
|
+
encoding: 'utf-8',
|
|
105
|
+
});
|
|
106
|
+
const untrackedFiles = untrackedOutput
|
|
107
|
+
.trim()
|
|
108
|
+
.split('\n')
|
|
109
|
+
.filter((f) => f && !f.includes('.spec.ts') && !f.includes('.test.ts'));
|
|
110
|
+
const allFiles = new Set([...changedFiles, ...untrackedFiles]);
|
|
111
|
+
return Array.from(allFiles);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return changedFiles;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return changedFiles;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Parse schema.prisma to extract all model names into a Set.
|
|
125
|
+
*/
|
|
126
|
+
function parsePrismaModels(schemaPath) {
|
|
127
|
+
const models = new Set();
|
|
128
|
+
if (!fs.existsSync(schemaPath)) {
|
|
129
|
+
return models;
|
|
130
|
+
}
|
|
131
|
+
const content = fs.readFileSync(schemaPath, 'utf-8');
|
|
132
|
+
const regex = /^model\s+(\w+)\s*\{/gm;
|
|
133
|
+
let match;
|
|
134
|
+
while ((match = regex.exec(content)) !== null) {
|
|
135
|
+
models.add(match[1]);
|
|
136
|
+
}
|
|
137
|
+
return models;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Derive the expected Dbo name from a return type ending in Dto.
|
|
141
|
+
* "XxxDto" -> "XxxDbo". Returns null if name doesn't end with Dto.
|
|
142
|
+
*/
|
|
143
|
+
function deriveExpectedDboName(returnType) {
|
|
144
|
+
if (!returnType.endsWith('Dto'))
|
|
145
|
+
return null;
|
|
146
|
+
return returnType.slice(0, -3) + 'Dbo';
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Check if a line has a webpieces-disable comment for prisma-converter.
|
|
150
|
+
*/
|
|
151
|
+
function hasDisableComment(lines, lineNumber) {
|
|
152
|
+
const startCheck = Math.max(0, lineNumber - 5);
|
|
153
|
+
for (let i = lineNumber - 2; i >= startCheck; i--) {
|
|
154
|
+
const line = lines[i]?.trim() ?? '';
|
|
155
|
+
if (line.startsWith('function ') || line.startsWith('class ') || line.endsWith('}')) {
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
if (line.includes('webpieces-disable') && line.includes('prisma-converter')) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Check if a method/function node has a @deprecated decorator.
|
|
166
|
+
*/
|
|
167
|
+
function hasDeprecatedDecorator(node) {
|
|
168
|
+
const modifiers = ts.canHaveDecorators(node) ? ts.getDecorators(node) : undefined;
|
|
169
|
+
if (!modifiers)
|
|
170
|
+
return false;
|
|
171
|
+
for (const decorator of modifiers) {
|
|
172
|
+
const expr = decorator.expression;
|
|
173
|
+
// @deprecated or @deprecated()
|
|
174
|
+
if (ts.isIdentifier(expr) && expr.text === 'deprecated')
|
|
175
|
+
return true;
|
|
176
|
+
if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression) && expr.expression.text === 'deprecated') {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Check if a node has @deprecated in its JSDoc comments.
|
|
184
|
+
*/
|
|
185
|
+
function hasDeprecatedJsDoc(node) {
|
|
186
|
+
const jsDocs = ts.getJSDocTags(node);
|
|
187
|
+
for (const tag of jsDocs) {
|
|
188
|
+
if (tag.tagName.text === 'deprecated')
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Check if a method is deprecated via decorator or JSDoc.
|
|
195
|
+
*/
|
|
196
|
+
function isDeprecated(node) {
|
|
197
|
+
return hasDeprecatedDecorator(node) || hasDeprecatedJsDoc(node);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Extract the text of a type node, stripping whitespace.
|
|
201
|
+
*/
|
|
202
|
+
function getTypeText(typeNode, sourceFile) {
|
|
203
|
+
return typeNode.getText(sourceFile).trim();
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Unwrap Promise<T> to get T. Returns the inner type text if wrapped, otherwise returns as-is.
|
|
207
|
+
*/
|
|
208
|
+
function unwrapPromise(typeText) {
|
|
209
|
+
const promiseMatch = typeText.match(/^Promise\s*<\s*(.+)\s*>$/);
|
|
210
|
+
if (promiseMatch) {
|
|
211
|
+
return { inner: promiseMatch[1].trim(), isAsync: true };
|
|
212
|
+
}
|
|
213
|
+
return { inner: typeText, isAsync: false };
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Check a standalone function declaration in a converter file and return a violation if applicable.
|
|
217
|
+
*/
|
|
218
|
+
function checkStandaloneFunction(node, ctx) {
|
|
219
|
+
if (!node.name)
|
|
220
|
+
return null;
|
|
221
|
+
const startPos = node.getStart(ctx.sourceFile);
|
|
222
|
+
const pos = ctx.sourceFile.getLineAndCharacterOfPosition(startPos);
|
|
223
|
+
const line = pos.line + 1;
|
|
224
|
+
if (hasDisableComment(ctx.fileLines, line) || isDeprecated(node))
|
|
225
|
+
return null;
|
|
226
|
+
return {
|
|
227
|
+
file: ctx.filePath,
|
|
228
|
+
line,
|
|
229
|
+
message: `Standalone function "${node.name.text}" found in converter file. ` +
|
|
230
|
+
'Move to a converter class so it can be injected via DI.',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Validate the parameters of a converter method that returns a Dto with a matching Dbo.
|
|
235
|
+
*/
|
|
236
|
+
function checkMethodParams(node, innerType, expectedDbo, ctx, line) {
|
|
237
|
+
const violations = [];
|
|
238
|
+
const params = node.parameters;
|
|
239
|
+
if (params.length === 0) {
|
|
240
|
+
violations.push({
|
|
241
|
+
file: ctx.filePath,
|
|
242
|
+
line,
|
|
243
|
+
message: `Method returns "${innerType}" but has no parameters. ` +
|
|
244
|
+
`First parameter must be of type "${expectedDbo}".`,
|
|
245
|
+
});
|
|
246
|
+
return violations;
|
|
247
|
+
}
|
|
248
|
+
const firstParam = params[0];
|
|
249
|
+
if (firstParam.type) {
|
|
250
|
+
const firstParamType = getTypeText(firstParam.type, ctx.sourceFile);
|
|
251
|
+
if (firstParamType !== expectedDbo) {
|
|
252
|
+
violations.push({
|
|
253
|
+
file: ctx.filePath,
|
|
254
|
+
line,
|
|
255
|
+
message: `Method returns "${innerType}" but first parameter is "${firstParamType}". ` +
|
|
256
|
+
`First parameter must be of type "${expectedDbo}".`,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
for (let i = 1; i < params.length; i++) {
|
|
261
|
+
const param = params[i];
|
|
262
|
+
if (param.type) {
|
|
263
|
+
const paramType = getTypeText(param.type, ctx.sourceFile);
|
|
264
|
+
if (paramType !== 'boolean') {
|
|
265
|
+
const paramName = param.name.getText(ctx.sourceFile);
|
|
266
|
+
violations.push({
|
|
267
|
+
file: ctx.filePath,
|
|
268
|
+
line,
|
|
269
|
+
message: `Extra parameter "${paramName}" has type "${paramType}" but must be "boolean". ` +
|
|
270
|
+
'Additional converter parameters are only for boolean flags (payload filtering / security).',
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return violations;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Check a class method declaration for converter pattern violations.
|
|
279
|
+
*/
|
|
280
|
+
function checkConverterMethod(node, ctx) {
|
|
281
|
+
if (!node.name || !node.type)
|
|
282
|
+
return [];
|
|
283
|
+
const startPos = node.getStart(ctx.sourceFile);
|
|
284
|
+
const pos = ctx.sourceFile.getLineAndCharacterOfPosition(startPos);
|
|
285
|
+
const line = pos.line + 1;
|
|
286
|
+
if (hasDisableComment(ctx.fileLines, line) || isDeprecated(node))
|
|
287
|
+
return [];
|
|
288
|
+
const returnTypeText = getTypeText(node.type, ctx.sourceFile);
|
|
289
|
+
const { inner: innerType, isAsync } = unwrapPromise(returnTypeText);
|
|
290
|
+
const expectedDbo = deriveExpectedDboName(innerType);
|
|
291
|
+
if (!expectedDbo || !ctx.prismaModels.has(expectedDbo))
|
|
292
|
+
return [];
|
|
293
|
+
if (isAsync) {
|
|
294
|
+
return [{
|
|
295
|
+
file: ctx.filePath,
|
|
296
|
+
line,
|
|
297
|
+
message: `Async converter method returning "Promise<${innerType}>" found. ` +
|
|
298
|
+
'Converters should be pure data mapping with no async work. Remove async/Promise.',
|
|
299
|
+
}];
|
|
300
|
+
}
|
|
301
|
+
return checkMethodParams(node, innerType, expectedDbo, ctx, line);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Find converter method violations in a single file.
|
|
305
|
+
* Checks class methods for proper Dbo parameter patterns and flags standalone functions.
|
|
306
|
+
*/
|
|
307
|
+
function findConverterViolationsInFile(filePath, workspaceRoot, prismaModels) {
|
|
308
|
+
const fullPath = path.join(workspaceRoot, filePath);
|
|
309
|
+
if (!fs.existsSync(fullPath))
|
|
310
|
+
return [];
|
|
311
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
312
|
+
const ctx = {
|
|
313
|
+
filePath,
|
|
314
|
+
fileLines: content.split('\n'),
|
|
315
|
+
sourceFile: ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true),
|
|
316
|
+
prismaModels,
|
|
317
|
+
};
|
|
318
|
+
const violations = [];
|
|
319
|
+
function visitNode(node) {
|
|
320
|
+
if (ts.isFunctionDeclaration(node)) {
|
|
321
|
+
const violation = checkStandaloneFunction(node, ctx);
|
|
322
|
+
if (violation)
|
|
323
|
+
violations.push(violation);
|
|
324
|
+
}
|
|
325
|
+
if (ts.isMethodDeclaration(node)) {
|
|
326
|
+
violations.push(...checkConverterMethod(node, ctx));
|
|
327
|
+
}
|
|
328
|
+
ts.forEachChild(node, visitNode);
|
|
329
|
+
}
|
|
330
|
+
visitNode(ctx.sourceFile);
|
|
331
|
+
return violations;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Find violations in non-converter files: creating `new XxxDto(...)` where XxxDbo exists in prisma.
|
|
335
|
+
* These Dto instances must only be created inside converter classes.
|
|
336
|
+
*/
|
|
337
|
+
// webpieces-disable max-lines-new-methods -- AST traversal for new-expression detection with prisma model matching
|
|
338
|
+
function findDtoCreationOutsideConverters(filePath, workspaceRoot, prismaModels, convertersPaths) {
|
|
339
|
+
const fullPath = path.join(workspaceRoot, filePath);
|
|
340
|
+
if (!fs.existsSync(fullPath))
|
|
341
|
+
return [];
|
|
342
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
343
|
+
const fileLines = content.split('\n');
|
|
344
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
345
|
+
const violations = [];
|
|
346
|
+
function visitNode(node) {
|
|
347
|
+
// Detect `new XxxDto(...)` expressions
|
|
348
|
+
if (ts.isNewExpression(node) && ts.isIdentifier(node.expression)) {
|
|
349
|
+
const className = node.expression.text;
|
|
350
|
+
const expectedDbo = deriveExpectedDboName(className);
|
|
351
|
+
if (expectedDbo && prismaModels.has(expectedDbo)) {
|
|
352
|
+
const startPos = node.getStart(sourceFile);
|
|
353
|
+
const pos = sourceFile.getLineAndCharacterOfPosition(startPos);
|
|
354
|
+
const line = pos.line + 1;
|
|
355
|
+
if (!hasDisableComment(fileLines, line)) {
|
|
356
|
+
const dirs = convertersPaths.map((p) => `"${p}"`).join(', ');
|
|
357
|
+
violations.push({
|
|
358
|
+
file: filePath,
|
|
359
|
+
line,
|
|
360
|
+
message: `"${className}" can only be created from its Dbo using a converter in one of these directories: ${dirs}. ` +
|
|
361
|
+
'Move this Dto construction into a converter class method.',
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
ts.forEachChild(node, visitNode);
|
|
367
|
+
}
|
|
368
|
+
visitNode(sourceFile);
|
|
369
|
+
return violations;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Report violations to console.
|
|
373
|
+
*/
|
|
374
|
+
function reportViolations(violations, mode) {
|
|
375
|
+
console.error('');
|
|
376
|
+
console.error('❌ Prisma converter violations found!');
|
|
377
|
+
console.error('');
|
|
378
|
+
console.error('📚 Converter methods returning XxxDto (where XxxDbo exists in schema.prisma)');
|
|
379
|
+
console.error(' must accept XxxDbo as the first parameter. This keeps single-table');
|
|
380
|
+
console.error(' converters clean and forces join converters to compose them.');
|
|
381
|
+
console.error('');
|
|
382
|
+
console.error(' GOOD: convertUserDbo(userDbo: UserDbo): UserDto { }');
|
|
383
|
+
console.error(' GOOD: convertVersionDbo(version: VersionDbo, partial?: boolean): VersionDto { }');
|
|
384
|
+
console.error(' GOOD: convertToJoinDto(item: SomeJoinType): CourseJoinDto { } // no matching JoinDbo');
|
|
385
|
+
console.error('');
|
|
386
|
+
console.error(' BAD: async convertUser(dbo: UserDbo): Promise<UserDto> { } // no async');
|
|
387
|
+
console.error(' BAD: convertCourse(course: CourseWithMeta): CourseDto { } // wrong first param');
|
|
388
|
+
console.error(' BAD: convertUser(dbo: UserDbo, name: string): UserDto { } // extra non-boolean');
|
|
389
|
+
console.error(' BAD: export function convertSession(s: SessionDbo): SessionDto // standalone function');
|
|
390
|
+
console.error('');
|
|
391
|
+
for (const v of violations) {
|
|
392
|
+
console.error(` ❌ ${v.file}:${v.line}`);
|
|
393
|
+
console.error(` ${v.message}`);
|
|
394
|
+
}
|
|
395
|
+
console.error('');
|
|
396
|
+
console.error(' Escape hatch (use sparingly):');
|
|
397
|
+
console.error(' // webpieces-disable prisma-converter -- [your reason]');
|
|
398
|
+
console.error('');
|
|
399
|
+
console.error(` Current mode: ${mode}`);
|
|
400
|
+
console.error('');
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Resolve git base ref from env vars or auto-detection.
|
|
404
|
+
*/
|
|
405
|
+
function resolveBase(workspaceRoot) {
|
|
406
|
+
const envBase = process.env['NX_BASE'];
|
|
407
|
+
if (envBase)
|
|
408
|
+
return envBase;
|
|
409
|
+
return detectBase(workspaceRoot) ?? undefined;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Collect all violations from converter and non-converter files.
|
|
413
|
+
*/
|
|
414
|
+
function collectAllViolations(changedFiles, convertersPaths, workspaceRoot, prismaModels) {
|
|
415
|
+
const converterFiles = changedFiles.filter((f) => convertersPaths.some((cp) => f.startsWith(cp)));
|
|
416
|
+
const nonConverterFiles = changedFiles.filter((f) => !convertersPaths.some((cp) => f.startsWith(cp)));
|
|
417
|
+
const allViolations = [];
|
|
418
|
+
if (converterFiles.length > 0) {
|
|
419
|
+
console.log(`📂 Checking ${converterFiles.length} converter file(s)...`);
|
|
420
|
+
for (const file of converterFiles) {
|
|
421
|
+
allViolations.push(...findConverterViolationsInFile(file, workspaceRoot, prismaModels));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (nonConverterFiles.length > 0) {
|
|
425
|
+
console.log(`📂 Checking ${nonConverterFiles.length} non-converter file(s) for Dto creation...`);
|
|
426
|
+
for (const file of nonConverterFiles) {
|
|
427
|
+
allViolations.push(...findDtoCreationOutsideConverters(file, workspaceRoot, prismaModels, convertersPaths));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return allViolations;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Run validation after early-exit checks have passed.
|
|
434
|
+
*/
|
|
435
|
+
function validateChangedFiles(workspaceRoot, schemaPath, convertersPaths, base, mode) {
|
|
436
|
+
const head = process.env['NX_HEAD'];
|
|
437
|
+
console.log(` Base: ${base}`);
|
|
438
|
+
console.log(` Head: ${head ?? 'working tree (includes uncommitted changes)'}`);
|
|
439
|
+
console.log('');
|
|
440
|
+
const fullSchemaPath = path.join(workspaceRoot, schemaPath);
|
|
441
|
+
const prismaModels = parsePrismaModels(fullSchemaPath);
|
|
442
|
+
if (prismaModels.size === 0) {
|
|
443
|
+
console.log('⏭️ No models found in schema.prisma');
|
|
444
|
+
console.log('');
|
|
445
|
+
return { success: true };
|
|
446
|
+
}
|
|
447
|
+
console.log(` Found ${prismaModels.size} model(s) in schema.prisma`);
|
|
448
|
+
const changedFiles = getChangedTypeScriptFiles(workspaceRoot, base, head);
|
|
449
|
+
if (changedFiles.length === 0) {
|
|
450
|
+
console.log('✅ No TypeScript files changed');
|
|
451
|
+
return { success: true };
|
|
452
|
+
}
|
|
453
|
+
const allViolations = collectAllViolations(changedFiles, convertersPaths, workspaceRoot, prismaModels);
|
|
454
|
+
if (allViolations.length === 0) {
|
|
455
|
+
console.log('✅ All converter patterns are valid');
|
|
456
|
+
return { success: true };
|
|
457
|
+
}
|
|
458
|
+
reportViolations(allViolations, mode);
|
|
459
|
+
return { success: false };
|
|
460
|
+
}
|
|
461
|
+
async function runExecutor(options, context) {
|
|
462
|
+
const workspaceRoot = context.root;
|
|
463
|
+
const mode = options.mode ?? 'OFF';
|
|
464
|
+
if (mode === 'OFF') {
|
|
465
|
+
console.log('\n⏭️ Skipping prisma-converter validation (mode: OFF)');
|
|
466
|
+
console.log('');
|
|
467
|
+
return { success: true };
|
|
468
|
+
}
|
|
469
|
+
const schemaPath = options.schemaPath;
|
|
470
|
+
const convertersPaths = options.convertersPaths ?? [];
|
|
471
|
+
if (!schemaPath || convertersPaths.length === 0) {
|
|
472
|
+
const reason = !schemaPath ? 'no schemaPath configured' : 'no convertersPaths configured';
|
|
473
|
+
console.log(`\n⏭️ Skipping prisma-converter validation (${reason})`);
|
|
474
|
+
console.log('');
|
|
475
|
+
return { success: true };
|
|
476
|
+
}
|
|
477
|
+
console.log('\n📏 Validating Prisma Converters\n');
|
|
478
|
+
console.log(` Mode: ${mode}`);
|
|
479
|
+
console.log(` Schema: ${schemaPath}`);
|
|
480
|
+
console.log(` Converter paths: ${convertersPaths.join(', ')}`);
|
|
481
|
+
const base = resolveBase(workspaceRoot);
|
|
482
|
+
if (!base) {
|
|
483
|
+
console.log('\n⏭️ Skipping prisma-converter validation (could not detect base branch)');
|
|
484
|
+
console.log('');
|
|
485
|
+
return { success: true };
|
|
486
|
+
}
|
|
487
|
+
return validateChangedFiles(workspaceRoot, schemaPath, convertersPaths, base, mode);
|
|
488
|
+
}
|
|
489
|
+
//# sourceMappingURL=executor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/dev-config/architecture/executors/validate-prisma-converters/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Validate Prisma Converters Executor\n *\n * Validates that Prisma converter methods follow a scalable pattern:\n * methods returning XxxDto (where XxxDbo exists in schema.prisma) must\n * accept that exact XxxDbo as the first parameter. This keeps single-table\n * converters clean and forces join converters to compose them.\n *\n * ============================================================================\n * RULES\n * ============================================================================\n *\n * 1. First param must be exact Dbo:\n * If method returns XxxDto and XxxDbo exists in schema.prisma,\n * the first parameter must be of type XxxDbo.\n *\n * 2. Extra params must be booleans:\n * Additional parameters beyond the Dbo are allowed but must be boolean\n * (used for filtering payloads / security info).\n *\n * 3. No async converters:\n * Methods returning Promise<XxxDto> are flagged — converters should be\n * pure data mapping, no async work.\n *\n * 4. No standalone functions:\n * Standalone functions in converter files are flagged — must be class\n * methods so the converter class can be injected (dependency tree tracing).\n *\n * 5. Dto creation outside converters directory:\n * Files outside the configured convertersPath that create `new XxxDto(...)`\n * where XxxDbo exists in schema.prisma are flagged — Dto instances tied to\n * a Dbo must only be created via a converter class.\n *\n * ============================================================================\n * SKIP CONDITIONS\n * ============================================================================\n * - Methods with @deprecated decorator or JSDoc tag\n * - Lines with: // webpieces-disable prisma-converter -- [reason]\n *\n * ============================================================================\n * MODES\n * ============================================================================\n * - OFF: Skip validation entirely\n * - MODIFIED_FILES: Validate converter files that were modified in the diff\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 PrismaConverterMode = 'OFF' | 'MODIFIED_FILES';\n\nexport interface ValidatePrismaConvertersOptions {\n mode?: PrismaConverterMode;\n schemaPath?: string;\n convertersPaths?: string[];\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\ninterface PrismaConverterViolation {\n file: string;\n line: number;\n message: string;\n}\n\ninterface UnwrapResult {\n inner: string;\n isAsync: boolean;\n}\n\ninterface FileContext {\n filePath: string;\n fileLines: string[];\n sourceFile: ts.SourceFile;\n prismaModels: 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 TypeScript 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 getChangedTypeScriptFiles(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} -- '*.ts' '*.tsx'`, {\n cwd: workspaceRoot,\n encoding: 'utf-8',\n });\n const changedFiles = output\n .trim()\n .split('\\n')\n .filter((f) => f && !f.includes('.spec.ts') && !f.includes('.test.ts'));\n\n if (!head) {\n try {\n const untrackedOutput = execSync(`git ls-files --others --exclude-standard '*.ts' '*.tsx'`, {\n cwd: workspaceRoot,\n encoding: 'utf-8',\n });\n const untrackedFiles = untrackedOutput\n .trim()\n .split('\\n')\n .filter((f) => f && !f.includes('.spec.ts') && !f.includes('.test.ts'));\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 extract all model names into a Set.\n */\nfunction parsePrismaModels(schemaPath: string): Set<string> {\n const models = new Set<string>();\n\n if (!fs.existsSync(schemaPath)) {\n return models;\n }\n\n const content = fs.readFileSync(schemaPath, 'utf-8');\n const regex = /^model\\s+(\\w+)\\s*\\{/gm;\n let match: RegExpExecArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n models.add(match[1]);\n }\n\n return models;\n}\n\n/**\n * Derive the expected Dbo name from a return type ending in Dto.\n * \"XxxDto\" -> \"XxxDbo\". Returns null if name doesn't end with Dto.\n */\nfunction deriveExpectedDboName(returnType: string): string | null {\n if (!returnType.endsWith('Dto')) return null;\n return returnType.slice(0, -3) + 'Dbo';\n}\n\n/**\n * Check if a line has a webpieces-disable comment for prisma-converter.\n */\nfunction hasDisableComment(lines: string[], lineNumber: number): boolean {\n const startCheck = Math.max(0, lineNumber - 5);\n for (let i = lineNumber - 2; i >= startCheck; i--) {\n const line = lines[i]?.trim() ?? '';\n if (line.startsWith('function ') || line.startsWith('class ') || line.endsWith('}')) {\n break;\n }\n if (line.includes('webpieces-disable') && line.includes('prisma-converter')) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Check if a method/function node has a @deprecated decorator.\n */\nfunction hasDeprecatedDecorator(node: ts.MethodDeclaration | ts.FunctionDeclaration): boolean {\n const modifiers = ts.canHaveDecorators(node) ? ts.getDecorators(node) : undefined;\n if (!modifiers) return false;\n\n for (const decorator of modifiers) {\n const expr = decorator.expression;\n // @deprecated or @deprecated()\n if (ts.isIdentifier(expr) && expr.text === 'deprecated') return true;\n if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression) && expr.expression.text === 'deprecated') {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Check if a node has @deprecated in its JSDoc comments.\n */\nfunction hasDeprecatedJsDoc(node: ts.Node): boolean {\n const jsDocs = ts.getJSDocTags(node);\n for (const tag of jsDocs) {\n if (tag.tagName.text === 'deprecated') return true;\n }\n return false;\n}\n\n/**\n * Check if a method is deprecated via decorator or JSDoc.\n */\nfunction isDeprecated(node: ts.MethodDeclaration | ts.FunctionDeclaration): boolean {\n return hasDeprecatedDecorator(node) || hasDeprecatedJsDoc(node);\n}\n\n/**\n * Extract the text of a type node, stripping whitespace.\n */\nfunction getTypeText(typeNode: ts.TypeNode, sourceFile: ts.SourceFile): string {\n return typeNode.getText(sourceFile).trim();\n}\n\n/**\n * Unwrap Promise<T> to get T. Returns the inner type text if wrapped, otherwise returns as-is.\n */\nfunction unwrapPromise(typeText: string): UnwrapResult {\n const promiseMatch = typeText.match(/^Promise\\s*<\\s*(.+)\\s*>$/);\n if (promiseMatch) {\n return { inner: promiseMatch[1].trim(), isAsync: true };\n }\n return { inner: typeText, isAsync: false };\n}\n\n/**\n * Check a standalone function declaration in a converter file and return a violation if applicable.\n */\nfunction checkStandaloneFunction(\n node: ts.FunctionDeclaration,\n ctx: FileContext\n): PrismaConverterViolation | null {\n if (!node.name) return null;\n\n const startPos = node.getStart(ctx.sourceFile);\n const pos = ctx.sourceFile.getLineAndCharacterOfPosition(startPos);\n const line = pos.line + 1;\n\n if (hasDisableComment(ctx.fileLines, line) || isDeprecated(node)) return null;\n\n return {\n file: ctx.filePath,\n line,\n message: `Standalone function \"${node.name.text}\" found in converter file. ` +\n 'Move to a converter class so it can be injected via DI.',\n };\n}\n\n/**\n * Validate the parameters of a converter method that returns a Dto with a matching Dbo.\n */\nfunction checkMethodParams(\n node: ts.MethodDeclaration,\n innerType: string,\n expectedDbo: string,\n ctx: FileContext,\n line: number\n): PrismaConverterViolation[] {\n const violations: PrismaConverterViolation[] = [];\n const params = node.parameters;\n\n if (params.length === 0) {\n violations.push({\n file: ctx.filePath,\n line,\n message: `Method returns \"${innerType}\" but has no parameters. ` +\n `First parameter must be of type \"${expectedDbo}\".`,\n });\n return violations;\n }\n\n const firstParam = params[0];\n if (firstParam.type) {\n const firstParamType = getTypeText(firstParam.type, ctx.sourceFile);\n if (firstParamType !== expectedDbo) {\n violations.push({\n file: ctx.filePath,\n line,\n message: `Method returns \"${innerType}\" but first parameter is \"${firstParamType}\". ` +\n `First parameter must be of type \"${expectedDbo}\".`,\n });\n }\n }\n\n for (let i = 1; i < params.length; i++) {\n const param = params[i];\n if (param.type) {\n const paramType = getTypeText(param.type, ctx.sourceFile);\n if (paramType !== 'boolean') {\n const paramName = param.name.getText(ctx.sourceFile);\n violations.push({\n file: ctx.filePath,\n line,\n message: `Extra parameter \"${paramName}\" has type \"${paramType}\" but must be \"boolean\". ` +\n 'Additional converter parameters are only for boolean flags (payload filtering / security).',\n });\n }\n }\n }\n\n return violations;\n}\n\n/**\n * Check a class method declaration for converter pattern violations.\n */\nfunction checkConverterMethod(\n node: ts.MethodDeclaration,\n ctx: FileContext\n): PrismaConverterViolation[] {\n if (!node.name || !node.type) return [];\n\n const startPos = node.getStart(ctx.sourceFile);\n const pos = ctx.sourceFile.getLineAndCharacterOfPosition(startPos);\n const line = pos.line + 1;\n\n if (hasDisableComment(ctx.fileLines, line) || isDeprecated(node)) return [];\n\n const returnTypeText = getTypeText(node.type, ctx.sourceFile);\n const { inner: innerType, isAsync } = unwrapPromise(returnTypeText);\n const expectedDbo = deriveExpectedDboName(innerType);\n\n if (!expectedDbo || !ctx.prismaModels.has(expectedDbo)) return [];\n\n if (isAsync) {\n return [{\n file: ctx.filePath,\n line,\n message: `Async converter method returning \"Promise<${innerType}>\" found. ` +\n 'Converters should be pure data mapping with no async work. Remove async/Promise.',\n }];\n }\n\n return checkMethodParams(node, innerType, expectedDbo, ctx, line);\n}\n\n/**\n * Find converter method violations in a single file.\n * Checks class methods for proper Dbo parameter patterns and flags standalone functions.\n */\nfunction findConverterViolationsInFile(\n filePath: string,\n workspaceRoot: string,\n prismaModels: Set<string>\n): PrismaConverterViolation[] {\n const fullPath = path.join(workspaceRoot, filePath);\n if (!fs.existsSync(fullPath)) return [];\n\n const content = fs.readFileSync(fullPath, 'utf-8');\n const ctx: FileContext = {\n filePath,\n fileLines: content.split('\\n'),\n sourceFile: ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true),\n prismaModels,\n };\n\n const violations: PrismaConverterViolation[] = [];\n\n function visitNode(node: ts.Node): void {\n if (ts.isFunctionDeclaration(node)) {\n const violation = checkStandaloneFunction(node, ctx);\n if (violation) violations.push(violation);\n }\n\n if (ts.isMethodDeclaration(node)) {\n violations.push(...checkConverterMethod(node, ctx));\n }\n\n ts.forEachChild(node, visitNode);\n }\n\n visitNode(ctx.sourceFile);\n return violations;\n}\n\n/**\n * Find violations in non-converter files: creating `new XxxDto(...)` where XxxDbo exists in prisma.\n * These Dto instances must only be created inside converter classes.\n */\n// webpieces-disable max-lines-new-methods -- AST traversal for new-expression detection with prisma model matching\nfunction findDtoCreationOutsideConverters(\n filePath: string,\n workspaceRoot: string,\n prismaModels: Set<string>,\n convertersPaths: string[]\n): PrismaConverterViolation[] {\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 violations: PrismaConverterViolation[] = [];\n\n function visitNode(node: ts.Node): void {\n // Detect `new XxxDto(...)` expressions\n if (ts.isNewExpression(node) && ts.isIdentifier(node.expression)) {\n const className = node.expression.text;\n const expectedDbo = deriveExpectedDboName(className);\n\n if (expectedDbo && prismaModels.has(expectedDbo)) {\n const startPos = node.getStart(sourceFile);\n const pos = sourceFile.getLineAndCharacterOfPosition(startPos);\n const line = pos.line + 1;\n\n if (!hasDisableComment(fileLines, line)) {\n const dirs = convertersPaths.map((p) => `\"${p}\"`).join(', ');\n violations.push({\n file: filePath,\n line,\n message: `\"${className}\" can only be created from its Dbo using a converter in one of these directories: ${dirs}. ` +\n 'Move this Dto construction into a converter class method.',\n });\n }\n }\n }\n\n ts.forEachChild(node, visitNode);\n }\n\n visitNode(sourceFile);\n return violations;\n}\n\n/**\n * Report violations to console.\n */\nfunction reportViolations(violations: PrismaConverterViolation[], mode: PrismaConverterMode): void {\n console.error('');\n console.error('❌ Prisma converter violations found!');\n console.error('');\n console.error('📚 Converter methods returning XxxDto (where XxxDbo exists in schema.prisma)');\n console.error(' must accept XxxDbo as the first parameter. This keeps single-table');\n console.error(' converters clean and forces join converters to compose them.');\n console.error('');\n console.error(' GOOD: convertUserDbo(userDbo: UserDbo): UserDto { }');\n console.error(' GOOD: convertVersionDbo(version: VersionDbo, partial?: boolean): VersionDto { }');\n console.error(' GOOD: convertToJoinDto(item: SomeJoinType): CourseJoinDto { } // no matching JoinDbo');\n console.error('');\n console.error(' BAD: async convertUser(dbo: UserDbo): Promise<UserDto> { } // no async');\n console.error(' BAD: convertCourse(course: CourseWithMeta): CourseDto { } // wrong first param');\n console.error(' BAD: convertUser(dbo: UserDbo, name: string): UserDto { } // extra non-boolean');\n console.error(' BAD: export function convertSession(s: SessionDbo): SessionDto // standalone function');\n console.error('');\n\n for (const v of violations) {\n console.error(` ❌ ${v.file}:${v.line}`);\n console.error(` ${v.message}`);\n }\n console.error('');\n\n console.error(' Escape hatch (use sparingly):');\n console.error(' // webpieces-disable prisma-converter -- [your reason]');\n console.error('');\n console.error(` Current mode: ${mode}`);\n console.error('');\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 * Collect all violations from converter and non-converter files.\n */\nfunction collectAllViolations(\n changedFiles: string[],\n convertersPaths: string[],\n workspaceRoot: string,\n prismaModels: Set<string>\n): PrismaConverterViolation[] {\n const converterFiles = changedFiles.filter((f) =>\n convertersPaths.some((cp) => f.startsWith(cp))\n );\n const nonConverterFiles = changedFiles.filter((f) =>\n !convertersPaths.some((cp) => f.startsWith(cp))\n );\n\n const allViolations: PrismaConverterViolation[] = [];\n\n if (converterFiles.length > 0) {\n console.log(`📂 Checking ${converterFiles.length} converter file(s)...`);\n for (const file of converterFiles) {\n allViolations.push(...findConverterViolationsInFile(file, workspaceRoot, prismaModels));\n }\n }\n\n if (nonConverterFiles.length > 0) {\n console.log(`📂 Checking ${nonConverterFiles.length} non-converter file(s) for Dto creation...`);\n for (const file of nonConverterFiles) {\n allViolations.push(...findDtoCreationOutsideConverters(file, workspaceRoot, prismaModels, convertersPaths));\n }\n }\n\n return allViolations;\n}\n\n/**\n * Run validation after early-exit checks have passed.\n */\nfunction validateChangedFiles(\n workspaceRoot: string,\n schemaPath: string,\n convertersPaths: string[],\n base: string,\n mode: PrismaConverterMode\n): ExecutorResult {\n const head = process.env['NX_HEAD'];\n\n console.log(` Base: ${base}`);\n console.log(` Head: ${head ?? 'working tree (includes uncommitted changes)'}`);\n console.log('');\n\n const fullSchemaPath = path.join(workspaceRoot, schemaPath);\n const prismaModels = parsePrismaModels(fullSchemaPath);\n\n if (prismaModels.size === 0) {\n console.log('⏭️ No models found in schema.prisma');\n console.log('');\n return { success: true };\n }\n\n console.log(` Found ${prismaModels.size} model(s) in schema.prisma`);\n\n const changedFiles = getChangedTypeScriptFiles(workspaceRoot, base, head);\n\n if (changedFiles.length === 0) {\n console.log('✅ No TypeScript files changed');\n return { success: true };\n }\n\n const allViolations = collectAllViolations(changedFiles, convertersPaths, workspaceRoot, prismaModels);\n\n if (allViolations.length === 0) {\n console.log('✅ All converter patterns are valid');\n return { success: true };\n }\n\n reportViolations(allViolations, mode);\n return { success: false };\n}\n\nexport default async function runExecutor(\n options: ValidatePrismaConvertersOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const workspaceRoot = context.root;\n const mode: PrismaConverterMode = options.mode ?? 'OFF';\n\n if (mode === 'OFF') {\n console.log('\\n⏭️ Skipping prisma-converter validation (mode: OFF)');\n console.log('');\n return { success: true };\n }\n\n const schemaPath = options.schemaPath;\n const convertersPaths = options.convertersPaths ?? [];\n\n if (!schemaPath || convertersPaths.length === 0) {\n const reason = !schemaPath ? 'no schemaPath configured' : 'no convertersPaths configured';\n console.log(`\\n⏭️ Skipping prisma-converter validation (${reason})`);\n console.log('');\n return { success: true };\n }\n\n console.log('\\n📏 Validating Prisma Converters\\n');\n console.log(` Mode: ${mode}`);\n console.log(` Schema: ${schemaPath}`);\n console.log(` Converter paths: ${convertersPaths.join(', ')}`);\n\n const base = resolveBase(workspaceRoot);\n\n if (!base) {\n console.log('\\n⏭️ Skipping prisma-converter validation (could not detect base branch)');\n console.log('');\n return { success: true };\n }\n\n return validateChangedFiles(workspaceRoot, schemaPath, convertersPaths, base, mode);\n}\n"]}
|