@apistudio/apim-cli
Version:
CLI for API Management Products
922 lines (837 loc) • 29.4 kB
text/typescript
import { getDefaultVersions } from "../resources/smith-defaultVersion.js";
import { getMasterContent } from "../resources/smith-master.js";
import { getCombinedRuleset } from "../resources/smith-ruleset.js";
import { getCombinedSource } from "../resources/smith-schemas-json.js";
import { IRuntimeInventory, MasterContent, PolicyInfo, AssetInfo, StagedPolicyGroup } from "./interfaces/IRuntimeInventory.js";
const POLICY_SEQUENCES = "policy-sequences";
const STAGED = "staged";
const FREE_FLOW = "free-flow";
const POLICIES = "policies";
const DEFAULT_VERSION_KEY = "defaultVersion";
export enum CommonKind {
Api = "api",
Plan = "plan",
Product = "product",
TestApi = "testapi",
Tuple = "tuple",
UriSchemes = "urischemes"
}
const kindEnums = [
"API",
"Scope",
"Project",
"StagedPolicySequence",
"InvokeAWSLambda",
"ValidateAPISpecification",
"CORS",
"Quota",
"Plan",
"Product",
"URISchemes",
"properties",
"Telemetry",
"Properties",
"LoadBalancer",
"SetAuthorization",
"Invoke",
"GlobalPolicy",
"InboundBulkHead",
"SetMediaType",
"InboundMessaging",
"IAM",
"AuthorizeUser",
"SetContextVariable",
"WebMethodsISService",
"Log",
"MonitorTraffic",
"CacheServiceResult",
"OutboundAlias",
"OutboundAnonymous",
"HTTPInvoke",
"InvokeMessagingExtension",
"DataMasking",
"TransformRequest",
"TransformResponse",
"Route",
"MessageConfig",
"HTTPEndpoint",
"MockEndpoint",
"MockResponse",
"ErrorProcessing",
"Set",
"RateLimitDef",
"RateLimit",
"Redact",
"Remove",
"Transform",
"DataPowerAssembly",
"Switch",
"If",
"OperationSwitch",
"Try",
"IBMCloudLogin",
"WatsonXAIInvoke",
"OpenAIInvoke",
"FreeFlowPolicySequence",
"Block",
"TokenMediation",
"EnforceCircuitBreaker",
"JavaScript",
"LuaScript",
"Cache",
"Antivirus",
"SQLInjectionFilter",
"CountLimit",
"CountLimitDef",
"Return",
"Retry",
"Throw",
"HandlebarsTemplate",
"ExtractIdentity",
"Authorize",
"Authenticate",
"Parse",
"test",
"assertion",
"environment"
];
const generic_rules = {
"rules": {
"invalid-kind-value-combined": {
"description": `Kind must be one of ${kindEnums.map(k => `'${k}'`).join(' | ')}`,
"severity": "error",
"given": "$",
"then": {
"field": "kind",
"function": "schema",
"functionOptions": {
"schema": {
"type": "string",
"enum": kindEnums
}
}
}
},
"kind-not-exist": {
"description": `Kind does not exist.`,
"severity": "error",
"given": "$",
"resolved": false,
"then": {
"field": "kind",
"function": "truthy"
}
},
"invalid-kind-spl-character": {
"description": "kind should not be having empty or special characters",
"severity": "error",
"given": "$",
"resolved": false,
"then": {
"field": "kind",
"function": "pattern",
"functionOptions": {
"match": "^(?![\\s\\W_]+$).+$"
}
}
},
"invalid-api-version": {
"description": "apiVersion must be one of the valid values 'api.ibm.com/v1'",
"severity": "error",
"given": "$",
"resolved": false,
"then": {
"field": "apiVersion",
"function": "schema",
"functionOptions": {
"schema": {
"type": "string",
"enum": [
"api.ibm.com/v1"
]
}
}
}
},
"api-version-not-exist": {
"description": "apiVersion does not exist.",
"severity": "error",
"given": "$",
"resolved": false,
"then": {
"field": "apiVersion",
"function": "truthy"
}
},
"metadata-not-exist": {
"description": "Metadata does not exist.",
"severity": "error",
"given": "$",
"resolved": false,
"then": {
"field": "metadata",
"function": "truthy"
}
},
"metadata-whitelist-check": {
"description": "Metadata should not be having empty or special characters",
"severity": "error",
"given": "$",
"resolved": false,
"then": {
"field": "metadata",
"function": "pattern",
"functionOptions": {
"match": "^(?![\\s\\W_]+$).+$"
}
}
},
"metadata-name-not-exist": {
"description": "Metadata name does not exist",
"severity": "error",
"given": "$.metadata",
"resolved": false,
"then": {
"field": "name",
"function": "truthy"
}
},
"metadata-name-whitelist-check": {
"description": "Metadata name should not be having empty or special characters",
"severity": "error",
"given": "$.metadata",
"resolved": false,
"then": {
"field": "name",
"function": "pattern",
"functionOptions": {
"match": "^(?![\\s\\W_]+$).+$"
}
}
},
"metadata-version-not-exist": {
"description": "Metadata version does not exist",
"severity": "error",
"given": "$.metadata",
"resolved": false,
"then": {
"field": "version",
"function": "truthy"
}
},
"metadata-version-whitelist-check": {
"description": "Metadata version should not be having empty or special characters",
"severity": "error",
"given": "$.metadata",
"resolved": false,
"then": {
"field": "version",
"function": "pattern",
"functionOptions": {
"match": "^(?![\\s\\W_]+$).+$"
}
}
},
"metadata-namespace-not-exist": {
"description": "Metadata namespace does not exist",
"severity": "warn",
"given": "$.metadata",
"resolved": false,
"then": {
"field": "namespace",
"function": "truthy"
}
},
"metadata-namespace-whitelist-check": {
"description": "Metadata namespace should not be having empty or special characters",
"severity": "warn",
"given": "$.metadata",
"resolved": false,
"then": {
"field": "namespace",
"function": "pattern",
"functionOptions": {
"match": "^(?![\\s\\W_]+$).+$"
}
}
},
"spec-details-not-exist": {
"description": "Spec details does not exist",
"severity": "error",
"given": "$",
"resolved": false,
"then": {
"field": "spec",
"function": "truthy"
}
},
"spec-details-whitelist-check": {
"description": "Spec should not be having empty or special characters",
"severity": "error",
"given": "$",
"resolved": false,
"then": {
"field": "spec",
"function": "pattern",
"functionOptions": {
"match": "^(?![\\s\\W_]+$).+$"
}
}
},
"tags-not-exist": {
"description": "Tag does not exist",
"severity": "warn",
"given": "$.metadata",
"resolved": false,
"then": {
"field": "tags",
"function": "truthy"
}
},
"invalid-tag-type": {
"description": "Invalid Tag Type",
"severity": "error",
"given": "$.metadata.tags",
"resolved": false,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"type": "array"
}
}
}
}
}
};
export type CommonKindType = `${CommonKind}`;
export class RuntimeInventory implements IRuntimeInventory {
public masterContent: MasterContent;
public schemaDefinitions: Record<string, any>;
public defaultVersionMap: Record<string, string>;
private rulesetDefinitions: Record<string, any>;
constructor() {
try {
// Initialize with the imported data
this.schemaDefinitions = getCombinedSource();
this.rulesetDefinitions = getCombinedRuleset();
this.defaultVersionMap = getDefaultVersions();
this.masterContent = getMasterContent();
// Log successful initialization
} catch (error) {
this.schemaDefinitions = {};
this.rulesetDefinitions = {};
this.defaultVersionMap = {};
this.masterContent = {};
}
}
/**
* Overrides the master content with new data
* @param newMasterContent - New master content to replace the existing data
*/
public setMasterContent(newMasterContent: MasterContent): void {
if (!newMasterContent) return;
this.masterContent = newMasterContent;
}
/**
* Extends or overrides the master content with custom data
* @param customMasterContent - Custom master content to merge with existing data
* @param overrideExisting - If true, will override existing entries; if false, will only add new entries
*/
public extendMasterContent(customMasterContent: Partial<MasterContent>, overrideExisting: boolean = false): void {
if (!customMasterContent) return;
// For complete replacement, use setMasterContent instead
if (overrideExisting && Object.keys(customMasterContent).length > 0) {
this.setMasterContent(customMasterContent as MasterContent);
return;
}
// Handle policy sequences
if (customMasterContent['policy-sequences']) {
if (!this.masterContent['policy-sequences']) {
this.masterContent['policy-sequences'] = {};
}
// Handle staged policies
if (customMasterContent['policy-sequences']?.staged) {
if (!this.masterContent['policy-sequences']?.staged) {
if (!this.masterContent['policy-sequences']) {
this.masterContent['policy-sequences'] = {};
}
this.masterContent['policy-sequences'].staged = [];
}
const stagedPolicies = this.masterContent['policy-sequences'].staged as StagedPolicyGroup[];
// For each custom staged policy group
customMasterContent['policy-sequences'].staged.forEach(customGroup => {
if (!customGroup.key) return;
// Find if this group already exists
const existingGroupIndex = stagedPolicies.findIndex(
group => group.key === customGroup.key
);
if (existingGroupIndex >= 0 && overrideExisting) {
// Replace existing group
stagedPolicies[existingGroupIndex] = customGroup;
} else if (existingGroupIndex < 0) {
// Add new group
stagedPolicies.push(customGroup);
} else if (existingGroupIndex >= 0 && !overrideExisting && customGroup.assets) {
// Merge assets into existing group
const existingGroup = stagedPolicies[existingGroupIndex];
if (!existingGroup.assets) existingGroup.assets = [];
customGroup.assets.forEach(asset => {
const existingAssetIndex = existingGroup.assets!.findIndex(a => a.kind === asset.kind);
if (existingAssetIndex >= 0 && overrideExisting) {
existingGroup.assets![existingAssetIndex] = asset;
} else if (existingAssetIndex < 0) {
existingGroup.assets!.push(asset);
}
});
}
});
}
// Handle free-flow policies
if (customMasterContent['policy-sequences']?.['free-flow']) {
if (!this.masterContent['policy-sequences']?.['free-flow']) {
if (!this.masterContent['policy-sequences']) {
this.masterContent['policy-sequences'] = {};
}
this.masterContent['policy-sequences']['free-flow'] = [];
}
const freeFlowPolicies = this.masterContent['policy-sequences']['free-flow'] as any[];
customMasterContent['policy-sequences']['free-flow'].forEach(customGroup => {
if (!customGroup.name) return;
const existingGroupIndex = freeFlowPolicies.findIndex(
group => group.name === customGroup.name
);
if (existingGroupIndex >= 0 && overrideExisting) {
freeFlowPolicies[existingGroupIndex] = customGroup;
} else if (existingGroupIndex < 0) {
freeFlowPolicies.push(customGroup);
}
});
}
}
// Handle other top-level properties
Object.entries(customMasterContent).forEach(([key, value]) => {
if (key !== 'policy-sequences') {
if (overrideExisting || !this.masterContent[key]) {
this.masterContent[key] = value;
}
}
});
}
/**
* Extends or overrides the schema definitions with custom schemas
* @param customSchemas - Custom schemas to merge with existing schemas
* @param overrideExisting - If true, will override existing schemas; if false, will only add new schemas
*/
public extendSchemaDefinitions(customSchemas: Record<string, any>, overrideExisting: boolean = false): void {
if (!customSchemas) return;
Object.entries(customSchemas).forEach(([key, value]) => {
if (overrideExisting || !this.schemaDefinitions[key]) {
this.schemaDefinitions[key] = value;
}
});
}
/**
* Extends or overrides the default versions with custom default versions
* @param customDefaultVersions - Custom default versions to merge with existing versions
* @param overrideExisting - If true, will override existing versions; if false, will only add new versions
*/
public extendDefaultVersions(customDefaultVersions: Record<string, string>, overrideExisting: boolean = false): void {
if (!customDefaultVersions) return;
Object.entries(customDefaultVersions).forEach(([key, value]) => {
if (overrideExisting || !this.defaultVersionMap[key]) {
this.defaultVersionMap[key] = value;
}
});
}
/**
* Extends or overrides the ruleset definitions with custom rulesets
* @param customRulesets - Custom rulesets to merge with existing rulesets
* @param overrideExisting - If true, will override existing rulesets; if false, will only add new rulesets
*/
public extendRulesetDefinitions(customRulesets: Record<string, any>, overrideExisting: boolean = false): void {
if (!customRulesets) return;
Object.entries(customRulesets).forEach(([key, value]) => {
if (overrideExisting || !this.rulesetDefinitions[key]) {
this.rulesetDefinitions[key] = value;
}
});
}
/**
* Hook method for subclasses to provide overridden schema for a specific schema key
* This is called automatically by getSchema() to check if there's an override
* Extended classes can override this to provide custom schemas
* @param schemaKey - The schema key (e.g., "api.ibm.com_v1_customkind.json")
* @returns The overridden schema object or undefined if no override
*/
public getOverriddenSchema(schemaKey: string): any | undefined {
// Base implementation returns undefined (no override)
// Subclasses can override to provide custom schemas
return undefined;
}
/**
* Hook method for subclasses to provide overridden ruleset for a specific ruleset key
* This is called automatically by getLintRuleset() to check if there's an override
* Extended classes can override this to provide custom rulesets
* @param rulesetKey - The ruleset key (e.g., "api.ibm.com_v1_customkind.ruleset.yaml")
* @returns The overridden ruleset object or undefined if no override
*/
public getOverriddenRule(rulesetKey: string): any | undefined {
// Base implementation returns undefined (no override)
// Subclasses can override to provide custom rulesets
return undefined;
}
public getSchema(
name: string,
version?: string
): any | undefined {
try {
const kindLower = name.toLowerCase();
if (!version) {
// Get default version from the defaultVersion.json
const matchedKey = Object.keys(this.defaultVersionMap).find(
(key) => key.toLowerCase() === name.toLowerCase()
);
if (matchedKey) {
version = this.defaultVersionMap[matchedKey];
} else {
}
if (!version) {
return undefined;
}
}
// Format the key to match the combined-source.json format
version = version.replace(/\//g, "_");
const schemaKey = `${version}_${kindLower}.json`;
if(this.getOverriddenSchema(schemaKey))
{
return this.getOverriddenSchema(schemaKey);
}
else if (this.schemaDefinitions[schemaKey]) {
return this.schemaDefinitions[schemaKey];
} else {
return undefined;
}
} catch (error) {
console.error(
`Error retrieving schema for ${name} version ${version}:`,
error
);
return undefined;
}
}
public getSchemaFromDestination(
name: string,
version?: string
): any | undefined {
try {
const kindLower = name.toLowerCase();
if (!version) {
// Get default version from the defaultVersion.json
const matchedKey = Object.keys(this.defaultVersionMap).find(
(key) => key.toLowerCase() === name.toLowerCase()
);
if (matchedKey) {
version = this.defaultVersionMap[matchedKey];
}
if (!version) {
return undefined;
}
}
// Format the key to match the combined-source.json format
version = version.replace(/\//g, "_");
const schemaKey = `${version}_${kindLower}.json`;
// For destination schemas, we would need to implement a similar approach
// Currently, destination schemas are not included in combined-source.json
// This is a placeholder for future implementation
return undefined;
} catch (error) {
console.error(
`Error retrieving schema for ${name} version ${version} in destination:`,
error
);
return undefined;
}
}
public getTypescript(name: string, version?: string): Record<string, any> | undefined {
try {
const kindLower = name.toLowerCase();
if (!version) {
// Get default version from the defaultVersion.json
const matchedKey = Object.keys(this.defaultVersionMap).find(
(key) => key.toLowerCase() === name.toLowerCase()
);
if (matchedKey) {
version = this.defaultVersionMap[matchedKey];
}
if (!version) {
return undefined;
}
}
// Format the key to match the combined-source.json format
version = version.replace(/\//g, "_");
const schemaKey = `${version}_${kindLower}.json`;
// TypeScript definitions are not included in combined-source.json
// This is a placeholder for future implementation
return undefined;
} catch (error) {
console.error(
`Error retrieving TypeScript for ${name} version ${version}:`,
error
);
return undefined;
}
}
public getLintRuleset(name: string, version?: string): Record<string, any> | undefined {
try {
const kindLower = name.toLowerCase();
if (!version) {
// Get default version from the defaultVersion.json
const matchedKey = Object.keys(this.defaultVersionMap).find(
(key) => key.toLowerCase() === name.toLowerCase()
);
if (matchedKey) {
version = this.defaultVersionMap[matchedKey];
}
if (!version) {
return undefined;
}
}
// Format the key to match the combined ruleset format
version = version.replace(/\//g, "_");
const rulesetKey = `${version}_${kindLower}.ruleset.yaml`;
if(this.getOverriddenRule(rulesetKey))
{
return this.getOverriddenRule(rulesetKey)
}
// Look for the ruleset in the ruleset definitions
else if (this.rulesetDefinitions[rulesetKey]) {
// Return the ruleset object directly
return this.rulesetDefinitions[rulesetKey];
} else {
return generic_rules;
}
} catch (error) {
console.error(
`Error retrieving lint ruleset for ${name} version ${version}:`,
error
);
return undefined;
}
}
public getPolicySequenceType(): { sequenceTypes: string[] } | undefined {
try {
const sequenceTypes: string[] = [];
const stagedPolicies = this.getStagedPolicies();
if (stagedPolicies && Object.keys(stagedPolicies).length > 0) {
sequenceTypes.push(STAGED);
}
const freeFlowPolicies = this.getFreeFlowPolicies();
if (freeFlowPolicies && Object.keys(freeFlowPolicies).length > 0) {
sequenceTypes.push(FREE_FLOW);
}
return sequenceTypes.length > 0 ? { sequenceTypes } : undefined;
} catch (error) {
console.error("Error getting policy sequence types:", error);
return undefined;
}
}
public getStagedPolicies():
| Record<string, { stage: string; policies: PolicyInfo[] }>
| undefined {
try {
const staged = this.masterContent?.[POLICY_SEQUENCES]?.[STAGED];
if (staged && Array.isArray(staged)) {
const result: Record<
string,
{ stage: string; policies: PolicyInfo[] }
> = {};
for (const stageGroup of staged) {
const stageName = stageGroup.key;
const assets = stageGroup.assets || [];
result[stageName] = {
stage: stageName,
policies: assets.map((asset: AssetInfo) => ({
name: asset.kind,
defaultVersion: asset.defautlVersion || "api.ibm.com/v1",
type: STAGED,
})),
};
}
return result;
}
return undefined;
} catch (error) {
console.error("Error getting staged policies:", error);
return undefined;
}
}
public getFreeFlowPolicies():
| Record<string, { group: string; type: string; policies: PolicyInfo[] }>
| undefined {
try {
const freeFlow = this.masterContent?.[POLICY_SEQUENCES]?.[FREE_FLOW];
if (!Array.isArray(freeFlow)) {
return undefined;
}
const result: Record<
string,
{ group: string; type: string; policies: PolicyInfo[] }
> = {};
freeFlow.forEach((group, index) => {
const groupName = group.name || `group_${index}`;
const flattenedPolicies = this.flattenPolicies(group[POLICIES] || []);
result[groupName] = {
group: groupName,
type: group.type,
policies: flattenedPolicies.map((policy: any) => ({
name: policy.name,
defaultVersion: policy[DEFAULT_VERSION_KEY] || "1.0.0",
type: FREE_FLOW,
})),
};
});
return result;
} catch (error) {
console.error("Error getting free flow policies:", error);
return undefined;
}
}
public getPolicyDefaultVersion(
sequenceType: typeof STAGED | typeof FREE_FLOW,
groupName: string,
policyName: string
): string | undefined {
try {
if (sequenceType === STAGED) {
const stagedGroups = this.masterContent[POLICY_SEQUENCES]?.[STAGED];
if (stagedGroups && Array.isArray(stagedGroups)) {
const group = stagedGroups.find(g => g.key === groupName);
if (group && group.assets) {
const asset = group.assets.find(a => a.kind === policyName);
if (asset) {
return asset.defautlVersion || "api.ibm.com/v1";
}
}
}
} else if (sequenceType === FREE_FLOW) {
const freeFlow = this.masterContent[POLICY_SEQUENCES]?.[FREE_FLOW];
if (Array.isArray(freeFlow)) {
for (const group of freeFlow) {
if (group.name === groupName && group[POLICIES]) {
for (const policy of this.flattenPolicies(group[POLICIES])) {
if (policy.name === policyName) {
return policy[DEFAULT_VERSION_KEY] || "1.0.0";
}
}
}
}
}
}
return undefined;
} catch (error) {
console.error(
`Error getting policy default version for ${sequenceType}/${groupName}/${policyName}:`,
error
);
return undefined;
}
}
public getMasterContents(): MasterContent {
// Create a copy of masterContent to ensure we don't modify the original
const masterContentCopy = { ...this.masterContent };
// Ensure assetProperties is included in the returned object
if (this.masterContent.assetProperties) {
masterContentCopy.assetProperties = this.masterContent.assetProperties;
}
return masterContentCopy;
}
/**
* Get the complete default versions mapping
* @returns A record mapping kind names to their default API versions
*/
public getDefaultVersions(): Record<string, string> {
// Return a copy of the default versions map to prevent direct modification
return { ...this.defaultVersionMap };
}
/**
* Get the list of required kinds
* @returns An array of required kind names
*/
public getRequiredKinds(): string[] {
// Return the requiredKinds array from masterContent, or an empty array if not defined
return this.masterContent.requiredKinds || [];
}
/**
* Get the list of optional kinds
* @returns An array of optional kind names
*/
public getOptionalKinds(): string[] {
// Return the optionalKinds array from masterContent, or an empty array if not defined
return this.masterContent.optionalKinds || [];
}
public getPolicyInfo(
sequenceType: typeof STAGED | typeof FREE_FLOW,
groupName: string,
policyName: string
) {
try {
if (sequenceType === STAGED) {
const stagedGroups = this.masterContent[POLICY_SEQUENCES]?.[STAGED];
if (stagedGroups && Array.isArray(stagedGroups)) {
const group = stagedGroups.find(g => g.key === groupName);
if (group && group.assets) {
const asset = group.assets.find(a => a.kind === policyName);
if (asset) {
return {
name: policyName,
sequenceType: STAGED,
group: groupName,
defaultVersion: asset.defautlVersion || "api.ibm.com/v1",
policy: asset,
};
}
}
}
} else if (sequenceType === FREE_FLOW) {
const freeFlow = this.masterContent[POLICY_SEQUENCES]?.[FREE_FLOW];
if (Array.isArray(freeFlow)) {
for (const group of freeFlow) {
if (group.name === groupName && group[POLICIES]) {
for (const policy of this.flattenPolicies(group[POLICIES])) {
if (policy.name === policyName) {
return {
name: policyName,
sequenceType: FREE_FLOW,
group: groupName,
defaultVersion: policy[DEFAULT_VERSION_KEY] || "1.0.0",
policy,
};
}
}
}
}
}
}
return undefined;
} catch (error) {
console.error(
`Error getting policy info for ${sequenceType}/${groupName}/${policyName}:`,
error
);
return undefined;
}
}
private flattenPolicies(policies: any[]): any[] {
let result: any[] = [];
for (const item of policies) {
if (item.type === "policy") {
result.push(item);
} else if (item.type === "group" && item[POLICIES]) {
result = result.concat(this.flattenPolicies(item[POLICIES]));
}
}
return result;
}
}
export const runtimeInventory = new RuntimeInventory();