Skip to content

Commit

Permalink
Feat: create adt services for ui-service generation (#1879)
Browse files Browse the repository at this point in the history
* feat(service-api): test

* Making everything more dynamic

* feat(ui-service-gen): adt services for ui service generation, test flow in odata-cli

* feat(ui-service-gen): code cleanup, add tests

* better logging

* feat(ui-service-gen): add more tests

* changeset

* feat(ui-service-gen): review comments

* feat(ui-service-gen): odata-cli test update, changeset update

* feat(ui-service-gen): update test

* feat(ui-service-gen): cleanup

* feat(ui-service-gen): cleanup

* feat(ui-service-gen): cleanup changeset

* feat(ui-service-gen): address review comment

---------

Co-authored-by: Tobias Queck <tobias.queck@sap.com>
  • Loading branch information
docirl and tobiasqueck committed May 8, 2024
1 parent f7f22b4 commit 312919e
Show file tree
Hide file tree
Showing 26 changed files with 6,350 additions and 11 deletions.
6 changes: 6 additions & 0 deletions .changeset/weak-toys-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@sap-ux/axios-extension': minor
'@sap-ux/odata-cli': patch
---

Add new adt services for ui service generation and publish
File renamed without changes.
3 changes: 2 additions & 1 deletion examples/odata-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"@sap-ux/axios-extension": "workspace:*",
"@sap-ux/btp-utils": "workspace:*",
"@sap-ux/logger": "workspace:*",
"dotenv": "16.3.1"
"dotenv": "16.3.1",
"fast-xml-parser": "4.2.7"
},
"files": [
"dist",
Expand Down
72 changes: 71 additions & 1 deletion examples/odata-cli/src/activities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import {
TransportChecksService,
TransportRequestService,
ListPackageService,
FileStoreService
FileStoreService,
BusinessObjectsService,
PublishService
} from '@sap-ux/axios-extension';
import { logger } from './types';

Expand Down Expand Up @@ -180,3 +182,71 @@ export async function testDeployUndeployDTA(
logger.error(`Error: ${error.message}`);
}
}

/**
* Test the UI service generation.
*
* @param provider instance of a service provider
* @param env object representing the content of the .env file.
*/
export async function testUiServiceGenerator(
provider: AbapServiceProvider,
env: {
TEST_BO_NAME: string;
TEST_PACKAGE: string;
TEST_TRANSPORT: string;
}
): Promise<void> {
const s4Cloud = await provider.isS4Cloud();
if (!s4Cloud) {
logger.warn('Not an S/4 Cloud system. UI service generation might not be supported.');
}

// Get BOs
const businessObjectsService = await provider.getAdtService<BusinessObjectsService>(BusinessObjectsService);
const bos = await businessObjectsService.getBusinessObjects();
const bo = bos.find((bo) => bo.name === env.TEST_BO_NAME);
logger.debug(bos.map((bo) => bo.name));

// Generator service
const generator = await provider.getUiServiceGenerator(bo);
const content = await generator.getContent(env.TEST_PACKAGE);
logger.debug('content: ' + content);
let generatedRefs;
try {
logger.info('Start generation of service');
generatedRefs = await generator.generate(content, env.TEST_TRANSPORT);
logger.debug('generatedRefs: ' + JSON.stringify(generatedRefs));
logger.info('Generation of service completed');
} catch (error) {
logger.error(`${error.code}: ${error.message}`);
logger.debug(error);
return;
}

// Publish (including lock service binding)
if (generatedRefs) {
const serviceLockGen = await provider.createLockServiceBindingGenerator(generatedRefs.objectReference.uri);
try {
await serviceLockGen.lockServiceBinding();
} catch (error) {
if (error.response && error.response.status === 403) {
logger.warn(`${error.code} ${error.response.status} ${error.response.data}`);
} else {
logger.warn(error);
return;
}
}
}
const publishService = await provider.getAdtService<PublishService>(PublishService);
try {
logger.info('Start publish');
const publishResult = await publishService.publish(
generatedRefs.objectReference.type,
generatedRefs.objectReference.name
);
logger.info(`Publish result: ${publishResult.SEVERITY} ${publishResult.LONG_TEXT || publishResult.SHORT_TEXT}`);
} catch (error) {
logger.error(error);
}
}
7 changes: 4 additions & 3 deletions examples/odata-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { join } from 'path';
import type { TestActivity, TestTarget } from './types';
import { logger } from './types';
import { testWithAbapSystem, testWithDestination, testWithAbapBtpSystem, testWithCloudAbapSystem } from './targets';
import { testDeployUndeployDTA, useAdtServices, useCatalogAndFetchSomeMetadata } from './activities';
import { testDeployUndeployDTA, testUiServiceGenerator, useAdtServices, useCatalogAndFetchSomeMetadata } from './activities';

const targets: { [name: string]: TestTarget } = {
abap: testWithAbapSystem,
Expand All @@ -24,7 +24,8 @@ const targets: { [name: string]: TestTarget } = {
const activities: { [name: string]: TestActivity } = {
odata: useCatalogAndFetchSomeMetadata,
adt: useAdtServices,
dta: testDeployUndeployDTA
dta: testDeployUndeployDTA,
gen: testUiServiceGenerator
};

// read CLI arguments as well as environment variables
Expand All @@ -47,4 +48,4 @@ if (isAppStudio()) {
target = args.length > 0 ? args[0] : 'unknown';
activity = args.length > 1 ? args[1] : 'odata';
}
targets[target](processEnv, activities[activity]).catch((error) => console.error(error));
targets[target](processEnv, activities[activity]).then(() => console.log('done')).catch((error) => console.error(error));
4 changes: 4 additions & 0 deletions examples/odata-cli/src/targets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export async function testWithAbapSystem(
TEST_SYSTEM: string;
TEST_USER?: string;
TEST_PASSWORD?: string;
TEST_CLIENT?: string;
},
activity: TestActivity
): Promise<void> {
Expand All @@ -34,6 +35,9 @@ export async function testWithAbapSystem(
auth: {
username: env.TEST_USER,
password: env.TEST_PASSWORD
},
params: {
'sap-client': env.TEST_CLIENT
}
});
activity(provider, env).catch((error) => console.error(error));
Expand Down
7 changes: 5 additions & 2 deletions examples/odata-cli/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { AbapServiceProvider } from '@sap-ux/axios-extension';
import { ToolsLogger } from '@sap-ux/logger';
import { ConsoleTransport, LogLevel, ToolsLogger } from '@sap-ux/logger';

export const logger = new ToolsLogger();
export const logger = new ToolsLogger({
logLevel: process.env.DEBUG ? LogLevel.Debug : LogLevel.Info,
transports: [ new ConsoleTransport() ]
});

export type TestActivity = (provider: AbapServiceProvider, config: unknown) => Promise<void>;

Expand Down
49 changes: 47 additions & 2 deletions packages/axios-extension/src/abap/abap-service-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import { AppIndexService } from './app-index-service';
import { ODataVersion } from '../base/odata-service';
import { LayeredRepositoryService } from './lrep-service';
import { AdtCatalogService } from './adt-catalog/adt-catalog-service';
import type { AtoSettings } from './types';
import type { AtoSettings, BusinessObject } from './types';
import { TenantType } from './types';
// Can't use an `import type` here. We need the classname at runtime to create object instances:
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import { AdtService, AtoService } from './adt-catalog/services';
import { AdtService, AtoService, GeneratorService } from './adt-catalog/services';
import { UiServiceGenerator } from './adt-catalog/generators/ui-service-generator';
import type { GeneratorEntry } from './adt-catalog/generators/types';

/**
* Extension of the service provider for ABAP services.
Expand Down Expand Up @@ -213,4 +215,47 @@ export class AbapServiceProvider extends ServiceProvider {

return this.services[subclassName] as T;
}

/**
* Create a UI Service generator for the given business object.
*
* @param bo - business object
* @returns a UI Service generator
*/
public async getUiServiceGenerator(bo: BusinessObject): Promise<UiServiceGenerator> {
const generatorService = await this.getAdtService<GeneratorService>(GeneratorService);
if (!generatorService) {
throw new Error('Generators are not support on this system');
}
const config = await generatorService.getUIServiceGeneratorConfig(bo.name);
const gen = this.createService<UiServiceGenerator>(this.getServiceUrlFromConfig(config), UiServiceGenerator);
gen.configure(config, bo);
return gen;
}

/**
* Get the service URL from the generator config.
*
* @param config - generator config
* @returns the service URL
*/
private getServiceUrlFromConfig(config: GeneratorEntry): string {
// make code in this function defensive against undefined href
if (Array.isArray(config.link) && !config.link[0]?.href) {
throw new Error('No service URL found in the generator config');
}
const endIndex = config.link[0].href.indexOf(config.id) + config.id.length;
return config.link[0].href.substring(0, endIndex);
}

/**
* Create a service provider to lock a binding path.
*
* @param path - service binding path
* @returns a service provider instance to lock the service binding
*/
public async createLockServiceBindingGenerator(path: string): Promise<UiServiceGenerator> {
const gen = this.createService<UiServiceGenerator>(path, UiServiceGenerator);
return gen;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface GeneratorEntry {
id: string;
link: {
href: string;
rel: string;
type: string;
}[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { Logger } from '@sap-ux/logger';
import type { GeneratorEntry } from './types';
import type { BusinessObject } from '../../types';
import { AdtService } from '../services';

/**
*
*/
export class UiServiceGenerator extends AdtService {
public log: Logger;

protected bo!: BusinessObject;

/**
* Configure the UI service generator.
*
* @param _config - The generator configuration.
* @param bo - The business object.
*/
public configure(_config: GeneratorEntry, bo: BusinessObject) {
this.bo = bo;
}

/**
* Get the content of the service binding.
*
* @param pckg - The package name.
* @returns The content of the service binding.
*/
public async getContent(pckg: string): Promise<string> {
const response = await this.get('/content', {
headers: {
Accept: 'application/vnd.sap.adt.repository.generator.content.v1+json'
},
params: {
referencedObject: this.bo.uri,
package: pckg
}
});
const content = response.data;
const contentObj = JSON.parse(content);
if (!contentObj['metadata']) {
contentObj['metadata'] = {
package: pckg
};
}

return JSON.stringify(contentObj);
}

/**
* Generate the service binding.
*
* @param content - The content of the service binding.
* @param transport - The transport.
* @returns The object references.
*/
public async generate(content: string, transport: string): Promise<any> {
const response = await this.post('', content, {
headers: {
'Content-Type': 'application/vnd.sap.adt.repository.generator.content.v1+json',
Accept: 'application/vnd.sap.adt.repository.generator.v1+json, application/vnd.sap.as+xml;charset=UTF-8;dataname=com.sap.adt.StatusMessage'
},
params: {
referencedObject: this.bo.uri,
corrNr: transport
}
});
// Service binding is in XML format, ready to be used for the subsequent activation and publish.
const data = this.parseResponse<any>(response.data);
return data.objectReferences;
}

/**
* Lock the service binding. The class should be configured with the uri of the service binding
* The uri is returned from the generate method.
*/
public async lockServiceBinding() {
await this.post('', '', {
headers: {
Accept: 'application/*,application/vnd.sap.as+xml;charset=UTF-8;dataname=com.sap.adt.lock.result',
'x-sap-adt-sessiontype': 'stateful'
},
params: {
_action: `LOCK`,
accessMode: 'MODIFY'
}
});
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Logger } from '@sap-ux/logger';
import type { AdtCategory, AdtCollection } from 'abap/types';
import { Axios } from 'axios';
import { XMLParser, XMLValidator } from 'fast-xml-parser';

interface AdtServiceExtension {
/**
Expand Down Expand Up @@ -39,4 +40,26 @@ export abstract class AdtService extends Axios implements AdtServiceExtension {
attachAdtSchema(serviceSchema: AdtCollection): void {
this.serviceSchema = serviceSchema;
}

/**
* Parse an XML document for ATO (Adaptation Transport Organizer) settings.
*
* @param xml xml document containing ATO settings
* @returns parsed ATO settings
*/
protected parseResponse<T>(xml: string): T {
if (XMLValidator.validate(xml) !== true) {
this.log.warn(`Invalid XML: ${xml}`);
return {} as T;
}
const options = {
attributeNamePrefix: '',
ignoreAttributes: false,
ignoreNameSpace: true,
parseAttributeValue: true,
removeNSPrefix: true
};
const parser: XMLParser = new XMLParser(options);
return parser.parse(xml, true) as T;
}
}

0 comments on commit 312919e

Please sign in to comment.