Skip to content
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

feat: Detecting and showing OData failures in Info Center #2732

Draft
wants to merge 4 commits into
base: feat/2511/info_center
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions packages/adp-tooling/src/base/abap/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './manifest-service';
export * from './metadata-fetchers';
export * from './odata-service';
130 changes: 42 additions & 88 deletions packages/adp-tooling/src/base/abap/manifest-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import { isAxiosError, type AbapServiceProvider, type Ui5AppInfoContent } from '
import { getWebappFiles } from '../helper';
import type { DescriptorVariant } from '../../types';

type DataSources = Record<string, ManifestNamespace.DataSource>;

/**
* Retrieves the inbound navigation configurations from the project's manifest.
*
Expand All @@ -33,7 +31,6 @@ export function getRegistrationIdFromManifest(manifest: Manifest): string | unde
* Service class for handling operations related to the manifest of a UI5 application.
* The class supports operations for both base and merged manifests.
* It provides methods to fetch the manifest, data sources and metadata of a data source.
*
*/
export class ManifestService {
private manifest: Manifest;
Expand All @@ -50,10 +47,10 @@ export class ManifestService {
/**
* Creates an instance of the ManifestService and fetches the base manifest of the application.
*
* @param provider - The ABAP service provider instance.
* @param appId - The application ID.
* @param logger - The logger instance.
* @returns A promise that resolves to an instance of ManifestService.
* @param {AbapServiceProvider} provider - The ABAP service provider instance.
* @param {string} appId - The application ID.
* @param {ToolsLogger} logger - The logger instance.
* @returns {Promise<ManifestService>} A promise that resolves to an instance of ManifestService.
*/
public static async initBaseManifest(
provider: AbapServiceProvider,
Expand All @@ -68,11 +65,11 @@ export class ManifestService {
/**
* Creates an instance of the ManifestService and fetches the merged manifest of the application.
*
* @param provider - The ABAP service provider instance.
* @param basePath - The base path of the application.
* @param variant - The descriptor variant.
* @param logger - The logger instance.
* @returns A promise that resolves to an instance of ManifestService.
* @param {AbapServiceProvider} provider - The ABAP service provider instance.
* @param {string} basePath - The base path of the application.
* @param {DescriptorVariant} variant - The descriptor variant.
* @param {ToolsLogger} logger - The logger instance.
* @returns {Promise<ManifestService>} A promise that resolves to an instance of ManifestService.
*/
public static async initMergedManifest(
provider: AbapServiceProvider,
Expand All @@ -87,57 +84,39 @@ export class ManifestService {
}

/**
* Fetches the base manifest for a given application ID.
* Returns the manifest fetched by the service during initialization.
*
* @param appId - The application ID.
* @returns A promise that resolves when the base manifest is fetched.
* @throws Error if the manifest URL is not found or fetching/parsing fails.
* @returns {Manifest} The current manifest.
*/
private async fetchBaseManifest(appId: string): Promise<void> {
await this.fetchAppInfo(appId);
const manifestUrl = this.appInfo.manifestUrl ?? this.appInfo.manifest;
if (!manifestUrl) {
throw new Error('Manifest URL not found');
}
try {
const response = await this.provider.get(manifestUrl);
this.manifest = JSON.parse(response.data);
} catch (error) {
if (isAxiosError(error)) {
this.logger.error('Manifest fetching failed');
} else {
this.logger.error('Manifest parsing error: Manifest is not in expected format.');
}
this.logger.debug(error);
throw error;
}
public getManifest(): Manifest {
return this.manifest;
}

/**
* Fetches the application information for a given application ID.
* Returns the UI5 application information content.
*
* @param appId - The application ID.
* @returns A promise that resolves when the application information is fetched.
* @returns {Ui5AppInfoContent} UI5 app info.
*/
private async fetchAppInfo(appId: string): Promise<void> {
this.appInfo = (await this.provider.getAppIndex().getAppInfo(appId))[appId];
public getAppInfo(): Ui5AppInfoContent {
return this.appInfo;
}

/**
* Returns the manifest fetched by the service during initialization.
* Fetches the application information for a given application ID.
*
* @returns The current manifest.
* @param {string} appId - The application ID.
* @returns {Promise<void>} A promise that resolves when the application information is fetched.
*/
public getManifest(): Manifest {
return this.manifest;
private async fetchAppInfo(appId: string): Promise<void> {
this.appInfo = (await this.provider.getAppIndex().getAppInfo(appId))[appId];
}

/**
* Fetches the merged manifest for a given application.
*
* @param basePath - The base path of the application.
* @param descriptorVariantId - The descriptor variant ID.
* @returns A promise that resolves to the merged manifest.
* @param {string} basePath - The base path of the application.
* @param {string} descriptorVariantId - The descriptor variant ID.
* @returns {Promise<void>} A promise that resolves to the merged manifest.
*/
private async fetchMergedManifest(basePath: string, descriptorVariantId: string): Promise<void> {
const zip = new ZipFile();
Expand All @@ -148,58 +127,33 @@ export class ManifestService {
const buffer = zip.toBuffer();
const lrep = this.provider.getLayeredRepository();
await lrep.getCsrfToken();
const response = await lrep.mergeAppDescriptorVariant(buffer);
const response = await lrep.mergeAppDescriptorVariant(buffer, '//');
this.manifest = response[descriptorVariantId].manifest;
}

/**
* Returns the data sources from the manifest.
*
* @returns The data sources from the manifest.
* @throws Error if no data sources are found in the manifest.
*/
public getManifestDataSources(): DataSources {
const dataSources = this.manifest['sap.app'].dataSources;
if (!dataSources) {
throw new Error('No data sources found in the manifest');
}
return dataSources;
}

/**
* Returns the metadata of a data source.
* Fetches the base manifest for a given application ID.
*
* @param dataSourceId - The ID of the data source.
* @returns A promise that resolves to the metadata of the data source.
* @throws Error if no metadata path is found in the manifest or fetching fails.
* @param {string} appId - The application ID.
* @returns {Promise<void>} A promise that resolves when the base manifest is fetched.
* @throws Error if the manifest URL is not found or fetching/parsing fails.
*/
public async getDataSourceMetadata(dataSourceId: string): Promise<string> {
const dataSource = this.manifest?.['sap.app']?.dataSources?.[dataSourceId];

if (!dataSource) {
throw new Error('No metadata path found in the manifest');
private async fetchBaseManifest(appId: string): Promise<void> {
await this.fetchAppInfo(appId);
const manifestUrl = this.appInfo.manifestUrl ?? this.appInfo.manifest;
if (!manifestUrl) {
throw new Error('Manifest URL not found');
}
const baseUrl = new URL(this.appInfo.url, this.provider.defaults.baseURL as string);
const metadataUrl = new URL(`${dataSource.uri}$metadata`, baseUrl.toString());
try {
const response = await this.provider.get(metadataUrl.toString());
return response.data;
const response = await this.provider.get(manifestUrl);
this.manifest = JSON.parse(response.data);
} catch (error) {
if (dataSource?.settings?.localUri) {
this.logger.warn('Metadata fetching failed. Fallback to local metadata');
try {
const fallbackUrl = new URL(
dataSource?.settings.localUri,
`${baseUrl.toString().endsWith('/') ? baseUrl.toString() : baseUrl.toString() + '/'}`
);
const response = await this.provider.get(fallbackUrl.toString());
return response.data;
} catch (fallbackError) {
this.logger.error('Local metadata fallback fetching failed');
throw fallbackError;
}
if (isAxiosError(error)) {
this.logger.error('Manifest fetching failed');
} else {
this.logger.error('Manifest parsing error: Manifest is not in expected format.');
}
this.logger.error('Metadata fetching failed');
this.logger.debug(error);
throw error;
}
}
Expand Down
54 changes: 54 additions & 0 deletions packages/adp-tooling/src/base/abap/metadata-fetchers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Logger } from '@sap-ux/logger';
import type { AbapServiceProvider } from '@sap-ux/axios-extension';
import type { ManifestNamespace } from '@sap-ux/project-access';

/**
* Fetch remote $metadata for the given data source.
*
* @param dataSource - The data source object from your manifest.
* @param baseUrl - The base URL.
* @param provider - The ABAP Service Provider.
* @param logger - Logger instance for logging.
* @returns The metadata as a string.
*/
export async function fetchMetadata(
dataSource: ManifestNamespace.DataSource,
baseUrl: URL,
provider: AbapServiceProvider,
logger: Logger
): Promise<string> {
const metadataUrl = new URL(`${dataSource.uri}$metadata`, baseUrl.toString());
logger.debug(`Fetching remote metadata from: ${metadataUrl.pathname}`);

const response = await provider.get(metadataUrl.toString());
return response.data;
}

/**
* Fetch fallback metadata if the remote fetch fails and localUri is provided.
*
* @param dataSource - The data source object from your manifest.
* @param baseUrl - The base URL.
* @param provider - The ABAP Service Provider.
* @param logger - Logger instance for logging.
* @returns The fallback metadata as a string.
*/
export async function fetchFallbackMetadata(
dataSource: ManifestNamespace.DataSource,
baseUrl: URL,
provider: AbapServiceProvider,
logger: Logger
): Promise<string> {
if (!dataSource.settings?.localUri) {
throw new Error('No localUri specified for the fallback fetch');
}

// Ensure trailing slash if needed
const normalizedBase = baseUrl.toString().endsWith('/') ? baseUrl.toString() : baseUrl.toString() + '/';

const fallbackUrl = new URL(dataSource.settings.localUri, normalizedBase);
logger.debug(`Fetching local metadata from: ${fallbackUrl.pathname}`);

const response = await provider.get(fallbackUrl.toString());
return response.data;
}
112 changes: 112 additions & 0 deletions packages/adp-tooling/src/base/abap/odata-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { Logger } from '@sap-ux/logger';
import type { Manifest, ManifestNamespace } from '@sap-ux/project-access';
import type { AbapServiceProvider, Ui5AppInfoContent } from '@sap-ux/axios-extension';

import { fetchMetadata, fetchFallbackMetadata } from './metadata-fetchers';

type DataSources = Record<string, ManifestNamespace.DataSource>;

/**
* Retrieves all data sources from the manifest.
*
* @param {Manifest} manifest - The UI5 manifest object.
* @returns {DataSources} The data sources from the manifest.
* @throws Error if no data sources are found in the manifest.
*/
export function getDataSources(manifest: Manifest): DataSources {
const dataSources = manifest?.['sap.app']?.dataSources;
if (!dataSources) {
throw new Error('No data sources found in the manifest');
}
return dataSources;
}

/**
* Retrieves a specific data source by ID from the manifest.
*
* @param {Manifest} manifest - The UI5 manifest object.
* @param {string} dataSourceId - The ID of the data source to retrieve.
* @returns {ManifestNamespace.DataSource} The corresponding data source object.
* @throws Error if the data source ID is not found in the manifest.
*/
export function getDataSourceById(manifest: Manifest, dataSourceId: string): ManifestNamespace.DataSource {
const dataSources = getDataSources(manifest);
const dataSource = dataSources?.[dataSourceId];
if (!dataSource) {
throw new Error(`Data source '${dataSourceId}' was not found in the manifest.`);
}
return dataSource;
}

/**
* Class responsible for making calls to get the metadata of a specific data source.
*/
export class ODataService {
/**
* The constructor requires all objects needed for metadata fetching:
* - An already-loaded manifest (or partial manifest focusing on dataSources).
* - The AbapServiceProvider for making requests.
* - appInfo containing the base URL.
* - The logger.
*/
constructor(
private readonly manifest: Manifest,
private readonly appInfo: Ui5AppInfoContent,
private readonly provider: AbapServiceProvider,
private readonly logger: Logger
) {}

/**
* A helper to build the base URL used by fetchMetadata/fetchFallbackMetadata.
*
* @returns {URL} The built base url.
*/
private buildBaseUrl(): URL {
return new URL(this.appInfo.url, this.provider.defaults.baseURL as string);
}

/**
* Fetches the main OData metadata for a given data source ID from the manifest.
*
* @param {string} dataSourceId - The ID of the data source in the manifest.
* @returns {Promise<string>} The metadata as a string.
*/
public async getMetadata(dataSourceId: string): Promise<string> {
const dataSource = getDataSourceById(this.manifest, dataSourceId);
const baseUrl = this.buildBaseUrl();

this.logger.debug(`Fetching remote metadata for data source '${dataSourceId}'`);
return fetchMetadata(dataSource, baseUrl, this.provider, this.logger);
}

/**
* Fetch OData metadata for a given data source ID, with fallback to localUri if the remote fails.
*
* @param {string} dataSourceId - The ID of the data source in the manifest.
* @returns The metadata as a string.
* @throws Error if neither remote nor fallback metadata can be fetched.
*/
public async getMetadataWithFallback(dataSourceId: string): Promise<string> {
const dataSource = getDataSourceById(this.manifest, dataSourceId);
const baseUrl = this.buildBaseUrl();

this.logger.debug(`Fetching metadata with fallback for data source '${dataSourceId}'...`);
try {
return await fetchMetadata(dataSource, baseUrl, this.provider, this.logger);
} catch (error) {
this.logger.warn(`Metadata fetching failed for '${dataSourceId}'. Will attempt fallback. Reason: ${error}`);

if (dataSource.settings?.localUri) {
try {
return await fetchFallbackMetadata(dataSource, baseUrl, this.provider, this.logger);
} catch (fallbackError) {
this.logger.error(`Local metadata fallback also failed for '${dataSourceId}'`);
throw fallbackError;
}
}

this.logger.error(`Metadata fetching failed, no local fallback available for '${dataSourceId}'`);
throw error;
}
}
}
2 changes: 1 addition & 1 deletion packages/adp-tooling/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export * from './types';
export * from './prompts';
export * from './common';
export * from './base/cf';
export * from './base/abap/manifest-service';
export * from './base/abap';
export * from './base/helper';
export * from './preview/adp-preview';
export { generate, migrate } from './writer';
Expand Down
Loading