@yesprasad/fluent-graph 0.1.0 ā 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +39 -78
- package/dist/blast/traversal.d.ts +48 -0
- package/dist/blast/traversal.js +141 -0
- package/dist/cli/commands/blast.js +132 -87
- package/dist/cli/commands/extract.js +2 -81
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +0 -25
- package/dist/cli/utils/banner.js +3 -3
- package/dist/cli/utils/display.d.ts +2 -1
- package/dist/cli/utils/display.js +86 -121
- package/dist/extractor/edges.d.ts +6 -4
- package/dist/extractor/edges.js +41 -25
- package/dist/extractor/index.d.ts +2 -1
- package/dist/extractor/index.js +2 -2
- package/dist/graph/extractor.d.ts +2 -1
- package/dist/graph/extractor.js +4 -2
- package/dist/sdk/loader.d.ts +2 -0
- package/dist/sdk/loader.js +4 -0
- package/dist/sdk/registry.d.ts +30 -0
- package/dist/sdk/registry.js +128 -0
- package/package.json +16 -13
|
@@ -13,85 +13,6 @@ const loader_1 = require("../../sdk/loader");
|
|
|
13
13
|
const extractor_1 = require("../../extractor");
|
|
14
14
|
const writer_1 = require("../../output/writer");
|
|
15
15
|
const display_1 = require("../utils/display");
|
|
16
|
-
// /**
|
|
17
|
-
// * The Analyze Command: Orchestrates the transformation of a ServiceNow Fluent
|
|
18
|
-
// * project into a formal directed graph.
|
|
19
|
-
// */
|
|
20
|
-
// export const extractCommand = new Command('analyze')
|
|
21
|
-
// .description('Analyze and Extract dependency graph from current project')
|
|
22
|
-
// .option('-o, --output <path>', 'Output file path', 'fluent-graph.json')
|
|
23
|
-
// .option('-q, --quiet', 'Suppress console output')
|
|
24
|
-
// .option('--no-display', 'Skip visual display')
|
|
25
|
-
// .option('--no-banner', 'Skip banner (inherited)')
|
|
26
|
-
// .action(async (options) => {
|
|
27
|
-
// try {
|
|
28
|
-
// const cwd = process.cwd();
|
|
29
|
-
// let spinner: any;
|
|
30
|
-
// // Step 1: Detect ServiceNow SDK Environment
|
|
31
|
-
// if (!options.quiet) {
|
|
32
|
-
// spinner = ora('Detecting ServiceNow SDK...').start();
|
|
33
|
-
// }
|
|
34
|
-
// const sdk = await detectSDK(cwd);
|
|
35
|
-
// if (!sdk.found) {
|
|
36
|
-
// if (spinner) spinner.fail('SDK not found');
|
|
37
|
-
// console.error(chalk.red('\nā Error:'), sdk.error);
|
|
38
|
-
// console.log(chalk.yellow('\nš” Make sure you\'re in a ServiceNow Fluent project directory'));
|
|
39
|
-
// process.exit(1);
|
|
40
|
-
// }
|
|
41
|
-
// if (spinner) {
|
|
42
|
-
// spinner.succeed(`ā
SDK detected at ${chalk.gray(sdk.sdkPath)}`);
|
|
43
|
-
// }
|
|
44
|
-
// // Step 2: Dynamically Load Workspace SDK
|
|
45
|
-
// if (!options.quiet) {
|
|
46
|
-
// spinner = ora('Loading workspace SDK...').start();
|
|
47
|
-
// }
|
|
48
|
-
// const { sdkapi } = loadWorkspaceSDK(sdk.sdkPath!);
|
|
49
|
-
// if (spinner) {
|
|
50
|
-
// spinner.succeed('Workspace SDK loaded');
|
|
51
|
-
// }
|
|
52
|
-
// // Step 3: Instantiate the Project Proxy
|
|
53
|
-
// if (!options.quiet) {
|
|
54
|
-
// spinner = ora('Loading project configuration...').start();
|
|
55
|
-
// }
|
|
56
|
-
// const project = new sdkapi.Project({ rootDir: cwd });
|
|
57
|
-
// if (spinner) {
|
|
58
|
-
// spinner.succeed('Project loaded');
|
|
59
|
-
// }
|
|
60
|
-
// // Step 4: Extract Lineage Graph
|
|
61
|
-
// // We pass the full project object, allowing the extractor to
|
|
62
|
-
// // access nodes, edges, and metadata (projectRoot, scope, etc.)
|
|
63
|
-
// if (!options.quiet) {
|
|
64
|
-
// spinner = ora('Building dependency graph...').start();
|
|
65
|
-
// }
|
|
66
|
-
// const graph = await extractLineageGraph(project);
|
|
67
|
-
// if (spinner) {
|
|
68
|
-
// spinner.succeed(`Graph built: ${chalk.cyan(graph.nodes.length)} nodes, ${chalk.cyan(graph.edges.length)} edges`);
|
|
69
|
-
// }
|
|
70
|
-
// // Step 5: Persist the Graph (The "Iterable" result)
|
|
71
|
-
// const outputPath = path.isAbsolute(options.output)
|
|
72
|
-
// ? options.output
|
|
73
|
-
// : path.join(cwd, options.output);
|
|
74
|
-
// writeGraph(graph, outputPath);
|
|
75
|
-
// if (!options.quiet) {
|
|
76
|
-
// console.log(chalk.green('\nā
Analysis complete'));
|
|
77
|
-
// console.log(chalk.gray(' Location: ') + chalk.white(outputPath));
|
|
78
|
-
// console.log(chalk.gray(' Complexity: ') + chalk.cyan((graph.edges.length / graph.nodes.length).toFixed(2)) + chalk.gray(' edges/node'));
|
|
79
|
-
// console.log(chalk.gray(' Generated at: ') + chalk.gray(new Date().toLocaleTimeString()));
|
|
80
|
-
// }
|
|
81
|
-
// // Step 6: Visual Lineage Map
|
|
82
|
-
// // "Artifact-Agnostic" display logic
|
|
83
|
-
// if (options.display && !options.quiet) {
|
|
84
|
-
// displayLineageMap(graph);
|
|
85
|
-
// }
|
|
86
|
-
// } catch (err: any) {
|
|
87
|
-
// console.error(chalk.red('\nā Error:'), err.message);
|
|
88
|
-
// if (process.env.DEBUG) {
|
|
89
|
-
// console.error(chalk.gray('\nStack trace:'));
|
|
90
|
-
// console.error(err.stack);
|
|
91
|
-
// }
|
|
92
|
-
// process.exit(1);
|
|
93
|
-
// }
|
|
94
|
-
// });
|
|
95
16
|
/**
|
|
96
17
|
* The Extract Command: Orchestrates the transformation of a ServiceNow Fluent
|
|
97
18
|
* project into a formal directed graph.
|
|
@@ -141,7 +62,7 @@ exports.extractCommand = new commander_1.Command('analyze')
|
|
|
141
62
|
if (!options.quiet) {
|
|
142
63
|
spinner = (0, ora_1.default)('Building dependency graph...').start();
|
|
143
64
|
}
|
|
144
|
-
const lineageGraph = await (0, extractor_1.extractLineageGraph)(projectInstance);
|
|
65
|
+
const lineageGraph = await (0, extractor_1.extractLineageGraph)(projectInstance, workspaceSDK.registry);
|
|
145
66
|
if (spinner) {
|
|
146
67
|
spinner.succeed(`Graph built: ${chalk_1.default.cyan(lineageGraph.nodes.length)} nodes, ${chalk_1.default.cyan(lineageGraph.edges.length)} edges`);
|
|
147
68
|
}
|
|
@@ -158,7 +79,7 @@ exports.extractCommand = new commander_1.Command('analyze')
|
|
|
158
79
|
}
|
|
159
80
|
// Step 6: Visual Lineage Map
|
|
160
81
|
if (options.display && !options.quiet) {
|
|
161
|
-
(0, display_1.displayLineageMap)(lineageGraph);
|
|
82
|
+
(0, display_1.displayLineageMap)(lineageGraph, workspaceSDK.registry);
|
|
162
83
|
}
|
|
163
84
|
}
|
|
164
85
|
catch (error) {
|
package/dist/cli/index.d.ts
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -1,33 +1,8 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
"use strict";
|
|
3
|
-
// import { Command } from 'commander';
|
|
4
|
-
// import chalk from 'chalk';
|
|
5
|
-
// import { extractCommand } from './commands/extract';
|
|
6
|
-
// import { blastCommand } from './commands/blast';
|
|
7
|
-
// import { displayBanner } from './utils/banner';
|
|
8
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
4
|
};
|
|
11
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
// const program = new Command();
|
|
13
|
-
// // Show banner on first run (unless --no-banner flag)
|
|
14
|
-
// const showBanner = !process.argv.includes('--no-banner') && !process.argv.includes('--quiet');
|
|
15
|
-
// if (showBanner) {
|
|
16
|
-
// displayBanner();
|
|
17
|
-
// }
|
|
18
|
-
// program
|
|
19
|
-
// .name('fluent-graph')
|
|
20
|
-
// .description('Dependency/ Lineage graph extraction & Blast-radius/ impact analysis for ServiceNow Fluent Artifacts')
|
|
21
|
-
// .version('1.0.0')
|
|
22
|
-
// .option('--no-banner', 'Suppress ASCII banner');
|
|
23
|
-
// // Add commands
|
|
24
|
-
// program.addCommand(extractCommand);
|
|
25
|
-
// program.addCommand(blastCommand);
|
|
26
|
-
// // Default behavior: show help if no command provided
|
|
27
|
-
// if (process.argv.length === 2) {
|
|
28
|
-
// program.help();
|
|
29
|
-
// }
|
|
30
|
-
// program.parse(process.argv);
|
|
31
6
|
const commander_1 = require("commander");
|
|
32
7
|
const path_1 = __importDefault(require("path"));
|
|
33
8
|
const fs_1 = __importDefault(require("fs"));
|
package/dist/cli/utils/banner.js
CHANGED
|
@@ -16,10 +16,10 @@ function displayBanner() {
|
|
|
16
16
|
āāā āāāāāāāā āāāāāāā āāāāāāāāāāā āāāāā āāā āāāāāāā āāā āāāāāā āāāāāā āāā āāā
|
|
17
17
|
`;
|
|
18
18
|
console.log(chalk_1.default.cyan(banner));
|
|
19
|
-
console.log(chalk_1.default.gray(' Dependency
|
|
20
|
-
console.log(chalk_1.default.gray('
|
|
19
|
+
console.log(chalk_1.default.gray(' Dependency visibility and impact analysis for ServiceNow Fluent SDK projects'));
|
|
20
|
+
console.log(chalk_1.default.gray(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n'));
|
|
21
21
|
}
|
|
22
22
|
function displayVersion() {
|
|
23
23
|
console.log(chalk_1.default.cyan.bold('Fluent Graph') + chalk_1.default.gray(' v0.1.0'));
|
|
24
|
-
console.log(chalk_1.default.gray('Dependency visibility for ServiceNow Fluent projects\n'));
|
|
24
|
+
console.log(chalk_1.default.gray('Dependency visibility and impact analysis for ServiceNow Fluent SDK projects\n'));
|
|
25
25
|
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import { LineageGraph } from '../../types/graphs';
|
|
2
|
-
|
|
2
|
+
import { ArtifactTypeRegistry } from '../../sdk/registry';
|
|
3
|
+
export declare function displayLineageMap(graph: LineageGraph, registry: ArtifactTypeRegistry): void;
|
|
@@ -4,35 +4,93 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.displayLineageMap = displayLineageMap;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
7
8
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
9
|
const cli_table3_1 = __importDefault(require("cli-table3"));
|
|
9
|
-
function displayLineageMap(graph) {
|
|
10
|
+
function displayLineageMap(graph, registry) {
|
|
10
11
|
console.log(chalk_1.default.yellow.bold('\nš LINEAGE MAP'));
|
|
11
12
|
console.log(chalk_1.default.gray('ā'.repeat(80)));
|
|
12
|
-
// =====
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
13
|
+
// ===== DYNAMIC PLUGIN GROUPS =====
|
|
14
|
+
const pluginGroups = new Map();
|
|
15
|
+
for (const node of graph.nodes) {
|
|
16
|
+
if (node.action === 'DELETE')
|
|
17
|
+
continue;
|
|
18
|
+
const groupName = registry.getDisplayGroupName(node.table);
|
|
19
|
+
if (!pluginGroups.has(groupName)) {
|
|
20
|
+
pluginGroups.set(groupName, []);
|
|
21
|
+
}
|
|
22
|
+
pluginGroups.get(groupName).push(node);
|
|
23
|
+
}
|
|
24
|
+
for (const [groupName, nodes] of pluginGroups) {
|
|
25
|
+
// Only show primary (non-child) nodes in the section
|
|
26
|
+
const primaryNodes = nodes.filter(n => !registry.isChild(n.table) && n.source.file);
|
|
27
|
+
if (primaryNodes.length === 0)
|
|
28
|
+
continue;
|
|
29
|
+
console.log(chalk_1.default.cyan.bold(`\n${groupName}`));
|
|
30
|
+
const isBusinessRuleGroup = primaryNodes.some(n => registry.getPluginName(n.table) === 'BusinessRulePlugin');
|
|
31
|
+
if (isBusinessRuleGroup) {
|
|
32
|
+
const table = new cli_table3_1.default({
|
|
33
|
+
head: [
|
|
34
|
+
chalk_1.default.cyan('Name'),
|
|
35
|
+
chalk_1.default.cyan('When'),
|
|
36
|
+
chalk_1.default.cyan('Table'),
|
|
37
|
+
chalk_1.default.cyan('On'),
|
|
38
|
+
chalk_1.default.cyan('Order'),
|
|
39
|
+
chalk_1.default.cyan('Status'),
|
|
40
|
+
chalk_1.default.cyan('File'),
|
|
41
|
+
],
|
|
42
|
+
colWidths: [32, 8, 28, 16, 7, 8, 28],
|
|
43
|
+
wordWrap: true,
|
|
44
|
+
style: { head: [], border: ['gray'] },
|
|
45
|
+
});
|
|
46
|
+
primaryNodes.forEach(node => {
|
|
47
|
+
const statusLabel = node.action === 'DELETE' ? chalk_1.default.red('DELETE') : chalk_1.default.green('ACTIVE');
|
|
48
|
+
const when = node.attributes.when || '';
|
|
49
|
+
const depEdge = graph.edges.find(e => e.from === node.id && e.type === 'dependency' && e.targetTable);
|
|
50
|
+
const targetTable = depEdge?.targetTable || '';
|
|
51
|
+
const ops = [];
|
|
52
|
+
if (node.attributes.action_insert)
|
|
53
|
+
ops.push('Insert');
|
|
54
|
+
if (node.attributes.action_update)
|
|
55
|
+
ops.push('Update');
|
|
56
|
+
if (node.attributes.action_delete)
|
|
57
|
+
ops.push('Delete');
|
|
58
|
+
if (node.attributes.action_query)
|
|
59
|
+
ops.push('Query');
|
|
60
|
+
table.push([
|
|
61
|
+
chalk_1.default.white(node.name),
|
|
62
|
+
chalk_1.default.yellow(when),
|
|
63
|
+
chalk_1.default.magenta(targetTable),
|
|
64
|
+
ops.length ? ops.join(', ') : chalk_1.default.gray('ā'),
|
|
65
|
+
String(node.attributes.order ?? ''),
|
|
66
|
+
statusLabel,
|
|
67
|
+
chalk_1.default.gray(path_1.default.basename(node.source.file)),
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
console.log(table.toString());
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const table = new cli_table3_1.default({
|
|
74
|
+
head: [
|
|
75
|
+
chalk_1.default.cyan('Name'),
|
|
76
|
+
chalk_1.default.cyan('Status'),
|
|
77
|
+
chalk_1.default.cyan('Source File')
|
|
78
|
+
],
|
|
79
|
+
wordWrap: true,
|
|
80
|
+
style: { head: [], border: ['gray'] }
|
|
81
|
+
});
|
|
82
|
+
primaryNodes.forEach(node => {
|
|
83
|
+
const statusLabel = node.action === 'DELETE' ? chalk_1.default.red('DELETE') : chalk_1.default.green('ACTIVE');
|
|
84
|
+
table.push([
|
|
85
|
+
chalk_1.default.white(node.name),
|
|
86
|
+
statusLabel,
|
|
87
|
+
chalk_1.default.gray(node.source.file)
|
|
88
|
+
]);
|
|
89
|
+
});
|
|
90
|
+
console.log(table.toString());
|
|
91
|
+
}
|
|
34
92
|
}
|
|
35
|
-
// =====
|
|
93
|
+
// ===== SCHEMA RELATIONSHIPS =====
|
|
36
94
|
const schemaEdges = graph.edges.filter(edge => edge.type === 'schema_relationship');
|
|
37
95
|
if (schemaEdges.length > 0) {
|
|
38
96
|
console.log(chalk_1.default.cyan.bold('\nš SCHEMA RELATIONSHIPS (Reference Fields)'));
|
|
@@ -56,111 +114,18 @@ function displayLineageMap(graph) {
|
|
|
56
114
|
});
|
|
57
115
|
console.log(table.toString());
|
|
58
116
|
}
|
|
59
|
-
// =====
|
|
60
|
-
const logicArtifacts = graph.nodes.filter(node => ['sys_script_client', 'sys_ui_action', 'sys_script', 'sys_script_include', 'sys_ui_policy'].includes(node.table) &&
|
|
61
|
-
node.action !== 'DELETE' &&
|
|
62
|
-
node.source.file);
|
|
63
|
-
if (logicArtifacts.length > 0) {
|
|
64
|
-
console.log(chalk_1.default.cyan.bold('\nāļø LOGIC ATTACHMENTS (Scripts & Actions)'));
|
|
65
|
-
const table = new cli_table3_1.default({
|
|
66
|
-
head: [
|
|
67
|
-
chalk_1.default.cyan('Artifact Name'),
|
|
68
|
-
chalk_1.default.cyan('Target Table'),
|
|
69
|
-
chalk_1.default.cyan('Type'),
|
|
70
|
-
chalk_1.default.cyan('Artifact Type')
|
|
71
|
-
],
|
|
72
|
-
wordWrap: true,
|
|
73
|
-
style: { head: [], border: ['gray'] }
|
|
74
|
-
});
|
|
75
|
-
logicArtifacts.forEach(logicNode => {
|
|
76
|
-
const dependencyEdge = graph.edges.find(edge => edge.from === logicNode.id && edge.type === 'dependency');
|
|
77
|
-
const targetTable = dependencyEdge
|
|
78
|
-
? (dependencyEdge.to.startsWith('platform:')
|
|
79
|
-
? dependencyEdge.to.split(':')[1]
|
|
80
|
-
: graph.nodes.find(node => node.id === dependencyEdge.to)?.name || dependencyEdge.to)
|
|
81
|
-
: '[global]';
|
|
82
|
-
const typeLabel = logicNode.attributes?.type ? `[${logicNode.attributes.type}]` : '[action]';
|
|
83
|
-
const artifactType = (() => {
|
|
84
|
-
switch (logicNode.table) {
|
|
85
|
-
case 'sys_script_client': return 'client script';
|
|
86
|
-
case 'sys_script': return 'script';
|
|
87
|
-
case 'sys_ui_action': return 'ui action';
|
|
88
|
-
case 'sys_script_include': return 'script include';
|
|
89
|
-
case 'sys_ui_policy': return 'ui policy';
|
|
90
|
-
default: return logicNode.table;
|
|
91
|
-
}
|
|
92
|
-
})();
|
|
93
|
-
table.push([
|
|
94
|
-
chalk_1.default.white(logicNode.name),
|
|
95
|
-
chalk_1.default.gray(targetTable),
|
|
96
|
-
chalk_1.default.gray(typeLabel),
|
|
97
|
-
artifactType
|
|
98
|
-
]);
|
|
99
|
-
});
|
|
100
|
-
console.log(table.toString());
|
|
101
|
-
}
|
|
102
|
-
// ===== SECTION 4: SECURITY & ACCESS (ACLs) =====
|
|
103
|
-
const aclNodes = graph.nodes.filter(node => node.table === 'sys_security_acl' && node.action !== 'DELETE');
|
|
104
|
-
if (aclNodes.length > 0) {
|
|
105
|
-
console.log(chalk_1.default.cyan.bold('\nš SECURITY & ACCESS (ACLs)'));
|
|
106
|
-
const table = new cli_table3_1.default({
|
|
107
|
-
head: [
|
|
108
|
-
chalk_1.default.cyan('ACL'),
|
|
109
|
-
chalk_1.default.cyan('Level'),
|
|
110
|
-
chalk_1.default.cyan('Operation'),
|
|
111
|
-
chalk_1.default.cyan('Roles'),
|
|
112
|
-
chalk_1.default.cyan('Target Table')
|
|
113
|
-
],
|
|
114
|
-
wordWrap: true,
|
|
115
|
-
style: { head: [], border: ['gray'] }
|
|
116
|
-
});
|
|
117
|
-
aclNodes.forEach(aclNode => {
|
|
118
|
-
const aclEdge = graph.edges.find(edge => edge.from === aclNode.id && edge.type === 'dependency');
|
|
119
|
-
const targetTable = aclEdge?.targetTable || (aclEdge?.to.startsWith('platform:') ? aclEdge.to.split(':')[1] : 'unknown');
|
|
120
|
-
const [nameTable, nameField] = aclNode.name.includes('.') ? aclNode.name.split('.') : [aclNode.name, '*'];
|
|
121
|
-
const isFieldLevel = nameField !== '*' && nameField !== undefined;
|
|
122
|
-
const level = isFieldLevel ? 'Field-level' : 'Table-level';
|
|
123
|
-
const operation = (aclNode.attributes?.operation || 'read').toUpperCase();
|
|
124
|
-
const roleEdges = graph.edges.filter(edge => edge.from === aclNode.id && edge.type === 'composition');
|
|
125
|
-
const roleNames = roleEdges
|
|
126
|
-
.map(edge => {
|
|
127
|
-
const roleNode = graph.nodes.find(node => node.id === edge.to);
|
|
128
|
-
return roleNode?.attributes?.name || roleNode?.name || 'unknown';
|
|
129
|
-
})
|
|
130
|
-
.filter(Boolean)
|
|
131
|
-
.sort();
|
|
132
|
-
const rolesDisplay = roleNames.length > 0 ? roleNames.join(', ') : chalk_1.default.gray('No roles specified');
|
|
133
|
-
const aclLabel = `${operation} on ${nameTable}${isFieldLevel ? `.${nameField}` : ''}`;
|
|
134
|
-
table.push([
|
|
135
|
-
chalk_1.default.yellow(aclLabel),
|
|
136
|
-
chalk_1.default.gray(level),
|
|
137
|
-
chalk_1.default.gray(operation),
|
|
138
|
-
rolesDisplay,
|
|
139
|
-
chalk_1.default.magenta(targetTable)
|
|
140
|
-
]);
|
|
141
|
-
});
|
|
142
|
-
console.log(table.toString());
|
|
143
|
-
}
|
|
144
|
-
// ===== SECTION 5: PROJECT INSIGHTS =====
|
|
117
|
+
// ===== PROJECT INSIGHTS =====
|
|
145
118
|
console.log(chalk_1.default.cyan.bold('\nš§ PROJECT INSIGHTS'));
|
|
146
119
|
const platformDeps = graph.edges.filter(edge => edge.to.startsWith('platform:')).length;
|
|
147
120
|
const internalDeps = graph.edges.filter(edge => !edge.to.startsWith('platform:')).length;
|
|
148
|
-
const userArtifactTables = [
|
|
149
|
-
'sys_db_object',
|
|
150
|
-
'sys_script_client',
|
|
151
|
-
'sys_script',
|
|
152
|
-
'sys_business_rule',
|
|
153
|
-
'sys_ui_action',
|
|
154
|
-
'sys_security_acl',
|
|
155
|
-
'sys_script_include'
|
|
156
|
-
];
|
|
157
121
|
const userArtifacts = graph.nodes.filter(node => node.action !== 'DELETE' &&
|
|
158
122
|
node.source.file &&
|
|
159
123
|
!node.id.startsWith('platform:') &&
|
|
160
|
-
|
|
124
|
+
!registry.isChild(node.table));
|
|
161
125
|
console.log(` ⢠Total User Artifacts: ${chalk_1.default.cyan(userArtifacts.length)}`);
|
|
162
126
|
const countsByType = userArtifacts.reduce((acc, node) => {
|
|
163
|
-
|
|
127
|
+
const groupName = registry.getDisplayGroupName(node.table);
|
|
128
|
+
acc[groupName] = (acc[groupName] || 0) + 1;
|
|
164
129
|
return acc;
|
|
165
130
|
}, {});
|
|
166
131
|
console.log(` ⢠User Artifacts Breakdown: ${JSON.stringify(countsByType, null, 2)}`);
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { ArtifactEdge, ArtifactNode } from '../types/graphs';
|
|
2
|
+
import type { ArtifactTypeRegistry } from '../sdk/registry';
|
|
2
3
|
/**
|
|
3
4
|
* UNIVERSAL EDGE EXTRACTOR
|
|
4
5
|
*
|
|
5
6
|
* Strategy:
|
|
6
|
-
* 1.
|
|
7
|
-
* 2.
|
|
8
|
-
* 3.
|
|
7
|
+
* 1. Property-based schema relationship detection (internal_type=reference)
|
|
8
|
+
* 2. Logic attachment via table/collection property
|
|
9
|
+
* 3. ACL name parsing ā detected via registry plugin name
|
|
10
|
+
* 4. Universal RecordId scan across all properties
|
|
9
11
|
*/
|
|
10
|
-
export declare function extractReferenceEdges(records: any, allNodes: ArtifactNode[]): ArtifactEdge[];
|
|
12
|
+
export declare function extractReferenceEdges(records: any, allNodes: ArtifactNode[], registry: ArtifactTypeRegistry): ArtifactEdge[];
|
|
11
13
|
export declare function extractCompositionEdges(records: any): ArtifactEdge[];
|
package/dist/extractor/edges.js
CHANGED
|
@@ -6,31 +6,30 @@ exports.extractCompositionEdges = extractCompositionEdges;
|
|
|
6
6
|
* UNIVERSAL EDGE EXTRACTOR
|
|
7
7
|
*
|
|
8
8
|
* Strategy:
|
|
9
|
-
* 1.
|
|
10
|
-
* 2.
|
|
11
|
-
* 3.
|
|
9
|
+
* 1. Property-based schema relationship detection (internal_type=reference)
|
|
10
|
+
* 2. Logic attachment via table/collection property
|
|
11
|
+
* 3. ACL name parsing ā detected via registry plugin name
|
|
12
|
+
* 4. Universal RecordId scan across all properties
|
|
12
13
|
*/
|
|
13
|
-
function extractReferenceEdges(records, allNodes) {
|
|
14
|
+
function extractReferenceEdges(records, allNodes, registry) {
|
|
14
15
|
const edges = [];
|
|
15
16
|
for (const record of records.query()) {
|
|
16
17
|
const fromId = record.getId()?.getValue()?.toString();
|
|
17
18
|
const props = record.setProperties || {};
|
|
18
19
|
const recordTable = record.getTable()?.toString();
|
|
19
20
|
// === STRATEGY 1: SCHEMA RELATIONSHIPS ===
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
});
|
|
33
|
-
}
|
|
21
|
+
// Detect by properties: any record with internal_type=reference and a reference property
|
|
22
|
+
const internalType = props.internal_type?.getValue?.()?.toString();
|
|
23
|
+
const refTable = props.reference?.getValue?.()?.toString();
|
|
24
|
+
const elementName = props.element?.getValue?.()?.toString();
|
|
25
|
+
if (internalType === 'reference' && refTable) {
|
|
26
|
+
const targetNode = allNodes.find(n => n.name === refTable);
|
|
27
|
+
edges.push({
|
|
28
|
+
from: fromId,
|
|
29
|
+
to: targetNode?.id || `platform:${refTable}`,
|
|
30
|
+
via: elementName || 'reference',
|
|
31
|
+
type: 'schema_relationship'
|
|
32
|
+
});
|
|
34
33
|
}
|
|
35
34
|
// === STRATEGY 2: LOGIC ATTACHMENTS ===
|
|
36
35
|
// Any artifact with 'table' or 'collection' property
|
|
@@ -43,12 +42,12 @@ function extractReferenceEdges(records, allNodes) {
|
|
|
43
42
|
to: targetNode?.id || `platform:${tableRef}`,
|
|
44
43
|
via: props.table ? 'table' : 'collection',
|
|
45
44
|
type: 'dependency',
|
|
46
|
-
targetTable: tableRef
|
|
45
|
+
targetTable: tableRef
|
|
47
46
|
});
|
|
48
47
|
}
|
|
49
48
|
// === STRATEGY 3: ACL NAME PARSING ===
|
|
50
|
-
//
|
|
51
|
-
if (recordTable === '
|
|
49
|
+
// Detect ACL records by plugin name (registry-derived) rather than hardcoded table name
|
|
50
|
+
if (registry.getPluginName(recordTable) === 'AclPlugin') {
|
|
52
51
|
const aclName = props.name?.getValue?.()?.toString();
|
|
53
52
|
if (aclName && aclName.includes('.')) {
|
|
54
53
|
const [targetTable] = aclName.split('.');
|
|
@@ -63,15 +62,17 @@ function extractReferenceEdges(records, allNodes) {
|
|
|
63
62
|
}
|
|
64
63
|
}
|
|
65
64
|
// === STRATEGY 4: UNIVERSAL REFERENCE SCAN ===
|
|
66
|
-
// Scan ALL properties for RecordId types (sys_id references)
|
|
65
|
+
// Scan ALL properties for RecordId and Record types (sys_id references).
|
|
66
|
+
// RecordId: getValue() = sys_id, getTable() = table name.
|
|
67
|
+
// Record: getId().getValue() = sys_id; table resolved via allNodes lookup.
|
|
68
|
+
// Used by SDK for cross-artifact references like flow_designer_flow on CatalogItem.
|
|
67
69
|
for (const [key, val] of Object.entries(props)) {
|
|
68
|
-
// Added: Handle arrays of RecordId (e.g., roles: [role1, role2]) to create multiple edges
|
|
69
70
|
if (Array.isArray(val)) {
|
|
70
71
|
val.forEach((item) => {
|
|
71
72
|
if (item && item.constructor?.name === 'RecordId') {
|
|
72
73
|
const targetId = item.getValue?.()?.toString();
|
|
73
74
|
const targetTable = item.getTable?.()?.toString();
|
|
74
|
-
if (targetId && targetTable !== '
|
|
75
|
+
if (targetId && registry.getPluginName(targetTable) !== 'TablePlugin') {
|
|
75
76
|
edges.push({
|
|
76
77
|
from: fromId,
|
|
77
78
|
to: targetId,
|
|
@@ -86,7 +87,7 @@ function extractReferenceEdges(records, allNodes) {
|
|
|
86
87
|
else if (val && val.constructor?.name === 'RecordId') {
|
|
87
88
|
const targetId = val.getValue?.()?.toString();
|
|
88
89
|
const targetTable = val.getTable?.()?.toString();
|
|
89
|
-
if (targetId && targetTable !== '
|
|
90
|
+
if (targetId && registry.getPluginName(targetTable) !== 'TablePlugin') {
|
|
90
91
|
edges.push({
|
|
91
92
|
from: fromId,
|
|
92
93
|
to: targetId,
|
|
@@ -96,6 +97,21 @@ function extractReferenceEdges(records, allNodes) {
|
|
|
96
97
|
});
|
|
97
98
|
}
|
|
98
99
|
}
|
|
100
|
+
else if (val && val.constructor?.name === 'Record') {
|
|
101
|
+
const targetId = val.getId?.()?.getValue?.()?.toString();
|
|
102
|
+
if (targetId) {
|
|
103
|
+
const targetNode = allNodes.find(n => n.id === targetId);
|
|
104
|
+
if (targetNode && registry.getPluginName(targetNode.table) !== 'TablePlugin') {
|
|
105
|
+
edges.push({
|
|
106
|
+
from: fromId,
|
|
107
|
+
to: targetId,
|
|
108
|
+
via: key,
|
|
109
|
+
type: 'dependency',
|
|
110
|
+
targetTable: targetNode.table
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
99
115
|
}
|
|
100
116
|
}
|
|
101
117
|
return edges;
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { LineageGraph } from '../types/graphs';
|
|
2
|
-
|
|
2
|
+
import type { ArtifactTypeRegistry } from '../sdk/registry';
|
|
3
|
+
export declare function extractLineageGraph(project: any, registry: ArtifactTypeRegistry): Promise<LineageGraph>;
|
package/dist/extractor/index.js
CHANGED
|
@@ -4,14 +4,14 @@ exports.extractLineageGraph = extractLineageGraph;
|
|
|
4
4
|
const nodes_1 = require("./nodes");
|
|
5
5
|
const edges_1 = require("./edges");
|
|
6
6
|
const metadata_1 = require("./metadata");
|
|
7
|
-
async function extractLineageGraph(project) {
|
|
7
|
+
async function extractLineageGraph(project, registry) {
|
|
8
8
|
// Get raw records from the ServiceNow SDK Project
|
|
9
9
|
const records = await project.getRecords();
|
|
10
10
|
const rootDir = project.getRootDir();
|
|
11
11
|
// 1. Extract Nodes (Includes dynamic attributes like order, when, active)
|
|
12
12
|
const nodes = (0, nodes_1.extractNodes)(records, rootDir);
|
|
13
13
|
// 2. Extract Edges (Requires node list for implicit string-to-table resolution)
|
|
14
|
-
const referenceEdges = (0, edges_1.extractReferenceEdges)(records, nodes);
|
|
14
|
+
const referenceEdges = (0, edges_1.extractReferenceEdges)(records, nodes, registry);
|
|
15
15
|
const compositionEdges = (0, edges_1.extractCompositionEdges)(records);
|
|
16
16
|
const edges = [...referenceEdges, ...compositionEdges];
|
|
17
17
|
// 3. Generate Summary Metadata
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { ArtifactTypeRegistry } from '../sdk/registry';
|
|
1
2
|
import type { LineageGraph } from '../types/graphs';
|
|
2
3
|
/**
|
|
3
4
|
* The Orchestrator: Converts raw SDK records into a formal Directed Graph.
|
|
4
5
|
* It treats every artifact as a node and every relationship as an iterable edge.
|
|
5
6
|
*/
|
|
6
|
-
export declare function extractLineageGraph(project: any): Promise<LineageGraph>;
|
|
7
|
+
export declare function extractLineageGraph(project: any, registry?: ArtifactTypeRegistry): Promise<LineageGraph>;
|
package/dist/graph/extractor.js
CHANGED
|
@@ -4,13 +4,15 @@ exports.extractLineageGraph = extractLineageGraph;
|
|
|
4
4
|
const nodes_1 = require("../extractor/nodes");
|
|
5
5
|
const edges_1 = require("../extractor/edges");
|
|
6
6
|
const metadata_1 = require("../extractor/metadata");
|
|
7
|
+
const registry_1 = require("../sdk/registry");
|
|
7
8
|
/**
|
|
8
9
|
* The Orchestrator: Converts raw SDK records into a formal Directed Graph.
|
|
9
10
|
* It treats every artifact as a node and every relationship as an iterable edge.
|
|
10
11
|
*/
|
|
11
|
-
async function extractLineageGraph(project) {
|
|
12
|
+
async function extractLineageGraph(project, registry) {
|
|
12
13
|
const records = project.getRecords();
|
|
13
14
|
const projectRoot = project.getRootDir();
|
|
15
|
+
const reg = registry ?? new registry_1.ArtifactTypeRegistry();
|
|
14
16
|
// 1. PHASE ONE: Vertex Extraction (Nodes)
|
|
15
17
|
// We only extract the "winning" records to avoid conflict noise.
|
|
16
18
|
// This creates the list of local artifacts.
|
|
@@ -20,7 +22,7 @@ async function extractLineageGraph(project) {
|
|
|
20
22
|
// This allows the edge extractor to differentiate between:
|
|
21
23
|
// a) A reference to a local table (e.g. x_1566039_seventh_account)
|
|
22
24
|
// b) A reference to a platform table (e.g. platform:incident)
|
|
23
|
-
const refEdges = (0, edges_1.extractReferenceEdges)(records, nodes);
|
|
25
|
+
const refEdges = (0, edges_1.extractReferenceEdges)(records, nodes, reg);
|
|
24
26
|
const compEdges = (0, edges_1.extractCompositionEdges)(records);
|
|
25
27
|
const allEdges = [...refEdges, ...compEdges];
|
|
26
28
|
// 3. PHASE THREE: Metadata & Context
|
package/dist/sdk/loader.d.ts
CHANGED
package/dist/sdk/loader.js
CHANGED
|
@@ -6,10 +6,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.loadWorkspaceSDK = loadWorkspaceSDK;
|
|
7
7
|
const module_1 = require("module");
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const registry_1 = require("./registry");
|
|
9
10
|
function loadWorkspaceSDK(sdkPath) {
|
|
10
11
|
const requireFromWorkspace = (0, module_1.createRequire)(path_1.default.join(sdkPath, 'package.json'));
|
|
12
|
+
const registry = new registry_1.ArtifactTypeRegistry();
|
|
13
|
+
registry.load(sdkPath);
|
|
11
14
|
return {
|
|
12
15
|
buildCore: requireFromWorkspace('@servicenow/sdk-build-core'),
|
|
13
16
|
sdkapi: requireFromWorkspace('@servicenow/sdk-api'),
|
|
17
|
+
registry,
|
|
14
18
|
};
|
|
15
19
|
}
|