speckeeper 0.8.0 → 0.9.0
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/README.md +137 -33
- package/dist/cli.js +622 -106
- package/dist/cli.js.map +1 -1
- package/dist/{config-api-Bh0zX8W7.d.ts → config-api-BDl4otlv.d.ts} +92 -2
- package/dist/dsl/index.d.ts +38 -82
- package/dist/dsl/index.js +431 -454
- package/dist/dsl/index.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/dsl/index.js
CHANGED
|
@@ -154,142 +154,6 @@ function markdownExporter(config) {
|
|
|
154
154
|
}
|
|
155
155
|
return exporter;
|
|
156
156
|
}
|
|
157
|
-
|
|
158
|
-
// src/core/dsl/type-compat.ts
|
|
159
|
-
var ALIASES = {
|
|
160
|
-
INTEGER: "INT",
|
|
161
|
-
BOOL: "BOOLEAN",
|
|
162
|
-
REAL: "FLOAT",
|
|
163
|
-
"DOUBLE PRECISION": "DOUBLE",
|
|
164
|
-
SERIAL: "INT",
|
|
165
|
-
BIGSERIAL: "BIGINT",
|
|
166
|
-
SMALLSERIAL: "SMALLINT",
|
|
167
|
-
TIMESTAMPTZ: "TIMESTAMP",
|
|
168
|
-
"TIMESTAMP WITH TIME ZONE": "TIMESTAMP",
|
|
169
|
-
"TIMESTAMP WITHOUT TIME ZONE": "TIMESTAMP",
|
|
170
|
-
"CHARACTER VARYING": "VARCHAR",
|
|
171
|
-
CHARACTER: "CHAR",
|
|
172
|
-
INT2: "SMALLINT",
|
|
173
|
-
INT4: "INT",
|
|
174
|
-
INT8: "BIGINT",
|
|
175
|
-
FLOAT4: "FLOAT",
|
|
176
|
-
FLOAT8: "DOUBLE"
|
|
177
|
-
};
|
|
178
|
-
var CONTAINMENT_CHAINS = [
|
|
179
|
-
["SMALLINT", "INT", "BIGINT"],
|
|
180
|
-
["FLOAT", "DOUBLE"],
|
|
181
|
-
["CHAR", "VARCHAR", "TEXT"],
|
|
182
|
-
["INTEGER", "NUMBER"]
|
|
183
|
-
];
|
|
184
|
-
function parseType(raw) {
|
|
185
|
-
const trimmed = raw.trim().toUpperCase();
|
|
186
|
-
const match = trimmed.match(/^([A-Z\s]+?)(?:\(\s*(\d+)(?:\s*,\s*(\d+))?\s*\))?$/);
|
|
187
|
-
if (!match) {
|
|
188
|
-
return { base: trimmed };
|
|
189
|
-
}
|
|
190
|
-
const base = match[1].trim();
|
|
191
|
-
return {
|
|
192
|
-
base,
|
|
193
|
-
length: match[2] !== void 0 ? Number(match[2]) : void 0,
|
|
194
|
-
precision: match[2] !== void 0 ? Number(match[2]) : void 0,
|
|
195
|
-
scale: match[3] !== void 0 ? Number(match[3]) : void 0
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
function normalize(base) {
|
|
199
|
-
return ALIASES[base] ?? base;
|
|
200
|
-
}
|
|
201
|
-
function findAllChainPositions(base, chains) {
|
|
202
|
-
const results = [];
|
|
203
|
-
const resolved = normalize(base);
|
|
204
|
-
for (const chain of chains) {
|
|
205
|
-
let idx = chain.indexOf(base);
|
|
206
|
-
if (idx === -1) idx = chain.indexOf(resolved);
|
|
207
|
-
if (idx !== -1) results.push({ chain, index: idx });
|
|
208
|
-
}
|
|
209
|
-
return results;
|
|
210
|
-
}
|
|
211
|
-
function isTypeContainedBy(specType, ddlType) {
|
|
212
|
-
const spec = parseType(specType);
|
|
213
|
-
const ddl = parseType(ddlType);
|
|
214
|
-
const specNorm = normalize(spec.base);
|
|
215
|
-
const ddlNorm = normalize(ddl.base);
|
|
216
|
-
if (specNorm === ddlNorm) {
|
|
217
|
-
if (specNorm === "DECIMAL" || specNorm === "NUMERIC") {
|
|
218
|
-
if (spec.precision !== void 0 && ddl.precision !== void 0) {
|
|
219
|
-
if (ddl.precision < spec.precision) return false;
|
|
220
|
-
}
|
|
221
|
-
if (spec.scale !== void 0 && ddl.scale !== void 0) {
|
|
222
|
-
if (ddl.scale < spec.scale) return false;
|
|
223
|
-
}
|
|
224
|
-
return true;
|
|
225
|
-
}
|
|
226
|
-
if (spec.length !== void 0 && ddl.length !== void 0) {
|
|
227
|
-
return ddl.length >= spec.length;
|
|
228
|
-
}
|
|
229
|
-
return true;
|
|
230
|
-
}
|
|
231
|
-
const specPositions = findAllChainPositions(spec.base, CONTAINMENT_CHAINS);
|
|
232
|
-
const ddlPositions = findAllChainPositions(ddl.base, CONTAINMENT_CHAINS);
|
|
233
|
-
for (const sp of specPositions) {
|
|
234
|
-
for (const dp of ddlPositions) {
|
|
235
|
-
if (sp.chain === dp.chain) {
|
|
236
|
-
return dp.index >= sp.index;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return false;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// src/core/dsl/checkers.ts
|
|
244
|
-
function testChecker(config) {
|
|
245
|
-
return {
|
|
246
|
-
targetType: "test",
|
|
247
|
-
sourcePath: config?.sourcePath ?? (() => "."),
|
|
248
|
-
check: (spec) => {
|
|
249
|
-
const errors = [];
|
|
250
|
-
const warnings = [];
|
|
251
|
-
const basePath = process.cwd();
|
|
252
|
-
const testPatterns = [
|
|
253
|
-
"test/**/*.test.ts",
|
|
254
|
-
"test/**/*.spec.ts",
|
|
255
|
-
"tests/**/*.test.ts",
|
|
256
|
-
"tests/**/*.spec.ts"
|
|
257
|
-
];
|
|
258
|
-
let testFiles = [];
|
|
259
|
-
for (const pattern of testPatterns) {
|
|
260
|
-
testFiles = testFiles.concat(glob.sync(pattern, { cwd: basePath }));
|
|
261
|
-
}
|
|
262
|
-
if (testFiles.length === 0) {
|
|
263
|
-
warnings.push({ message: `No test files found for ${spec.id}`, specId: spec.id });
|
|
264
|
-
return { success: true, errors, warnings };
|
|
265
|
-
}
|
|
266
|
-
let specIdFound = false;
|
|
267
|
-
for (const testFile of testFiles) {
|
|
268
|
-
const fullPath = join(basePath, testFile);
|
|
269
|
-
try {
|
|
270
|
-
const content = readFileSync(fullPath, "utf-8");
|
|
271
|
-
const patterns = [
|
|
272
|
-
new RegExp(`describe\\s*\\(\\s*['"\`].*${spec.id}`, "m"),
|
|
273
|
-
new RegExp(`it\\s*\\(\\s*['"\`].*${spec.id}`, "m"),
|
|
274
|
-
new RegExp(`test\\s*\\(\\s*['"\`].*${spec.id}`, "m")
|
|
275
|
-
];
|
|
276
|
-
if (patterns.some((p) => p.test(content))) {
|
|
277
|
-
specIdFound = true;
|
|
278
|
-
break;
|
|
279
|
-
}
|
|
280
|
-
} catch {
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
if (!specIdFound) {
|
|
284
|
-
warnings.push({
|
|
285
|
-
message: `Spec ID "${spec.id}" not found in any test file`,
|
|
286
|
-
specId: spec.id
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
return { success: errors.length === 0, errors, warnings };
|
|
290
|
-
}
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
157
|
function relationTypeToAnnotation(relationType) {
|
|
294
158
|
return relationType === "verifiedBy" ? "verifies" : relationType;
|
|
295
159
|
}
|
|
@@ -307,106 +171,6 @@ function setArtifactsConfig(config) {
|
|
|
307
171
|
function getArtifactsConfig() {
|
|
308
172
|
return _artifactsConfig;
|
|
309
173
|
}
|
|
310
|
-
function runAnnotationScan(specId, artifact, relationType, contentPatterns, basePath) {
|
|
311
|
-
const matchedFiles = [];
|
|
312
|
-
const warnings = [];
|
|
313
|
-
const artifactsConfig = getArtifactsConfig();
|
|
314
|
-
const artifactConfig = artifactsConfig?.[artifact];
|
|
315
|
-
if (!artifactConfig) {
|
|
316
|
-
warnings.push({
|
|
317
|
-
message: `Artifact config "${artifact}" not found; set artifacts in config and call setArtifactsConfig()`,
|
|
318
|
-
specId
|
|
319
|
-
});
|
|
320
|
-
return { matchedFiles, warnings };
|
|
321
|
-
}
|
|
322
|
-
const allFiles = /* @__PURE__ */ new Set();
|
|
323
|
-
for (const pattern of artifactConfig.globs) {
|
|
324
|
-
const found = glob.sync(pattern, {
|
|
325
|
-
cwd: basePath,
|
|
326
|
-
ignore: artifactConfig.exclude ?? []
|
|
327
|
-
});
|
|
328
|
-
for (const f of found) {
|
|
329
|
-
allFiles.add(f);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
const patterns = contentPatterns.length > 0 ? contentPatterns : [getDefaultPatternForRelation(relationType)];
|
|
333
|
-
for (const filePath of allFiles) {
|
|
334
|
-
const fullPath = join(basePath, filePath);
|
|
335
|
-
let content;
|
|
336
|
-
try {
|
|
337
|
-
content = readFileSync(fullPath, "utf-8");
|
|
338
|
-
} catch {
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
const lines = content.split("\n");
|
|
342
|
-
for (let lineNum = 1; lineNum <= lines.length; lineNum++) {
|
|
343
|
-
const line = lines[lineNum - 1];
|
|
344
|
-
for (const re of patterns) {
|
|
345
|
-
const copy = new RegExp(re.source, re.flags);
|
|
346
|
-
let m;
|
|
347
|
-
while ((m = copy.exec(line)) !== null) {
|
|
348
|
-
const ids = extractSpecIds(m[1] ?? "");
|
|
349
|
-
if (ids.includes(specId)) {
|
|
350
|
-
matchedFiles.push({ specId, filePath, line: lineNum, relationType });
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
return { matchedFiles, warnings };
|
|
357
|
-
}
|
|
358
|
-
function annotationChecker(config) {
|
|
359
|
-
return {
|
|
360
|
-
targetType: "annotation",
|
|
361
|
-
sourcePath: () => ".",
|
|
362
|
-
check: (spec, _externalData) => {
|
|
363
|
-
const errors = [];
|
|
364
|
-
const warnings = [];
|
|
365
|
-
const allMatchedFiles = [];
|
|
366
|
-
const basePath = process.cwd();
|
|
367
|
-
const checks = config?.artifact && config?.relationType ? [
|
|
368
|
-
{
|
|
369
|
-
artifact: config.artifact,
|
|
370
|
-
relationType: config.relationType,
|
|
371
|
-
contentPatterns: config.contentPatterns
|
|
372
|
-
}
|
|
373
|
-
] : config?.checks ?? [];
|
|
374
|
-
for (const entry of checks) {
|
|
375
|
-
if (entry.checker) {
|
|
376
|
-
const result = entry.checker.check(spec, void 0);
|
|
377
|
-
errors.push(...result.errors);
|
|
378
|
-
warnings.push(...result.warnings);
|
|
379
|
-
if (result.matchedFiles) {
|
|
380
|
-
allMatchedFiles.push(...result.matchedFiles);
|
|
381
|
-
}
|
|
382
|
-
} else {
|
|
383
|
-
const patterns = entry.contentPatterns ?? getArtifactsConfig()?.[entry.artifact]?.contentPatterns ?? [];
|
|
384
|
-
const { matchedFiles, warnings: w } = runAnnotationScan(
|
|
385
|
-
spec.id,
|
|
386
|
-
entry.artifact,
|
|
387
|
-
entry.relationType,
|
|
388
|
-
Array.isArray(patterns) ? patterns : [patterns],
|
|
389
|
-
basePath
|
|
390
|
-
);
|
|
391
|
-
warnings.push(...w);
|
|
392
|
-
allMatchedFiles.push(...matchedFiles);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
if (checks.length > 0 && allMatchedFiles.length === 0) {
|
|
396
|
-
warnings.push({
|
|
397
|
-
message: `No annotation matches found for spec "${spec.id}"`,
|
|
398
|
-
specId: spec.id
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
return {
|
|
402
|
-
success: errors.length === 0,
|
|
403
|
-
errors,
|
|
404
|
-
warnings,
|
|
405
|
-
matchedFiles: allMatchedFiles.length > 0 ? allMatchedFiles : void 0
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
174
|
function annotationCoverage(config) {
|
|
411
175
|
return {
|
|
412
176
|
targetModel: "annotation",
|
|
@@ -479,6 +243,144 @@ function annotationCoverage(config) {
|
|
|
479
243
|
}
|
|
480
244
|
};
|
|
481
245
|
}
|
|
246
|
+
function relationCoverage(config) {
|
|
247
|
+
return {
|
|
248
|
+
targetModel: config.targetModel,
|
|
249
|
+
description: config.description,
|
|
250
|
+
check: (specs, registry) => {
|
|
251
|
+
const targets = registry[config.targetModel];
|
|
252
|
+
if (!targets) {
|
|
253
|
+
return { total: 0, covered: 0, uncovered: 0, coveragePercent: 100, coveredItems: [], uncoveredItems: [] };
|
|
254
|
+
}
|
|
255
|
+
const allTargetIds = /* @__PURE__ */ new Set();
|
|
256
|
+
const targetDescriptions = /* @__PURE__ */ new Map();
|
|
257
|
+
for (const t of targets.values()) {
|
|
258
|
+
if (config.targetPrefix && !t.id.startsWith(config.targetPrefix)) continue;
|
|
259
|
+
allTargetIds.add(t.id);
|
|
260
|
+
targetDescriptions.set(t.id, t.name ?? t.description ?? t.id);
|
|
261
|
+
}
|
|
262
|
+
const coveredIds = /* @__PURE__ */ new Set();
|
|
263
|
+
for (const spec of specs) {
|
|
264
|
+
if (!spec.relations) continue;
|
|
265
|
+
for (const rel of spec.relations) {
|
|
266
|
+
if (config.relationType && rel.type !== config.relationType) continue;
|
|
267
|
+
if (allTargetIds.has(rel.target)) coveredIds.add(rel.target);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const coveredItems = [];
|
|
271
|
+
const uncoveredItems = [];
|
|
272
|
+
for (const id of allTargetIds) {
|
|
273
|
+
const desc = targetDescriptions.get(id);
|
|
274
|
+
if (coveredIds.has(id)) {
|
|
275
|
+
coveredItems.push({ id, description: desc });
|
|
276
|
+
} else {
|
|
277
|
+
uncoveredItems.push({ id, description: desc });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const total = allTargetIds.size;
|
|
281
|
+
const covered = coveredItems.length;
|
|
282
|
+
const coveragePercent = total > 0 ? Math.round(covered / total * 100) : 100;
|
|
283
|
+
return { total, covered, uncovered: total - covered, coveragePercent, coveredItems, uncoveredItems };
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/core/dsl/type-compat.ts
|
|
289
|
+
var ALIASES = {
|
|
290
|
+
INTEGER: "INT",
|
|
291
|
+
BOOL: "BOOLEAN",
|
|
292
|
+
REAL: "FLOAT",
|
|
293
|
+
"DOUBLE PRECISION": "DOUBLE",
|
|
294
|
+
SERIAL: "INT",
|
|
295
|
+
BIGSERIAL: "BIGINT",
|
|
296
|
+
SMALLSERIAL: "SMALLINT",
|
|
297
|
+
TIMESTAMPTZ: "TIMESTAMP",
|
|
298
|
+
"TIMESTAMP WITH TIME ZONE": "TIMESTAMP",
|
|
299
|
+
"TIMESTAMP WITHOUT TIME ZONE": "TIMESTAMP",
|
|
300
|
+
"CHARACTER VARYING": "VARCHAR",
|
|
301
|
+
CHARACTER: "CHAR",
|
|
302
|
+
INT2: "SMALLINT",
|
|
303
|
+
INT4: "INT",
|
|
304
|
+
INT8: "BIGINT",
|
|
305
|
+
FLOAT4: "FLOAT",
|
|
306
|
+
FLOAT8: "DOUBLE"
|
|
307
|
+
};
|
|
308
|
+
var CONTAINMENT_CHAINS = [
|
|
309
|
+
["SMALLINT", "INT", "BIGINT"],
|
|
310
|
+
["FLOAT", "DOUBLE"],
|
|
311
|
+
["CHAR", "VARCHAR", "TEXT"],
|
|
312
|
+
["INTEGER", "NUMBER"]
|
|
313
|
+
];
|
|
314
|
+
function parseType(raw) {
|
|
315
|
+
const trimmed = raw.trim().toUpperCase();
|
|
316
|
+
const match = trimmed.match(/^([A-Z\s]+?)(?:\(\s*(\d+)(?:\s*,\s*(\d+))?\s*\))?$/);
|
|
317
|
+
if (!match) {
|
|
318
|
+
return { base: trimmed };
|
|
319
|
+
}
|
|
320
|
+
const base = match[1].trim();
|
|
321
|
+
return {
|
|
322
|
+
base,
|
|
323
|
+
length: match[2] !== void 0 ? Number(match[2]) : void 0,
|
|
324
|
+
precision: match[2] !== void 0 ? Number(match[2]) : void 0,
|
|
325
|
+
scale: match[3] !== void 0 ? Number(match[3]) : void 0
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function normalize(base) {
|
|
329
|
+
return ALIASES[base] ?? base;
|
|
330
|
+
}
|
|
331
|
+
function findAllChainPositions(base, chains) {
|
|
332
|
+
const results = [];
|
|
333
|
+
const resolved = normalize(base);
|
|
334
|
+
for (const chain of chains) {
|
|
335
|
+
let idx = chain.indexOf(base);
|
|
336
|
+
if (idx === -1) idx = chain.indexOf(resolved);
|
|
337
|
+
if (idx !== -1) results.push({ chain, index: idx });
|
|
338
|
+
}
|
|
339
|
+
return results;
|
|
340
|
+
}
|
|
341
|
+
function isTypeContainedBy(specType, ddlType) {
|
|
342
|
+
const spec = parseType(specType);
|
|
343
|
+
const ddl = parseType(ddlType);
|
|
344
|
+
const specNorm = normalize(spec.base);
|
|
345
|
+
const ddlNorm = normalize(ddl.base);
|
|
346
|
+
if (specNorm === ddlNorm) {
|
|
347
|
+
if (specNorm === "DECIMAL" || specNorm === "NUMERIC") {
|
|
348
|
+
if (spec.precision !== void 0 && ddl.precision !== void 0) {
|
|
349
|
+
if (ddl.precision < spec.precision) return false;
|
|
350
|
+
}
|
|
351
|
+
if (spec.scale !== void 0 && ddl.scale !== void 0) {
|
|
352
|
+
if (ddl.scale < spec.scale) return false;
|
|
353
|
+
}
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
if (spec.length !== void 0 && ddl.length !== void 0) {
|
|
357
|
+
return ddl.length >= spec.length;
|
|
358
|
+
}
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
const specPositions = findAllChainPositions(spec.base, CONTAINMENT_CHAINS);
|
|
362
|
+
const ddlPositions = findAllChainPositions(ddl.base, CONTAINMENT_CHAINS);
|
|
363
|
+
for (const sp of specPositions) {
|
|
364
|
+
for (const dp of ddlPositions) {
|
|
365
|
+
if (sp.chain === dp.chain) {
|
|
366
|
+
return dp.index >= sp.index;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
var ImplementsRelationSchema = z.object({
|
|
373
|
+
target: z.string(),
|
|
374
|
+
targetType: z.string(),
|
|
375
|
+
sourcePath: z.string().optional(),
|
|
376
|
+
description: z.string().optional()
|
|
377
|
+
});
|
|
378
|
+
var VerifiedByRelationSchema = z.object({
|
|
379
|
+
target: z.string(),
|
|
380
|
+
testPath: z.string().optional(),
|
|
381
|
+
framework: z.string().optional(),
|
|
382
|
+
description: z.string().optional()
|
|
383
|
+
});
|
|
482
384
|
function resolveRef(doc, ref) {
|
|
483
385
|
if (!ref.startsWith("#/")) return void 0;
|
|
484
386
|
const parts = ref.substring(2).split("/");
|
|
@@ -528,117 +430,27 @@ function findSpecIdInOpenAPI(doc, specId) {
|
|
|
528
430
|
if (searchXSpecId(doc)) return { pathKey: "(x-spec-id match)" };
|
|
529
431
|
return null;
|
|
530
432
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
let content;
|
|
545
|
-
try {
|
|
546
|
-
content = readFileSync(fullPath, "utf-8");
|
|
547
|
-
} catch {
|
|
548
|
-
errors.push({ message: `Failed to read OpenAPI file: ${filePath}`, specId: spec.id });
|
|
549
|
-
return { success: false, errors, warnings };
|
|
550
|
-
}
|
|
551
|
-
if (content.length === 0) {
|
|
552
|
-
errors.push({ message: `OpenAPI file is empty: ${filePath}`, specId: spec.id });
|
|
553
|
-
return { success: false, errors, warnings };
|
|
554
|
-
}
|
|
555
|
-
let doc;
|
|
556
|
-
try {
|
|
557
|
-
if (fullPath.endsWith(".json")) {
|
|
558
|
-
doc = JSON.parse(content);
|
|
559
|
-
} else {
|
|
560
|
-
doc = parse(content);
|
|
561
|
-
}
|
|
562
|
-
} catch (e) {
|
|
563
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
564
|
-
errors.push({ message: `Failed to parse OpenAPI file ${filePath}: ${msg}`, specId: spec.id });
|
|
565
|
-
return { success: false, errors, warnings };
|
|
566
|
-
}
|
|
567
|
-
const match = findSpecIdInOpenAPI(doc, spec.id);
|
|
568
|
-
if (!match) {
|
|
569
|
-
warnings.push({ message: `Spec ID "${spec.id}" not found in OpenAPI document ${filePath}`, specId: spec.id });
|
|
570
|
-
return { success: true, errors, warnings };
|
|
571
|
-
}
|
|
572
|
-
if (!config?.mapper) {
|
|
573
|
-
return { success: errors.length === 0, errors, warnings };
|
|
574
|
-
}
|
|
575
|
-
const mapping = config.mapper(spec);
|
|
576
|
-
if (mapping.method && match.operation) {
|
|
577
|
-
const expectedMethod = mapping.method.toLowerCase();
|
|
578
|
-
const actualMethod = match.method?.toLowerCase();
|
|
579
|
-
if (actualMethod && actualMethod !== expectedMethod) {
|
|
580
|
-
warnings.push({
|
|
581
|
-
message: `Method mismatch for "${spec.id}": expected ${expectedMethod.toUpperCase()}, found ${actualMethod.toUpperCase()}`,
|
|
582
|
-
specId: spec.id
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
if (match.operation && mapping.parameters) {
|
|
587
|
-
const opParams = (match.operation.parameters || []).map(
|
|
588
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
589
|
-
(p) => maybeResolveRef(doc, p)
|
|
590
|
-
);
|
|
591
|
-
for (const expectedParam of mapping.parameters) {
|
|
592
|
-
const found = opParams.find(
|
|
593
|
-
(p) => p?.name === expectedParam.name && (!expectedParam.in || p?.in === expectedParam.in)
|
|
594
|
-
);
|
|
595
|
-
if (!found) {
|
|
596
|
-
warnings.push({
|
|
597
|
-
message: `Parameter "${expectedParam.name}" not found in operation "${spec.id}"`,
|
|
598
|
-
specId: spec.id,
|
|
599
|
-
field: expectedParam.name
|
|
600
|
-
});
|
|
601
|
-
} else if (expectedParam.type && found.schema?.type) {
|
|
602
|
-
if (!isTypeContainedBy(expectedParam.type, found.schema.type)) {
|
|
603
|
-
warnings.push({
|
|
604
|
-
message: `Parameter "${expectedParam.name}" type mismatch: expected ${expectedParam.type}, found ${found.schema.type}`,
|
|
605
|
-
specId: spec.id,
|
|
606
|
-
field: expectedParam.name
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
if (match.operation && mapping.responseProperties) {
|
|
613
|
-
const successResponse = match.operation.responses?.["200"] || match.operation.responses?.["201"];
|
|
614
|
-
let responseSchema = successResponse?.content?.["application/json"]?.schema;
|
|
615
|
-
responseSchema = maybeResolveRef(doc, responseSchema);
|
|
616
|
-
if (responseSchema?.type === "array") {
|
|
617
|
-
responseSchema = maybeResolveRef(doc, responseSchema.items);
|
|
618
|
-
}
|
|
619
|
-
const responseProps = responseSchema?.properties || {};
|
|
620
|
-
for (const expectedProp of mapping.responseProperties) {
|
|
621
|
-
const foundProp = responseProps[expectedProp.name];
|
|
622
|
-
if (!foundProp) {
|
|
623
|
-
warnings.push({
|
|
624
|
-
message: `Response property "${expectedProp.name}" not found in operation "${spec.id}"`,
|
|
625
|
-
specId: spec.id,
|
|
626
|
-
field: expectedProp.name
|
|
627
|
-
});
|
|
628
|
-
} else if (expectedProp.type && foundProp.type) {
|
|
629
|
-
if (!isTypeContainedBy(expectedProp.type, foundProp.type)) {
|
|
630
|
-
warnings.push({
|
|
631
|
-
message: `Response property "${expectedProp.name}" type mismatch: expected ${expectedProp.type}, found ${foundProp.type}`,
|
|
632
|
-
specId: spec.id,
|
|
633
|
-
field: expectedProp.name
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
433
|
+
var openapiScanner = {
|
|
434
|
+
findSpecIds(content, specIds, filePath) {
|
|
435
|
+
const doc = content;
|
|
436
|
+
if (!doc || typeof doc !== "object") return [];
|
|
437
|
+
const matches = [];
|
|
438
|
+
for (const specId of specIds) {
|
|
439
|
+
const match = findSpecIdInOpenAPI(doc, specId);
|
|
440
|
+
if (match) {
|
|
441
|
+
matches.push({
|
|
442
|
+
specId,
|
|
443
|
+
location: `${filePath}:${match.pathKey}`,
|
|
444
|
+
context: { doc, match }
|
|
445
|
+
});
|
|
638
446
|
}
|
|
639
|
-
return { success: errors.length === 0, errors, warnings };
|
|
640
447
|
}
|
|
641
|
-
|
|
448
|
+
return matches;
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
function stripSchemaPrefix(name) {
|
|
452
|
+
const dotIndex = name.lastIndexOf(".");
|
|
453
|
+
return dotIndex >= 0 ? name.substring(dotIndex + 1) : name;
|
|
642
454
|
}
|
|
643
455
|
function parseDDLWithParser(content) {
|
|
644
456
|
const parser = new nodeSqlParser.Parser();
|
|
@@ -648,7 +460,8 @@ function parseDDLWithParser(content) {
|
|
|
648
460
|
const statements = Array.isArray(ast) ? ast : [ast];
|
|
649
461
|
for (const stmt of statements) {
|
|
650
462
|
if (stmt.type !== "create" || stmt.keyword !== "table") continue;
|
|
651
|
-
const
|
|
463
|
+
const rawName = typeof stmt.table === "string" ? stmt.table : stmt.table?.[0]?.table ?? "";
|
|
464
|
+
const tableName = stripSchemaPrefix(rawName);
|
|
652
465
|
const columns = (stmt.create_definitions || []).filter((def) => def.resource === "column").map((def) => {
|
|
653
466
|
const colName = def.column?.column ?? "";
|
|
654
467
|
let colType = def.definition?.dataType ?? "";
|
|
@@ -671,7 +484,7 @@ function parseDDLWithParser(content) {
|
|
|
671
484
|
}
|
|
672
485
|
function parseDDLWithRegex(content) {
|
|
673
486
|
const tables = [];
|
|
674
|
-
const tableRegex = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(
|
|
487
|
+
const tableRegex = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:(?:[`"']?\w+[`"']?\s*\.\s*)?)[`"']?(\w+)[`"']?\s*\(([\s\S]*?)\);/gi;
|
|
675
488
|
let match;
|
|
676
489
|
while ((match = tableRegex.exec(content)) !== null) {
|
|
677
490
|
const tableName = match[1];
|
|
@@ -691,132 +504,296 @@ function parseDDLWithRegex(content) {
|
|
|
691
504
|
}
|
|
692
505
|
return tables;
|
|
693
506
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
if (!existsSync(fullPath)) {
|
|
704
|
-
errors.push({ message: `DDL file not found: ${filePath}`, specId: spec.id });
|
|
705
|
-
return { success: false, errors, warnings };
|
|
706
|
-
}
|
|
707
|
-
let content;
|
|
708
|
-
try {
|
|
709
|
-
content = readFileSync(fullPath, "utf-8");
|
|
710
|
-
} catch {
|
|
711
|
-
errors.push({ message: `Failed to read DDL file: ${filePath}`, specId: spec.id });
|
|
712
|
-
return { success: false, errors, warnings };
|
|
713
|
-
}
|
|
714
|
-
if (content.length === 0) {
|
|
715
|
-
errors.push({ message: `DDL file is empty: ${filePath}`, specId: spec.id });
|
|
716
|
-
return { success: false, errors, warnings };
|
|
717
|
-
}
|
|
718
|
-
let tables = parseDDLWithParser(content);
|
|
719
|
-
if (!tables) {
|
|
720
|
-
warnings.push({ message: `DDL parser failed for ${filePath}, using regex fallback`, specId: spec.id });
|
|
721
|
-
tables = parseDDLWithRegex(content);
|
|
722
|
-
}
|
|
723
|
-
if (!config?.mapper) {
|
|
724
|
-
return { success: errors.length === 0, errors, warnings };
|
|
725
|
-
}
|
|
726
|
-
const mapping = config.mapper(spec);
|
|
507
|
+
var ddlScanner = {
|
|
508
|
+
findSpecIds(content, specIds, filePath) {
|
|
509
|
+
if (typeof content !== "string") return [];
|
|
510
|
+
let tables = parseDDLWithParser(content);
|
|
511
|
+
if (!tables) {
|
|
512
|
+
tables = parseDDLWithRegex(content);
|
|
513
|
+
}
|
|
514
|
+
const matches = [];
|
|
515
|
+
for (const specId of specIds) {
|
|
727
516
|
const table = tables.find(
|
|
728
|
-
(t) => t.name.toLowerCase() ===
|
|
517
|
+
(t) => t.name.toLowerCase() === specId.toLowerCase()
|
|
729
518
|
);
|
|
730
|
-
if (
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
519
|
+
if (table) {
|
|
520
|
+
matches.push({
|
|
521
|
+
specId,
|
|
522
|
+
location: `${filePath}:${table.name}`,
|
|
523
|
+
context: { table }
|
|
734
524
|
});
|
|
735
|
-
return { success: errors.length === 0, errors, warnings };
|
|
736
525
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
526
|
+
}
|
|
527
|
+
return matches;
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
function extractSpecIds2(captured) {
|
|
531
|
+
return captured.split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
|
|
532
|
+
}
|
|
533
|
+
var annotationScanner = {
|
|
534
|
+
findSpecIds(content, specIds, filePath) {
|
|
535
|
+
if (typeof content !== "string") return [];
|
|
536
|
+
const specIdSet = new Set(specIds);
|
|
537
|
+
const matches = [];
|
|
538
|
+
const defaultPattern = /@(?:verifies|implements|traces)\s+([\w-]+(?:[,\s]+[\w-]+)*)/g;
|
|
539
|
+
const lines = content.split("\n");
|
|
540
|
+
for (let lineNum = 1; lineNum <= lines.length; lineNum++) {
|
|
541
|
+
const line = lines[lineNum - 1];
|
|
542
|
+
const copy = new RegExp(defaultPattern.source, defaultPattern.flags);
|
|
543
|
+
let m;
|
|
544
|
+
while ((m = copy.exec(line)) !== null) {
|
|
545
|
+
const ids = extractSpecIds2(m[1] ?? "");
|
|
546
|
+
for (const id of ids) {
|
|
547
|
+
if (specIdSet.has(id)) {
|
|
548
|
+
matches.push({
|
|
549
|
+
specId: id,
|
|
550
|
+
location: `${filePath}:${lineNum}`,
|
|
551
|
+
context: { filePath, line: lineNum }
|
|
758
552
|
});
|
|
759
553
|
}
|
|
760
554
|
}
|
|
761
555
|
}
|
|
762
|
-
return { success: errors.length === 0, errors, warnings };
|
|
763
556
|
}
|
|
764
|
-
|
|
765
|
-
}
|
|
766
|
-
|
|
557
|
+
return matches;
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
function createAnnotationScanner(contentPatterns) {
|
|
767
561
|
return {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
|
|
562
|
+
findSpecIds(content, specIds, filePath) {
|
|
563
|
+
if (typeof content !== "string") return [];
|
|
564
|
+
const specIdSet = new Set(specIds);
|
|
565
|
+
const matches = [];
|
|
566
|
+
const lines = content.split("\n");
|
|
567
|
+
for (let lineNum = 1; lineNum <= lines.length; lineNum++) {
|
|
568
|
+
const line = lines[lineNum - 1];
|
|
569
|
+
for (const re of contentPatterns) {
|
|
570
|
+
const copy = new RegExp(re.source, re.flags);
|
|
571
|
+
let m;
|
|
572
|
+
while ((m = copy.exec(line)) !== null) {
|
|
573
|
+
const ids = extractSpecIds2(m[1] ?? "");
|
|
574
|
+
for (const id of ids) {
|
|
575
|
+
if (specIdSet.has(id)) {
|
|
576
|
+
matches.push({
|
|
577
|
+
specId: id,
|
|
578
|
+
location: `${filePath}:${lineNum}`,
|
|
579
|
+
context: { filePath, line: lineNum }
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
774
585
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
586
|
+
return matches;
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
var BUILT_IN_SCANNERS = {
|
|
591
|
+
openapi: openapiScanner,
|
|
592
|
+
ddl: ddlScanner,
|
|
593
|
+
annotation: annotationScanner
|
|
594
|
+
};
|
|
595
|
+
function getScannerForSource(source) {
|
|
596
|
+
if (source.scanner) return source.scanner;
|
|
597
|
+
if (source.type === "annotation" && source.contentPatterns && source.contentPatterns.length > 0) {
|
|
598
|
+
return createAnnotationScanner(source.contentPatterns);
|
|
599
|
+
}
|
|
600
|
+
return BUILT_IN_SCANNERS[source.type] ?? null;
|
|
601
|
+
}
|
|
602
|
+
function loadFileContent(filePath, sourceType) {
|
|
603
|
+
const content = readFileSync(filePath, "utf-8");
|
|
604
|
+
if (!content || content.length === 0) return null;
|
|
605
|
+
if (sourceType === "openapi") {
|
|
606
|
+
if (filePath.endsWith(".json")) {
|
|
607
|
+
return JSON.parse(content);
|
|
608
|
+
}
|
|
609
|
+
return parse(content);
|
|
610
|
+
}
|
|
611
|
+
return content;
|
|
612
|
+
}
|
|
613
|
+
function runGlobalScan(sources, specIds, basePath) {
|
|
614
|
+
const cwd = basePath ?? process.cwd();
|
|
615
|
+
const result = /* @__PURE__ */ new Map();
|
|
616
|
+
const warnings = [];
|
|
617
|
+
for (const source of sources) {
|
|
618
|
+
const scanner = getScannerForSource(source);
|
|
619
|
+
if (!scanner) {
|
|
620
|
+
warnings.push({
|
|
621
|
+
message: `No scanner found for source type "${source.type}". Provide a custom scanner.`,
|
|
622
|
+
sourceType: source.type
|
|
623
|
+
});
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
const allFiles = /* @__PURE__ */ new Set();
|
|
627
|
+
for (const pattern of source.paths) {
|
|
628
|
+
const found = glob.sync(pattern, {
|
|
629
|
+
cwd,
|
|
630
|
+
ignore: source.exclude ?? []
|
|
631
|
+
});
|
|
632
|
+
for (const f of found) {
|
|
633
|
+
allFiles.add(f);
|
|
781
634
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
635
|
+
}
|
|
636
|
+
for (const relPath of allFiles) {
|
|
637
|
+
const fullPath = isAbsolute(relPath) ? relPath : join(cwd, relPath);
|
|
638
|
+
if (!existsSync(fullPath)) continue;
|
|
639
|
+
let content;
|
|
640
|
+
try {
|
|
641
|
+
content = loadFileContent(fullPath, source.type);
|
|
642
|
+
} catch (e) {
|
|
643
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
644
|
+
warnings.push({
|
|
645
|
+
message: `Failed to load ${relPath}: ${msg}`,
|
|
646
|
+
sourceType: source.type,
|
|
647
|
+
filePath: relPath
|
|
648
|
+
});
|
|
649
|
+
continue;
|
|
789
650
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
651
|
+
if (content == null) continue;
|
|
652
|
+
let matches;
|
|
653
|
+
try {
|
|
654
|
+
matches = scanner.findSpecIds(content, specIds, relPath);
|
|
655
|
+
} catch (e) {
|
|
656
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
657
|
+
warnings.push({
|
|
658
|
+
message: `Scanner error for ${relPath}: ${msg}`,
|
|
659
|
+
sourceType: source.type,
|
|
660
|
+
filePath: relPath
|
|
661
|
+
});
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
for (const match of matches) {
|
|
665
|
+
const globalMatch = {
|
|
666
|
+
...match,
|
|
667
|
+
sourceType: source.type,
|
|
668
|
+
relation: source.relation,
|
|
669
|
+
filePath: relPath
|
|
670
|
+
};
|
|
671
|
+
const existing = result.get(match.specId);
|
|
672
|
+
if (existing) {
|
|
673
|
+
existing.push(globalMatch);
|
|
796
674
|
} else {
|
|
797
|
-
|
|
675
|
+
result.set(match.specId, [globalMatch]);
|
|
798
676
|
}
|
|
799
677
|
}
|
|
800
|
-
const total = allTargetIds.size;
|
|
801
|
-
const covered = coveredItems.length;
|
|
802
|
-
const coveragePercent = total > 0 ? Math.round(covered / total * 100) : 100;
|
|
803
|
-
return { total, covered, uncovered: total - covered, coveragePercent, coveredItems, uncoveredItems };
|
|
804
678
|
}
|
|
679
|
+
}
|
|
680
|
+
return { matches: result, warnings };
|
|
681
|
+
}
|
|
682
|
+
function runDeepValidation(specId, matches, deepValidation, spec) {
|
|
683
|
+
const errors = [];
|
|
684
|
+
const warnings = [];
|
|
685
|
+
for (const match of matches) {
|
|
686
|
+
const rule = deepValidation[match.sourceType];
|
|
687
|
+
if (!rule) continue;
|
|
688
|
+
if (match.sourceType === "openapi") {
|
|
689
|
+
const mapping = rule.mapper(spec);
|
|
690
|
+
const ctx = match.context;
|
|
691
|
+
if (!ctx?.match?.operation) continue;
|
|
692
|
+
validateOpenAPIDeep(specId, mapping, ctx.doc, ctx.match, warnings);
|
|
693
|
+
} else if (match.sourceType === "ddl") {
|
|
694
|
+
const mapping = rule.mapper(spec);
|
|
695
|
+
const ctx = match.context;
|
|
696
|
+
if (!ctx?.table) continue;
|
|
697
|
+
validateDDLDeep(specId, mapping, ctx.table, warnings);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
success: errors.length === 0,
|
|
702
|
+
errors,
|
|
703
|
+
warnings
|
|
805
704
|
};
|
|
806
705
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
706
|
+
function validateOpenAPIDeep(specId, mapping, doc, match, warnings) {
|
|
707
|
+
if (mapping.method && match.operation) {
|
|
708
|
+
const expectedMethod = mapping.method.toLowerCase();
|
|
709
|
+
const actualMethod = match.method?.toLowerCase();
|
|
710
|
+
if (actualMethod && actualMethod !== expectedMethod) {
|
|
711
|
+
warnings.push({
|
|
712
|
+
message: `Method mismatch for "${specId}": expected ${expectedMethod.toUpperCase()}, found ${actualMethod.toUpperCase()}`,
|
|
713
|
+
specId
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (match.operation && mapping.parameters) {
|
|
718
|
+
const opParams = (match.operation.parameters || []).map(
|
|
719
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
720
|
+
(p) => maybeResolveRef(doc, p)
|
|
721
|
+
);
|
|
722
|
+
for (const expectedParam of mapping.parameters) {
|
|
723
|
+
const found = opParams.find(
|
|
724
|
+
(p) => p?.name === expectedParam.name && (!expectedParam.in || p?.in === expectedParam.in)
|
|
725
|
+
);
|
|
726
|
+
if (!found) {
|
|
727
|
+
warnings.push({
|
|
728
|
+
message: `Parameter "${expectedParam.name}" not found in operation "${specId}"`,
|
|
729
|
+
specId,
|
|
730
|
+
field: expectedParam.name
|
|
731
|
+
});
|
|
732
|
+
} else if (expectedParam.type && found.schema?.type) {
|
|
733
|
+
if (!isTypeContainedBy(expectedParam.type, found.schema.type)) {
|
|
734
|
+
warnings.push({
|
|
735
|
+
message: `Parameter "${expectedParam.name}" type mismatch: expected ${expectedParam.type}, found ${found.schema.type}`,
|
|
736
|
+
specId,
|
|
737
|
+
field: expectedParam.name
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (match.operation && mapping.responseProperties) {
|
|
744
|
+
const successResponse = match.operation.responses?.["200"] || match.operation.responses?.["201"];
|
|
745
|
+
let responseSchema = successResponse?.content?.["application/json"]?.schema;
|
|
746
|
+
responseSchema = maybeResolveRef(doc, responseSchema);
|
|
747
|
+
if (responseSchema?.type === "array") {
|
|
748
|
+
responseSchema = maybeResolveRef(doc, responseSchema.items);
|
|
749
|
+
}
|
|
750
|
+
const responseProps = responseSchema?.properties || {};
|
|
751
|
+
for (const expectedProp of mapping.responseProperties) {
|
|
752
|
+
const foundProp = responseProps[expectedProp.name];
|
|
753
|
+
if (!foundProp) {
|
|
754
|
+
warnings.push({
|
|
755
|
+
message: `Response property "${expectedProp.name}" not found in operation "${specId}"`,
|
|
756
|
+
specId,
|
|
757
|
+
field: expectedProp.name
|
|
758
|
+
});
|
|
759
|
+
} else if (expectedProp.type && foundProp.type) {
|
|
760
|
+
if (!isTypeContainedBy(expectedProp.type, foundProp.type)) {
|
|
761
|
+
warnings.push({
|
|
762
|
+
message: `Response property "${expectedProp.name}" type mismatch: expected ${expectedProp.type}, found ${foundProp.type}`,
|
|
763
|
+
specId,
|
|
764
|
+
field: expectedProp.name
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
function validateDDLDeep(specId, mapping, table, warnings) {
|
|
772
|
+
if (!mapping.columns) return;
|
|
773
|
+
for (const expectedCol of mapping.columns) {
|
|
774
|
+
const foundCol = table.columns.find(
|
|
775
|
+
(c) => c.name.toLowerCase() === expectedCol.name.toLowerCase()
|
|
776
|
+
);
|
|
777
|
+
if (!foundCol) {
|
|
778
|
+
warnings.push({
|
|
779
|
+
message: `Column "${expectedCol.name}" not found in table "${mapping.tableName}"`,
|
|
780
|
+
specId,
|
|
781
|
+
field: expectedCol.name
|
|
782
|
+
});
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
if (mapping.checkTypes && expectedCol.type && foundCol.type) {
|
|
786
|
+
if (!isTypeContainedBy(expectedCol.type, foundCol.type)) {
|
|
787
|
+
warnings.push({
|
|
788
|
+
message: `Column "${expectedCol.name}" type mismatch in table "${mapping.tableName}": expected ${expectedCol.type}, found ${foundCol.type}`,
|
|
789
|
+
specId,
|
|
790
|
+
field: expectedCol.name
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
819
796
|
|
|
820
|
-
export { ImplementsRelationSchema, VerifiedByRelationSchema,
|
|
797
|
+
export { ImplementsRelationSchema, VerifiedByRelationSchema, annotationCoverage, annotationScanner, arrayMinLength, baseSpecSchema, childIdFormat, createAnnotationScanner, ddlScanner, getArtifactsConfig, idFormat, isTypeContainedBy, markdownExporter, openapiScanner, relationCoverage, requireField, runDeepValidation, runGlobalScan, setArtifactsConfig };
|
|
821
798
|
//# sourceMappingURL=index.js.map
|
|
822
799
|
//# sourceMappingURL=index.js.map
|