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: create adt services for ui-service generation #1879

Merged
merged 19 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}
}