11/*
2- * Copyright (c) 2021 , salesforce.com, inc.
2+ * Copyright (c) 2025 , salesforce.com, inc.
33 * All rights reserved.
44 * SPDX-License-Identifier: BSD-3-Clause
55 * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
@@ -30,6 +30,8 @@ const ANYPOINT_API_URI_V2 = `${ANYPOINT_BASE_URI}/api/v2`;
3030const DEPLOYMENT_DEPRECATION_WARNING =
3131 "The 'deployment' argument is deprecated. The latest RAML specification that is published to Anypoint Exchange will be downloaded always." ;
3232
33+ // Only allows MAJOR.MINOR.PATCH (no suffixes). see https://semver.org/
34+ const releaseSemverRegex = / ^ ( 0 | [ 1 - 9 ] \d * ) \. ( 0 | [ 1 - 9 ] \d * ) \. ( 0 | [ 1 - 9 ] \d * ) $ / ;
3335/**
3436 * Makes an HTTP call to the url with the options passed. If the calls due to
3537 * a 5xx, 408, 420 or 429, it retries the call with the retry options passed
@@ -73,7 +75,15 @@ export async function downloadRestApi(
7375 }
7476 try {
7577 await fs . ensureDir ( destinationFolder ) ;
76- const zipFilePath = path . join ( destinationFolder , `${ restApi . assetId } .zip` ) ;
78+ let zipFilePath = path . join ( destinationFolder , `${ restApi . assetId } .zip` ) ;
79+
80+ if ( isOas ) {
81+ //For OAS, download clean latest versions from multiple version groups
82+ zipFilePath = path . join (
83+ destinationFolder ,
84+ `${ restApi . assetId } -${ restApi . version } .zip`
85+ ) ;
86+ }
7787
7888 const fatRaml = restApi . fatRaml ;
7989 const fatOas = restApi . fatOas ;
@@ -104,6 +114,7 @@ export async function downloadRestApi(
104114 * Download the API specifications
105115 * @param restApi - Metadata of the API
106116 * @param destinationFolder - Destination directory for the download
117+ * @param isOas - True for Open Api Specification
107118 */
108119export async function downloadRestApis (
109120 restApi : RestApi [ ] ,
@@ -204,8 +215,9 @@ export async function searchExchange(
204215 accessToken : string ,
205216 searchString : string
206217) : Promise < RestApi [ ] > {
218+ //TODO: We may have to handle pagination in the future if the number of APIs returned is more than 50
207219 return runFetch (
208- `${ ANYPOINT_API_URI_V2 } /assets?search=${ searchString } &types=rest-api` ,
220+ `${ ANYPOINT_API_URI_V2 } /assets?search=${ searchString } &types=rest-api&limit=50&offset=0 ` ,
209221 {
210222 headers : {
211223 Authorization : `Bearer ${ accessToken } ` ,
@@ -299,11 +311,13 @@ export async function getSpecificApi(
299311 * @param query - Exchange search query
300312 * @param [deployment] - RegExp matching the desired deployment targets
301313 *
314+ * @param isOas - True to get Open API Specifications, false for RAML
302315 * @returns Information about the APIs found.
303316 */
304317export async function search (
305318 query : string ,
306- deployment ?: RegExp
319+ deployment ?: RegExp ,
320+ isOas = false
307321) : Promise < RestApi [ ] > {
308322 if ( deployment ) {
309323 ramlToolLogger . warn ( DEPLOYMENT_DEPRECATION_WARNING ) ;
@@ -314,6 +328,9 @@ export async function search(
314328 process . env . ANYPOINT_PASSWORD
315329 ) ;
316330 const apis = await searchExchange ( token , query ) ;
331+ if ( isOas ) {
332+ return getLatestCleanApis ( apis , token ) ;
333+ }
317334 const promises = apis . map ( async ( api ) => {
318335 const version = await getVersionByDeployment ( token , api , deployment ) ;
319336 return version
@@ -322,3 +339,108 @@ export async function search(
322339 } ) ;
323340 return Promise . all ( promises ) ;
324341}
342+
343+ /**
344+ * Gets information about all the APIs from exchange that match the given search
345+ * string.
346+ * If it fails to get information about the deployed version of an API, it
347+ * removes all the version specific information from the returned object.
348+ *
349+ * @param apis - Array of apis to get the latest versions
350+ * @param {string } accessToken
351+ *
352+ * @returns Information about the APIs found.
353+ */
354+ export async function getLatestCleanApis (
355+ apis : RestApi [ ] ,
356+ accessToken : string
357+ ) : Promise < RestApi [ ] > {
358+ // Get all API versions in parallel
359+ const apiVersionPromises = apis . map ( async ( api ) => {
360+ const versions = await getLatestCleanApiVersions ( accessToken , api ) ;
361+ if ( ! versions || versions . length === 0 ) {
362+ return { api, versions : [ ] } ;
363+ }
364+ return { api, versions } ;
365+ } ) ;
366+
367+ const allApiVersions = await Promise . all ( apiVersionPromises ) ;
368+ // Create promises for all API versions and process them in parallel
369+ const promises = [ ] ;
370+ for ( const { api, versions } of allApiVersions ) {
371+ for ( const version of versions ) {
372+ promises . push (
373+ getSpecificApi ( accessToken , api . groupId , api . assetId , version )
374+ ) ;
375+ }
376+ }
377+ return Promise . all ( promises ) ;
378+ }
379+
380+ /**
381+ * @description Returns the latest clean (MAJOR.MINOR.PATCH) API versions from multiple version groups (V1, V2..) of an API
382+ *
383+ * @export
384+ * @param {string } accessToken
385+ * @param {RestApi } restApi
386+ * @returns {Promise<string> } Returned the version string from the instance fetched asset.version value
387+ */
388+ export async function getLatestCleanApiVersions (
389+ accessToken : string ,
390+ restApi : RestApi
391+ ) : Promise < void | string [ ] > {
392+ const logPrefix = "[exchangeDownloader][getLatestCleanApiVersions]" ;
393+
394+ let asset : void | RawRestApi ;
395+ try {
396+ asset = await getAsset (
397+ accessToken ,
398+ `${ restApi . groupId } /${ restApi . assetId } `
399+ ) ;
400+ } catch ( error ) {
401+ ramlToolLogger . error ( `${ logPrefix } Error fetching asset:` , error ) ;
402+ return ;
403+ }
404+
405+ if ( ! asset ) {
406+ ramlToolLogger . log (
407+ `${ logPrefix } No asset found for ${ restApi . assetId } , returning`
408+ ) ;
409+ return ;
410+ }
411+
412+ if ( ! asset . versionGroups ) {
413+ ramlToolLogger . error (
414+ `${ logPrefix } The rest API ${ restApi . assetId } is missing asset.versionGroups`
415+ ) ;
416+ return ;
417+ }
418+ const versions : string [ ] = [ ] ;
419+ asset . versionGroups . forEach ( ( versionGroup ) => {
420+ const version = getLatestReleaseVersion ( versionGroup ) ;
421+ if ( version ) {
422+ versions . push ( version ) ;
423+ }
424+ } ) ;
425+ return versions ;
426+ }
427+
428+ function getLatestReleaseVersion ( versionGroup : {
429+ versions : Array < { version : string } > ;
430+ } ) : void | string {
431+ if ( ! versionGroup . versions || versionGroup . versions . length === 0 ) {
432+ return ;
433+ }
434+ const releaseAssetVersions = versionGroup . versions . filter ( ( version ) => {
435+ return releaseSemverRegex . test ( version . version ) ;
436+ } ) ;
437+ // Sort versions and get the latest
438+ return releaseAssetVersions . sort ( ( instanceA , instanceB ) => {
439+ const [ aMajor , aMinor , aPatch ] = instanceA . version . split ( "." ) . map ( Number ) ;
440+ const [ bMajor , bMinor , bPatch ] = instanceB . version . split ( "." ) . map ( Number ) ;
441+
442+ if ( aMajor !== bMajor ) return bMajor - aMajor ;
443+ if ( aMinor !== bMinor ) return bMinor - aMinor ;
444+ return bPatch - aPatch ;
445+ } ) [ 0 ] . version ;
446+ }
0 commit comments