@yesprasad/fluent-graph 0.2.1 → 0.2.3
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/dist/cli/index.js +32 -36
- package/dist/index.d.ts +117 -2
- package/dist/index.js +1 -18
- package/package.json +5 -5
- package/dist/blast/traversal.d.ts +0 -48
- package/dist/blast/traversal.js +0 -141
- package/dist/cli/commands/blast.d.ts +0 -2
- package/dist/cli/commands/blast.js +0 -247
- package/dist/cli/commands/extract.d.ts +0 -6
- package/dist/cli/commands/extract.js +0 -99
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/utils/banner.d.ts +0 -2
- package/dist/cli/utils/banner.js +0 -25
- package/dist/cli/utils/display.d.ts +0 -3
- package/dist/cli/utils/display.js +0 -137
- package/dist/extractor/edges.d.ts +0 -13
- package/dist/extractor/edges.js +0 -136
- package/dist/extractor/index.d.ts +0 -3
- package/dist/extractor/index.js +0 -24
- package/dist/extractor/metadata.d.ts +0 -6
- package/dist/extractor/metadata.js +0 -37
- package/dist/extractor/nodes.d.ts +0 -2
- package/dist/extractor/nodes.js +0 -64
- package/dist/graph/extractor.d.ts +0 -7
- package/dist/graph/extractor.js +0 -36
- package/dist/output/writer.d.ts +0 -9
- package/dist/output/writer.js +0 -28
- package/dist/sdk/detector.d.ts +0 -6
- package/dist/sdk/detector.js +0 -32
- package/dist/sdk/loader.d.ts +0 -7
- package/dist/sdk/loader.js +0 -19
- package/dist/sdk/registry.d.ts +0 -30
- package/dist/sdk/registry.js +0 -128
- package/dist/types/graphs.d.ts +0 -35
- package/dist/types/graphs.js +0 -2
package/dist/cli/index.js
CHANGED
|
@@ -1,36 +1,32 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (process.argv.length === 2) {
|
|
34
|
-
program.help();
|
|
35
|
-
}
|
|
36
|
-
program.parse(process.argv);
|
|
1
|
+
"use strict";var ue=Object.create;var H=Object.defineProperty;var me=Object.getOwnPropertyDescriptor;var he=Object.getOwnPropertyNames;var ye=Object.getPrototypeOf,be=Object.prototype.hasOwnProperty;var Se=(o,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of he(e))!be.call(o,n)&&n!==t&&H(o,n,{get:()=>e[n],enumerable:!(r=me(e,n))||r.enumerable});return o};var E=(o,e,t)=>(t=o!=null?ue(ye(o)):{},Se(e||!o||!o.__esModule?H(t,"default",{value:o,enumerable:!0}):t,o));var pe=require("commander"),ge=E(require("path")),fe=E(require("fs"));var ae=require("commander"),M=E(require("path")),w=E(require("chalk")),R=E(require("ora"));var F=E(require("path")),$=E(require("fs")),P=async o=>{let e=F.default.join(o,"node_modules");if(!$.default.existsSync(e))return{found:!1,error:"node_modules not found. Run npm install first."};let t=F.default.join(e,"@servicenow","sdk");return $.default.existsSync(t)?{found:!0,sdkPath:t}:{found:!1,error:"@servicenow/sdk not found. This is not a ServiceNow Fluent project."}};var J=require("module"),U=E(require("path"));var _=require("module"),q=E(require("path")),I=class{constructor(){this.tables=new Map}load(e){try{let r=(0,_.createRequire)(q.default.join(e,"package.json"))("@servicenow/sdk-build-plugins");for(let[,n]of Object.entries(r)){let i=n;if(!i?.config?.name)continue;let p=i.config.name,h=Object.keys(i.config.records||{}).filter(s=>s!=="*");if(h.length!==0){for(let s of h)this.register(s,p);try{let s=i.getRelationships();s&&typeof s=="object"&&this.parseRelationships(s,p)}catch{}}}}catch{}}register(e,t){this.tables.has(e)||this.tables.set(e,{table:e,pluginName:t,isChild:!1,children:[]})}parseRelationships(e,t){for(let[r,n]of Object.entries(e))this.register(r,t),n&&typeof n=="object"&&this.processChildren(n,t,r)}processChildren(e,t,r){for(let[n,i]of Object.entries(e)){if(n==="via"||n==="descendant"||n==="relationships"||n===r)continue;let p=n;this.register(p,t);let h=this.tables.get(p);h.isChild||(h.isChild=!0,h.parentTable=r,i&&typeof i=="object"&&typeof i.via=="string"&&(h.parentVia=i.via));let s=this.tables.get(r);s&&!s.children.includes(p)&&s.children.push(p),i&&typeof i=="object"&&i.relationships&&typeof i.relationships=="object"&&this.processChildren(i.relationships,t,p)}}get(e){return this.tables.get(e)}getPluginName(e){return this.tables.get(e)?.pluginName??"Record"}getDisplayGroupName(e){let t=this.tables.get(e);return t?t.pluginName.replace(/Plugin$/,"").replace(/([a-z])([A-Z])/g,"$1 $2").replace(/^./,r=>r.toUpperCase()).trim():"Other"}isChild(e){return this.tables.get(e)?.isChild??!1}getParentTable(e){return this.tables.get(e)?.parentTable}getChildren(e){return this.tables.get(e)?.children??[]}getTablesByPlugin(e){return[...this.tables.values()].filter(t=>t.pluginName===e).map(t=>t.table)}getAllPluginNames(){return[...new Set([...this.tables.values()].map(e=>e.pluginName))].sort()}getAllTables(){return[...this.tables.keys()]}isKnown(e){return this.tables.has(e)}};function B(o){let e=(0,J.createRequire)(U.default.join(o,"package.json")),t=new I;return t.load(o),{buildCore:e("@servicenow/sdk-build-core"),sdkapi:e("@servicenow/sdk-api"),registry:t}}var z=E(require("path"));function ve(o){let e={};for(let[t,r]of Object.entries(o))if(r)try{let n=typeof r.getValue=="function"?r.getValue():r;(typeof n!="object"||n===null||Array.isArray(n))&&(e[t]=n)}catch{continue}return e}function X(o,e){let t=[];for(let r of o.query()){let n=r.setProperties||{},i=r.getId()?.getValue()?.toString()||"",p=r.getTable()?.toString()||"",h=n.name?.getValue?.()?.toString()||n.label?.getValue?.()?.toString()||n.sys_name?.getValue?.()?.toString()||i,s=r.getOriginalFilePath()?.toString()||"",y=s?z.default.relative(e,s):"";t.push({id:i,table:p,action:r.getAction(),name:h,attributes:ve(n),source:{file:y,type:r.getOriginalSource()?.getKindName?.()||"SourceFile"}})}return t}function Y(o,e,t){let r=[];for(let n of o.query()){let i=n.getId()?.getValue()?.toString(),p=n.setProperties||{},h=n.getTable()?.toString(),s=p.internal_type?.getValue?.()?.toString(),y=p.reference?.getValue?.()?.toString(),S=p.element?.getValue?.()?.toString();if(s==="reference"&&y){let f=e.find(l=>l.name===y);r.push({from:i,to:f?.id||`platform:${y}`,via:S||"reference",type:"schema_relationship"})}let N=p.table?.getValue?.()?.toString()||p.collection?.getValue?.()?.toString();if(N){let f=e.find(l=>l.name===N);r.push({from:i,to:f?.id||`platform:${N}`,via:p.table?"table":"collection",type:"dependency",targetTable:N})}if(t.getPluginName(h)==="AclPlugin"){let f=p.name?.getValue?.()?.toString();if(f&&f.includes(".")){let[l]=f.split("."),u=e.find(m=>m.name===l);r.push({from:i,to:u?.id||`platform:${l}`,via:"name",type:"dependency",targetTable:l})}}for(let[f,l]of Object.entries(p))if(Array.isArray(l))l.forEach(u=>{if(u&&u.constructor?.name==="RecordId"){let m=u.getValue?.()?.toString(),v=u.getTable?.()?.toString();m&&t.getPluginName(v)!=="TablePlugin"&&r.push({from:i,to:m,via:f,type:"dependency",targetTable:v})}});else if(l&&l.constructor?.name==="RecordId"){let u=l.getValue?.()?.toString(),m=l.getTable?.()?.toString();u&&t.getPluginName(m)!=="TablePlugin"&&r.push({from:i,to:u,via:f,type:"dependency",targetTable:m})}else if(l&&l.constructor?.name==="Record"){let u=l.getId?.()?.getValue?.()?.toString();if(u){let m=e.find(v=>v.id===u);m&&t.getPluginName(m.table)!=="TablePlugin"&&r.push({from:i,to:u,via:f,type:"dependency",targetTable:m.table})}}}return r}function Z(o){let e=[];for(let t of o.query()){let r=t.getId()?.getValue()?.toString(),n=t.getRelated?.()||[];for(let i of n){let p=i.getId()?.getValue()?.toString();p&&r&&e.push({from:r,to:p,type:"composition"})}}return e}function Q(o,e,t,r){let n=o.getConfig()||{},i=o.getPackage()||{};return{projectRoot:o.getRootDir()||process.cwd(),scopeId:n.scopeId||"unknown",scopeName:n.scope||"Global",recordCount:e,referenceCount:t,compositionCount:r,generatedAt:new Date().toISOString(),sdkVersion:i.dependencies?.["@servicenow/sdk"]||i.devDependencies?.["@servicenow/sdk"]||"unknown"}}async function ee(o,e){let t=await o.getRecords(),r=o.getRootDir(),n=X(t,r),i=Y(t,n,e),p=Z(t),h=[...i,...p];return{metadata:Q(o,t.query().length,i.length,p.length),nodes:n,edges:h}}var C=E(require("fs")),te=E(require("path"));function oe(o,e){let t=JSON.stringify(o,null,2),r=te.default.dirname(e);C.default.existsSync(r)||C.default.mkdirSync(r,{recursive:!0}),C.default.writeFileSync(e,t,"utf-8")}var re=E(require("path")),d=E(require("chalk")),L=E(require("cli-table3"));function ne(o,e){console.log(d.default.yellow.bold(`
|
|
2
|
+
\u{1F4CA} LINEAGE MAP`)),console.log(d.default.gray("\u2550".repeat(80)));let t=new Map;for(let s of o.nodes){if(s.action==="DELETE")continue;let y=e.getDisplayGroupName(s.table);t.has(y)||t.set(y,[]),t.get(y).push(s)}for(let[s,y]of t){let S=y.filter(f=>!e.isChild(f.table)&&f.source.file);if(S.length===0)continue;if(console.log(d.default.cyan.bold(`
|
|
3
|
+
${s}`)),S.some(f=>e.getPluginName(f.table)==="BusinessRulePlugin")){let f=new L.default({head:[d.default.cyan("Name"),d.default.cyan("When"),d.default.cyan("Table"),d.default.cyan("On"),d.default.cyan("Order"),d.default.cyan("Status"),d.default.cyan("File")],colWidths:[32,8,28,16,7,8,28],wordWrap:!0,style:{head:[],border:["gray"]}});S.forEach(l=>{let u=l.action==="DELETE"?d.default.red("DELETE"):d.default.green("ACTIVE"),m=l.attributes.when||"",g=o.edges.find(c=>c.from===l.id&&c.type==="dependency"&&c.targetTable)?.targetTable||"",b=[];l.attributes.action_insert&&b.push("Insert"),l.attributes.action_update&&b.push("Update"),l.attributes.action_delete&&b.push("Delete"),l.attributes.action_query&&b.push("Query"),f.push([d.default.white(l.name),d.default.yellow(m),d.default.magenta(g),b.length?b.join(", "):d.default.gray("\u2014"),String(l.attributes.order??""),u,d.default.gray(re.default.basename(l.source.file))])}),console.log(f.toString())}else{let f=new L.default({head:[d.default.cyan("Name"),d.default.cyan("Status"),d.default.cyan("Source File")],wordWrap:!0,style:{head:[],border:["gray"]}});S.forEach(l=>{let u=l.action==="DELETE"?d.default.red("DELETE"):d.default.green("ACTIVE");f.push([d.default.white(l.name),u,d.default.gray(l.source.file)])}),console.log(f.toString())}}let r=o.edges.filter(s=>s.type==="schema_relationship");if(r.length>0){console.log(d.default.cyan.bold(`
|
|
4
|
+
\u{1F517} SCHEMA RELATIONSHIPS (Reference Fields)`));let s=new L.default({head:[d.default.cyan("Field"),d.default.cyan("From Table"),d.default.cyan("Target Table")],wordWrap:!0,style:{head:[],border:["gray"]}});r.forEach(y=>{let S=o.nodes.find(u=>u.id===y.from),N=S?.attributes?.element||y.via||"unknown",f=S?.attributes?.name||"unknown",l=y.to.startsWith("platform:")?y.to.split(":")[1]:o.nodes.find(u=>u.id===y.to)?.name||y.to;s.push([d.default.white(N),d.default.gray(f),d.default.magenta(l)])}),console.log(s.toString())}console.log(d.default.cyan.bold(`
|
|
5
|
+
\u{1F9E0} PROJECT INSIGHTS`));let n=o.edges.filter(s=>s.to.startsWith("platform:")).length,i=o.edges.filter(s=>!s.to.startsWith("platform:")).length,p=o.nodes.filter(s=>s.action!=="DELETE"&&s.source.file&&!s.id.startsWith("platform:")&&!e.isChild(s.table));console.log(` \u2022 Total User Artifacts: ${d.default.cyan(p.length)}`);let h=p.reduce((s,y)=>{let S=e.getDisplayGroupName(y.table);return s[S]=(s[S]||0)+1,s},{});console.log(` \u2022 User Artifacts Breakdown: ${JSON.stringify(h,null,2)}`),console.log(` \u2022 Platform Dependencies: ${d.default.magenta(n)}`),console.log(` \u2022 Internal References: ${d.default.white(i)}`),console.log(` \u2022 Total Artifacts: ${d.default.cyan(o.nodes.length)}`),console.log(` \u2022 Total Relationships: ${d.default.cyan(o.edges.length)}`),console.log(d.default.gray("\u2550".repeat(80))+`
|
|
6
|
+
`)}var se=new ae.Command("analyze").description("Analyze and extract dependency graph from the current project").option("-o, --output <path>","Output file path","fluent-graph.json").option("-f, --format <type>","Output format: table, json","table").option("-q, --quiet","Suppress console output").option("--no-display","Skip visual display").option("--no-banner","Skip banner (inherited)").action(async o=>{let e=o.format==="json",t;try{let r=process.cwd();!o.quiet&&!e&&(t=(0,R.default)("Detecting ServiceNow SDK...").start());let n=await P(r);n.found||(t&&t.fail("SDK not found"),e?console.log(JSON.stringify({error:n.error,code:"SDK_NOT_FOUND"})):(console.error(w.default.red(`
|
|
7
|
+
\u274C Error:`),n.error),console.log(w.default.yellow(`
|
|
8
|
+
\u{1F4A1} Make sure you're in a ServiceNow Fluent project directory`))),process.exit(1)),t&&t.succeed(`\u2705 SDK detected at ${w.default.gray(n.sdkPath)}`),!o.quiet&&!e&&(t=(0,R.default)("Loading workspace SDK...").start());let i=B(n.sdkPath);t&&t.succeed("Workspace SDK loaded"),!o.quiet&&!e&&(t=(0,R.default)("Loading project configuration...").start());let p=new i.sdkapi.Project({rootDir:r});t&&t.succeed("Project loaded"),!o.quiet&&!e&&(t=(0,R.default)("Building dependency graph...").start());let h;if(e){let S=process.stdout.write.bind(process.stdout);process.stdout.write=process.stderr.write.bind(process.stderr),h=()=>{process.stdout.write=S}}let s=await ee(p,i.registry);h?.(),t&&t.succeed(`Graph built: ${w.default.cyan(s.nodes.length)} nodes, ${w.default.cyan(s.edges.length)} edges`);let y=M.default.isAbsolute(o.output)?o.output:M.default.join(r,o.output);oe(s,y),e?console.log(JSON.stringify(s,null,2)):(o.quiet||(console.log(w.default.green(`
|
|
9
|
+
\u2705 Analysis complete`)),console.log(w.default.gray(" Location: ")+w.default.white(y)),console.log(w.default.gray(" Complexity: ")+w.default.cyan((s.edges.length/s.nodes.length).toFixed(2))+w.default.gray(" edges/node")),console.log(w.default.gray(" Generated at: ")+w.default.gray(new Date().toLocaleTimeString()))),o.display&&!o.quiet&&ne(s,i.registry))}catch(r){if(e){let n=r instanceof Error?r.message:"Unexpected error";console.log(JSON.stringify({error:n,code:"EXTRACTION_FAILED"}))}else r instanceof Error?(console.error(w.default.red(`
|
|
10
|
+
\u274C Error:`),r.message),process.env.DEBUG&&(console.error(w.default.gray(`
|
|
11
|
+
Stack trace:`)),console.error(r.stack))):(console.error(w.default.red(`
|
|
12
|
+
\u274C Unknown error occurred`)),console.error(r));process.exit(1)}});var ce=require("commander"),O=E(require("fs")),G=E(require("path")),a=E(require("chalk")),V=E(require("cli-table3"));function Ee(o){let e=new Map;for(let t of o.nodes){let r=e.get(t.name)??[];r.push(t.id),e.set(t.name,r)}return e}function ie(o,e,t){let r=Math.min(t,25),n=Ee(o),i=o.nodes.filter(m=>m.name===e),p=i[0]??null,h=new Set,s=`platform:${e}`;h.add(s);for(let m of i)h.add(m.id);let S=[...new Set([s,...i.map(m=>m.id)])].map(m=>({nodeId:m,nodeName:e,hop:0,path:[e]})),N=[],f=new Set([e]),l=0,u=0;for(;S.length>0;){let m=[];for(let v of S){if(v.hop>=r)continue;let g=o.edges.filter(b=>b.to===v.nodeId||v.nodeName.length>0&&b.targetTable===v.nodeName);for(let b of g){let c=o.nodes.find(k=>k.id===b.from);if(!c)continue;if(h.has(c.id)){u++;continue}h.add(c.id);let A=n.get(c.name)??[];for(let k of A)h.has(k)||h.add(k);let T=v.hop+1,D=[...v.path,c.name];f.has(c.name)||(f.add(c.name),N.push({node:c,hop:T,path:D,viaEdge:b}));for(let k of A)m.push({nodeId:k,nodeName:c.name,hop:T,path:D});T>l&&(l=T)}}S=m}return N.sort((m,v)=>m.hop-v.hop||m.node.name.localeCompare(v.node.name)),{target:p,targetName:e,hops:N,maxHopReached:l,cyclesPrevented:u}}var le=new ce.Command("blast").description("Analyze blast radius for a target table").argument("<target>","Target table name (e.g., incident, x_bank_account)").option("-g, --graph <path>","Path to graph JSON file","fluent-graph.json").option("-f, --format <type>","Output format: table, json, csv","table").option("--depth <n>","Maximum traversal depth","10").option("--show-paths","Show the full dependency chain for each transitive artifact").option("--direct-only","Show only hop-1 (direct) impacts").option("--include-deleted","Include deleted artifacts in the analysis").option("--no-banner","Skip banner (inherited)").action(async(o,e)=>{let t=e.format==="json";try{let r=G.isAbsolute(e.graph)?e.graph:G.join(process.cwd(),e.graph),n=new I,i=await P(process.cwd());i.found&&i.sdkPath&&n.load(i.sdkPath),O.existsSync(r)||(t?console.log(JSON.stringify({error:`Graph file not found at ${r}`,code:"GRAPH_NOT_FOUND"})):console.error(a.default.red(`
|
|
13
|
+
\u274C Error: Graph file not found at ${r}`)),process.exit(1));let p=JSON.parse(O.readFileSync(r,"utf8")),h=p.nodes.find(g=>g.name===o),s=h?h.id:`platform:${o}`,y=!h,S=p.edges.some(g=>g.to===s||g.targetTable===o);if(!h&&!S){let g=p.nodes.filter(c=>n.getPluginName(c.table)==="TablePlugin"&&!n.isChild(c.table)&&c.action!=="DELETE").map(c=>c.name).sort(),b=[...new Set(p.edges.filter(c=>c.to.startsWith("platform:")).map(c=>c.to.split(":")[1]))].sort();t?console.log(JSON.stringify({error:`Target "${o}" not found or referenced`,code:"TARGET_NOT_FOUND",availableCustomTables:g,availablePlatformTables:b})):(console.log(a.default.red(`
|
|
14
|
+
\u274C Target "${o}" not found or referenced`)),console.log(a.default.yellow(`
|
|
15
|
+
\u{1F4A1} Available custom tables:`)),g.forEach(c=>console.log(a.default.gray(" \u2022 ")+a.default.white(c))),console.log(a.default.yellow(`
|
|
16
|
+
\u{1F4A1} Referenced platform tables:`)),b.forEach(c=>console.log(a.default.gray(" \u2022 ")+a.default.magenta(c)))),process.exit(1)}t||(console.log(a.default.yellow.bold(`
|
|
17
|
+
\u{1F525} BLAST RADIUS ANALYSIS`)),console.log(a.default.gray("\u2550".repeat(80))),console.log(a.default.gray("Target: ")+a.default.white(o)+(y?a.default.yellow(" [Base Table]"):"")),y||console.log(a.default.gray("File: ")+a.default.gray(h.source.file)),console.log(a.default.gray("\u2550".repeat(80))));let N=e.directOnly?1:Math.max(1,parseInt(e.depth,10)||10),f=ie(p,o,N),l=g=>!e.includeDeleted&&g.node.action==="DELETE"?!1:g.node.source.file?!0:!n.isChild(g.node.table),u=f.hops.filter(l),m=u.filter(g=>g.hop===1),v=u.filter(g=>g.hop>1);if(u.length===0){console.log(t?JSON.stringify({target:o,isBaseTable:y,totalImpacted:0,direct:0,transitive:0,maxHopReached:0,cyclesPrevented:f.cyclesPrevented,riskLevel:"LOW",impacts:[]},null,2):a.default.green(`
|
|
18
|
+
\u2705 Zero dependencies detected in local project.
|
|
19
|
+
`));return}switch(e.format){case"json":{let g=u.length,b=g<3?"LOW":g<8?"MEDIUM":"HIGH";console.log(JSON.stringify({target:o,isBaseTable:y,totalImpacted:g,direct:m.length,transitive:v.length,maxHopReached:f.maxHopReached,cyclesPrevented:f.cyclesPrevented,riskLevel:b,impacts:u.map(c=>({artifact:c.node.name,type:n.getDisplayGroupName(c.node.table).toLowerCase(),file:c.node.source.file,hop:c.hop,path:c.path,via:c.viaEdge.via??c.viaEdge.type,edgeType:c.viaEdge.type,state:c.node.action}))},null,2));break}case"csv":{console.log("Artifact,Type,File,Hop,Via,State,Path"),u.forEach(g=>{let b=g.viaEdge.via??g.viaEdge.type,c=g.path.join(" > ");console.log(`"${g.node.name}","${n.getDisplayGroupName(g.node.table).toLowerCase()}","${g.node.source.file}",${g.hop},"${b}","${g.node.action}","${c}"`)});break}default:{if(m.length>0){console.log(a.default.bold(`
|
|
20
|
+
\u26A1 DIRECT IMPACT`)+a.default.gray(" (Hop 1)"));let A=new V.default({head:[a.default.cyan("Artifact"),a.default.cyan("Type"),a.default.cyan("File"),a.default.cyan("Via"),a.default.cyan("State")],wordWrap:!0,style:{head:[],border:["gray"]}});m.forEach(T=>{let D=we(T),k=T.node.action==="DELETE"?a.default.red:a.default.green;A.push([a.default.white(T.node.name),a.default.gray(n.getDisplayGroupName(T.node.table).toLowerCase()),a.default.gray(T.node.source.file||"\u2014"),a.default.gray(D),k(T.node.action==="DELETE"?"DELETE":"ACTIVE")])}),console.log(A.toString())}if(v.length>0&&!e.directOnly){console.log(a.default.bold(`
|
|
21
|
+
\u{1F30A} TRANSITIVE IMPACT`)+a.default.gray(" (Hop 2+)"));let A=[a.default.cyan("Artifact"),a.default.cyan("Type"),a.default.cyan("File"),a.default.cyan("Hop"),a.default.cyan("State")];e.showPaths&&A.push(a.default.cyan("Path"));let T=new V.default({head:A,wordWrap:!0,style:{head:[],border:["gray"]}});v.forEach(D=>{let k=D.node.action==="DELETE"?a.default.red:a.default.green,K=[a.default.white(D.node.name),a.default.gray(n.getDisplayGroupName(D.node.table).toLowerCase()),a.default.gray(D.node.source.file||"\u2014"),a.default.yellow(String(D.hop)),k(D.node.action==="DELETE"?"DELETE":"ACTIVE")];e.showPaths&&K.push(a.default.gray(D.path.join(" \u2192 "))),T.push(K)}),console.log(T.toString())}console.log(a.default.yellow(`
|
|
22
|
+
\u{1F4CA} IMPACT SUMMARY`));let g=u.length,b=g<3?"LOW":g<8?"MEDIUM":"HIGH",c=b==="LOW"?a.default.green:b==="MEDIUM"?a.default.yellow:a.default.red;console.log(a.default.gray("Total Impacted: ")+a.default.cyan(g)+a.default.gray(` (${m.length} direct, ${v.length} transitive)`)),console.log(a.default.gray("Max Depth Reached: ")+a.default.cyan(f.maxHopReached)),console.log(a.default.gray("Risk Level: ")+c.bold(b)),console.log(a.default.gray("\u2500".repeat(80))+`
|
|
23
|
+
`),b==="HIGH"?console.log(a.default.yellow("\u26A0\uFE0F High impact \u2014 review carefully before deploying")):b==="MEDIUM"&&console.log(a.default.yellow("\u{1F4A1} Moderate impact"));break}}}catch(r){t?console.log(JSON.stringify({error:r.message??"Unexpected error",code:"UNEXPECTED_ERROR"})):console.error(a.default.red(`
|
|
24
|
+
\u274C Error:`),r.message),process.exit(1)}});function we(o){let e=o.viaEdge;return e.type==="schema_relationship"?`${e.via??"ref"} (ref field)`:e.via??e.type}var j=E(require("chalk"));function de(){console.log(j.default.cyan(`
|
|
25
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557
|
|
26
|
+
\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551
|
|
27
|
+
\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551
|
|
28
|
+
\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551
|
|
29
|
+
\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551
|
|
30
|
+
\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D
|
|
31
|
+
`)),console.log(j.default.gray(" Dependency visibility and impact analysis for ServiceNow Fluent SDK projects")),console.log(j.default.gray(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
32
|
+
`))}var Te=ge.default.join(__dirname,"../../package.json"),Ne=fe.default.readFileSync(Te,"utf-8"),W=JSON.parse(Ne),x=new pe.Command,De=(()=>{let o=process.argv;for(let e=0;e<o.length;e++){let t=o[e];if(t==="--format"||t==="-f")return o[e+1]==="json";if(t==="--format=json"||t==="-f=json")return!0}return!1})(),ke=!process.argv.includes("--no-banner")&&!process.argv.includes("--quiet")&&!De;ke&&de();x.name(W.name).description(W.description).version(W.version).option("--no-banner","Suppress ASCII banner");x.addCommand(se);x.addCommand(le);process.argv.length===2&&x.help();x.parse(process.argv);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,117 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
interface ArtifactNode {
|
|
2
|
+
id: string;
|
|
3
|
+
table: string;
|
|
4
|
+
name: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
action: 'INSERT_OR_UPDATE' | 'DELETE';
|
|
7
|
+
attributes: Record<string, any>;
|
|
8
|
+
source: {
|
|
9
|
+
file: string;
|
|
10
|
+
type: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
interface ArtifactEdge {
|
|
14
|
+
from: string;
|
|
15
|
+
to: string;
|
|
16
|
+
via?: string;
|
|
17
|
+
type: 'reference' | 'composition' | 'dependency' | 'inheritance' | 'schema_relationship';
|
|
18
|
+
targetTable?: string;
|
|
19
|
+
childTable?: string;
|
|
20
|
+
}
|
|
21
|
+
interface GraphMetadata {
|
|
22
|
+
projectRoot: string;
|
|
23
|
+
scopeId: string;
|
|
24
|
+
scopeName: string;
|
|
25
|
+
recordCount: number;
|
|
26
|
+
referenceCount: number;
|
|
27
|
+
compositionCount: number;
|
|
28
|
+
generatedAt: string;
|
|
29
|
+
sdkVersion: string;
|
|
30
|
+
}
|
|
31
|
+
interface LineageGraph {
|
|
32
|
+
nodes: ArtifactNode[];
|
|
33
|
+
edges: ArtifactEdge[];
|
|
34
|
+
metadata: GraphMetadata;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface TableInfo {
|
|
38
|
+
table: string;
|
|
39
|
+
pluginName: string;
|
|
40
|
+
isChild: boolean;
|
|
41
|
+
parentTable?: string;
|
|
42
|
+
parentVia?: string;
|
|
43
|
+
children: string[];
|
|
44
|
+
}
|
|
45
|
+
declare class ArtifactTypeRegistry {
|
|
46
|
+
private tables;
|
|
47
|
+
load(sdkPath: string): void;
|
|
48
|
+
private register;
|
|
49
|
+
/**
|
|
50
|
+
* Parse the top-level relationships map:
|
|
51
|
+
* { parentTable: { childTable: { via, descendant, relationships: { grandchild: ... } } } }
|
|
52
|
+
*/
|
|
53
|
+
private parseRelationships;
|
|
54
|
+
private processChildren;
|
|
55
|
+
get(table: string): TableInfo | undefined;
|
|
56
|
+
getPluginName(table: string): string;
|
|
57
|
+
getDisplayGroupName(table: string): string;
|
|
58
|
+
isChild(table: string): boolean;
|
|
59
|
+
getParentTable(table: string): string | undefined;
|
|
60
|
+
getChildren(table: string): string[];
|
|
61
|
+
getTablesByPlugin(pluginName: string): string[];
|
|
62
|
+
getAllPluginNames(): string[];
|
|
63
|
+
getAllTables(): string[];
|
|
64
|
+
isKnown(table: string): boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
declare function extractLineageGraph(project: any, registry: ArtifactTypeRegistry): Promise<LineageGraph>;
|
|
68
|
+
|
|
69
|
+
interface TraversalNode {
|
|
70
|
+
node: ArtifactNode;
|
|
71
|
+
hop: number;
|
|
72
|
+
/** Chain of node names from target → this node (inclusive). */
|
|
73
|
+
path: string[];
|
|
74
|
+
/** The edge that caused this node to be discovered. */
|
|
75
|
+
viaEdge: ArtifactEdge;
|
|
76
|
+
}
|
|
77
|
+
interface TraversalResult {
|
|
78
|
+
/** The representative local graph node for the target (non-child preferred), or null if platform. */
|
|
79
|
+
target: ArtifactNode | null;
|
|
80
|
+
targetName: string;
|
|
81
|
+
/** All discovered nodes across all hops, sorted by hop ASC then name ASC. */
|
|
82
|
+
hops: TraversalNode[];
|
|
83
|
+
maxHopReached: number;
|
|
84
|
+
/** Times a visited node was skipped (cycle prevention + same-name deduplication). */
|
|
85
|
+
cyclesPrevented: number;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* BFS reverse-traversal over the dependency graph.
|
|
89
|
+
*
|
|
90
|
+
* KEY DESIGN DECISIONS:
|
|
91
|
+
*
|
|
92
|
+
* 1. A logical "entity" is identified by node NAME, not by node ID.
|
|
93
|
+
* Multiple physical nodes can share a name (e.g., a table and all its
|
|
94
|
+
* field/documentation records are all named "x_my_table"). The BFS
|
|
95
|
+
* treats them as one logical node — once any physical node for a name
|
|
96
|
+
* is discovered, ALL siblings for that name are immediately marked
|
|
97
|
+
* visited so they never appear again.
|
|
98
|
+
*
|
|
99
|
+
* 2. The initial target is seeded with ALL physical nodes sharing the
|
|
100
|
+
* target name (plus the synthetic "platform:tableName" ID). This
|
|
101
|
+
* ensures incoming edges are found regardless of which physical node
|
|
102
|
+
* the edge was recorded against.
|
|
103
|
+
*
|
|
104
|
+
* 3. Edge matching uses BOTH:
|
|
105
|
+
* - edge.to === currentNodeId (exact physical node)
|
|
106
|
+
* - edge.targetTable === currentNodeName (canonical name — reliable
|
|
107
|
+
* for platform nodes and catches mis-targeted edges)
|
|
108
|
+
*
|
|
109
|
+
* 4. ALL edge types are traversed. Display filtering is the caller's job.
|
|
110
|
+
*
|
|
111
|
+
* @param graph The loaded LineageGraph.
|
|
112
|
+
* @param targetTableName The table name the user asked about.
|
|
113
|
+
* @param maxDepth Maximum hop depth. Silently clamped to 25.
|
|
114
|
+
*/
|
|
115
|
+
declare function traverseBlastRadius(graph: LineageGraph, targetTableName: string, maxDepth: number): TraversalResult;
|
|
116
|
+
|
|
117
|
+
export { type ArtifactEdge, type ArtifactNode, type GraphMetadata, type LineageGraph, type TraversalNode, type TraversalResult, extractLineageGraph, traverseBlastRadius };
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
-
};
|
|
16
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
__exportStar(require("./extractor"), exports);
|
|
18
|
-
__exportStar(require("./types/graphs"), exports);
|
|
1
|
+
"use strict";var k=Object.create;var A=Object.defineProperty;var G=Object.getOwnPropertyDescriptor;var F=Object.getOwnPropertyNames;var M=Object.getPrototypeOf,L=Object.prototype.hasOwnProperty;var D=(t,e)=>{for(var n in e)A(t,n,{get:e[n],enumerable:!0})},x=(t,e,n,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of F(e))!L.call(t,r)&&r!==n&&A(t,r,{get:()=>e[r],enumerable:!(o=G(e,r))||o.enumerable});return t};var O=(t,e,n)=>(n=t!=null?k(M(t)):{},x(e||!t||!t.__esModule?A(n,"default",{value:t,enumerable:!0}):n,t)),q=t=>x(A({},"__esModule",{value:!0}),t);var z={};D(z,{extractLineageGraph:()=>$,traverseBlastRadius:()=>_});module.exports=q(z);var R=O(require("path"));function C(t){let e={};for(let[n,o]of Object.entries(t))if(o)try{let r=typeof o.getValue=="function"?o.getValue():o;(typeof r!="object"||r===null||Array.isArray(r))&&(e[n]=r)}catch{continue}return e}function V(t,e){let n=[];for(let o of t.query()){let r=o.setProperties||{},i=o.getId()?.getValue()?.toString()||"",c=o.getTable()?.toString()||"",f=r.name?.getValue?.()?.toString()||r.label?.getValue?.()?.toString()||r.sys_name?.getValue?.()?.toString()||i,m=o.getOriginalFilePath()?.toString()||"",y=m?R.default.relative(e,m):"";n.push({id:i,table:c,action:o.getAction(),name:f,attributes:C(r),source:{file:y,type:o.getOriginalSource()?.getKindName?.()||"SourceFile"}})}return n}function E(t,e,n){let o=[];for(let r of t.query()){let i=r.getId()?.getValue()?.toString(),c=r.setProperties||{},f=r.getTable()?.toString(),m=c.internal_type?.getValue?.()?.toString(),y=c.reference?.getValue?.()?.toString(),b=c.element?.getValue?.()?.toString();if(m==="reference"&&y){let g=e.find(s=>s.name===y);o.push({from:i,to:g?.id||`platform:${y}`,via:b||"reference",type:"schema_relationship"})}let u=c.table?.getValue?.()?.toString()||c.collection?.getValue?.()?.toString();if(u){let g=e.find(s=>s.name===u);o.push({from:i,to:g?.id||`platform:${u}`,via:c.table?"table":"collection",type:"dependency",targetTable:u})}if(n.getPluginName(f)==="AclPlugin"){let g=c.name?.getValue?.()?.toString();if(g&&g.includes(".")){let[s]=g.split("."),d=e.find(a=>a.name===s);o.push({from:i,to:d?.id||`platform:${s}`,via:"name",type:"dependency",targetTable:s})}}for(let[g,s]of Object.entries(c))if(Array.isArray(s))s.forEach(d=>{if(d&&d.constructor?.name==="RecordId"){let a=d.getValue?.()?.toString(),p=d.getTable?.()?.toString();a&&n.getPluginName(p)!=="TablePlugin"&&o.push({from:i,to:a,via:g,type:"dependency",targetTable:p})}});else if(s&&s.constructor?.name==="RecordId"){let d=s.getValue?.()?.toString(),a=s.getTable?.()?.toString();d&&n.getPluginName(a)!=="TablePlugin"&&o.push({from:i,to:d,via:g,type:"dependency",targetTable:a})}else if(s&&s.constructor?.name==="Record"){let d=s.getId?.()?.getValue?.()?.toString();if(d){let a=e.find(p=>p.id===d);a&&n.getPluginName(a.table)!=="TablePlugin"&&o.push({from:i,to:d,via:g,type:"dependency",targetTable:a.table})}}}return o}function T(t){let e=[];for(let n of t.query()){let o=n.getId()?.getValue()?.toString(),r=n.getRelated?.()||[];for(let i of r){let c=i.getId()?.getValue()?.toString();c&&o&&e.push({from:o,to:c,type:"composition"})}}return e}function P(t,e,n,o){let r=t.getConfig()||{},i=t.getPackage()||{};return{projectRoot:t.getRootDir()||process.cwd(),scopeId:r.scopeId||"unknown",scopeName:r.scope||"Global",recordCount:e,referenceCount:n,compositionCount:o,generatedAt:new Date().toISOString(),sdkVersion:i.dependencies?.["@servicenow/sdk"]||i.devDependencies?.["@servicenow/sdk"]||"unknown"}}async function $(t,e){let n=await t.getRecords(),o=t.getRootDir(),r=V(n,o),i=E(n,r,e),c=T(n),f=[...i,...c];return{metadata:P(t,n.query().length,i.length,c.length),nodes:r,edges:f}}function H(t){let e=new Map;for(let n of t.nodes){let o=e.get(n.name)??[];o.push(n.id),e.set(n.name,o)}return e}function _(t,e,n){let o=Math.min(n,25),r=H(t),i=t.nodes.filter(a=>a.name===e),c=i[0]??null,f=new Set,m=`platform:${e}`;f.add(m);for(let a of i)f.add(a.id);let b=[...new Set([m,...i.map(a=>a.id)])].map(a=>({nodeId:a,nodeName:e,hop:0,path:[e]})),u=[],g=new Set([e]),s=0,d=0;for(;b.length>0;){let a=[];for(let p of b){if(p.hop>=o)continue;let w=t.edges.filter(N=>N.to===p.nodeId||p.nodeName.length>0&&N.targetTable===p.nodeName);for(let N of w){let l=t.nodes.find(h=>h.id===N.from);if(!l)continue;if(f.has(l.id)){d++;continue}f.add(l.id);let I=r.get(l.name)??[];for(let h of I)f.has(h)||f.add(h);let S=p.hop+1,v=[...p.path,l.name];g.has(l.name)||(g.add(l.name),u.push({node:l,hop:S,path:v,viaEdge:N}));for(let h of I)a.push({nodeId:h,nodeName:l.name,hop:S,path:v});S>s&&(s=S)}}b=a}return u.sort((a,p)=>a.hop-p.hop||a.node.name.localeCompare(p.node.name)),{target:c,targetName:e,hops:u,maxHopReached:s,cyclesPrevented:d}}0&&(module.exports={extractLineageGraph,traverseBlastRadius});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yesprasad/fluent-graph",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Dependency visibility and impact analysis for ServiceNow Fluent SDK projects",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fluent-graph": "./bin/fluent-graph.js"
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"bin"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"build": "
|
|
15
|
+
"build": "tsup && chmod +x bin/fluent-graph.js",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
16
17
|
"dev": "tsx src/cli/index.ts",
|
|
17
18
|
"prepublishOnly": "npm run build"
|
|
18
19
|
},
|
|
@@ -40,17 +41,16 @@
|
|
|
40
41
|
"chalk": "^4.1.2",
|
|
41
42
|
"cli-table3": "^0.6.5",
|
|
42
43
|
"commander": "^12.0.0",
|
|
44
|
+
"login": "^0.8.0",
|
|
43
45
|
"ora": "^5.4.1"
|
|
44
46
|
},
|
|
45
47
|
"devDependencies": {
|
|
46
48
|
"@types/node": "^20.19.39",
|
|
49
|
+
"tsup": "^8.5.1",
|
|
47
50
|
"tsx": "^4.0.0",
|
|
48
51
|
"typescript": "^5.0.0"
|
|
49
52
|
},
|
|
50
53
|
"publishConfig": {
|
|
51
54
|
"access": "public"
|
|
52
|
-
},
|
|
53
|
-
"peerDependencies": {
|
|
54
|
-
"@servicenow/sdk": "4.5.0"
|
|
55
55
|
}
|
|
56
56
|
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import type { LineageGraph, ArtifactNode, ArtifactEdge } from '../types/graphs';
|
|
2
|
-
export interface TraversalNode {
|
|
3
|
-
node: ArtifactNode;
|
|
4
|
-
hop: number;
|
|
5
|
-
/** Chain of node names from target → this node (inclusive). */
|
|
6
|
-
path: string[];
|
|
7
|
-
/** The edge that caused this node to be discovered. */
|
|
8
|
-
viaEdge: ArtifactEdge;
|
|
9
|
-
}
|
|
10
|
-
export interface TraversalResult {
|
|
11
|
-
/** The representative local graph node for the target (non-child preferred), or null if platform. */
|
|
12
|
-
target: ArtifactNode | null;
|
|
13
|
-
targetName: string;
|
|
14
|
-
/** All discovered nodes across all hops, sorted by hop ASC then name ASC. */
|
|
15
|
-
hops: TraversalNode[];
|
|
16
|
-
maxHopReached: number;
|
|
17
|
-
/** Times a visited node was skipped (cycle prevention + same-name deduplication). */
|
|
18
|
-
cyclesPrevented: number;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* BFS reverse-traversal over the dependency graph.
|
|
22
|
-
*
|
|
23
|
-
* KEY DESIGN DECISIONS:
|
|
24
|
-
*
|
|
25
|
-
* 1. A logical "entity" is identified by node NAME, not by node ID.
|
|
26
|
-
* Multiple physical nodes can share a name (e.g., a table and all its
|
|
27
|
-
* field/documentation records are all named "x_my_table"). The BFS
|
|
28
|
-
* treats them as one logical node — once any physical node for a name
|
|
29
|
-
* is discovered, ALL siblings for that name are immediately marked
|
|
30
|
-
* visited so they never appear again.
|
|
31
|
-
*
|
|
32
|
-
* 2. The initial target is seeded with ALL physical nodes sharing the
|
|
33
|
-
* target name (plus the synthetic "platform:tableName" ID). This
|
|
34
|
-
* ensures incoming edges are found regardless of which physical node
|
|
35
|
-
* the edge was recorded against.
|
|
36
|
-
*
|
|
37
|
-
* 3. Edge matching uses BOTH:
|
|
38
|
-
* - edge.to === currentNodeId (exact physical node)
|
|
39
|
-
* - edge.targetTable === currentNodeName (canonical name — reliable
|
|
40
|
-
* for platform nodes and catches mis-targeted edges)
|
|
41
|
-
*
|
|
42
|
-
* 4. ALL edge types are traversed. Display filtering is the caller's job.
|
|
43
|
-
*
|
|
44
|
-
* @param graph The loaded LineageGraph.
|
|
45
|
-
* @param targetTableName The table name the user asked about.
|
|
46
|
-
* @param maxDepth Maximum hop depth. Silently clamped to 25.
|
|
47
|
-
*/
|
|
48
|
-
export declare function traverseBlastRadius(graph: LineageGraph, targetTableName: string, maxDepth: number): TraversalResult;
|
package/dist/blast/traversal.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.traverseBlastRadius = traverseBlastRadius;
|
|
4
|
-
/**
|
|
5
|
-
* Build a map from node name → all node IDs with that name.
|
|
6
|
-
* Used to treat same-name siblings (sys_dictionary, sys_db_object, etc.
|
|
7
|
-
* representing the same table) as a single logical entity.
|
|
8
|
-
*/
|
|
9
|
-
function buildNameIndex(graph) {
|
|
10
|
-
const index = new Map();
|
|
11
|
-
for (const node of graph.nodes) {
|
|
12
|
-
const ids = index.get(node.name) ?? [];
|
|
13
|
-
ids.push(node.id);
|
|
14
|
-
index.set(node.name, ids);
|
|
15
|
-
}
|
|
16
|
-
return index;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* BFS reverse-traversal over the dependency graph.
|
|
20
|
-
*
|
|
21
|
-
* KEY DESIGN DECISIONS:
|
|
22
|
-
*
|
|
23
|
-
* 1. A logical "entity" is identified by node NAME, not by node ID.
|
|
24
|
-
* Multiple physical nodes can share a name (e.g., a table and all its
|
|
25
|
-
* field/documentation records are all named "x_my_table"). The BFS
|
|
26
|
-
* treats them as one logical node — once any physical node for a name
|
|
27
|
-
* is discovered, ALL siblings for that name are immediately marked
|
|
28
|
-
* visited so they never appear again.
|
|
29
|
-
*
|
|
30
|
-
* 2. The initial target is seeded with ALL physical nodes sharing the
|
|
31
|
-
* target name (plus the synthetic "platform:tableName" ID). This
|
|
32
|
-
* ensures incoming edges are found regardless of which physical node
|
|
33
|
-
* the edge was recorded against.
|
|
34
|
-
*
|
|
35
|
-
* 3. Edge matching uses BOTH:
|
|
36
|
-
* - edge.to === currentNodeId (exact physical node)
|
|
37
|
-
* - edge.targetTable === currentNodeName (canonical name — reliable
|
|
38
|
-
* for platform nodes and catches mis-targeted edges)
|
|
39
|
-
*
|
|
40
|
-
* 4. ALL edge types are traversed. Display filtering is the caller's job.
|
|
41
|
-
*
|
|
42
|
-
* @param graph The loaded LineageGraph.
|
|
43
|
-
* @param targetTableName The table name the user asked about.
|
|
44
|
-
* @param maxDepth Maximum hop depth. Silently clamped to 25.
|
|
45
|
-
*/
|
|
46
|
-
function traverseBlastRadius(graph, targetTableName, maxDepth) {
|
|
47
|
-
const depth = Math.min(maxDepth, 25);
|
|
48
|
-
const nameIndex = buildNameIndex(graph);
|
|
49
|
-
// All physical nodes for the target name (may be many — sys_dictionary,
|
|
50
|
-
// sys_db_object, sys_documentation, etc. all named the same thing).
|
|
51
|
-
const targetPhysicalNodes = graph.nodes.filter(n => n.name === targetTableName);
|
|
52
|
-
// Prefer a non-child / primary node as the canonical representative.
|
|
53
|
-
// We can't use the registry here (pure function), so heuristic: prefer
|
|
54
|
-
// any node where the table name itself matches the target name (for custom
|
|
55
|
-
// tables this is the sys_db_object entry); otherwise first found.
|
|
56
|
-
const target = targetPhysicalNodes[0] ?? null;
|
|
57
|
-
// Pre-visit the platform shadow ID and ALL physical nodes for the target.
|
|
58
|
-
const visited = new Set();
|
|
59
|
-
const platformId = `platform:${targetTableName}`;
|
|
60
|
-
visited.add(platformId);
|
|
61
|
-
for (const n of targetPhysicalNodes)
|
|
62
|
-
visited.add(n.id);
|
|
63
|
-
// Seed the frontier with one entry per physical target node + platform shadow.
|
|
64
|
-
// This ensures we catch edges recorded against any of the physical nodes.
|
|
65
|
-
const seedIds = new Set([platformId, ...targetPhysicalNodes.map(n => n.id)]);
|
|
66
|
-
let frontier = [...seedIds].map(id => ({
|
|
67
|
-
nodeId: id,
|
|
68
|
-
nodeName: targetTableName,
|
|
69
|
-
hop: 0,
|
|
70
|
-
path: [targetTableName],
|
|
71
|
-
}));
|
|
72
|
-
const results = [];
|
|
73
|
-
// Track which logical names we have already emitted in results (for dedup).
|
|
74
|
-
const emittedNames = new Set([targetTableName]);
|
|
75
|
-
let maxHopReached = 0;
|
|
76
|
-
let cyclesPrevented = 0;
|
|
77
|
-
while (frontier.length > 0) {
|
|
78
|
-
const nextFrontier = [];
|
|
79
|
-
for (const entry of frontier) {
|
|
80
|
-
if (entry.hop >= depth)
|
|
81
|
-
continue;
|
|
82
|
-
// Reverse-edge scan: all edges whose destination is the current node.
|
|
83
|
-
const incomingEdges = graph.edges.filter(edge => edge.to === entry.nodeId ||
|
|
84
|
-
(entry.nodeName.length > 0 && edge.targetTable === entry.nodeName));
|
|
85
|
-
for (const edge of incomingEdges) {
|
|
86
|
-
const sourceNode = graph.nodes.find(n => n.id === edge.from);
|
|
87
|
-
if (!sourceNode)
|
|
88
|
-
continue;
|
|
89
|
-
if (visited.has(sourceNode.id)) {
|
|
90
|
-
cyclesPrevented++;
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
// Mark this physical node visited.
|
|
94
|
-
visited.add(sourceNode.id);
|
|
95
|
-
// Mark ALL sibling physical nodes (same name) visited immediately
|
|
96
|
-
// so they never surface as separate hop results.
|
|
97
|
-
const siblings = nameIndex.get(sourceNode.name) ?? [];
|
|
98
|
-
for (const sibId of siblings) {
|
|
99
|
-
if (!visited.has(sibId))
|
|
100
|
-
visited.add(sibId);
|
|
101
|
-
}
|
|
102
|
-
const newHop = entry.hop + 1;
|
|
103
|
-
const newPath = [...entry.path, sourceNode.name];
|
|
104
|
-
// Only emit one result per logical name (the first physical node found).
|
|
105
|
-
if (!emittedNames.has(sourceNode.name)) {
|
|
106
|
-
emittedNames.add(sourceNode.name);
|
|
107
|
-
results.push({
|
|
108
|
-
node: sourceNode,
|
|
109
|
-
hop: newHop,
|
|
110
|
-
path: newPath,
|
|
111
|
-
viaEdge: edge,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
// Continue BFS from this logical entity, using the canonical name.
|
|
115
|
-
// Also seed entries for all sibling physical IDs so their incoming
|
|
116
|
-
// edges are explored too.
|
|
117
|
-
for (const sibId of siblings) {
|
|
118
|
-
nextFrontier.push({
|
|
119
|
-
nodeId: sibId,
|
|
120
|
-
nodeName: sourceNode.name,
|
|
121
|
-
hop: newHop,
|
|
122
|
-
path: newPath,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
if (newHop > maxHopReached) {
|
|
126
|
-
maxHopReached = newHop;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
frontier = nextFrontier;
|
|
131
|
-
}
|
|
132
|
-
// Sort by hop ASC, then name ASC for stable output.
|
|
133
|
-
results.sort((a, b) => a.hop - b.hop || a.node.name.localeCompare(b.node.name));
|
|
134
|
-
return {
|
|
135
|
-
target,
|
|
136
|
-
targetName: targetTableName,
|
|
137
|
-
hops: results,
|
|
138
|
-
maxHopReached,
|
|
139
|
-
cyclesPrevented,
|
|
140
|
-
};
|
|
141
|
-
}
|