Skip to content

feat: Enhance system and app prompt validations #3065

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 44 commits into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
aad7f99
feat: add system and app validations
nikmace Mar 31, 2025
d67d040
Linting auto fix commit
github-actions[bot] Mar 31, 2025
a37f6c3
refactor: remove unused code
nikmace Mar 31, 2025
478571c
Merge branch 'feat/3015/enhance-validations' of https://github.com/SA…
nikmace Mar 31, 2025
82a5d6b
refactor: extract method
nikmace Mar 31, 2025
48be5bf
Merge branch 'main' into feat/3015/enhance-validations
nikmace Apr 1, 2025
f61d114
refactor: ui5 manager class
nikmace Apr 1, 2025
007b9be
test: add new test suites
nikmace Apr 1, 2025
7280400
Linting auto fix commit
github-actions[bot] Apr 1, 2025
b2f2803
refactor: improve code
nikmace Apr 1, 2025
60330b7
test: add new tests and fix existing
nikmace Apr 1, 2025
b4fa0e8
Merge branch 'feat/3015/enhance-validations' of https://github.com/SA…
nikmace Apr 1, 2025
ddb4073
Merge branch 'main' into feat/3015/enhance-validations
nikmace Apr 1, 2025
796d49a
test: fix test
nikmace Apr 1, 2025
dc84654
fix: sonar issues
nikmace Apr 1, 2025
0606cb8
test: add ui5 module tests
nikmace Apr 1, 2025
6309f65
test: add tests
nikmace Apr 1, 2025
cc4170f
Linting auto fix commit
github-actions[bot] Apr 1, 2025
f1d13f4
refactor: change i18n texts and adjust tests
nikmace Apr 2, 2025
b80a9c3
Merge branch 'feat/3015/enhance-validations' of https://github.com/SA…
nikmace Apr 2, 2025
71b1a14
chore: add cset
nikmace Apr 2, 2025
7b7a9e5
Merge branch 'main' into feat/3015/enhance-validations
nikmace Apr 3, 2025
d98ea1b
refactor: classes, folder names, and validations
nikmace Apr 3, 2025
d89d7db
refactor: rename methods
nikmace Apr 3, 2025
d4b5b2b
test: fix source and abap tests
nikmace Apr 3, 2025
379de4d
test: fix tooling tests
nikmace Apr 3, 2025
5d102cc
refactor: improve code, fix lint errors, tests
nikmace Apr 4, 2025
72013fd
test: remove unneeded test
nikmace Apr 4, 2025
f35748a
Merge branch 'main' into feat/3015/enhance-validations
nikmace Apr 4, 2025
dddcab7
Linting auto fix commit
github-actions[bot] Apr 4, 2025
9f0cc38
test: add prompts tests
nikmace Apr 4, 2025
3574132
refactor: improve manifest class
nikmace Apr 4, 2025
0f5602d
refactor: sonar issue
nikmace Apr 4, 2025
373e8d6
Merge branch 'main' into feat/3015/enhance-validations
nikmace Apr 4, 2025
160fe87
revert: changes to abap deploy packages by mistake
nikmace Apr 4, 2025
5b35056
refactor: rename source systems
nikmace Apr 4, 2025
1369822
refactor: rename source systems
nikmace Apr 7, 2025
0fa8377
refactor: remove redundant await
nikmace Apr 7, 2025
e5b993a
Merge branch 'main' into feat/3015/enhance-validations
nikmace Apr 7, 2025
03e8b63
refactor: rename files and folders
nikmace Apr 7, 2025
e488624
Merge branch 'feat/3015/enhance-validations' of https://github.com/SA…
nikmace Apr 7, 2025
eb6872f
Merge branch 'main' into feat/3015/enhance-validations
nikmace Apr 8, 2025
8a9b049
refactor: rename config file
nikmace Apr 8, 2025
46dd2d9
refactor: add review changes
nikmace Apr 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/adp-tooling/src/base/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
export const UI5_CDN_URL = 'https://ui5.sap.com';
export const UI5_VERSIONS_CDN_URL = 'https://sapui5.hana.ondemand.com/version.json';
export const UI5_VERSIONS_NEO_CDN_URL = 'https://ui5.sap.com/neo-app.json';
export const SNAPSHOT_CDN_URL = 'https://sapui5preview-sapui5.dispatcher.int.sap.eu2.hana.ondemand.com:443';

export const CURRENT_SYSTEM_VERSION = '(system version)';
export const LATEST_VERSION = '(latest)';
export const SNAPSHOT_VERSION = 'snapshot';
export const SNAPSHOT_UNTESTED_VERSION = 'snapshot-untested';

export const S4HANA_APPS_PARAMS = {
'sap.app/type': 'application',
'sap.fiori/cloudDevAdaptationStatus': 'released',
Expand Down
46 changes: 46 additions & 0 deletions packages/adp-tooling/src/client/abap-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { type AbapServiceProvider, AdtCatalogService, UI5RtVersionService } from '@sap-ux/axios-extension';
import type { FlexUISupportedSystem } from '../types';

/**
* Fetches system supports Flex UI features.
*
* @param provider
* @param isCustomerBase
* @returns {Promise<FlexUISupportedSystem | undefined>} settings indicating support for onPremise and UI Flex capabilities.
*/
export async function getFlexUISupportedSystem(
provider: AbapServiceProvider,
isCustomerBase: boolean
): Promise<FlexUISupportedSystem> {
if (!isCustomerBase) {
return {
isOnPremise: true,
isUIFlex: true
};
}
const FILTER = {
'scheme': 'http://www.sap.com/adt/categories/ui_flex',
'term': 'dta_folder'
};
const acceptHeaders = {
headers: {
Accept: 'application/*'
}
};
const response = await provider.get(AdtCatalogService.ADT_DISCOVERY_SERVICE_PATH, acceptHeaders);

return { isOnPremise: response.data.includes(FILTER.term), isUIFlex: response.data.includes(FILTER.scheme) };
}

/**
* Fetches system UI5 Version from UI5RtVersionService.
*
* @param provider
* @returns {string | undefined} system UI5 version
*/
export async function getSystemUI5Version(provider: AbapServiceProvider): Promise<string | undefined> {
const service = await provider.getAdtService<UI5RtVersionService>(UI5RtVersionService);
const version = await service?.getUI5Version();

return version;
}
2 changes: 2 additions & 0 deletions packages/adp-tooling/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './target-applications';
export * from './target-systems';
export * from './abap-provider';
export * from './abap-client';
export * from './target-manifest';
149 changes: 149 additions & 0 deletions packages/adp-tooling/src/client/target-manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import type { ToolsLogger } from '@sap-ux/logger';
import type { Manifest } from '@sap-ux/project-access';
import type { AbapServiceProvider } from '@sap-ux/axios-extension';

import { t } from '../i18n';

/**
* Interface representing cached manifest data.
*/
export interface ManifestCache {
/**
* The URL from which the manifest was fetched.
*/
url: string;
/**
* The parsed manifest data.
*/
manifest: Manifest | undefined;
}

/**
* Service class for handling operations related to application manifests.
*/
export class TargetManifest {
private manifestCache = new Map<string, ManifestCache>();

/**
* Creates an instance of ManifestManager.
*
* @param {AbapProvider} provider - The ABAP provider service.
* @param {ToolsLogger} [logger] - The logger.
*/
constructor(private readonly provider: AbapServiceProvider, private readonly logger?: ToolsLogger) {}

/**
* Resets the manifest cache.
*/
public resetCache(): void {
this.manifestCache = new Map();
}

/**
* Retrieves the cached manifest for a specified application.
*
* @param {string} id - The ID of the application whose manifest is needed.
* @returns {Promise<Manifest | undefined>} The cached manifest or null if not available.
*/
public async getManifest(id: string): Promise<Manifest | undefined> {
await this.loadManifest(id);
return this.manifestCache.get(id)?.manifest;
}

/**
* Retrieves the cached manifest URL for a specified application.
*
* @param {string} id - The ID of the application whose manifest URL is needed.
* @returns {Promise<string | undefined>} The cached URL or an empty string if not available.
*/
public async getUrl(id: string): Promise<string | undefined> {
await this.loadManifestUrl(id);
return this.manifestCache.get(id)?.url;
}

/**
* Retrieves and caches the manifest URL and the manifest itself for a specific application.
* Uses caching to avoid redundant network requests.
*
* @param {string} id - The ID of the application for which to load the manifest.
* @returns {Promise<void>} The manifest URL.
*/
private async loadManifestUrl(id: string): Promise<void> {
const cached = this.manifestCache.get(id);
if (cached?.url) {
return;
}

const appIndex = this.provider.getAppIndex();
const data = await appIndex.getAppInfo(id);

if (data) {
const appInfo = Object.values(data)[0];
const url = appInfo?.manifestUrl ?? appInfo?.manifest ?? '';
this.manifestCache.set(id, { url, manifest: undefined });
}
}

/**
* Fetches and stores the application manifest from a URL.
*
* @param {string} id - The application ID.
* @returns {Promise<Manifest>} The fetched manifest.
*/
private async loadManifest(id: string): Promise<void> {
let cached = this.manifestCache.get(id);

if (cached?.manifest) {
return;
}

if (!cached?.url) {
await this.loadManifestUrl(id);
cached = this.manifestCache.get(id);
}

if (!cached?.url) {
throw new Error('Manifest URL could not be loaded.');
}

try {
const response = await this.provider.request({ url: cached.url });

const manifest = JSON.parse(response.data) as Manifest;

if (typeof manifest !== 'object' || manifest === null) {
throw new Error('Manifest parsing error. Manifest is not in expected format.');
}

this.manifestCache.set(id, { url: cached.url, manifest });
} catch (e) {
this.logger?.debug(`Failed to load manifest, error: ${e.message}`);
throw new Error(`Failed to load manifest from URL: ${e.message}`);
}
}

/**
* Determines if the application supports manifest-first approach and manifest url exists.
*
* @param {string} id - The application ID.
* @returns {Promise<boolean>} True if supported, otherwise throws an error.
*/
public async isAppSupported(id: string): Promise<boolean> {
const appIndex = this.provider.getAppIndex();
const isSupported = await appIndex.getIsManiFirstSupported(id);

if (!isSupported) {
this.logger?.debug(`Application '${id}' is not supported by Adaptation Project`);
throw new Error(t('validators.appDoesNotSupportManifest'));
}

const url = await this.getUrl(id);

if (!url) {
this.logger?.debug(`Manifest url for app '${id}' was not found!`);
throw new Error(t('validators.adpDoesNotSupportSelectedApp'));
}

return true;
}
}
1 change: 1 addition & 0 deletions packages/adp-tooling/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './base/helper';
export * from './client';
export * from './preview/adp-preview';
export * from './writer/writer-config';
export * from './ui5';
export { getCustomConfig } from './writer/project-utils';
export { generate, migrate } from './writer';
export { generateChange } from './writer/editors';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@
"errorDuplicateNamesOData": "OData Service Name must be different from OData Annotation Data Source Name",
"errorInputInvalidValuePrefix": "{{value}} should start with '{{prefix}}'",
"errorCustomerEmptyValue": "{{value}} should contain at least one character in addition to '{{prefix}}'",
"errorInvalidDataSourceURI": "Invalid URI. Should start and end with '/' and contain no spaces"
"errorInvalidDataSourceURI": "Invalid URI. Should start and end with '/' and contain no spaces",
"appDoesNotSupportManifest": "Select a different application. The selected application is not supported by Adaptation Project. Please refer to SAPUI5 Adaptation Project documentation for more information.",
"adpDoesNotSupportSelectedApp": "Select a different application. Adaptation project doesn't support the selected application."
},
"choices": {
"true": "true",
Expand Down
16 changes: 16 additions & 0 deletions packages/adp-tooling/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,22 @@ export interface TargetApplication {
bspName: string;
}

export interface FlexUISupportedSystem {
isUIFlex: boolean;
isOnPremise: boolean;
}

export interface UI5Version {
latest: VersionDetail;
[key: string]: VersionDetail;
}

export interface VersionDetail {
version: string;
support: string;
lts: boolean;
}

export interface Endpoint extends Partial<Destination> {
Name: string;
Url?: string;
Expand Down
33 changes: 33 additions & 0 deletions packages/adp-tooling/src/ui5/fetchers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { UI5Version } from '../types';
import { UI5_VERSIONS_CDN_URL, UI5_VERSIONS_NEO_CDN_URL, LATEST_VERSION } from '../base/constants';

/**
* Fetches public UI5 version data from the SAP CDN.
*
* @returns {Promise<UI5Version>} A promise that resolves to the UI5 version data object.
* @throws Will throw an error if the fetch fails.
*/
export async function fetchPublicVersions(): Promise<UI5Version> {
const response = await fetch(UI5_VERSIONS_CDN_URL);
if (!response.ok) {
throw new Error(`Failed to fetch public UI5 versions. Status: ${response.status}`);
}
return await response.json();
}

/**
* Fetches internal UI5 versions from the Neo CDN and maps them to formatted version strings.
*
* @param {string} latestVersion - The latest public UI5 version.
* @returns {Promise<string[]>} A promise that resolves to an array of formatted internal version strings.
*/
export async function fetchInternalVersions(latestVersion: string): Promise<string[]> {
const response = await fetch(UI5_VERSIONS_NEO_CDN_URL);
const data = await response.json();

return data?.routes?.map((route: { target: { version: string } }) => {
return route.target.version === latestVersion
? `${route.target.version} ${LATEST_VERSION}`
: route.target.version;
});
}
2 changes: 2 additions & 0 deletions packages/adp-tooling/src/ui5/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './ui5-version-manager';
export * from './utils';
Loading