speckeeper 0.8.1 → 0.9.1
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 -66
- package/dist/cli.js +639 -102
- package/dist/cli.js.map +1 -1
- package/dist/{config-api-U2pt1aHJ.d.ts → config-api-CfxXt9Zt.d.ts} +110 -2
- package/dist/dsl/index.d.ts +50 -82
- package/dist/dsl/index.js +434 -453
- package/dist/dsl/index.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +26 -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,118 +430,24 @@ 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
|
-
|
|
642
|
-
}
|
|
448
|
+
return matches;
|
|
449
|
+
}
|
|
450
|
+
};
|
|
643
451
|
function stripSchemaPrefix(name) {
|
|
644
452
|
const dotIndex = name.lastIndexOf(".");
|
|
645
453
|
return dotIndex >= 0 ? name.substring(dotIndex + 1) : name;
|
|
@@ -696,132 +504,305 @@ function parseDDLWithRegex(content) {
|
|
|
696
504
|
}
|
|
697
505
|
return tables;
|
|
698
506
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
if (!existsSync(fullPath)) {
|
|
709
|
-
errors.push({ message: `DDL file not found: ${filePath}`, specId: spec.id });
|
|
710
|
-
return { success: false, errors, warnings };
|
|
711
|
-
}
|
|
712
|
-
let content;
|
|
713
|
-
try {
|
|
714
|
-
content = readFileSync(fullPath, "utf-8");
|
|
715
|
-
} catch {
|
|
716
|
-
errors.push({ message: `Failed to read DDL file: ${filePath}`, specId: spec.id });
|
|
717
|
-
return { success: false, errors, warnings };
|
|
718
|
-
}
|
|
719
|
-
if (content.length === 0) {
|
|
720
|
-
errors.push({ message: `DDL file is empty: ${filePath}`, specId: spec.id });
|
|
721
|
-
return { success: false, errors, warnings };
|
|
722
|
-
}
|
|
723
|
-
let tables = parseDDLWithParser(content);
|
|
724
|
-
if (!tables) {
|
|
725
|
-
warnings.push({ message: `DDL parser failed for ${filePath}, using regex fallback`, specId: spec.id });
|
|
726
|
-
tables = parseDDLWithRegex(content);
|
|
727
|
-
}
|
|
728
|
-
if (!config?.mapper) {
|
|
729
|
-
return { success: errors.length === 0, errors, warnings };
|
|
730
|
-
}
|
|
731
|
-
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) {
|
|
732
516
|
const table = tables.find(
|
|
733
|
-
(t) => t.name.toLowerCase() ===
|
|
517
|
+
(t) => t.name.toLowerCase() === specId.toLowerCase()
|
|
734
518
|
);
|
|
735
|
-
if (
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
519
|
+
if (table) {
|
|
520
|
+
matches.push({
|
|
521
|
+
specId,
|
|
522
|
+
location: `${filePath}:${table.name}`,
|
|
523
|
+
context: { table }
|
|
739
524
|
});
|
|
740
|
-
return { success: errors.length === 0, errors, warnings };
|
|
741
525
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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 }
|
|
763
552
|
});
|
|
764
553
|
}
|
|
765
554
|
}
|
|
766
555
|
}
|
|
767
|
-
return { success: errors.length === 0, errors, warnings };
|
|
768
556
|
}
|
|
769
|
-
|
|
770
|
-
}
|
|
771
|
-
|
|
557
|
+
return matches;
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
function createAnnotationScanner(contentPatterns) {
|
|
772
561
|
return {
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
const
|
|
777
|
-
|
|
778
|
-
|
|
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
|
+
}
|
|
779
585
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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, lookupKeyMap) {
|
|
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 keyToSpecId = /* @__PURE__ */ new Map();
|
|
627
|
+
for (const specId of specIds) {
|
|
628
|
+
const overrides = lookupKeyMap?.get(specId);
|
|
629
|
+
const key = overrides?.[source.type] ?? specId;
|
|
630
|
+
keyToSpecId.set(key, specId);
|
|
631
|
+
}
|
|
632
|
+
const keysToSearch = Array.from(keyToSpecId.keys());
|
|
633
|
+
const allFiles = /* @__PURE__ */ new Set();
|
|
634
|
+
for (const pattern of source.paths) {
|
|
635
|
+
const found = glob.sync(pattern, {
|
|
636
|
+
cwd,
|
|
637
|
+
ignore: source.exclude ?? []
|
|
638
|
+
});
|
|
639
|
+
for (const f of found) {
|
|
640
|
+
allFiles.add(f);
|
|
786
641
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
642
|
+
}
|
|
643
|
+
for (const relPath of allFiles) {
|
|
644
|
+
const fullPath = isAbsolute(relPath) ? relPath : join(cwd, relPath);
|
|
645
|
+
if (!existsSync(fullPath)) continue;
|
|
646
|
+
let content;
|
|
647
|
+
try {
|
|
648
|
+
content = loadFileContent(fullPath, source.type);
|
|
649
|
+
} catch (e) {
|
|
650
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
651
|
+
warnings.push({
|
|
652
|
+
message: `Failed to load ${relPath}: ${msg}`,
|
|
653
|
+
sourceType: source.type,
|
|
654
|
+
filePath: relPath
|
|
655
|
+
});
|
|
656
|
+
continue;
|
|
794
657
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
658
|
+
if (content == null) continue;
|
|
659
|
+
let matches;
|
|
660
|
+
try {
|
|
661
|
+
matches = scanner.findSpecIds(content, keysToSearch, relPath);
|
|
662
|
+
} catch (e) {
|
|
663
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
664
|
+
warnings.push({
|
|
665
|
+
message: `Scanner error for ${relPath}: ${msg}`,
|
|
666
|
+
sourceType: source.type,
|
|
667
|
+
filePath: relPath
|
|
668
|
+
});
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
for (const match of matches) {
|
|
672
|
+
const originalSpecId = keyToSpecId.get(match.specId) ?? match.specId;
|
|
673
|
+
const globalMatch = {
|
|
674
|
+
...match,
|
|
675
|
+
specId: originalSpecId,
|
|
676
|
+
sourceType: source.type,
|
|
677
|
+
relation: source.relation,
|
|
678
|
+
filePath: relPath
|
|
679
|
+
};
|
|
680
|
+
const existing = result.get(originalSpecId);
|
|
681
|
+
if (existing) {
|
|
682
|
+
existing.push(globalMatch);
|
|
801
683
|
} else {
|
|
802
|
-
|
|
684
|
+
result.set(originalSpecId, [globalMatch]);
|
|
803
685
|
}
|
|
804
686
|
}
|
|
805
|
-
const total = allTargetIds.size;
|
|
806
|
-
const covered = coveredItems.length;
|
|
807
|
-
const coveragePercent = total > 0 ? Math.round(covered / total * 100) : 100;
|
|
808
|
-
return { total, covered, uncovered: total - covered, coveragePercent, coveredItems, uncoveredItems };
|
|
809
687
|
}
|
|
688
|
+
}
|
|
689
|
+
return { matches: result, warnings };
|
|
690
|
+
}
|
|
691
|
+
function runDeepValidation(specId, matches, deepValidation, spec) {
|
|
692
|
+
const errors = [];
|
|
693
|
+
const warnings = [];
|
|
694
|
+
for (const match of matches) {
|
|
695
|
+
const rule = deepValidation[match.sourceType];
|
|
696
|
+
if (!rule) continue;
|
|
697
|
+
if (match.sourceType === "openapi") {
|
|
698
|
+
const mapping = rule.mapper(spec);
|
|
699
|
+
const ctx = match.context;
|
|
700
|
+
if (!ctx?.match?.operation) continue;
|
|
701
|
+
validateOpenAPIDeep(specId, mapping, ctx.doc, ctx.match, warnings);
|
|
702
|
+
} else if (match.sourceType === "ddl") {
|
|
703
|
+
const mapping = rule.mapper(spec);
|
|
704
|
+
const ctx = match.context;
|
|
705
|
+
if (!ctx?.table) continue;
|
|
706
|
+
validateDDLDeep(specId, mapping, ctx.table, warnings);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return {
|
|
710
|
+
success: errors.length === 0,
|
|
711
|
+
errors,
|
|
712
|
+
warnings
|
|
810
713
|
};
|
|
811
714
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
715
|
+
function validateOpenAPIDeep(specId, mapping, doc, match, warnings) {
|
|
716
|
+
if (mapping.method && match.operation) {
|
|
717
|
+
const expectedMethod = mapping.method.toLowerCase();
|
|
718
|
+
const actualMethod = match.method?.toLowerCase();
|
|
719
|
+
if (actualMethod && actualMethod !== expectedMethod) {
|
|
720
|
+
warnings.push({
|
|
721
|
+
message: `Method mismatch for "${specId}": expected ${expectedMethod.toUpperCase()}, found ${actualMethod.toUpperCase()}`,
|
|
722
|
+
specId
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (match.operation && mapping.parameters) {
|
|
727
|
+
const opParams = (match.operation.parameters || []).map(
|
|
728
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
729
|
+
(p) => maybeResolveRef(doc, p)
|
|
730
|
+
);
|
|
731
|
+
for (const expectedParam of mapping.parameters) {
|
|
732
|
+
const found = opParams.find(
|
|
733
|
+
(p) => p?.name === expectedParam.name && (!expectedParam.in || p?.in === expectedParam.in)
|
|
734
|
+
);
|
|
735
|
+
if (!found) {
|
|
736
|
+
warnings.push({
|
|
737
|
+
message: `Parameter "${expectedParam.name}" not found in operation "${specId}"`,
|
|
738
|
+
specId,
|
|
739
|
+
field: expectedParam.name
|
|
740
|
+
});
|
|
741
|
+
} else if (expectedParam.type && found.schema?.type) {
|
|
742
|
+
if (!isTypeContainedBy(expectedParam.type, found.schema.type)) {
|
|
743
|
+
warnings.push({
|
|
744
|
+
message: `Parameter "${expectedParam.name}" type mismatch: expected ${expectedParam.type}, found ${found.schema.type}`,
|
|
745
|
+
specId,
|
|
746
|
+
field: expectedParam.name
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (match.operation && mapping.responseProperties) {
|
|
753
|
+
const successResponse = match.operation.responses?.["200"] || match.operation.responses?.["201"];
|
|
754
|
+
let responseSchema = successResponse?.content?.["application/json"]?.schema;
|
|
755
|
+
responseSchema = maybeResolveRef(doc, responseSchema);
|
|
756
|
+
if (responseSchema?.type === "array") {
|
|
757
|
+
responseSchema = maybeResolveRef(doc, responseSchema.items);
|
|
758
|
+
}
|
|
759
|
+
const responseProps = responseSchema?.properties || {};
|
|
760
|
+
for (const expectedProp of mapping.responseProperties) {
|
|
761
|
+
const foundProp = responseProps[expectedProp.name];
|
|
762
|
+
if (!foundProp) {
|
|
763
|
+
warnings.push({
|
|
764
|
+
message: `Response property "${expectedProp.name}" not found in operation "${specId}"`,
|
|
765
|
+
specId,
|
|
766
|
+
field: expectedProp.name
|
|
767
|
+
});
|
|
768
|
+
} else if (expectedProp.type && foundProp.type) {
|
|
769
|
+
if (!isTypeContainedBy(expectedProp.type, foundProp.type)) {
|
|
770
|
+
warnings.push({
|
|
771
|
+
message: `Response property "${expectedProp.name}" type mismatch: expected ${expectedProp.type}, found ${foundProp.type}`,
|
|
772
|
+
specId,
|
|
773
|
+
field: expectedProp.name
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function validateDDLDeep(specId, mapping, table, warnings) {
|
|
781
|
+
if (!mapping.columns) return;
|
|
782
|
+
for (const expectedCol of mapping.columns) {
|
|
783
|
+
const foundCol = table.columns.find(
|
|
784
|
+
(c) => c.name.toLowerCase() === expectedCol.name.toLowerCase()
|
|
785
|
+
);
|
|
786
|
+
if (!foundCol) {
|
|
787
|
+
warnings.push({
|
|
788
|
+
message: `Column "${expectedCol.name}" not found in table "${mapping.tableName}"`,
|
|
789
|
+
specId,
|
|
790
|
+
field: expectedCol.name
|
|
791
|
+
});
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
if (mapping.checkTypes && expectedCol.type && foundCol.type) {
|
|
795
|
+
if (!isTypeContainedBy(expectedCol.type, foundCol.type)) {
|
|
796
|
+
warnings.push({
|
|
797
|
+
message: `Column "${expectedCol.name}" type mismatch in table "${mapping.tableName}": expected ${expectedCol.type}, found ${foundCol.type}`,
|
|
798
|
+
specId,
|
|
799
|
+
field: expectedCol.name
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
824
805
|
|
|
825
|
-
export { ImplementsRelationSchema, VerifiedByRelationSchema,
|
|
806
|
+
export { ImplementsRelationSchema, VerifiedByRelationSchema, annotationCoverage, annotationScanner, arrayMinLength, baseSpecSchema, childIdFormat, createAnnotationScanner, ddlScanner, getArtifactsConfig, idFormat, isTypeContainedBy, markdownExporter, openapiScanner, relationCoverage, requireField, runDeepValidation, runGlobalScan, setArtifactsConfig };
|
|
826
807
|
//# sourceMappingURL=index.js.map
|
|
827
808
|
//# sourceMappingURL=index.js.map
|