snow-flow 1.1.43 → 1.1.47
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/.claude/settings.json +11 -0
- package/README.md +97 -0
- package/dist/mcp/servicenow-intelligent-mcp.js +4 -0
- package/dist/mcp/servicenow-intelligent-mcp.js.map +1 -1
- package/dist/mcp/servicenow-operations-mcp.js +1242 -2
- package/dist/mcp/servicenow-operations-mcp.js.map +1 -1
- package/dist/orchestrator/flow-composer.d.ts.map +1 -1
- package/dist/orchestrator/flow-composer.js +4 -0
- package/dist/orchestrator/flow-composer.js.map +1 -1
- package/dist/orchestrator/flow-pattern-templates.d.ts.map +1 -1
- package/dist/orchestrator/flow-pattern-templates.js +2 -0
- package/dist/orchestrator/flow-pattern-templates.js.map +1 -1
- package/dist/strategies/global-scope-strategy.d.ts.map +1 -1
- package/dist/strategies/global-scope-strategy.js +9 -0
- package/dist/strategies/global-scope-strategy.js.map +1 -1
- package/dist/version.d.ts +26 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +128 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
|
@@ -482,6 +482,218 @@ class ServiceNowOperationsMCP {
|
|
|
482
482
|
},
|
|
483
483
|
required: ['action']
|
|
484
484
|
}
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
name: 'snow_catalog_item_search',
|
|
488
|
+
description: 'Search for catalog items with intelligent matching and discovery',
|
|
489
|
+
inputSchema: {
|
|
490
|
+
type: 'object',
|
|
491
|
+
properties: {
|
|
492
|
+
query: {
|
|
493
|
+
type: 'string',
|
|
494
|
+
description: 'Search query (e.g., "iPhone", "laptop", "mobile device")'
|
|
495
|
+
},
|
|
496
|
+
include_inactive: {
|
|
497
|
+
type: 'boolean',
|
|
498
|
+
description: 'Include inactive catalog items',
|
|
499
|
+
default: false
|
|
500
|
+
},
|
|
501
|
+
fuzzy_match: {
|
|
502
|
+
type: 'boolean',
|
|
503
|
+
description: 'Enable fuzzy matching for similar items',
|
|
504
|
+
default: true
|
|
505
|
+
},
|
|
506
|
+
category_filter: {
|
|
507
|
+
type: 'string',
|
|
508
|
+
description: 'Filter by category name or sys_id'
|
|
509
|
+
},
|
|
510
|
+
limit: {
|
|
511
|
+
type: 'number',
|
|
512
|
+
description: 'Maximum results to return',
|
|
513
|
+
default: 50
|
|
514
|
+
},
|
|
515
|
+
include_variables: {
|
|
516
|
+
type: 'boolean',
|
|
517
|
+
description: 'Include catalog item variables in results',
|
|
518
|
+
default: false
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
required: ['query']
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
name: 'snow_test_flow_with_mock',
|
|
526
|
+
description: 'Test flows with mock data - create test users, mock catalog items, and simulate flow execution',
|
|
527
|
+
inputSchema: {
|
|
528
|
+
type: 'object',
|
|
529
|
+
properties: {
|
|
530
|
+
flow_id: {
|
|
531
|
+
type: 'string',
|
|
532
|
+
description: 'Flow sys_id or name to test'
|
|
533
|
+
},
|
|
534
|
+
create_test_user: {
|
|
535
|
+
type: 'boolean',
|
|
536
|
+
description: 'Create a test user for the flow',
|
|
537
|
+
default: true
|
|
538
|
+
},
|
|
539
|
+
test_user_data: {
|
|
540
|
+
type: 'object',
|
|
541
|
+
description: 'Test user details',
|
|
542
|
+
properties: {
|
|
543
|
+
user_name: { type: 'string' },
|
|
544
|
+
first_name: { type: 'string' },
|
|
545
|
+
last_name: { type: 'string' },
|
|
546
|
+
email: { type: 'string' },
|
|
547
|
+
department: { type: 'string' },
|
|
548
|
+
manager: { type: 'string' }
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
mock_catalog_items: {
|
|
552
|
+
type: 'boolean',
|
|
553
|
+
description: 'Create mock catalog items for testing',
|
|
554
|
+
default: true
|
|
555
|
+
},
|
|
556
|
+
mock_catalog_data: {
|
|
557
|
+
type: 'array',
|
|
558
|
+
description: 'Mock catalog items to create',
|
|
559
|
+
items: {
|
|
560
|
+
type: 'object',
|
|
561
|
+
properties: {
|
|
562
|
+
name: { type: 'string' },
|
|
563
|
+
short_description: { type: 'string' },
|
|
564
|
+
price: { type: 'string' },
|
|
565
|
+
variables: { type: 'array' }
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
test_inputs: {
|
|
570
|
+
type: 'object',
|
|
571
|
+
description: 'Input values to test the flow with'
|
|
572
|
+
},
|
|
573
|
+
simulate_approvals: {
|
|
574
|
+
type: 'boolean',
|
|
575
|
+
description: 'Automatically approve any approval requests',
|
|
576
|
+
default: true
|
|
577
|
+
},
|
|
578
|
+
cleanup_after_test: {
|
|
579
|
+
type: 'boolean',
|
|
580
|
+
description: 'Remove test data after testing',
|
|
581
|
+
default: true
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
required: ['flow_id']
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
name: 'snow_link_catalog_to_flow',
|
|
589
|
+
description: 'Link catalog items directly to flows - configure flow as fulfillment process',
|
|
590
|
+
inputSchema: {
|
|
591
|
+
type: 'object',
|
|
592
|
+
properties: {
|
|
593
|
+
catalog_item_id: {
|
|
594
|
+
type: 'string',
|
|
595
|
+
description: 'Catalog item sys_id or name'
|
|
596
|
+
},
|
|
597
|
+
flow_id: {
|
|
598
|
+
type: 'string',
|
|
599
|
+
description: 'Flow sys_id or name to use for fulfillment'
|
|
600
|
+
},
|
|
601
|
+
link_type: {
|
|
602
|
+
type: 'string',
|
|
603
|
+
description: 'Type of link to create',
|
|
604
|
+
enum: ['workflow', 'flow_catalog_process', 'process_engine'],
|
|
605
|
+
default: 'flow_catalog_process'
|
|
606
|
+
},
|
|
607
|
+
variable_mapping: {
|
|
608
|
+
type: 'array',
|
|
609
|
+
description: 'Map catalog variables to flow inputs',
|
|
610
|
+
items: {
|
|
611
|
+
type: 'object',
|
|
612
|
+
properties: {
|
|
613
|
+
catalog_variable: { type: 'string', description: 'Catalog variable name' },
|
|
614
|
+
flow_input: { type: 'string', description: 'Flow input name' },
|
|
615
|
+
transform: { type: 'string', description: 'Optional transformation script' }
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
trigger_condition: {
|
|
620
|
+
type: 'string',
|
|
621
|
+
description: 'Condition for when to trigger the flow',
|
|
622
|
+
default: 'current.stage == "request_approved"'
|
|
623
|
+
},
|
|
624
|
+
execution_options: {
|
|
625
|
+
type: 'object',
|
|
626
|
+
description: 'Flow execution options',
|
|
627
|
+
properties: {
|
|
628
|
+
run_as: {
|
|
629
|
+
type: 'string',
|
|
630
|
+
description: 'User context for flow execution',
|
|
631
|
+
enum: ['requester', 'system', 'fulfiller'],
|
|
632
|
+
default: 'system'
|
|
633
|
+
},
|
|
634
|
+
wait_for_completion: {
|
|
635
|
+
type: 'boolean',
|
|
636
|
+
description: 'Wait for flow to complete before closing request',
|
|
637
|
+
default: true
|
|
638
|
+
},
|
|
639
|
+
update_request_on_progress: {
|
|
640
|
+
type: 'boolean',
|
|
641
|
+
description: 'Update request item with flow progress',
|
|
642
|
+
default: true
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
},
|
|
646
|
+
test_link: {
|
|
647
|
+
type: 'boolean',
|
|
648
|
+
description: 'Test the link by creating a sample request',
|
|
649
|
+
default: false
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
required: ['catalog_item_id', 'flow_id']
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
name: 'snow_cleanup_test_artifacts',
|
|
657
|
+
description: 'Clean up test artifacts while preserving Update Set audit trail',
|
|
658
|
+
inputSchema: {
|
|
659
|
+
type: 'object',
|
|
660
|
+
properties: {
|
|
661
|
+
artifact_types: {
|
|
662
|
+
type: 'array',
|
|
663
|
+
description: 'Types of artifacts to clean up',
|
|
664
|
+
items: {
|
|
665
|
+
type: 'string',
|
|
666
|
+
enum: ['catalog_items', 'flows', 'users', 'requests', 'all']
|
|
667
|
+
},
|
|
668
|
+
default: ['catalog_items', 'flows', 'users']
|
|
669
|
+
},
|
|
670
|
+
test_patterns: {
|
|
671
|
+
type: 'array',
|
|
672
|
+
description: 'Name patterns that identify test artifacts',
|
|
673
|
+
items: { type: 'string' },
|
|
674
|
+
default: ['Test%', 'Mock%', 'Demo%', '%_test_%', '%test', '%mock']
|
|
675
|
+
},
|
|
676
|
+
max_age_hours: {
|
|
677
|
+
type: 'number',
|
|
678
|
+
description: 'Only clean artifacts older than this (hours)',
|
|
679
|
+
default: 1
|
|
680
|
+
},
|
|
681
|
+
dry_run: {
|
|
682
|
+
type: 'boolean',
|
|
683
|
+
description: 'Preview what would be deleted without actually deleting',
|
|
684
|
+
default: false
|
|
685
|
+
},
|
|
686
|
+
preserve_update_set_entries: {
|
|
687
|
+
type: 'boolean',
|
|
688
|
+
description: 'Keep Update Set entries as audit trail',
|
|
689
|
+
default: true
|
|
690
|
+
},
|
|
691
|
+
update_set_filter: {
|
|
692
|
+
type: 'string',
|
|
693
|
+
description: 'Only clean from specific Update Set (optional)'
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
485
697
|
}
|
|
486
698
|
],
|
|
487
699
|
};
|
|
@@ -515,6 +727,14 @@ class ServiceNowOperationsMCP {
|
|
|
515
727
|
return await this.handlePredictiveAnalysis(args);
|
|
516
728
|
case 'snow_catalog_item_manager':
|
|
517
729
|
return await this.handleCatalogItemManager(args);
|
|
730
|
+
case 'snow_catalog_item_search':
|
|
731
|
+
return await this.handleCatalogItemSearch(args);
|
|
732
|
+
case 'snow_test_flow_with_mock':
|
|
733
|
+
return await this.handleTestFlowWithMock(args);
|
|
734
|
+
case 'snow_link_catalog_to_flow':
|
|
735
|
+
return await this.handleLinkCatalogToFlow(args);
|
|
736
|
+
case 'snow_cleanup_test_artifacts':
|
|
737
|
+
return await this.handleCleanupTestArtifacts(args);
|
|
518
738
|
default:
|
|
519
739
|
throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Tool ${name} not found`);
|
|
520
740
|
}
|
|
@@ -1382,6 +1602,34 @@ class ServiceNowOperationsMCP {
|
|
|
1382
1602
|
try {
|
|
1383
1603
|
switch (action) {
|
|
1384
1604
|
case 'create': {
|
|
1605
|
+
// Get default catalog if none provided
|
|
1606
|
+
let catalogId = params.sc_catalogs || params.catalog_id;
|
|
1607
|
+
let categoryId = params.sc_categories || params.category_id;
|
|
1608
|
+
if (!catalogId) {
|
|
1609
|
+
// Find default Service Catalog
|
|
1610
|
+
const defaultCatalogResult = await this.client.searchRecords('sc_catalog', 'active=true^ORDERBYsys_created_on', 1);
|
|
1611
|
+
if (defaultCatalogResult.success && defaultCatalogResult.data?.result?.length > 0) {
|
|
1612
|
+
catalogId = defaultCatalogResult.data.result[0].sys_id;
|
|
1613
|
+
logger_js_1.logger.info('Using default catalog:', {
|
|
1614
|
+
catalogId,
|
|
1615
|
+
catalogName: defaultCatalogResult.data.result[0].title
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
else {
|
|
1619
|
+
logger_js_1.logger.warn('No catalogs found, catalog item may not be visible');
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
if (!categoryId) {
|
|
1623
|
+
// Find a default category like "Hardware" or "General"
|
|
1624
|
+
const defaultCategoryResult = await this.client.searchRecords('sc_category', 'active=true^titleLIKEHardware^ORtitleLIKEGeneral^ORtitleLIKEIT^ORDERBYsys_created_on', 1);
|
|
1625
|
+
if (defaultCategoryResult.success && defaultCategoryResult.data?.result?.length > 0) {
|
|
1626
|
+
categoryId = defaultCategoryResult.data.result[0].sys_id;
|
|
1627
|
+
logger_js_1.logger.info('Using default category:', {
|
|
1628
|
+
categoryId,
|
|
1629
|
+
categoryName: defaultCategoryResult.data.result[0].title
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1385
1633
|
const catalogItem = {
|
|
1386
1634
|
name: params.name,
|
|
1387
1635
|
short_description: params.short_description,
|
|
@@ -1392,8 +1640,8 @@ class ServiceNowOperationsMCP {
|
|
|
1392
1640
|
active: params.active !== false,
|
|
1393
1641
|
workflow: params.workflow,
|
|
1394
1642
|
picture: params.picture,
|
|
1395
|
-
sc_catalogs:
|
|
1396
|
-
sc_categories:
|
|
1643
|
+
sc_catalogs: catalogId,
|
|
1644
|
+
sc_categories: categoryId,
|
|
1397
1645
|
sys_class_name: 'sc_cat_item'
|
|
1398
1646
|
};
|
|
1399
1647
|
const result = await this.client.createRecord('sc_cat_item', catalogItem);
|
|
@@ -1597,6 +1845,998 @@ class ServiceNowOperationsMCP {
|
|
|
1597
1845
|
};
|
|
1598
1846
|
return typeMap[type] || '6'; // Default to single line text
|
|
1599
1847
|
}
|
|
1848
|
+
async handleCatalogItemSearch(args) {
|
|
1849
|
+
const { query, include_inactive = false, fuzzy_match = true, category_filter, limit = 50, include_variables = false } = args;
|
|
1850
|
+
logger_js_1.logger.info(`Searching catalog items for: ${query}`, { fuzzy_match, category_filter, limit });
|
|
1851
|
+
try {
|
|
1852
|
+
// Build search queries
|
|
1853
|
+
const searchQueries = [];
|
|
1854
|
+
// Exact match
|
|
1855
|
+
searchQueries.push(`name=${query}`);
|
|
1856
|
+
// Contains match
|
|
1857
|
+
searchQueries.push(`nameLIKE${query}`);
|
|
1858
|
+
searchQueries.push(`short_descriptionLIKE${query}`);
|
|
1859
|
+
// Fuzzy matching for similar items
|
|
1860
|
+
if (fuzzy_match) {
|
|
1861
|
+
// Handle common variations
|
|
1862
|
+
const variations = this.generateSearchVariations(query);
|
|
1863
|
+
variations.forEach(variation => {
|
|
1864
|
+
searchQueries.push(`nameLIKE${variation}`);
|
|
1865
|
+
searchQueries.push(`short_descriptionLIKE${variation}`);
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
// Add active filter
|
|
1869
|
+
const activeFilter = include_inactive ? '' : '^active=true';
|
|
1870
|
+
// Add category filter if provided
|
|
1871
|
+
let categoryFilter = '';
|
|
1872
|
+
if (category_filter) {
|
|
1873
|
+
// First try to find the category
|
|
1874
|
+
const categoryResult = await this.client.searchRecords('sc_category', `name=${category_filter}^ORsys_id=${category_filter}`, 1);
|
|
1875
|
+
if (categoryResult.success && categoryResult.data?.result?.length > 0) {
|
|
1876
|
+
categoryFilter = `^sc_categories=${categoryResult.data.result[0].sys_id}`;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
// Execute searches
|
|
1880
|
+
const allResults = new Map();
|
|
1881
|
+
for (const searchQuery of searchQueries) {
|
|
1882
|
+
const fullQuery = searchQuery + activeFilter + categoryFilter;
|
|
1883
|
+
const result = await this.client.searchRecords('sc_cat_item', fullQuery, limit);
|
|
1884
|
+
if (result.success && result.data?.result) {
|
|
1885
|
+
result.data.result.forEach((item) => {
|
|
1886
|
+
if (!allResults.has(item.sys_id)) {
|
|
1887
|
+
allResults.set(item.sys_id, {
|
|
1888
|
+
sys_id: item.sys_id,
|
|
1889
|
+
name: item.name,
|
|
1890
|
+
short_description: item.short_description,
|
|
1891
|
+
price: item.price,
|
|
1892
|
+
recurring_price: item.recurring_price,
|
|
1893
|
+
active: item.active,
|
|
1894
|
+
category: item.sc_categories?.display_value,
|
|
1895
|
+
catalog: item.sc_catalogs?.display_value,
|
|
1896
|
+
model_number: item.model,
|
|
1897
|
+
manufacturer: item.vendor,
|
|
1898
|
+
variables: []
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
// Get variables if requested
|
|
1905
|
+
if (include_variables && allResults.size > 0) {
|
|
1906
|
+
for (const [sys_id, item] of allResults) {
|
|
1907
|
+
const variablesResult = await this.client.searchRecords('item_option_new', `cat_item=${sys_id}`, 50);
|
|
1908
|
+
if (variablesResult.success && variablesResult.data?.result) {
|
|
1909
|
+
item.variables = variablesResult.data.result.map((v) => ({
|
|
1910
|
+
name: v.name,
|
|
1911
|
+
label: v.question_text,
|
|
1912
|
+
type: v.type,
|
|
1913
|
+
mandatory: v.mandatory,
|
|
1914
|
+
default_value: v.default_value
|
|
1915
|
+
}));
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
const resultsArray = Array.from(allResults.values());
|
|
1920
|
+
// Sort by relevance (exact matches first)
|
|
1921
|
+
resultsArray.sort((a, b) => {
|
|
1922
|
+
const aExact = a.name.toLowerCase() === query.toLowerCase();
|
|
1923
|
+
const bExact = b.name.toLowerCase() === query.toLowerCase();
|
|
1924
|
+
if (aExact && !bExact)
|
|
1925
|
+
return -1;
|
|
1926
|
+
if (!aExact && bExact)
|
|
1927
|
+
return 1;
|
|
1928
|
+
return 0;
|
|
1929
|
+
});
|
|
1930
|
+
if (resultsArray.length === 0) {
|
|
1931
|
+
// Provide suggestions if no results found
|
|
1932
|
+
const suggestions = this.generateSearchSuggestions(query);
|
|
1933
|
+
return {
|
|
1934
|
+
content: [{
|
|
1935
|
+
type: 'text',
|
|
1936
|
+
text: `❌ No catalog items found for "${query}"\n\n` +
|
|
1937
|
+
`💡 Suggestions:\n` +
|
|
1938
|
+
suggestions.map(s => `- Try searching for "${s}"`).join('\n') +
|
|
1939
|
+
`\n\n💡 Tips:\n` +
|
|
1940
|
+
`- Check if the item name is spelled correctly\n` +
|
|
1941
|
+
`- Try searching with partial names (e.g., "iPhone" instead of "iPhone 6S")\n` +
|
|
1942
|
+
`- Use category filter to narrow down results\n` +
|
|
1943
|
+
`- Enable include_inactive if looking for retired items`
|
|
1944
|
+
}]
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
// Format results
|
|
1948
|
+
let resultText = `🔍 Found ${resultsArray.length} catalog items matching "${query}":\n\n`;
|
|
1949
|
+
resultsArray.forEach((item, index) => {
|
|
1950
|
+
resultText += `${index + 1}. **${item.name}** (${item.sys_id})\n`;
|
|
1951
|
+
resultText += ` 📝 ${item.short_description || 'No description'}\n`;
|
|
1952
|
+
resultText += ` 💰 Price: $${item.price || '0'} ${item.recurring_price ? `(+ $${item.recurring_price}/month)` : ''}\n`;
|
|
1953
|
+
resultText += ` 📁 Category: ${item.category || 'Uncategorized'}\n`;
|
|
1954
|
+
resultText += ` 📚 Catalog: ${item.catalog || 'Default'}\n`;
|
|
1955
|
+
if (item.model_number) {
|
|
1956
|
+
resultText += ` 🔢 Model: ${item.model_number}\n`;
|
|
1957
|
+
}
|
|
1958
|
+
if (item.manufacturer) {
|
|
1959
|
+
resultText += ` 🏭 Manufacturer: ${item.manufacturer}\n`;
|
|
1960
|
+
}
|
|
1961
|
+
resultText += ` ✅ Status: ${item.active ? 'Active' : 'Inactive'}\n`;
|
|
1962
|
+
if (item.variables && item.variables.length > 0) {
|
|
1963
|
+
resultText += ` 📋 Variables (${item.variables.length}):\n`;
|
|
1964
|
+
item.variables.forEach((v) => {
|
|
1965
|
+
resultText += ` - ${v.label} (${v.name}) - ${v.type}${v.mandatory ? ' *Required' : ''}\n`;
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
resultText += '\n';
|
|
1969
|
+
});
|
|
1970
|
+
resultText += `\n💡 Use snow_catalog_item_manager to create or update catalog items`;
|
|
1971
|
+
return {
|
|
1972
|
+
content: [{
|
|
1973
|
+
type: 'text',
|
|
1974
|
+
text: resultText
|
|
1975
|
+
}]
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
catch (error) {
|
|
1979
|
+
logger_js_1.logger.error('Error searching catalog items:', error);
|
|
1980
|
+
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Catalog search error: ${error}`);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
generateSearchVariations(query) {
|
|
1984
|
+
const variations = [];
|
|
1985
|
+
// Handle common product variations
|
|
1986
|
+
if (query.toLowerCase().includes('iphone')) {
|
|
1987
|
+
// Add variations without spaces, with different numbers
|
|
1988
|
+
const baseModel = query.replace(/iphone\s*/i, 'iPhone ');
|
|
1989
|
+
variations.push(baseModel);
|
|
1990
|
+
variations.push(baseModel.replace(' ', ''));
|
|
1991
|
+
// Try different model numbers
|
|
1992
|
+
const modelMatch = query.match(/\d+/);
|
|
1993
|
+
if (modelMatch) {
|
|
1994
|
+
const modelNum = parseInt(modelMatch[0]);
|
|
1995
|
+
variations.push(query.replace(modelNum.toString(), (modelNum + 1).toString()));
|
|
1996
|
+
variations.push(query.replace(modelNum.toString(), (modelNum - 1).toString()));
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
// Handle laptop/computer variations
|
|
2000
|
+
if (query.toLowerCase().includes('laptop') || query.toLowerCase().includes('computer')) {
|
|
2001
|
+
variations.push('notebook', 'macbook', 'thinkpad', 'dell', 'hp');
|
|
2002
|
+
}
|
|
2003
|
+
// Handle phone variations
|
|
2004
|
+
if (query.toLowerCase().includes('phone') || query.toLowerCase().includes('mobile')) {
|
|
2005
|
+
variations.push('smartphone', 'android', 'samsung', 'pixel');
|
|
2006
|
+
}
|
|
2007
|
+
return variations;
|
|
2008
|
+
}
|
|
2009
|
+
generateSearchSuggestions(query) {
|
|
2010
|
+
const suggestions = [];
|
|
2011
|
+
if (query.toLowerCase().includes('iphone')) {
|
|
2012
|
+
suggestions.push('iPhone', 'Apple iPhone', 'iOS device', 'Apple mobile');
|
|
2013
|
+
}
|
|
2014
|
+
else if (query.toLowerCase().includes('laptop')) {
|
|
2015
|
+
suggestions.push('notebook', 'computer', 'workstation', 'MacBook', 'ThinkPad');
|
|
2016
|
+
}
|
|
2017
|
+
else if (query.toLowerCase().includes('software')) {
|
|
2018
|
+
suggestions.push('license', 'application', 'subscription', 'SaaS');
|
|
2019
|
+
}
|
|
2020
|
+
else {
|
|
2021
|
+
// Generic suggestions
|
|
2022
|
+
suggestions.push(query.split(' ')[0], // First word only
|
|
2023
|
+
query.substring(0, Math.floor(query.length / 2)), // First half
|
|
2024
|
+
'hardware', 'software', 'equipment', 'device');
|
|
2025
|
+
}
|
|
2026
|
+
return [...new Set(suggestions)].filter(s => s && s !== query);
|
|
2027
|
+
}
|
|
2028
|
+
async handleTestFlowWithMock(args) {
|
|
2029
|
+
const { flow_id, create_test_user = true, test_user_data, mock_catalog_items = true, mock_catalog_data, test_inputs = {}, simulate_approvals = true, cleanup_after_test = true } = args;
|
|
2030
|
+
logger_js_1.logger.info(`Testing flow ${flow_id} with mock data`, {
|
|
2031
|
+
create_test_user,
|
|
2032
|
+
mock_catalog_items,
|
|
2033
|
+
simulate_approvals,
|
|
2034
|
+
cleanup_after_test
|
|
2035
|
+
});
|
|
2036
|
+
const testResults = {
|
|
2037
|
+
flow_id,
|
|
2038
|
+
test_run_id: `test_${Date.now()}`,
|
|
2039
|
+
created_data: {
|
|
2040
|
+
test_user: null,
|
|
2041
|
+
catalog_items: [],
|
|
2042
|
+
test_requests: []
|
|
2043
|
+
},
|
|
2044
|
+
execution_results: {
|
|
2045
|
+
status: 'pending',
|
|
2046
|
+
start_time: new Date().toISOString(),
|
|
2047
|
+
end_time: null,
|
|
2048
|
+
duration_ms: 0,
|
|
2049
|
+
execution_id: null,
|
|
2050
|
+
steps_executed: [],
|
|
2051
|
+
errors: [],
|
|
2052
|
+
approvals_simulated: []
|
|
2053
|
+
},
|
|
2054
|
+
cleanup_status: null
|
|
2055
|
+
};
|
|
2056
|
+
try {
|
|
2057
|
+
// Step 1: Find the flow
|
|
2058
|
+
const flowResult = await this.client.searchRecords('sys_hub_flow', `sys_id=${flow_id}^ORname=${flow_id}`, 1);
|
|
2059
|
+
if (!flowResult.success || !flowResult.data?.result?.length) {
|
|
2060
|
+
throw new Error(`Flow not found: ${flow_id}`);
|
|
2061
|
+
}
|
|
2062
|
+
const flow = flowResult.data.result[0];
|
|
2063
|
+
testResults.flow_id = flow.sys_id;
|
|
2064
|
+
// Step 2: Create test user if requested
|
|
2065
|
+
if (create_test_user) {
|
|
2066
|
+
const userData = test_user_data || {
|
|
2067
|
+
user_name: `test_user_${Date.now()}`,
|
|
2068
|
+
first_name: 'Test',
|
|
2069
|
+
last_name: 'User',
|
|
2070
|
+
email: `test_${Date.now()}@example.com`,
|
|
2071
|
+
department: 'IT',
|
|
2072
|
+
active: true
|
|
2073
|
+
};
|
|
2074
|
+
const userResult = await this.client.createRecord('sys_user', userData);
|
|
2075
|
+
if (userResult.success && userResult.data?.result) {
|
|
2076
|
+
testResults.created_data.test_user = {
|
|
2077
|
+
sys_id: userResult.data.result.sys_id,
|
|
2078
|
+
user_name: userData.user_name,
|
|
2079
|
+
email: userData.email
|
|
2080
|
+
};
|
|
2081
|
+
logger_js_1.logger.info('Created test user:', testResults.created_data.test_user);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
// Step 3: Create mock catalog items if requested
|
|
2085
|
+
if (mock_catalog_items) {
|
|
2086
|
+
const catalogData = mock_catalog_data || [
|
|
2087
|
+
{
|
|
2088
|
+
name: 'Test iPhone 6S',
|
|
2089
|
+
short_description: 'Test mobile device for flow testing',
|
|
2090
|
+
price: '699.00',
|
|
2091
|
+
active: true,
|
|
2092
|
+
billable: true
|
|
2093
|
+
},
|
|
2094
|
+
{
|
|
2095
|
+
name: 'Test Laptop',
|
|
2096
|
+
short_description: 'Test laptop for equipment provisioning',
|
|
2097
|
+
price: '1299.00',
|
|
2098
|
+
active: true,
|
|
2099
|
+
billable: true
|
|
2100
|
+
}
|
|
2101
|
+
];
|
|
2102
|
+
for (const item of catalogData) {
|
|
2103
|
+
const catalogResult = await this.client.createRecord('sc_cat_item', item);
|
|
2104
|
+
if (catalogResult.success && catalogResult.data?.result) {
|
|
2105
|
+
testResults.created_data.catalog_items.push({
|
|
2106
|
+
sys_id: catalogResult.data.result.sys_id,
|
|
2107
|
+
name: item.name,
|
|
2108
|
+
price: item.price
|
|
2109
|
+
});
|
|
2110
|
+
logger_js_1.logger.info('Created mock catalog item:', item.name);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
// Step 4: Execute the flow with test data
|
|
2115
|
+
try {
|
|
2116
|
+
// Prepare test inputs
|
|
2117
|
+
const flowInputs = {
|
|
2118
|
+
...test_inputs,
|
|
2119
|
+
test_mode: true,
|
|
2120
|
+
test_run_id: testResults.test_run_id
|
|
2121
|
+
};
|
|
2122
|
+
// If we created a test user, use them as the requested_for
|
|
2123
|
+
if (testResults.created_data.test_user) {
|
|
2124
|
+
flowInputs.requested_for = testResults.created_data.test_user.sys_id;
|
|
2125
|
+
flowInputs.opened_by = testResults.created_data.test_user.sys_id;
|
|
2126
|
+
}
|
|
2127
|
+
// If we created catalog items, use the first one
|
|
2128
|
+
if (testResults.created_data.catalog_items.length > 0) {
|
|
2129
|
+
flowInputs.cat_item = testResults.created_data.catalog_items[0].sys_id;
|
|
2130
|
+
}
|
|
2131
|
+
// Execute flow via REST API
|
|
2132
|
+
const executeResult = await this.executeFlowViaAPI(flow.sys_id, flowInputs);
|
|
2133
|
+
if (executeResult.success) {
|
|
2134
|
+
testResults.execution_results.status = 'running';
|
|
2135
|
+
testResults.execution_results.execution_id = executeResult.execution_id;
|
|
2136
|
+
// Monitor flow execution
|
|
2137
|
+
const executionStatus = await this.monitorFlowExecution(executeResult.execution_id, simulate_approvals);
|
|
2138
|
+
testResults.execution_results = {
|
|
2139
|
+
...testResults.execution_results,
|
|
2140
|
+
...executionStatus
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
else {
|
|
2144
|
+
testResults.execution_results.status = 'failed';
|
|
2145
|
+
testResults.execution_results.errors.push(executeResult.error || 'Unknown execution error');
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
catch (execError) {
|
|
2149
|
+
testResults.execution_results.status = 'error';
|
|
2150
|
+
testResults.execution_results.errors.push(execError.message);
|
|
2151
|
+
logger_js_1.logger.error('Flow execution error:', execError);
|
|
2152
|
+
}
|
|
2153
|
+
// Step 5: Cleanup if requested
|
|
2154
|
+
if (cleanup_after_test) {
|
|
2155
|
+
const cleanupResults = {
|
|
2156
|
+
user_deleted: false,
|
|
2157
|
+
catalog_items_deleted: 0,
|
|
2158
|
+
requests_cancelled: 0
|
|
2159
|
+
};
|
|
2160
|
+
// Delete test user
|
|
2161
|
+
if (testResults.created_data.test_user) {
|
|
2162
|
+
const deleteUserResult = await this.client.deleteRecord('sys_user', testResults.created_data.test_user.sys_id);
|
|
2163
|
+
cleanupResults.user_deleted = deleteUserResult.success;
|
|
2164
|
+
}
|
|
2165
|
+
// Delete catalog items
|
|
2166
|
+
for (const item of testResults.created_data.catalog_items) {
|
|
2167
|
+
const deleteItemResult = await this.client.deleteRecord('sc_cat_item', item.sys_id);
|
|
2168
|
+
if (deleteItemResult.success) {
|
|
2169
|
+
cleanupResults.catalog_items_deleted++;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
testResults.cleanup_status = cleanupResults;
|
|
2173
|
+
}
|
|
2174
|
+
// Format results
|
|
2175
|
+
const resultText = this.formatTestResults(testResults);
|
|
2176
|
+
return {
|
|
2177
|
+
content: [{
|
|
2178
|
+
type: 'text',
|
|
2179
|
+
text: resultText
|
|
2180
|
+
}]
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
catch (error) {
|
|
2184
|
+
logger_js_1.logger.error('Error testing flow with mock data:', error);
|
|
2185
|
+
testResults.execution_results.status = 'error';
|
|
2186
|
+
testResults.execution_results.errors.push(error.message);
|
|
2187
|
+
return {
|
|
2188
|
+
content: [{
|
|
2189
|
+
type: 'text',
|
|
2190
|
+
text: `❌ Flow test failed\n\nError: ${error.message}\n\nTest Results:\n${JSON.stringify(testResults, null, 2)}`
|
|
2191
|
+
}]
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
async executeFlowViaAPI(flowId, inputs) {
|
|
2196
|
+
try {
|
|
2197
|
+
// Use the ServiceNow Flow API to execute the flow
|
|
2198
|
+
const result = await this.client.createRecord('sys_flow_context', {
|
|
2199
|
+
flow: flowId,
|
|
2200
|
+
inputs: JSON.stringify(inputs),
|
|
2201
|
+
state: 'executing'
|
|
2202
|
+
});
|
|
2203
|
+
if (result.success && result.data?.result) {
|
|
2204
|
+
return {
|
|
2205
|
+
success: true,
|
|
2206
|
+
execution_id: result.data.result.sys_id
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
return {
|
|
2210
|
+
success: false,
|
|
2211
|
+
error: 'Failed to start flow execution'
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
catch (error) {
|
|
2215
|
+
logger_js_1.logger.error('Error executing flow via API:', error);
|
|
2216
|
+
return {
|
|
2217
|
+
success: false,
|
|
2218
|
+
error: error.message
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
async monitorFlowExecution(executionId, simulateApprovals) {
|
|
2223
|
+
const maxWaitTime = 60000; // 60 seconds
|
|
2224
|
+
const pollInterval = 2000; // 2 seconds
|
|
2225
|
+
const startTime = Date.now();
|
|
2226
|
+
const executionResults = {
|
|
2227
|
+
status: 'running',
|
|
2228
|
+
steps_executed: [],
|
|
2229
|
+
approvals_simulated: [],
|
|
2230
|
+
errors: [],
|
|
2231
|
+
end_time: null,
|
|
2232
|
+
duration_ms: 0
|
|
2233
|
+
};
|
|
2234
|
+
while (Date.now() - startTime < maxWaitTime) {
|
|
2235
|
+
try {
|
|
2236
|
+
// Check flow context status
|
|
2237
|
+
const contextResult = await this.client.getRecord('sys_flow_context', executionId);
|
|
2238
|
+
if (!contextResult.success || !contextResult.data?.result) {
|
|
2239
|
+
executionResults.errors.push('Failed to get flow context');
|
|
2240
|
+
break;
|
|
2241
|
+
}
|
|
2242
|
+
const context = contextResult.data.result;
|
|
2243
|
+
executionResults.status = context.state;
|
|
2244
|
+
// Check for completed state
|
|
2245
|
+
if (context.state === 'complete' || context.state === 'cancelled' || context.state === 'error') {
|
|
2246
|
+
executionResults.end_time = context.sys_updated_on;
|
|
2247
|
+
executionResults.duration_ms = Date.now() - startTime;
|
|
2248
|
+
break;
|
|
2249
|
+
}
|
|
2250
|
+
// Check for approvals if simulating
|
|
2251
|
+
if (simulateApprovals) {
|
|
2252
|
+
const approvalResult = await this.client.searchRecords('sysapproval_approver', `source_table=sys_flow_context^document_id=${executionId}^state=requested`, 10);
|
|
2253
|
+
if (approvalResult.success && approvalResult.data?.result?.length > 0) {
|
|
2254
|
+
for (const approval of approvalResult.data.result) {
|
|
2255
|
+
// Auto-approve
|
|
2256
|
+
const approveResult = await this.client.updateRecord('sysapproval_approver', approval.sys_id, {
|
|
2257
|
+
state: 'approved',
|
|
2258
|
+
comments: 'Auto-approved by test framework'
|
|
2259
|
+
});
|
|
2260
|
+
if (approveResult.success) {
|
|
2261
|
+
executionResults.approvals_simulated.push({
|
|
2262
|
+
sys_id: approval.sys_id,
|
|
2263
|
+
approver: approval.approver.display_value,
|
|
2264
|
+
approved_at: new Date().toISOString()
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
// Wait before next poll
|
|
2271
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
2272
|
+
}
|
|
2273
|
+
catch (error) {
|
|
2274
|
+
executionResults.errors.push(error.message);
|
|
2275
|
+
executionResults.status = 'error';
|
|
2276
|
+
break;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
if (executionResults.status === 'running') {
|
|
2280
|
+
executionResults.status = 'timeout';
|
|
2281
|
+
executionResults.errors.push('Flow execution timed out after 60 seconds');
|
|
2282
|
+
}
|
|
2283
|
+
return executionResults;
|
|
2284
|
+
}
|
|
2285
|
+
formatTestResults(testResults) {
|
|
2286
|
+
let text = `🧪 Flow Test Results\n${'='.repeat(50)}\n\n`;
|
|
2287
|
+
text += `**Flow ID:** ${testResults.flow_id}\n`;
|
|
2288
|
+
text += `**Test Run ID:** ${testResults.test_run_id}\n\n`;
|
|
2289
|
+
// Test Data Created
|
|
2290
|
+
text += `📋 **Test Data Created:**\n`;
|
|
2291
|
+
if (testResults.created_data.test_user) {
|
|
2292
|
+
text += ` ✅ Test User: ${testResults.created_data.test_user.user_name} (${testResults.created_data.test_user.sys_id})\n`;
|
|
2293
|
+
}
|
|
2294
|
+
if (testResults.created_data.catalog_items.length > 0) {
|
|
2295
|
+
text += ` ✅ Catalog Items: ${testResults.created_data.catalog_items.length} created\n`;
|
|
2296
|
+
testResults.created_data.catalog_items.forEach((item) => {
|
|
2297
|
+
text += ` - ${item.name} ($${item.price})\n`;
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
// Execution Results
|
|
2301
|
+
text += `\n🚀 **Execution Results:**\n`;
|
|
2302
|
+
text += ` Status: ${testResults.execution_results.status === 'complete' ? '✅' : '❌'} ${testResults.execution_results.status}\n`;
|
|
2303
|
+
if (testResults.execution_results.start_time) {
|
|
2304
|
+
text += ` Start Time: ${testResults.execution_results.start_time}\n`;
|
|
2305
|
+
}
|
|
2306
|
+
if (testResults.execution_results.end_time) {
|
|
2307
|
+
text += ` End Time: ${testResults.execution_results.end_time}\n`;
|
|
2308
|
+
}
|
|
2309
|
+
if (testResults.execution_results.duration_ms) {
|
|
2310
|
+
text += ` Duration: ${(testResults.execution_results.duration_ms / 1000).toFixed(2)}s\n`;
|
|
2311
|
+
}
|
|
2312
|
+
if (testResults.execution_results.approvals_simulated.length > 0) {
|
|
2313
|
+
text += `\n ✅ Approvals Simulated: ${testResults.execution_results.approvals_simulated.length}\n`;
|
|
2314
|
+
testResults.execution_results.approvals_simulated.forEach((approval) => {
|
|
2315
|
+
text += ` - ${approval.approver} at ${approval.approved_at}\n`;
|
|
2316
|
+
});
|
|
2317
|
+
}
|
|
2318
|
+
if (testResults.execution_results.errors.length > 0) {
|
|
2319
|
+
text += `\n ❌ Errors:\n`;
|
|
2320
|
+
testResults.execution_results.errors.forEach((error) => {
|
|
2321
|
+
text += ` - ${error}\n`;
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
// Cleanup Status
|
|
2325
|
+
if (testResults.cleanup_status) {
|
|
2326
|
+
text += `\n🧹 **Cleanup Status:**\n`;
|
|
2327
|
+
text += ` User Deleted: ${testResults.cleanup_status.user_deleted ? '✅' : '❌'}\n`;
|
|
2328
|
+
text += ` Catalog Items Deleted: ${testResults.cleanup_status.catalog_items_deleted}\n`;
|
|
2329
|
+
text += ` Requests Cancelled: ${testResults.cleanup_status.requests_cancelled}\n`;
|
|
2330
|
+
}
|
|
2331
|
+
// Recommendations
|
|
2332
|
+
text += `\n💡 **Recommendations:**\n`;
|
|
2333
|
+
if (testResults.execution_results.status === 'complete') {
|
|
2334
|
+
text += ` ✅ Flow executed successfully! Ready for production use.\n`;
|
|
2335
|
+
}
|
|
2336
|
+
else if (testResults.execution_results.status === 'timeout') {
|
|
2337
|
+
text += ` ⚠️ Flow execution timed out. Consider:\n`;
|
|
2338
|
+
text += ` - Checking for infinite loops\n`;
|
|
2339
|
+
text += ` - Reviewing wait conditions\n`;
|
|
2340
|
+
text += ` - Optimizing flow performance\n`;
|
|
2341
|
+
}
|
|
2342
|
+
else {
|
|
2343
|
+
text += ` ❌ Flow execution failed. Please:\n`;
|
|
2344
|
+
text += ` - Review error messages above\n`;
|
|
2345
|
+
text += ` - Check flow configuration\n`;
|
|
2346
|
+
text += ` - Verify all dependencies exist\n`;
|
|
2347
|
+
}
|
|
2348
|
+
return text;
|
|
2349
|
+
}
|
|
2350
|
+
async handleLinkCatalogToFlow(args) {
|
|
2351
|
+
const { catalog_item_id, flow_id, link_type = 'flow_catalog_process', variable_mapping = [], trigger_condition = 'current.stage == "request_approved"', execution_options = {}, test_link = false } = args;
|
|
2352
|
+
logger_js_1.logger.info(`Linking catalog item ${catalog_item_id} to flow ${flow_id}`, {
|
|
2353
|
+
link_type,
|
|
2354
|
+
variable_mapping_count: variable_mapping.length
|
|
2355
|
+
});
|
|
2356
|
+
const linkResults = {
|
|
2357
|
+
catalog_item: null,
|
|
2358
|
+
flow: null,
|
|
2359
|
+
link_created: false,
|
|
2360
|
+
link_details: null,
|
|
2361
|
+
variable_mappings: [],
|
|
2362
|
+
test_results: null,
|
|
2363
|
+
errors: []
|
|
2364
|
+
};
|
|
2365
|
+
try {
|
|
2366
|
+
// Step 1: Find the catalog item
|
|
2367
|
+
const catalogResult = await this.client.searchRecords('sc_cat_item', `sys_id=${catalog_item_id}^ORname=${catalog_item_id}`, 1);
|
|
2368
|
+
if (!catalogResult.success || !catalogResult.data?.result?.length) {
|
|
2369
|
+
throw new Error(`Catalog item not found: ${catalog_item_id}`);
|
|
2370
|
+
}
|
|
2371
|
+
linkResults.catalog_item = {
|
|
2372
|
+
sys_id: catalogResult.data.result[0].sys_id,
|
|
2373
|
+
name: catalogResult.data.result[0].name,
|
|
2374
|
+
category: catalogResult.data.result[0].sc_categories?.display_value
|
|
2375
|
+
};
|
|
2376
|
+
// Step 2: Find the flow
|
|
2377
|
+
const flowResult = await this.client.searchRecords('sys_hub_flow', `sys_id=${flow_id}^ORname=${flow_id}`, 1);
|
|
2378
|
+
if (!flowResult.success || !flowResult.data?.result?.length) {
|
|
2379
|
+
throw new Error(`Flow not found: ${flow_id}`);
|
|
2380
|
+
}
|
|
2381
|
+
linkResults.flow = {
|
|
2382
|
+
sys_id: flowResult.data.result[0].sys_id,
|
|
2383
|
+
name: flowResult.data.result[0].name,
|
|
2384
|
+
type: flowResult.data.result[0].type
|
|
2385
|
+
};
|
|
2386
|
+
// Step 3: Create the link based on link_type
|
|
2387
|
+
switch (link_type) {
|
|
2388
|
+
case 'flow_catalog_process': {
|
|
2389
|
+
// Modern approach: Use Flow Designer catalog process
|
|
2390
|
+
const processData = {
|
|
2391
|
+
catalog_item: linkResults.catalog_item.sys_id,
|
|
2392
|
+
flow: linkResults.flow.sys_id,
|
|
2393
|
+
active: true,
|
|
2394
|
+
condition: trigger_condition,
|
|
2395
|
+
run_as: execution_options.run_as || 'system',
|
|
2396
|
+
wait_for_completion: execution_options.wait_for_completion !== false
|
|
2397
|
+
};
|
|
2398
|
+
const processResult = await this.client.createRecord('sc_cat_item_producer', processData);
|
|
2399
|
+
if (processResult.success && processResult.data?.result) {
|
|
2400
|
+
linkResults.link_created = true;
|
|
2401
|
+
linkResults.link_details = {
|
|
2402
|
+
type: 'flow_catalog_process',
|
|
2403
|
+
sys_id: processResult.data.result.sys_id,
|
|
2404
|
+
table: 'sc_cat_item_producer'
|
|
2405
|
+
};
|
|
2406
|
+
// Create variable mappings
|
|
2407
|
+
for (const mapping of variable_mapping) {
|
|
2408
|
+
const mappingData = {
|
|
2409
|
+
producer: processResult.data.result.sys_id,
|
|
2410
|
+
catalog_variable: mapping.catalog_variable,
|
|
2411
|
+
flow_input: mapping.flow_input,
|
|
2412
|
+
transform_script: mapping.transform || ''
|
|
2413
|
+
};
|
|
2414
|
+
const mappingResult = await this.client.createRecord('sc_cat_item_producer_mapping', mappingData);
|
|
2415
|
+
if (mappingResult.success) {
|
|
2416
|
+
linkResults.variable_mappings.push({
|
|
2417
|
+
catalog_variable: mapping.catalog_variable,
|
|
2418
|
+
flow_input: mapping.flow_input,
|
|
2419
|
+
mapping_sys_id: mappingResult.data?.result?.sys_id
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
break;
|
|
2425
|
+
}
|
|
2426
|
+
case 'workflow': {
|
|
2427
|
+
// Legacy approach: Direct workflow assignment
|
|
2428
|
+
const updateData = {
|
|
2429
|
+
workflow: linkResults.flow.sys_id,
|
|
2430
|
+
no_order: false,
|
|
2431
|
+
no_proceed_checkout: false,
|
|
2432
|
+
no_quantity: false,
|
|
2433
|
+
no_delivery_time: false
|
|
2434
|
+
};
|
|
2435
|
+
const updateResult = await this.client.updateRecord('sc_cat_item', linkResults.catalog_item.sys_id, updateData);
|
|
2436
|
+
if (updateResult.success) {
|
|
2437
|
+
linkResults.link_created = true;
|
|
2438
|
+
linkResults.link_details = {
|
|
2439
|
+
type: 'workflow',
|
|
2440
|
+
field: 'workflow',
|
|
2441
|
+
value: linkResults.flow.sys_id
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
break;
|
|
2445
|
+
}
|
|
2446
|
+
case 'process_engine': {
|
|
2447
|
+
// Process Engine approach
|
|
2448
|
+
const engineData = {
|
|
2449
|
+
catalog_item: linkResults.catalog_item.sys_id,
|
|
2450
|
+
engine_type: 'flow',
|
|
2451
|
+
engine_id: linkResults.flow.sys_id,
|
|
2452
|
+
active: true,
|
|
2453
|
+
order: 100,
|
|
2454
|
+
condition: trigger_condition
|
|
2455
|
+
};
|
|
2456
|
+
const engineResult = await this.client.createRecord('sc_process_flow', engineData);
|
|
2457
|
+
if (engineResult.success && engineResult.data?.result) {
|
|
2458
|
+
linkResults.link_created = true;
|
|
2459
|
+
linkResults.link_details = {
|
|
2460
|
+
type: 'process_engine',
|
|
2461
|
+
sys_id: engineResult.data.result.sys_id,
|
|
2462
|
+
table: 'sc_process_flow'
|
|
2463
|
+
};
|
|
2464
|
+
}
|
|
2465
|
+
break;
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
// Step 4: Test the link if requested
|
|
2469
|
+
if (test_link && linkResults.link_created) {
|
|
2470
|
+
logger_js_1.logger.info('Testing catalog-flow link with sample request');
|
|
2471
|
+
const testRequestData = {
|
|
2472
|
+
cat_item: linkResults.catalog_item.sys_id,
|
|
2473
|
+
requested_for: 'admin', // Default to admin user
|
|
2474
|
+
quantity: 1,
|
|
2475
|
+
comments: `Test request for catalog-flow link verification [${new Date().toISOString()}]`
|
|
2476
|
+
};
|
|
2477
|
+
// Add any mapped variables with test values
|
|
2478
|
+
for (const mapping of linkResults.variable_mappings) {
|
|
2479
|
+
testRequestData[`variables.${mapping.catalog_variable}`] = 'Test Value';
|
|
2480
|
+
}
|
|
2481
|
+
const testResult = await this.client.createRecord('sc_request', testRequestData);
|
|
2482
|
+
if (testResult.success && testResult.data?.result) {
|
|
2483
|
+
linkResults.test_results = {
|
|
2484
|
+
request_created: true,
|
|
2485
|
+
request_number: testResult.data.result.number,
|
|
2486
|
+
request_sys_id: testResult.data.result.sys_id,
|
|
2487
|
+
status: 'Check the request in ServiceNow to verify flow execution'
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
else {
|
|
2491
|
+
linkResults.test_results = {
|
|
2492
|
+
request_created: false,
|
|
2493
|
+
error: 'Failed to create test request'
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
// Format results
|
|
2498
|
+
let resultText = `🔗 Catalog-Flow Link Results\n${'='.repeat(50)}\n\n`;
|
|
2499
|
+
resultText += `**Catalog Item:** ${linkResults.catalog_item.name} (${linkResults.catalog_item.sys_id})\n`;
|
|
2500
|
+
resultText += `**Flow:** ${linkResults.flow.name} (${linkResults.flow.sys_id})\n`;
|
|
2501
|
+
resultText += `**Link Type:** ${link_type}\n\n`;
|
|
2502
|
+
if (linkResults.link_created) {
|
|
2503
|
+
resultText += `✅ **Link Created Successfully!**\n`;
|
|
2504
|
+
resultText += ` Type: ${linkResults.link_details.type}\n`;
|
|
2505
|
+
if (linkResults.link_details.sys_id) {
|
|
2506
|
+
resultText += ` Record: ${linkResults.link_details.table} (${linkResults.link_details.sys_id})\n`;
|
|
2507
|
+
}
|
|
2508
|
+
if (linkResults.variable_mappings.length > 0) {
|
|
2509
|
+
resultText += `\n📋 **Variable Mappings Created:**\n`;
|
|
2510
|
+
linkResults.variable_mappings.forEach(mapping => {
|
|
2511
|
+
resultText += ` - ${mapping.catalog_variable} → ${mapping.flow_input}\n`;
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
if (linkResults.test_results) {
|
|
2515
|
+
resultText += `\n🧪 **Test Results:**\n`;
|
|
2516
|
+
if (linkResults.test_results.request_created) {
|
|
2517
|
+
resultText += ` ✅ Test request created: ${linkResults.test_results.request_number}\n`;
|
|
2518
|
+
resultText += ` 📍 ${linkResults.test_results.status}\n`;
|
|
2519
|
+
}
|
|
2520
|
+
else {
|
|
2521
|
+
resultText += ` ❌ ${linkResults.test_results.error}\n`;
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
resultText += `\n💡 **Next Steps:**\n`;
|
|
2525
|
+
resultText += `1. Test the catalog item by creating a request\n`;
|
|
2526
|
+
resultText += `2. Verify the flow executes with correct inputs\n`;
|
|
2527
|
+
resultText += `3. Monitor the request fulfillment process\n`;
|
|
2528
|
+
if (link_type === 'flow_catalog_process') {
|
|
2529
|
+
resultText += `\n📝 **Note:** Using modern Flow Designer catalog process\n`;
|
|
2530
|
+
resultText += ` - Flow will trigger on: ${trigger_condition}\n`;
|
|
2531
|
+
resultText += ` - Execution context: ${execution_options.run_as || 'system'}\n`;
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
else {
|
|
2535
|
+
resultText += `❌ **Failed to create link**\n`;
|
|
2536
|
+
if (linkResults.errors.length > 0) {
|
|
2537
|
+
resultText += `\nErrors:\n`;
|
|
2538
|
+
linkResults.errors.forEach(error => {
|
|
2539
|
+
resultText += ` - ${error}\n`;
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
return {
|
|
2544
|
+
content: [{
|
|
2545
|
+
type: 'text',
|
|
2546
|
+
text: resultText
|
|
2547
|
+
}]
|
|
2548
|
+
};
|
|
2549
|
+
}
|
|
2550
|
+
catch (error) {
|
|
2551
|
+
logger_js_1.logger.error('Error linking catalog to flow:', error);
|
|
2552
|
+
linkResults.errors.push(error.message);
|
|
2553
|
+
return {
|
|
2554
|
+
content: [{
|
|
2555
|
+
type: 'text',
|
|
2556
|
+
text: `❌ Failed to link catalog to flow\n\nError: ${error.message}\n\n` +
|
|
2557
|
+
`💡 Troubleshooting:\n` +
|
|
2558
|
+
`1. Verify catalog item exists: ${catalog_item_id}\n` +
|
|
2559
|
+
`2. Verify flow exists: ${flow_id}\n` +
|
|
2560
|
+
`3. Check permissions for creating ${link_type} links\n` +
|
|
2561
|
+
`4. Ensure flow is active and published\n\n` +
|
|
2562
|
+
`Results: ${JSON.stringify(linkResults, null, 2)}`
|
|
2563
|
+
}]
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
async handleCleanupTestArtifacts(args) {
|
|
2568
|
+
const { artifact_types = ['catalog_items', 'flows', 'users'], test_patterns = ['Test%', 'Mock%', 'Demo%', '%_test_%', '%test', '%mock'], max_age_hours = 1, dry_run = false, preserve_update_set_entries = true, update_set_filter } = args;
|
|
2569
|
+
logger_js_1.logger.info('Starting test artifact cleanup', {
|
|
2570
|
+
artifact_types,
|
|
2571
|
+
test_patterns,
|
|
2572
|
+
max_age_hours,
|
|
2573
|
+
dry_run
|
|
2574
|
+
});
|
|
2575
|
+
const cleanupResults = {
|
|
2576
|
+
start_time: new Date().toISOString(),
|
|
2577
|
+
dry_run,
|
|
2578
|
+
artifacts_found: {
|
|
2579
|
+
catalog_items: [],
|
|
2580
|
+
flows: [],
|
|
2581
|
+
users: [],
|
|
2582
|
+
requests: []
|
|
2583
|
+
},
|
|
2584
|
+
artifacts_deleted: {
|
|
2585
|
+
catalog_items: 0,
|
|
2586
|
+
flows: 0,
|
|
2587
|
+
users: 0,
|
|
2588
|
+
requests: 0
|
|
2589
|
+
},
|
|
2590
|
+
update_set_entries_preserved: 0,
|
|
2591
|
+
errors: [],
|
|
2592
|
+
summary: ''
|
|
2593
|
+
};
|
|
2594
|
+
try {
|
|
2595
|
+
// Calculate cutoff time
|
|
2596
|
+
const cutoffTime = new Date();
|
|
2597
|
+
cutoffTime.setHours(cutoffTime.getHours() - max_age_hours);
|
|
2598
|
+
const cutoffISO = cutoffTime.toISOString();
|
|
2599
|
+
// Process each artifact type
|
|
2600
|
+
for (const artifactType of artifact_types) {
|
|
2601
|
+
if (artifactType === 'all' || artifact_types.includes('catalog_items')) {
|
|
2602
|
+
await this.cleanupTestCatalogItems(test_patterns, cutoffISO, dry_run, cleanupResults, update_set_filter);
|
|
2603
|
+
}
|
|
2604
|
+
if (artifactType === 'all' || artifact_types.includes('flows')) {
|
|
2605
|
+
await this.cleanupTestFlows(test_patterns, cutoffISO, dry_run, cleanupResults, update_set_filter);
|
|
2606
|
+
}
|
|
2607
|
+
if (artifactType === 'all' || artifact_types.includes('users')) {
|
|
2608
|
+
await this.cleanupTestUsers(test_patterns, cutoffISO, dry_run, cleanupResults, update_set_filter);
|
|
2609
|
+
}
|
|
2610
|
+
if (artifactType === 'all' || artifact_types.includes('requests')) {
|
|
2611
|
+
await this.cleanupTestRequests(test_patterns, cutoffISO, dry_run, cleanupResults, update_set_filter);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
// Generate summary
|
|
2615
|
+
const totalFound = Object.values(cleanupResults.artifacts_found)
|
|
2616
|
+
.reduce((sum, items) => sum + (Array.isArray(items) ? items.length : 0), 0);
|
|
2617
|
+
const totalDeleted = Object.values(cleanupResults.artifacts_deleted)
|
|
2618
|
+
.reduce((sum, count) => sum + count, 0);
|
|
2619
|
+
cleanupResults.summary = dry_run
|
|
2620
|
+
? `🔍 Dry Run: Found ${totalFound} test artifacts that would be cleaned up`
|
|
2621
|
+
: `🧹 Cleaned up ${totalDeleted} test artifacts successfully`;
|
|
2622
|
+
// Format results
|
|
2623
|
+
let resultText = `🧹 Test Artifact Cleanup Results\n${'='.repeat(50)}\n\n`;
|
|
2624
|
+
if (dry_run) {
|
|
2625
|
+
resultText += `🔍 **DRY RUN MODE** - No actual deletion performed\n\n`;
|
|
2626
|
+
}
|
|
2627
|
+
resultText += `⏰ **Cleanup Configuration:**\n`;
|
|
2628
|
+
resultText += `- Artifact Types: ${artifact_types.join(', ')}\n`;
|
|
2629
|
+
resultText += `- Test Patterns: ${test_patterns.join(', ')}\n`;
|
|
2630
|
+
resultText += `- Max Age: ${max_age_hours} hours\n`;
|
|
2631
|
+
resultText += `- Cutoff Time: ${cutoffISO}\n`;
|
|
2632
|
+
resultText += `- Preserve Update Set Entries: ${preserve_update_set_entries ? '✅' : '❌'}\n\n`;
|
|
2633
|
+
// Report findings by type
|
|
2634
|
+
['catalog_items', 'flows', 'users', 'requests'].forEach(type => {
|
|
2635
|
+
const found = cleanupResults.artifacts_found[type] || [];
|
|
2636
|
+
const deleted = cleanupResults.artifacts_deleted[type] || 0;
|
|
2637
|
+
if (found.length > 0) {
|
|
2638
|
+
resultText += `📦 **${type.replace('_', ' ').toUpperCase()}:**\n`;
|
|
2639
|
+
resultText += ` ${dry_run ? 'Found' : 'Deleted'}: ${dry_run ? found.length : deleted}\n`;
|
|
2640
|
+
if (dry_run && found.length > 0) {
|
|
2641
|
+
found.slice(0, 10).forEach((item) => {
|
|
2642
|
+
resultText += ` - ${item.name} (${item.sys_id}) - Created: ${item.sys_created_on}\n`;
|
|
2643
|
+
});
|
|
2644
|
+
if (found.length > 10) {
|
|
2645
|
+
resultText += ` ... and ${found.length - 10} more\n`;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
resultText += '\n';
|
|
2649
|
+
}
|
|
2650
|
+
});
|
|
2651
|
+
if (cleanupResults.update_set_entries_preserved > 0) {
|
|
2652
|
+
resultText += `📋 **Update Set Audit Trail Preserved:** ${cleanupResults.update_set_entries_preserved} entries\n\n`;
|
|
2653
|
+
}
|
|
2654
|
+
if (cleanupResults.errors.length > 0) {
|
|
2655
|
+
resultText += `❌ **Errors:**\n`;
|
|
2656
|
+
cleanupResults.errors.forEach(error => {
|
|
2657
|
+
resultText += ` - ${error}\n`;
|
|
2658
|
+
});
|
|
2659
|
+
resultText += '\n';
|
|
2660
|
+
}
|
|
2661
|
+
resultText += `✅ **${cleanupResults.summary}**\n\n`;
|
|
2662
|
+
if (!dry_run && totalDeleted > 0) {
|
|
2663
|
+
resultText += `💡 **Note:** Update Set entries have been preserved as audit trail.\n`;
|
|
2664
|
+
resultText += `This shows that testing was performed and cleanup was completed.\n\n`;
|
|
2665
|
+
}
|
|
2666
|
+
if (dry_run) {
|
|
2667
|
+
resultText += `▶️ **Next Steps:**\n`;
|
|
2668
|
+
resultText += `1. Review the artifacts that would be deleted\n`;
|
|
2669
|
+
resultText += `2. Run again with dry_run: false to perform actual cleanup\n`;
|
|
2670
|
+
resultText += `3. Verify Update Set entries are preserved as intended\n`;
|
|
2671
|
+
}
|
|
2672
|
+
return {
|
|
2673
|
+
content: [{
|
|
2674
|
+
type: 'text',
|
|
2675
|
+
text: resultText
|
|
2676
|
+
}]
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2679
|
+
catch (error) {
|
|
2680
|
+
logger_js_1.logger.error('Error during test artifact cleanup:', error);
|
|
2681
|
+
return {
|
|
2682
|
+
content: [{
|
|
2683
|
+
type: 'text',
|
|
2684
|
+
text: `❌ Test artifact cleanup failed\n\nError: ${error.message}\n\n` +
|
|
2685
|
+
`🔧 Troubleshooting:\n` +
|
|
2686
|
+
`1. Check ServiceNow connection and permissions\n` +
|
|
2687
|
+
`2. Verify test patterns are correct\n` +
|
|
2688
|
+
`3. Ensure artifacts exist and are accessible\n\n` +
|
|
2689
|
+
`Debug Info: ${JSON.stringify(cleanupResults, null, 2)}`
|
|
2690
|
+
}]
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
async cleanupTestCatalogItems(patterns, cutoffTime, dryRun, results, updateSetFilter) {
|
|
2695
|
+
try {
|
|
2696
|
+
// Build query for test catalog items
|
|
2697
|
+
const patternQueries = patterns.map(pattern => {
|
|
2698
|
+
if (pattern.includes('%')) {
|
|
2699
|
+
return `nameLIKE${pattern.replace(/%/g, '')}`;
|
|
2700
|
+
}
|
|
2701
|
+
else {
|
|
2702
|
+
return `name=${pattern}`;
|
|
2703
|
+
}
|
|
2704
|
+
});
|
|
2705
|
+
const query = `(${patternQueries.join('^OR')})^sys_created_on<${cutoffTime}`;
|
|
2706
|
+
const searchResult = await this.client.searchRecords('sc_cat_item', query, 100);
|
|
2707
|
+
if (searchResult.success && searchResult.data?.result) {
|
|
2708
|
+
results.artifacts_found.catalog_items = searchResult.data.result.map((item) => ({
|
|
2709
|
+
sys_id: item.sys_id,
|
|
2710
|
+
name: item.name,
|
|
2711
|
+
sys_created_on: item.sys_created_on
|
|
2712
|
+
}));
|
|
2713
|
+
if (!dryRun) {
|
|
2714
|
+
// Delete each catalog item
|
|
2715
|
+
for (const item of searchResult.data.result) {
|
|
2716
|
+
const deleteResult = await this.client.deleteRecord('sc_cat_item', item.sys_id);
|
|
2717
|
+
if (deleteResult.success) {
|
|
2718
|
+
results.artifacts_deleted.catalog_items++;
|
|
2719
|
+
}
|
|
2720
|
+
else {
|
|
2721
|
+
results.errors.push(`Failed to delete catalog item ${item.name}: ${deleteResult.error}`);
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
catch (error) {
|
|
2728
|
+
results.errors.push(`Error cleaning catalog items: ${error.message}`);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
async cleanupTestFlows(patterns, cutoffTime, dryRun, results, updateSetFilter) {
|
|
2732
|
+
try {
|
|
2733
|
+
const patternQueries = patterns.map(pattern => {
|
|
2734
|
+
if (pattern.includes('%')) {
|
|
2735
|
+
return `nameLIKE${pattern.replace(/%/g, '')}`;
|
|
2736
|
+
}
|
|
2737
|
+
else {
|
|
2738
|
+
return `name=${pattern}`;
|
|
2739
|
+
}
|
|
2740
|
+
});
|
|
2741
|
+
const query = `(${patternQueries.join('^OR')})^sys_created_on<${cutoffTime}`;
|
|
2742
|
+
const searchResult = await this.client.searchRecords('sys_hub_flow', query, 100);
|
|
2743
|
+
if (searchResult.success && searchResult.data?.result) {
|
|
2744
|
+
results.artifacts_found.flows = searchResult.data.result.map((flow) => ({
|
|
2745
|
+
sys_id: flow.sys_id,
|
|
2746
|
+
name: flow.name,
|
|
2747
|
+
sys_created_on: flow.sys_created_on
|
|
2748
|
+
}));
|
|
2749
|
+
if (!dryRun) {
|
|
2750
|
+
for (const flow of searchResult.data.result) {
|
|
2751
|
+
const deleteResult = await this.client.deleteRecord('sys_hub_flow', flow.sys_id);
|
|
2752
|
+
if (deleteResult.success) {
|
|
2753
|
+
results.artifacts_deleted.flows++;
|
|
2754
|
+
}
|
|
2755
|
+
else {
|
|
2756
|
+
results.errors.push(`Failed to delete flow ${flow.name}: ${deleteResult.error}`);
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
catch (error) {
|
|
2763
|
+
results.errors.push(`Error cleaning flows: ${error.message}`);
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
async cleanupTestUsers(patterns, cutoffTime, dryRun, results, updateSetFilter) {
|
|
2767
|
+
try {
|
|
2768
|
+
const patternQueries = patterns.map(pattern => {
|
|
2769
|
+
if (pattern.includes('%')) {
|
|
2770
|
+
return `user_nameLIKE${pattern.replace(/%/g, '')}^ORfirst_nameLIKE${pattern.replace(/%/g, '')}`;
|
|
2771
|
+
}
|
|
2772
|
+
else {
|
|
2773
|
+
return `user_name=${pattern}^ORfirst_name=${pattern}`;
|
|
2774
|
+
}
|
|
2775
|
+
});
|
|
2776
|
+
const query = `(${patternQueries.join('^OR')})^sys_created_on<${cutoffTime}^active=false`;
|
|
2777
|
+
const searchResult = await this.client.searchRecords('sys_user', query, 100);
|
|
2778
|
+
if (searchResult.success && searchResult.data?.result) {
|
|
2779
|
+
results.artifacts_found.users = searchResult.data.result.map((user) => ({
|
|
2780
|
+
sys_id: user.sys_id,
|
|
2781
|
+
name: user.user_name || user.name,
|
|
2782
|
+
sys_created_on: user.sys_created_on
|
|
2783
|
+
}));
|
|
2784
|
+
if (!dryRun) {
|
|
2785
|
+
for (const user of searchResult.data.result) {
|
|
2786
|
+
const deleteResult = await this.client.deleteRecord('sys_user', user.sys_id);
|
|
2787
|
+
if (deleteResult.success) {
|
|
2788
|
+
results.artifacts_deleted.users++;
|
|
2789
|
+
}
|
|
2790
|
+
else {
|
|
2791
|
+
results.errors.push(`Failed to delete user ${user.user_name}: ${deleteResult.error}`);
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
catch (error) {
|
|
2798
|
+
results.errors.push(`Error cleaning users: ${error.message}`);
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
async cleanupTestRequests(patterns, cutoffTime, dryRun, results, updateSetFilter) {
|
|
2802
|
+
try {
|
|
2803
|
+
const patternQueries = patterns.map(pattern => {
|
|
2804
|
+
if (pattern.includes('%')) {
|
|
2805
|
+
return `short_descriptionLIKE${pattern.replace(/%/g, '')}`;
|
|
2806
|
+
}
|
|
2807
|
+
else {
|
|
2808
|
+
return `short_description=${pattern}`;
|
|
2809
|
+
}
|
|
2810
|
+
});
|
|
2811
|
+
const query = `(${patternQueries.join('^OR')})^sys_created_on<${cutoffTime}`;
|
|
2812
|
+
const searchResult = await this.client.searchRecords('sc_request', query, 100);
|
|
2813
|
+
if (searchResult.success && searchResult.data?.result) {
|
|
2814
|
+
results.artifacts_found.requests = searchResult.data.result.map((req) => ({
|
|
2815
|
+
sys_id: req.sys_id,
|
|
2816
|
+
name: req.number || req.short_description,
|
|
2817
|
+
sys_created_on: req.sys_created_on
|
|
2818
|
+
}));
|
|
2819
|
+
if (!dryRun) {
|
|
2820
|
+
for (const request of searchResult.data.result) {
|
|
2821
|
+
// Cancel request instead of deleting (safer)
|
|
2822
|
+
const updateResult = await this.client.updateRecord('sc_request', request.sys_id, {
|
|
2823
|
+
state: '4', // Cancelled
|
|
2824
|
+
comments: 'Cancelled by test cleanup automation'
|
|
2825
|
+
});
|
|
2826
|
+
if (updateResult.success) {
|
|
2827
|
+
results.artifacts_deleted.requests++;
|
|
2828
|
+
}
|
|
2829
|
+
else {
|
|
2830
|
+
results.errors.push(`Failed to cancel request ${request.number}: ${updateResult.error}`);
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
catch (error) {
|
|
2837
|
+
results.errors.push(`Error cleaning requests: ${error.message}`);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
1600
2840
|
getServiceNowUrl() {
|
|
1601
2841
|
// This would need to get the instance URL from credentials
|
|
1602
2842
|
return 'https://instance.service-now.com';
|