@dypai-ai/mcp 1.5.11 → 1.5.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.js +1 -1
- package/src/tools/sync/validate.js +340 -6
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1121,7 +1121,7 @@ async function handleRequest(msg) {
|
|
|
1121
1121
|
return makeResponse(id, {
|
|
1122
1122
|
protocolVersion: "2024-11-05",
|
|
1123
1123
|
capabilities: { tools: {} },
|
|
1124
|
-
serverInfo: { name: "dypai", version: "1.
|
|
1124
|
+
serverInfo: { name: "dypai", version: "1.5.12" },
|
|
1125
1125
|
instructions: SERVER_INSTRUCTIONS,
|
|
1126
1126
|
})
|
|
1127
1127
|
}
|
|
@@ -252,7 +252,10 @@ const RUNTIME_PLACEHOLDER_OPERATOR_RE = /(\|\||&&|\?\?|[?:+*/=<>!()]|\s+\b(?:or|
|
|
|
252
252
|
|
|
253
253
|
function unsupportedRuntimePlaceholder(expr) {
|
|
254
254
|
const clean = expr.trim()
|
|
255
|
-
|
|
255
|
+
if (expr !== clean) return true
|
|
256
|
+
if (RUNTIME_PLACEHOLDER_PATH_RE.test(clean)) return false
|
|
257
|
+
if (!RUNTIME_PLACEHOLDER_OPERATOR_RE.test(clean)) return true
|
|
258
|
+
return extractRuntimePaths(clean).length === 0
|
|
256
259
|
}
|
|
257
260
|
|
|
258
261
|
/** Minimal Levenshtein distance, caps at 3 for "did you mean" typo suggestions. */
|
|
@@ -287,6 +290,31 @@ async function readSchemaTables(rootDir) {
|
|
|
287
290
|
}
|
|
288
291
|
}
|
|
289
292
|
|
|
293
|
+
/** Parse schema.sql to extract public table columns when available. */
|
|
294
|
+
async function readSchemaColumns(rootDir) {
|
|
295
|
+
try {
|
|
296
|
+
const raw = await readFile(join(rootDir, "schema.sql"), "utf8")
|
|
297
|
+
const tables = {}
|
|
298
|
+
const tableRe = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?public\.(?:"([^"]+)"|([A-Za-z_]\w*))\s*\(([\s\S]*?)\);/gi
|
|
299
|
+
let m
|
|
300
|
+
while ((m = tableRe.exec(raw)) !== null) {
|
|
301
|
+
const tableName = m[1] || m[2]
|
|
302
|
+
const body = m[3] || ""
|
|
303
|
+
const cols = new Set()
|
|
304
|
+
for (const rawLine of body.split("\n")) {
|
|
305
|
+
const line = rawLine.trim().replace(/,$/, "")
|
|
306
|
+
if (!line || /^(CONSTRAINT|PRIMARY|FOREIGN|UNIQUE|CHECK|EXCLUDE)\b/i.test(line)) continue
|
|
307
|
+
const col = /^"?([A-Za-z_]\w*)"?\s+/.exec(line)?.[1]
|
|
308
|
+
if (col) cols.add(col)
|
|
309
|
+
}
|
|
310
|
+
tables[tableName] = cols
|
|
311
|
+
}
|
|
312
|
+
return tables
|
|
313
|
+
} catch {
|
|
314
|
+
return {}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
290
318
|
/** Extract referenced table names from a SQL string: `FROM public.X`, `JOIN public.X`, `INTO public.X`, `UPDATE public.X`. */
|
|
291
319
|
function extractSqlTables(sql) {
|
|
292
320
|
const tables = new Set()
|
|
@@ -353,6 +381,288 @@ function lookupFileMapContent(fileMap, ref) {
|
|
|
353
381
|
return fileMap[key] || fileMap[key.replace(/^dypai\//, "")] || fileMap[`dypai/${key}`] || ""
|
|
354
382
|
}
|
|
355
383
|
|
|
384
|
+
function stripSqlForInference(sql) {
|
|
385
|
+
return String(sql || "")
|
|
386
|
+
.replace(/--[^\n]*/g, " ")
|
|
387
|
+
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
388
|
+
.replace(/'(?:[^']|'')*'/g, "''")
|
|
389
|
+
.replace(/\s+/g, " ")
|
|
390
|
+
.trim()
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function isWordAt(sql, index, word) {
|
|
394
|
+
if (sql.slice(index, index + word.length).toLowerCase() !== word) return false
|
|
395
|
+
const before = index === 0 ? "" : sql[index - 1]
|
|
396
|
+
const after = sql[index + word.length] || ""
|
|
397
|
+
return !/[A-Za-z0-9_]/.test(before) && !/[A-Za-z0-9_]/.test(after)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function findOuterSelectList(sqlText) {
|
|
401
|
+
const sql = stripSqlForInference(sqlText)
|
|
402
|
+
if (!sql) return null
|
|
403
|
+
let depth = 0
|
|
404
|
+
let selectStart = -1
|
|
405
|
+
for (let i = 0; i < sql.length; i++) {
|
|
406
|
+
const ch = sql[i]
|
|
407
|
+
if (ch === "(") depth++
|
|
408
|
+
else if (ch === ")") depth = Math.max(0, depth - 1)
|
|
409
|
+
|
|
410
|
+
if (depth !== 0) continue
|
|
411
|
+
if (selectStart < 0 && isWordAt(sql, i, "select")) {
|
|
412
|
+
selectStart = i + "select".length
|
|
413
|
+
i += "select".length - 1
|
|
414
|
+
continue
|
|
415
|
+
}
|
|
416
|
+
if (selectStart >= 0 && isWordAt(sql, i, "from")) {
|
|
417
|
+
return sql.slice(selectStart, i).trim()
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return null
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function splitTopLevelComma(list) {
|
|
424
|
+
const parts = []
|
|
425
|
+
let depth = 0
|
|
426
|
+
let start = 0
|
|
427
|
+
for (let i = 0; i < list.length; i++) {
|
|
428
|
+
const ch = list[i]
|
|
429
|
+
if (ch === "(") depth++
|
|
430
|
+
else if (ch === ")") depth = Math.max(0, depth - 1)
|
|
431
|
+
else if (ch === "," && depth === 0) {
|
|
432
|
+
parts.push(list.slice(start, i).trim())
|
|
433
|
+
start = i + 1
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const tail = list.slice(start).trim()
|
|
437
|
+
if (tail) parts.push(tail)
|
|
438
|
+
return parts
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function inferSelectOutputColumns(sqlText) {
|
|
442
|
+
const selectList = findOuterSelectList(sqlText)
|
|
443
|
+
if (!selectList) return { properties: null, strict: false }
|
|
444
|
+
|
|
445
|
+
const props = new Set()
|
|
446
|
+
let strict = true
|
|
447
|
+
for (const itemRaw of splitTopLevelComma(selectList)) {
|
|
448
|
+
const item = itemRaw.trim()
|
|
449
|
+
if (!item || item === "*" || /\.\*$/.test(item)) {
|
|
450
|
+
strict = false
|
|
451
|
+
continue
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const asAlias = /\bas\s+"?([A-Za-z_]\w*)"?$/i.exec(item)?.[1]
|
|
455
|
+
if (asAlias) {
|
|
456
|
+
props.add(asAlias)
|
|
457
|
+
continue
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const simpleColumn = /^(?:(?:"?[A-Za-z_]\w*"?\.)?)"?([A-Za-z_]\w*)"?$/.exec(item)?.[1]
|
|
461
|
+
if (simpleColumn) {
|
|
462
|
+
props.add(simpleColumn)
|
|
463
|
+
continue
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const trailingAlias = /\s+"?([A-Za-z_]\w*)"?$/.exec(item)?.[1]
|
|
467
|
+
if (trailingAlias && !SQL_KEYWORDS_AFTER_FROM.has(trailingAlias.toUpperCase())) {
|
|
468
|
+
props.add(trailingAlias)
|
|
469
|
+
continue
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
strict = false
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return { properties: props.size > 0 ? props : null, strict }
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function inferSqlCardinality(sqlText) {
|
|
479
|
+
const sql = stripSqlForInference(sqlText)
|
|
480
|
+
if (!sql) return null
|
|
481
|
+
if (/\blimit\s+1(?:\D|$)/i.test(sql)) return "single"
|
|
482
|
+
const startsLikeRead = /^(select|with)\b/i.test(sql)
|
|
483
|
+
const hasAggregate = /\b(count|sum|avg|min|max|json_agg|jsonb_agg|array_agg|string_agg)\s*\(/i.test(sql)
|
|
484
|
+
const hasGroupBy = /\bgroup\s+by\b/i.test(sql)
|
|
485
|
+
if (startsLikeRead && hasAggregate && !hasGroupBy) return "single"
|
|
486
|
+
return "many"
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function returningColumns(returning, table, schemaColumns) {
|
|
490
|
+
if (Array.isArray(returning)) return new Set(returning.filter(v => typeof v === "string" && v !== "*"))
|
|
491
|
+
if (typeof returning === "string" && returning.trim() && returning.trim() !== "*") {
|
|
492
|
+
return new Set(returning.split(",").map(s => s.trim()).filter(Boolean))
|
|
493
|
+
}
|
|
494
|
+
if (table && schemaColumns?.[table]?.size) return new Set(schemaColumns[table])
|
|
495
|
+
return null
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function buildNodeOutputContracts(nodes, fileMap, ctx) {
|
|
499
|
+
const contracts = new Map()
|
|
500
|
+
for (const node of nodes || []) {
|
|
501
|
+
if (!node || typeof node !== "object" || !node.id) continue
|
|
502
|
+
const nodeType = node.type ?? node.node_type
|
|
503
|
+
const params = nodeParams(node)
|
|
504
|
+
|
|
505
|
+
if (nodeType === "set_fields") {
|
|
506
|
+
const op = nodeField(node, params, "operation") || "set"
|
|
507
|
+
const fields = nodeField(node, params, "fields")
|
|
508
|
+
if (fields && typeof fields === "object" && !Array.isArray(fields)) {
|
|
509
|
+
contracts.set(node.id, {
|
|
510
|
+
type: "object",
|
|
511
|
+
properties: new Set(Object.keys(fields).map(k => k.split(".")[0])),
|
|
512
|
+
strict: op === "compose",
|
|
513
|
+
})
|
|
514
|
+
}
|
|
515
|
+
continue
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (nodeType !== "dypai_database") continue
|
|
519
|
+
const op = nodeField(node, params, "operation")
|
|
520
|
+
const table = nodeField(node, params, "table") ?? nodeField(node, params, "table_name")
|
|
521
|
+
|
|
522
|
+
if (op === "query" || op === "custom_query") {
|
|
523
|
+
const query = nodeField(node, params, "query") || lookupFileMapContent(fileMap, nodeField(node, params, "query_file"))
|
|
524
|
+
const inferred = inferSelectOutputColumns(query)
|
|
525
|
+
contracts.set(node.id, {
|
|
526
|
+
type: "array",
|
|
527
|
+
cardinality: inferSqlCardinality(query),
|
|
528
|
+
properties: inferred.properties,
|
|
529
|
+
strict: inferred.strict,
|
|
530
|
+
})
|
|
531
|
+
continue
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (op === "select") {
|
|
535
|
+
contracts.set(node.id, {
|
|
536
|
+
type: "array",
|
|
537
|
+
cardinality: "many",
|
|
538
|
+
properties: table ? ctx.schemaColumns?.[table] ?? null : null,
|
|
539
|
+
strict: Boolean(table && ctx.schemaColumns?.[table]),
|
|
540
|
+
})
|
|
541
|
+
continue
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (op === "aggregate") {
|
|
545
|
+
contracts.set(node.id, { type: "object", properties: new Set(["value"]), strict: true })
|
|
546
|
+
continue
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (op === "insert") {
|
|
550
|
+
const bulk = nodeField(node, params, "mode") === "bulk" || Array.isArray(nodeField(node, params, "data"))
|
|
551
|
+
contracts.set(node.id, {
|
|
552
|
+
type: bulk ? "array" : "object",
|
|
553
|
+
cardinality: bulk ? "many" : "single",
|
|
554
|
+
properties: table ? ctx.schemaColumns?.[table] ?? null : null,
|
|
555
|
+
strict: Boolean(table && ctx.schemaColumns?.[table]),
|
|
556
|
+
})
|
|
557
|
+
continue
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (op === "upsert") {
|
|
561
|
+
contracts.set(node.id, {
|
|
562
|
+
type: "object",
|
|
563
|
+
cardinality: "single",
|
|
564
|
+
properties: table ? ctx.schemaColumns?.[table] ?? null : null,
|
|
565
|
+
strict: Boolean(table && ctx.schemaColumns?.[table]),
|
|
566
|
+
})
|
|
567
|
+
continue
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (op === "update" || op === "delete" || op === "copy_to") {
|
|
571
|
+
contracts.set(node.id, {
|
|
572
|
+
type: "array",
|
|
573
|
+
cardinality: "many",
|
|
574
|
+
properties: table ? ctx.schemaColumns?.[table] ?? null : null,
|
|
575
|
+
strict: Boolean(table && ctx.schemaColumns?.[table]),
|
|
576
|
+
})
|
|
577
|
+
continue
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (op === "mutation") {
|
|
581
|
+
const insertValue = nodeField(node, params, "insert")
|
|
582
|
+
const updateValue = nodeField(node, params, "update")
|
|
583
|
+
const deleteValue = nodeField(node, params, "delete")
|
|
584
|
+
const returning = nodeField(node, params, "returning") ?? "*"
|
|
585
|
+
const props = returningColumns(returning, table, ctx.schemaColumns)
|
|
586
|
+
if (insertValue !== undefined && insertValue !== null) {
|
|
587
|
+
const bulk = nodeField(node, params, "mode") === "bulk" || Array.isArray(insertValue)
|
|
588
|
+
const hasConflict = nodeField(node, params, "on_conflict") !== undefined
|
|
589
|
+
contracts.set(node.id, {
|
|
590
|
+
type: bulk || hasConflict ? "array" : "object",
|
|
591
|
+
cardinality: bulk ? "many" : hasConflict ? "zero_or_one" : "single",
|
|
592
|
+
properties: props,
|
|
593
|
+
strict: Boolean(props),
|
|
594
|
+
})
|
|
595
|
+
} else if (updateValue !== undefined || deleteValue === true) {
|
|
596
|
+
contracts.set(node.id, {
|
|
597
|
+
type: "array",
|
|
598
|
+
cardinality: "many",
|
|
599
|
+
properties: props,
|
|
600
|
+
strict: Boolean(props),
|
|
601
|
+
})
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return contracts
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function extractRuntimePaths(expr) {
|
|
609
|
+
const clean = String(expr || "").replace(/'(?:[^']|'')*'|"(?:[^"\\]|\\.)*"/g, " ")
|
|
610
|
+
const paths = []
|
|
611
|
+
const re = /\b(?:input|nodes|vars|current_user)\.[A-Za-z_][A-Za-z0-9_-]*(?:\[[0-9]+\]|\.[A-Za-z_][A-Za-z0-9_-]*)*|\bcurrent_user_id\b|\bcurrent_user_role\b/g
|
|
612
|
+
let m
|
|
613
|
+
while ((m = re.exec(clean)) !== null) paths.push(m[0])
|
|
614
|
+
if (paths.length === 0 && RUNTIME_PLACEHOLDER_PATH_RE.test(expr.trim())) paths.push(expr.trim())
|
|
615
|
+
return [...new Set(paths)]
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function parseNodeOutputRef(path) {
|
|
619
|
+
const m = /^nodes\.([A-Za-z_][A-Za-z0-9_-]*)(.*)$/.exec(path)
|
|
620
|
+
if (!m) return null
|
|
621
|
+
const nodeId = m[1]
|
|
622
|
+
let rest = m[2] || ""
|
|
623
|
+
let indexed = false
|
|
624
|
+
if (rest.startsWith("[")) {
|
|
625
|
+
const idx = /^\[[0-9]+\]/.exec(rest)?.[0]
|
|
626
|
+
if (idx) {
|
|
627
|
+
indexed = true
|
|
628
|
+
rest = rest.slice(idx.length)
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const prop = rest.startsWith(".") ? /^\.([A-Za-z_][A-Za-z0-9_-]*)/.exec(rest)?.[1] : null
|
|
632
|
+
return { nodeId, indexed, prop }
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function validateNodeOutputRef({ expr, path, contract, endpoint, file, loc }) {
|
|
636
|
+
const ref = parseNodeOutputRef(path)
|
|
637
|
+
if (!ref || !ref.prop || !contract) return null
|
|
638
|
+
|
|
639
|
+
if (contract.strict && contract.properties && !contract.properties.has(ref.prop)) {
|
|
640
|
+
const known = [...contract.properties]
|
|
641
|
+
const suggestions = known.filter(k => levenshteinSmall(k, ref.prop) <= 2).slice(0, 3)
|
|
642
|
+
return {
|
|
643
|
+
severity: "warn",
|
|
644
|
+
rule: "node_output_property_unknown",
|
|
645
|
+
endpoint, file, loc,
|
|
646
|
+
message: `\${${expr}} references '${ref.prop}', but node '${ref.nodeId}' is inferred to output: ${known.join(", ") || "(no known properties)"}.`,
|
|
647
|
+
fix_hint: suggestions.length
|
|
648
|
+
? `Did you mean: ${suggestions.join(", ")}?`
|
|
649
|
+
: `Use one of: ${known.join(", ") || "(none)"}. If this is a dynamic SQL shape, verify with dypai_test_endpoint.`,
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (contract.type === "array" && contract.cardinality === "many" && !ref.indexed) {
|
|
654
|
+
return {
|
|
655
|
+
severity: "warn",
|
|
656
|
+
rule: "node_output_many_direct_property",
|
|
657
|
+
endpoint, file, loc,
|
|
658
|
+
message: `\${${expr}} reads property '${ref.prop}' directly from node '${ref.nodeId}', but that node is inferred to return many rows.`,
|
|
659
|
+
fix_hint: `Use \${nodes.${ref.nodeId}} for the full array, or \${nodes.${ref.nodeId}[0].${ref.prop}} if you intentionally want the first row.`,
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return null
|
|
664
|
+
}
|
|
665
|
+
|
|
356
666
|
function collectMissingTableCandidates(ctx, referencedTables, endpoint, file) {
|
|
357
667
|
if (!ctx.schemaTables) return
|
|
358
668
|
for (const table of referencedTables) {
|
|
@@ -377,6 +687,7 @@ function validateEndpoint(entry, ctx) {
|
|
|
377
687
|
|
|
378
688
|
const inputProps = doc.input?.properties || {}
|
|
379
689
|
const nodeIds = new Set((doc.workflow?.nodes || []).map(n => n.id))
|
|
690
|
+
const outputContracts = buildNodeOutputContracts(doc.workflow?.nodes || [], fileMap, ctx)
|
|
380
691
|
|
|
381
692
|
const jwt = ruleUsesJwt(doc.trigger)
|
|
382
693
|
|
|
@@ -409,6 +720,7 @@ function validateEndpoint(entry, ctx) {
|
|
|
409
720
|
for (const { source, loc, value } of sources) {
|
|
410
721
|
// --- Placeholder checks ---
|
|
411
722
|
for (const expr of extractPlaceholders(value)) {
|
|
723
|
+
const runtimePaths = extractRuntimePaths(expr)
|
|
412
724
|
if (unsupportedRuntimePlaceholder(expr)) {
|
|
413
725
|
diagnostics.push({
|
|
414
726
|
severity: "error",
|
|
@@ -423,10 +735,10 @@ function validateEndpoint(entry, ctx) {
|
|
|
423
735
|
continue
|
|
424
736
|
}
|
|
425
737
|
|
|
426
|
-
// Normalize
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
const e
|
|
738
|
+
// Normalize every runtime path we can find in this placeholder. Expressions
|
|
739
|
+
// such as `${nodes.x.stock >= input.qty}` are valid at runtime, so validate
|
|
740
|
+
// each referenced path instead of rejecting the whole expression.
|
|
741
|
+
for (const e of runtimePaths) {
|
|
430
742
|
|
|
431
743
|
// ${input.X} or ${input.X.Y}
|
|
432
744
|
// Only validate against the input schema if one is declared; DYPAI allows
|
|
@@ -462,6 +774,16 @@ function validateEndpoint(entry, ctx) {
|
|
|
462
774
|
if (!missingNodeRefs.has(nodeId)) {
|
|
463
775
|
missingNodeRefs.set(nodeId, { loc, expr })
|
|
464
776
|
}
|
|
777
|
+
} else {
|
|
778
|
+
const outputDiag = validateNodeOutputRef({
|
|
779
|
+
expr,
|
|
780
|
+
path: e,
|
|
781
|
+
contract: outputContracts.get(nodeId),
|
|
782
|
+
endpoint: name,
|
|
783
|
+
file,
|
|
784
|
+
loc,
|
|
785
|
+
})
|
|
786
|
+
if (outputDiag) diagnostics.push(outputDiag)
|
|
465
787
|
}
|
|
466
788
|
}
|
|
467
789
|
|
|
@@ -477,6 +799,7 @@ function validateEndpoint(entry, ctx) {
|
|
|
477
799
|
})
|
|
478
800
|
}
|
|
479
801
|
}
|
|
802
|
+
}
|
|
480
803
|
}
|
|
481
804
|
|
|
482
805
|
// NOTE: SQL table extraction used to live here (anywhere a string looked
|
|
@@ -1241,10 +1564,11 @@ export async function runValidation(rootDir, projectId) {
|
|
|
1241
1564
|
const config = await readLocalConfig(rootDir)
|
|
1242
1565
|
const targetProjectId = projectId || config?.project_id || null
|
|
1243
1566
|
|
|
1244
|
-
const [local, remote, schemaTables, nodeCatalog] = await Promise.all([
|
|
1567
|
+
const [local, remote, schemaTables, schemaColumns, nodeCatalog] = await Promise.all([
|
|
1245
1568
|
readLocalState(rootDir),
|
|
1246
1569
|
fetchRemoteState(targetProjectId),
|
|
1247
1570
|
readSchemaTables(rootDir),
|
|
1571
|
+
readSchemaColumns(rootDir),
|
|
1248
1572
|
loadNodeCatalog(rootDir),
|
|
1249
1573
|
])
|
|
1250
1574
|
|
|
@@ -1261,6 +1585,7 @@ export async function runValidation(rootDir, projectId) {
|
|
|
1261
1585
|
remoteCredentials,
|
|
1262
1586
|
toolEndpoints,
|
|
1263
1587
|
schemaTables,
|
|
1588
|
+
schemaColumns,
|
|
1264
1589
|
catalog: nodeCatalog.schemas,
|
|
1265
1590
|
knownTypes: nodeCatalog.knownTypes,
|
|
1266
1591
|
fileByName: Object.fromEntries(Object.values(local.byName).map(e => [e.doc.name, `endpoints/${e.file || `${e.doc.name}.yaml`}`])),
|
|
@@ -1370,6 +1695,15 @@ export async function runValidation(rootDir, projectId) {
|
|
|
1370
1695
|
}
|
|
1371
1696
|
}
|
|
1372
1697
|
|
|
1698
|
+
export const __testing = {
|
|
1699
|
+
buildNodeOutputContracts,
|
|
1700
|
+
extractRuntimePaths,
|
|
1701
|
+
inferSelectOutputColumns,
|
|
1702
|
+
inferSqlCardinality,
|
|
1703
|
+
unsupportedRuntimePlaceholder,
|
|
1704
|
+
validateNodeOutputRef,
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1373
1707
|
export const dypaiValidateTool = {
|
|
1374
1708
|
name: "dypai_validate",
|
|
1375
1709
|
description:
|