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 10 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/tricky-socks-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@sap-ux/axios-extension': patch
slin-sap marked this conversation as resolved.
Show resolved Hide resolved
'@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
95 changes: 94 additions & 1 deletion examples/odata-cli/src/activities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import {
TransportChecksService,
TransportRequestService,
ListPackageService,
FileStoreService
FileStoreService,
BusinessObjectsService,
PublishService
} from '@sap-ux/axios-extension';
import { logger } from './types';
import { XMLParser, XMLValidator } from 'fast-xml-parser';

/**
* Execute a sequence of test calls using the given provider.
Expand Down Expand Up @@ -180,3 +183,93 @@ export async function testDeployUndeployDTA(
logger.error(`Error: ${error.message}`);
}
}

/**
* Parse an XML document for ATO (Adaptation Transport Organizer) settings.
*
* @param xml xml document containing ATO settings
* @returns parsed ATO settings
*/
function _parseResponse<T>(xml: string): T {
slin-sap marked this conversation as resolved.
Show resolved Hide resolved
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;
}

/**
* 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 businesObjects = await provider.getAdtService<BusinessObjectsService>(BusinessObjectsService);
slin-sap marked this conversation as resolved.
Show resolved Hide resolved
const bos = await businesObjects.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.lockServiceBinding(generatedRefs.objectReference.uri);
slin-sap marked this conversation as resolved.
Show resolved Hide resolved
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
34 changes: 33 additions & 1 deletion packages/axios-extension/src/abap/abap-service-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import type { AtoSettings } 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 { BusinessObject } from './adt-catalog/services/businessobjects-service';

/**
* Extension of the service provider for ABAP services.
Expand Down Expand Up @@ -213,4 +215,34 @@ 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);
// quick and dirty
const path = config.link[0].href.split(config.id)[0] + config.id;
slin-sap marked this conversation as resolved.
Show resolved Hide resolved
const gen = this.createService<UiServiceGenerator>(path, UiServiceGenerator);
gen.configure(config, bo);
return gen;
}

/**
* Create a service provider to locking a binding path.
*
* @param path - service binding path
* @returns a service provider instance to lock the service binding
*/
public async lockServiceBinding(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 '../services/businessobjects-service';
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;
}
}