Skip to content

Commit 7c09c8e

Browse files
815areKlaus-Keller
andauthored
feat: New public method for ApplicationAccess - readManifest, readFlexChanges, readAnnotationFiles (#3854)
* feat: new public methods for application access new public methods for application access * changeset changeset * fix: review comments review comments --------- Co-authored-by: Klaus Keller <[email protected]>
1 parent 7fac148 commit 7c09c8e

File tree

8 files changed

+339
-4
lines changed

8 files changed

+339
-4
lines changed

.changeset/great-rivers-lick.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@sap-ux/project-access': patch
3+
---
4+
5+
New public method for `ApplicationAccess`
6+
- `readManifest` - reads and returns the parsed `manifest.json` file for the application.
7+
- `readFlexChanges` - reads and returns all Flex Changes (`*.change` files) associated with the application.
8+
- `readAnnotationFiles` - reads and returns all annotation files associated with the application's main service.

packages/project-access/src/project/access.ts

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import type {
1111
ProjectType,
1212
ApplicationStructure,
1313
Package,
14-
Manifest
14+
Manifest,
15+
AnnotationFile
1516
} from '../types';
1617

1718
import {
@@ -27,9 +28,11 @@ import { getProject } from './info';
2728
import { findAllApps } from './search';
2829

2930
import type { Editor } from 'mem-fs-editor';
30-
import { updateManifestJSON, updatePackageJSON } from '../file';
31+
import { readFile, readJSON, updateManifestJSON, updatePackageJSON } from '../file';
3132
import { FileName } from '../constants';
3233
import { getSpecification } from './specification';
34+
import { readFlexChanges } from './flex-changes';
35+
import { readCapServiceMetadataEdmx } from './cap';
3336

3437
/**
3538
*
@@ -202,6 +205,72 @@ class ApplicationAccessImp implements ApplicationAccess {
202205
await updateManifestJSON(this.app.manifest, manifest, memFs ?? this.options?.fs);
203206
}
204207

208+
/**
209+
* Reads and returns the parsed `manifest.json` file for the application.
210+
*
211+
* @param memFs - optional mem-fs-editor instance
212+
* @returns A promise resolving to the parsed `manifest.json` content.
213+
*/
214+
async readManifest(memFs?: Editor): Promise<Manifest> {
215+
return readJSON<Manifest>(this.app.manifest, memFs ?? this.options?.fs);
216+
}
217+
218+
/**
219+
* Reads and returns all Flex Changes (`*.change` files) associated with the application.
220+
*
221+
* @param memFs - optional mem-fs-editor instance
222+
* @returns A promise that resolves to an array of flex change files.
223+
*/
224+
async readFlexChanges(memFs?: Editor): Promise<{
225+
[key: string]: string;
226+
}> {
227+
return readFlexChanges(this.app.changes, memFs ?? this.options?.fs);
228+
}
229+
230+
/**
231+
* Reads and returns all annotation files associated with the application's main service.
232+
*
233+
* @param memFs - optional mem-fs-editor instance
234+
* @returns A promise resolving to an array of annotation file descriptors.
235+
*/
236+
async readAnnotationFiles(memFs?: Editor): Promise<AnnotationFile[]> {
237+
const annotationData: AnnotationFile[] = [];
238+
const mainServiceName = this.app.mainService ?? 'mainService';
239+
const mainService = this.app?.services?.[mainServiceName];
240+
if (!mainService) {
241+
return [];
242+
}
243+
if (mainService.uri && (this.projectType === 'CAPJava' || this.projectType === 'CAPNodejs')) {
244+
const serviceUri = mainService?.uri ?? '';
245+
if (serviceUri) {
246+
const edmx = await readCapServiceMetadataEdmx(this.root, serviceUri);
247+
annotationData.push({
248+
fileContent: edmx,
249+
dataSourceUri: serviceUri
250+
});
251+
}
252+
} else {
253+
if (mainService.local) {
254+
const serviceFile = await readFile(mainService.local, memFs ?? this.options?.fs);
255+
annotationData.push({
256+
dataSourceUri: mainService.local,
257+
fileContent: serviceFile.toString()
258+
});
259+
}
260+
const { annotations = [] } = mainService;
261+
for (const annotation of annotations) {
262+
if (annotation.local) {
263+
const annotationFile = await readFile(annotation.local, memFs ?? this.options?.fs);
264+
annotationData.push({
265+
dataSourceUri: annotation.local,
266+
fileContent: annotationFile.toString()
267+
});
268+
}
269+
}
270+
}
271+
return annotationData;
272+
}
273+
205274
/**
206275
* Project structure.
207276
*

packages/project-access/src/types/access/index.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { I18nBundles } from '../i18n';
44
import type { ApplicationStructure, I18nPropertiesPaths, Project, ProjectType } from '../info';
55
import type { Editor } from 'mem-fs-editor';
66
import type { Package } from '../package';
7-
import type { Manifest } from '../webapp';
7+
import type { AnnotationFile, Manifest } from '../webapp';
88

99
interface BaseAccess {
1010
readonly project: Project;
@@ -118,6 +118,32 @@ export interface ApplicationAccess extends BaseAccess {
118118
* @param memFs - optional mem-fs-editor instance
119119
*/
120120
updateManifestJSON(manifest: Manifest, memFs?: Editor): Promise<void>;
121+
122+
/**
123+
* Reads and returns the parsed `manifest.json` file for the application.
124+
*
125+
* @param memFs - optional mem-fs-editor instance
126+
* @returns A promise resolving to the parsed `manifest.json` content.
127+
*/
128+
readManifest(memFs?: Editor): Promise<Manifest>;
129+
130+
/**
131+
* Reads and returns all Flex Changes (`*.change` files) associated with the application.
132+
*
133+
* @param memFs - optional mem-fs-editor instance
134+
* @returns A promise that resolves to an array of flex change files.
135+
*/
136+
readFlexChanges(memFs?: Editor): Promise<{
137+
[key: string]: string;
138+
}>;
139+
140+
/**
141+
* Reads and returns all annotation files associated with the application's main service.
142+
*
143+
* @param memFs - optional mem-fs-editor instance
144+
* @returns A promise resolving to an array of annotation file descriptors.
145+
*/
146+
readAnnotationFiles(memFs?: Editor): Promise<AnnotationFile[]>;
121147
}
122148

123149
export interface ProjectAccessOptions {

packages/project-access/src/types/webapp/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,8 @@ import type * as ManifestNamespace from '@ui5/manifest/types/manifest';
22

33
export { ManifestNamespace };
44
export type Manifest = ManifestNamespace.SAPJSONSchemaForWebApplicationManifestFile;
5+
6+
export interface AnnotationFile {
7+
dataSourceUri: string;
8+
fileContent: string;
9+
}

packages/project-access/test/project/access.test.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import type { Manifest, Package } from '../../src';
33
import { createApplicationAccess, createProjectAccess } from '../../src';
44
import * as i18nMock from '../../src/project/i18n/write';
55
import * as specMock from '../../src/project/specification';
6+
import * as capMock from '../../src/project/cap';
67
import { create as createStorage } from 'mem-fs';
78
import { create } from 'mem-fs-editor';
89
import { promises } from 'node:fs';
10+
import { readFile, readJSON } from '../../src/file';
911

1012
describe('Test function createApplicationAccess()', () => {
1113
const memFs = create(createStorage());
@@ -373,6 +375,165 @@ describe('Test function createApplicationAccess()', () => {
373375
expect(error.message).toContain('non-existing-app');
374376
}
375377
});
378+
379+
test('Read manifest.json of standalone app without mem-fs', async () => {
380+
const appRoot = join(sampleRoot, 'fiori_elements');
381+
// Test execution
382+
const appAccess = await createApplicationAccess(appRoot);
383+
const manifest = await appAccess.readManifest();
384+
// Result check
385+
expect(manifest).toEqual(await readJSON(join(appRoot, 'webapp/manifest.json')));
386+
});
387+
388+
test('Read manifest.json of standalone app with mem-fs', async () => {
389+
const appRoot = join(sampleRoot, 'fiori_elements');
390+
const manifestPath = join(appRoot, 'webapp/manifest.json');
391+
const newManifest = await readJSON<{ dummy: boolean }>(join(appRoot, 'webapp/manifest.json'), memFs);
392+
newManifest.dummy = true;
393+
memFs.writeJSON(manifestPath, newManifest, undefined, 4);
394+
// Test execution
395+
const appAccess = await createApplicationAccess(appRoot);
396+
const manifest = await appAccess.readManifest(memFs);
397+
// Result check
398+
expect('dummy' in manifest ? manifest.dummy : undefined).toEqual(true);
399+
});
400+
401+
test('Read manifest.json of standalone app with mem-fs(mem-fs is passed on creation)', async () => {
402+
const appRoot = join(sampleRoot, 'fiori_elements');
403+
const manifestPath = join(appRoot, 'webapp/manifest.json');
404+
const newManifest = await readJSON<{ dummy: string }>(join(appRoot, 'webapp/manifest.json'), memFs);
405+
newManifest.dummy = 'Test';
406+
memFs.writeJSON(manifestPath, newManifest, undefined, 4);
407+
// Test execution
408+
const appAccess = await createApplicationAccess(appRoot, memFs);
409+
const manifest = await appAccess.readManifest();
410+
// Result check
411+
expect('dummy' in manifest ? manifest.dummy : undefined).toEqual('Test');
412+
});
413+
414+
test('Read flex changes of standalone app without mem-fs - without flex changes', async () => {
415+
const appRoot = join(sampleRoot, 'fiori_elements');
416+
// Test execution
417+
const appAccess = await createApplicationAccess(appRoot);
418+
const changes = await appAccess.readFlexChanges();
419+
// Result check
420+
expect(Object.keys(changes)).toEqual([]);
421+
});
422+
423+
test('Read flex changes of standalone app without mem-fs - with flex changes', async () => {
424+
const appRoot = join(sampleRoot, 'fiori_elements');
425+
// Test execution
426+
const appAccess = await createApplicationAccess(appRoot);
427+
// Mock changes folder
428+
appAccess.app.changes = join(__dirname, '../test-data/project/flex-changes/webapp/changes');
429+
const changes = await appAccess.readFlexChanges();
430+
// Result check
431+
expect(Object.keys(changes)).toEqual([
432+
'id_1761320220775_1_propertyChange.change',
433+
'id_1761320220775_2_propertyChange.change'
434+
]);
435+
});
436+
437+
test('Read flex changes with mem-fs', async () => {
438+
const changeFileName = 'id_1761320220775_1_propertyChange.change';
439+
const appRoot = join(sampleRoot, 'fiori_elements');
440+
const changesPath = join(__dirname, '../test-data/project/flex-changes/webapp/changes');
441+
const changeFilePath = join(changesPath, changeFileName);
442+
memFs.write(changeFilePath, '{"dummy": true}');
443+
// Test execution
444+
const appAccess = await createApplicationAccess(appRoot);
445+
appAccess.app.changes = changesPath;
446+
const changes = await appAccess.readFlexChanges(memFs);
447+
// Result check
448+
expect(changes[changeFileName]).toEqual('{"dummy": true}');
449+
});
450+
451+
test('Read flex changes with mem-fs(mem-fs is passed on creation)', async () => {
452+
const changeFileName = 'id_1761320220775_1_propertyChange.change';
453+
const appRoot = join(sampleRoot, 'fiori_elements');
454+
const changesPath = join(__dirname, '../test-data/project/flex-changes/webapp/changes');
455+
const changeFilePath = join(changesPath, changeFileName);
456+
memFs.write(changeFilePath, '{"dummy": "test"}');
457+
// Test execution
458+
const appAccess = await createApplicationAccess(appRoot, memFs);
459+
appAccess.app.changes = changesPath;
460+
const changes = await appAccess.readFlexChanges();
461+
// Result check
462+
expect(changes[changeFileName]).toEqual('{"dummy": "test"}');
463+
});
464+
465+
describe('readAnnotationFiles', () => {
466+
test('Read annotation files of standalone EDMX app without mem-fs', async () => {
467+
const appRoot = join(sampleRoot, 'fiori_elements');
468+
// Test execution
469+
const appAccess = await createApplicationAccess(appRoot);
470+
const annotationFiles = await appAccess.readAnnotationFiles();
471+
// Result check
472+
expect(annotationFiles.map((annotationFile) => annotationFile.dataSourceUri)).toEqual([
473+
join(appRoot, 'webapp/localService/metadata.xml'),
474+
join(appRoot, 'webapp/annotations/annotation.xml')
475+
]);
476+
expect(annotationFiles[0].fileContent.includes('Alias="Measures"')).toBeTruthy();
477+
expect(annotationFiles[1].fileContent.includes('/catalog-admin-noauth/$metadata')).toBeTruthy();
478+
});
479+
480+
test('Read annotation files of standalone EDMX app with mem-fs', async () => {
481+
const appRoot = join(sampleRoot, 'fiori_elements');
482+
const expectedFiles = [
483+
join(appRoot, 'webapp/localService/metadata.xml'),
484+
join(appRoot, 'webapp/annotations/annotation.xml')
485+
];
486+
memFs.write(expectedFiles[0], 'Test metadata.xml');
487+
memFs.write(expectedFiles[1], 'Test annotation.xml');
488+
// Test execution
489+
const appAccess = await createApplicationAccess(appRoot);
490+
const annotationFiles = await appAccess.readAnnotationFiles(memFs);
491+
// Result check
492+
expect(annotationFiles.map((annotationFile) => annotationFile.dataSourceUri)).toEqual(expectedFiles);
493+
expect(annotationFiles[0].fileContent).toEqual('Test metadata.xml');
494+
expect(annotationFiles[1].fileContent).toEqual('Test annotation.xml');
495+
});
496+
497+
test('Read annotation files of standalone EDMX app with mem-fs(mem-fs is passed on creation)', async () => {
498+
const appRoot = join(sampleRoot, 'fiori_elements');
499+
const expectedFiles = [
500+
join(appRoot, 'webapp/localService/metadata.xml'),
501+
join(appRoot, 'webapp/annotations/annotation.xml')
502+
];
503+
memFs.write(expectedFiles[0], 'Test2 metadata.xml');
504+
memFs.write(expectedFiles[1], 'Test2 annotation.xml');
505+
// Make sure manifest.json has original content in mem-fs
506+
const manifestPath = join(appRoot, 'webapp/manifest.json');
507+
const originalManifest = await readJSON<{ dummy: string }>(join(appRoot, 'webapp/manifest.json'));
508+
memFs.writeJSON(manifestPath, originalManifest, undefined, 4);
509+
// Test execution
510+
const appAccess = await createApplicationAccess(appRoot, memFs);
511+
const annotationFiles = await appAccess.readAnnotationFiles();
512+
// Result check
513+
expect(annotationFiles.map((annotationFile) => annotationFile.dataSourceUri)).toEqual(expectedFiles);
514+
expect(annotationFiles[0].fileContent).toEqual('Test2 metadata.xml');
515+
expect(annotationFiles[1].fileContent).toEqual('Test2 annotation.xml');
516+
});
517+
518+
test('Read annotation files of CAP app', async () => {
519+
// Mock setup
520+
const mockedMetadata = await readFile(
521+
join(sampleRoot, 'fiori_elements', 'webapp/localService/metadata.xml')
522+
);
523+
jest.spyOn(capMock, 'readCapServiceMetadataEdmx').mockResolvedValue(mockedMetadata);
524+
525+
const projectRoot = join(sampleRoot, 'cap-project');
526+
const appRoot = join(projectRoot, 'apps/two');
527+
// Test execution
528+
const appAccess = await createApplicationAccess(appRoot);
529+
const annotationFiles = await appAccess.readAnnotationFiles();
530+
// Result check
531+
expect(annotationFiles.map((annotationFile) => annotationFile.dataSourceUri)).toEqual([
532+
'/sap/opu/odata4/dmo/ODATA_SERVICE/'
533+
]);
534+
expect(annotationFiles[0].fileContent.includes('Alias="Measures"')).toBeTruthy();
535+
});
536+
});
376537
});
377538

378539
describe('Test function createProjectAccess()', () => {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<edmx:Edmx xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" Version="4.0">
2+
<edmx:Reference Uri="/sap/opu/odata/IWFND/CATALOGSERVICE;v=2/Vocabularies(TechnicalName='%2FIWBEP%2FVOC_COMMON',Version='0001',SAP__Origin='LOCAL')/$value">
3+
<edmx:Include Namespace="com.sap.vocabularies.Common.v1" Alias="Common"/>
4+
</edmx:Reference>
5+
<edmx:Reference Uri="/sap/opu/odata/IWFND/CATALOGSERVICE;v=2/Vocabularies(TechnicalName='%2FIWBEP%2FVOC_UI',Version='0001',SAP__Origin='LOCAL')/$value">
6+
<edmx:Include Namespace="com.sap.vocabularies.UI.v1" Alias="UI"/>
7+
</edmx:Reference>
8+
<edmx:Reference Uri="/sap/opu/odata/IWFND/CATALOGSERVICE;v=2/Vocabularies(TechnicalName='%2FIWBEP%2FVOC_COMMUNICATION',Version='0001',SAP__Origin='LOCAL')/$value">
9+
<edmx:Include Namespace="com.sap.vocabularies.Communication.v1" Alias="Communication"/>
10+
</edmx:Reference>
11+
<edmx:Reference Uri="/catalog-admin-noauth/$metadata">
12+
<edmx:Include Namespace="sap.capire.officesupplies.CatalogAdminNoauth" Alias="sap.capire.officesupplies.CatalogAdminNoauth"/>
13+
</edmx:Reference>
14+
<edmx:DataServices>
15+
<Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="Namespace1">
16+
</Schema>
17+
</edmx:DataServices>
18+
</edmx:Edmx>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
3+
<edmx:Reference Uri="https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Capabilities.V1.xml">
4+
<edmx:Include Alias="Capabilities" Namespace="Org.OData.Capabilities.V1"/>
5+
</edmx:Reference>
6+
<edmx:Reference Uri="https://sap.github.io/odata-vocabularies/vocabularies/Common.xml">
7+
<edmx:Include Alias="Common" Namespace="com.sap.vocabularies.Common.v1"/>
8+
</edmx:Reference>
9+
<edmx:Reference Uri="https://sap.github.io/odata-vocabularies/vocabularies/Communication.xml">
10+
<edmx:Include Alias="Communication" Namespace="com.sap.vocabularies.Communication.v1"/>
11+
</edmx:Reference>
12+
<edmx:Reference Uri="https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Core.V1.xml">
13+
<edmx:Include Alias="Core" Namespace="Org.OData.Core.V1"/>
14+
</edmx:Reference>
15+
<edmx:Reference Uri="https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Measures.V1.xml">
16+
<edmx:Include Alias="Measures" Namespace="Org.OData.Measures.V1"/>
17+
</edmx:Reference>
18+
<edmx:Reference Uri="https://sap.github.io/odata-vocabularies/vocabularies/UI.xml">
19+
<edmx:Include Alias="UI" Namespace="com.sap.vocabularies.UI.v1"/>
20+
</edmx:Reference>
21+
<edmx:Reference Uri="https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Validation.V1.xml">
22+
<edmx:Include Alias="Validation" Namespace="Org.OData.Validation.V1"/>
23+
</edmx:Reference>
24+
<edmx:DataServices>
25+
<Schema Namespace="sap.officesupplies.Catalog" xmlns="http://docs.oasis-open.org/odata/ns/edm">
26+
</Schema>
27+
</edmx:DataServices>
28+
</edmx:Edmx>

0 commit comments

Comments
 (0)