UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

300 lines (299 loc) 13.9 kB
/** * 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; } } }