@apistudio/apim-cli
Version:
CLI for API Management Products
300 lines (299 loc) • 13.9 kB
JavaScript
/**
* Copyright IBM Corp. 2024, 2025
*/
import { VCM } from '../engine/variable-context-manager/context-manager.js';
import { AssertionFactory } from './assertion.factory.js';
import { EnvironmentFactory } from './environment.factory.js';
import { GatewayFactory } from './gateway.factory.js';
import { TestFactory } from './test.factory.js';
export class ModelFactory {
constructor() {
this.tests = new Map();
this.environments = new Map();
this.assertions = new Map();
this.gateways = new Map();
this.testFactory = new TestFactory();
}
create(raw) {
raw?.forEach((ele) => {
if (!ele?.kind) {
this.gateways.set('gateway', new GatewayFactory().create(ele));
return;
}
const { kind, metadata: { name, namespace, version }, } = ele;
const key = this.buildModelKey(kind, namespace, name, version);
switch (kind) {
case 'test':
this.tests.set(key, this.testFactory.create(ele));
break;
case 'assertion':
this.assertions.set(key, new AssertionFactory().create(ele));
break;
case 'environment':
this.environments.set(key, new EnvironmentFactory().create(ele));
break;
default:
throw new Error(`Unsupported kind: ${ele.kind}`);
}
});
}
destroy() {
this.tests.forEach((test) => {
if (test.vcmId)
VCM.deleteContext(test.vcmId);
});
this.tests.clear();
this.environments.clear();
this.assertions.clear();
}
getTest(namespace, name, version, endpoint) {
return this.tests.get(this.buildModelKey('test', namespace, name, version, endpoint));
}
getAllTests() {
return Array.from(this.tests.values());
}
getAllTestsWithKey() {
return Array.from(this.tests.entries()).map(([key, test]) => ({
key,
test,
}));
}
getEnvironment(namespace, name, version) {
return this.environments.get(this.buildModelKey('environment', namespace, name, version));
}
getAllEnvironment() {
return Array.from(this.environments.values());
}
getAssertions(namespace, name, version) {
return this.assertions.get(this.buildModelKey('assertion', namespace, name, version));
}
getAllAssertions() {
return Array.from(this.assertions.values());
}
getGateway() {
return this.gateways.get('gateway');
}
buildModelKey(kind, namespace, name, version, id) {
return id
? `${kind}::${namespace}::${name}::${version}::${id}`
: `${kind}::${namespace}::${name}::${version}`;
}
resolveRefs() {
// For Test collection logic needs to be updated based on test created by resolve environments
let tests = this.getAllTestsWithKey();
tests.map(({ test }) => {
// Resolve assertions ref
this.resolveAssertions(test);
});
// For gateway collection logic needs to be updated based on test created by assertions
tests = this.getAllTestsWithKey();
tests.map(({ test, key }) => {
// Resolve api ref
this.resolveAPI(test, key);
});
// For Environment collection logic needs to be updated based on test created by gateways
tests = this.getAllTestsWithKey();
tests.map(({ test, key }) => {
// Resolve environment ref
this.resolveEnvironment(test, key);
});
}
// Resolve API reference with gateway endpoint if available.
// If ref is not available, function will be skipped.
resolveAPI(test, testKey) {
try {
if (test.spec?.api?.$ref) {
let refKeys = test.spec.api.$ref;
const gateway = this.getGateway();
if (!Array.isArray(refKeys)) {
refKeys = [refKeys];
}
refKeys.forEach((refKey) => {
const endpoints = gateway?.[refKey];
if (!endpoints) {
throw new Error(`Reference variable '${refKey}' not defined`);
}
delete test.spec.api.$ref;
const { kind, metadata } = test;
const { name, namespace, version } = metadata ?? {};
endpoints.forEach((endpoint) => {
test.spec.api.$endpoint = endpoint;
// Create a deep copy of the test to avoid modifying the original
const testCopy = JSON.parse(JSON.stringify(test));
const parsed = this.testFactory.create(testCopy);
const key = this.buildModelKey(kind, namespace, name, version, `${refKey}:${endpoint}`);
this.tests.set(key, parsed);
});
});
// Need to delete the test based on environment name too
this.tests.delete(testKey);
}
}
catch (error) {
throw error;
}
}
// Resolve API reference with environment if available.
// If ref is not available, function will be skipped.
resolveEnvironment(test, testKey) {
try {
// check $ref is array to create test collections
// for consistency make the string to array
if (test.spec.environment) {
let environments = [];
if (Array.isArray(test.spec.environment)) {
// Format 1: array of objects with $ref
const refArray = test.spec.environment;
environments = refArray.flatMap((refObj) => {
if (!refObj || !refObj.$ref)
return [];
const ref = refObj.$ref;
const [namespace, name, version] = ref.split(':');
const environment = this.getEnvironment(namespace, name, version);
if (!environment) {
throw new Error(`${ref} is not available in environment`);
}
if (!environment.metadata?.name ||
!environment.metadata?.version ||
!environment.metadata?.namespace) {
throw new Error(`Environment ${namespace}:${name}:${version} has incomplete metadata`);
}
return [environment];
});
}
else if (test.spec.environment.$ref) {
const ref = test.spec.environment.$ref;
const [namespace, name, version] = ref.split(':');
const environment = this.getEnvironment(namespace, name, version);
if (!environment) {
throw new Error(`${ref} is not available in environment`);
}
if (!environment.metadata?.name ||
!environment.metadata?.version ||
!environment.metadata?.namespace) {
throw new Error(`Environment ${namespace}:${name}:${version} has incomplete metadata`);
}
environments = [environment];
}
else {
// actual data is present instead of references to files
return;
}
const { kind: testKind, metadata } = test;
const { name: testName, namespace: testNamespace, version: testVersion, } = metadata ?? {};
environments.forEach((environment) => {
// Create a deep copy of the test to avoid modifying the original
const testCopy = JSON.parse(JSON.stringify(test));
const envSpecVariables = (environment.spec
?.variables ?? []);
// Build the new environment structure for the test copy
testCopy.spec.environment = {
variables: [
{
metadata: {
name: environment.metadata.name,
version: environment.metadata.version,
namespace: environment.metadata.namespace,
},
kind: 'environment',
spec: {
variables: envSpecVariables,
},
},
],
};
// New test model. Generates new vcmId.
const parsedTest = this.testFactory.create(testCopy);
// TestFactory.create() only loads variables from testCopy.spec.environment.
VCM.loadEnv(parsedTest.vcmId, envSpecVariables);
const key = this.buildModelKey(testKind, testNamespace, testName, testVersion, `${testKey}${environment.metadata.namespace}:${environment.metadata.name}:${environment.metadata.version}`);
this.tests.set(key, parsedTest);
});
// Based on environment key, new test got created.
// So test with default settings are removed from registry.
this.tests.delete(testKey);
}
}
catch (error) {
throw error;
}
}
// Resolve API reference with assertions if available.
// If ref is not available, function will be skipped.
resolveAssertions(test) {
try {
const requests = test.spec.request;
requests.forEach((request) => {
if (request.assertions) {
let assertions = [];
// Handle both formats of assertions
if (Array.isArray(request.assertions)) {
// Format 1: array of objects with $ref
const refArray = request.assertions;
assertions = refArray.flatMap((refObj) => {
if (!refObj || !refObj.$ref)
return [];
const ref = refObj.$ref;
const [namespace, name, version] = ref.split(':');
const assertionModel = this.getAssertions(namespace, name, version);
if (!assertionModel) {
throw new Error(`${ref} is not available in assertions`);
}
return [assertionModel];
});
}
else if (request.assertions.$ref) {
// Format 2: single assertion with direct $ref property
const ref = request.assertions.$ref;
const [namespace, name, version] = ref.split(':');
const assertionModel = this.getAssertions(namespace, name, version);
if (!assertionModel) {
throw new Error(`${ref} is not available in assertions`);
}
assertions = [assertionModel];
}
else if (request.assertions.assertions) {
// Format with expressions already defined
assertions = request.assertions.assertions;
}
// Due to APIC model definition we are forced to have below
request.assertions = {
//Looping over each items in assertion list
assertions: assertions.map((data) => {
//Checking for required fields
if (!data.metadata || !data.spec) {
throw new Error('Assertion data is missing required fields');
}
//Creating new object from data with metadata, kind and apiVersion
return {
metadata: {
name: data.metadata.name,
version: data.metadata.version,
namespace: data.metadata.namespace,
},
kind: 'assertion',
apiVersion: data.apiVersion,
spec: data.spec.map((a) => {
const extended = a;
return {
name: a.name ?? '',
key: a.key ?? '',
action: a.action ?? '',
value: a.value,
...(extended.stopOnFail ? { stopOnFail: true } : {}),
...(extended.if !== undefined ? { if: extended.if } : {}),
};
}),
};
}),
};
}
});
// Create a new test instance to validate the schema
this.testFactory.create(test);
}
catch (error) {
throw error;
}
}
}