Skip to content

Commit 10e0b09

Browse files
Add @EntityRelationship annotation (#56)
* add test * Delete .vscode/settings.json * clean up * Update CHANGELOG.md * remove snapshot and refactor similar test pattern * add fixed title --------- Co-authored-by: Roshni Naveena S <[email protected]>
1 parent eb62038 commit 10e0b09

File tree

4 files changed

+174
-56
lines changed

4 files changed

+174
-56
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
66

77
## Version 1.2.0 - tbd
88

9+
### Added
10+
11+
- Now supports `@EntityRelationship` annotations.
12+
13+
### Fixed
14+
915
- Fixed action/function invocation on navigation path to align with CAP runtime.
1016

1117
## Version 1.1.2 - 27.01.2025

lib/compile/csdl2openapi.js

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,19 @@ const ODM_ANNOTATIONS = Object.freeze(
4141
'@ODM.oid': 'x-sap-odm-oid'
4242
});
4343

44+
const ER_ANNOTATION_PREFIX = '@EntityRelationship'
45+
const ER_ANNOTATIONS = Object.freeze(
46+
{
47+
'@EntityRelationship.entityType': 'x-entity-relationship-entity-type',
48+
'@EntityRelationship.entityIds': 'x-entity-relationship-entity-ids',
49+
'@EntityRelationship.propertyType': 'x-entity-relationship-property-type',
50+
'@EntityRelationship.reference': 'x-entity-relationship-reference',
51+
'@EntityRelationship.compositeReferences': 'x-entity-relationship-composite-references',
52+
'@EntityRelationship.temporalIds': 'x-entity-relationship-temporal-ids',
53+
'@EntityRelationship.temporalReferences': 'x-entity-relationship-temporal-references',
54+
'@EntityRelationship.referencesWithConstantIds': 'x-entity-relationship-references-with-constant-ids'
55+
});
56+
4457
/**
4558
* Construct an OpenAPI description from a CSDL document
4659
* @param {object} csdl CSDL document
@@ -589,7 +602,7 @@ module.exports.csdl2openapi = function (
589602
*/
590603
function getTags(container) {
591604
const tags = new Map();
592-
// all entity sets and singletons
605+
// all entity sets and singletons
593606
Object.keys(container)
594607
.filter(name => isIdentifier(name) && container[name].$Type)
595608
.forEach(child => {
@@ -2023,6 +2036,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot
20232036

20242037
if (suffix === SUFFIX.read && type["@ODM.root"]) schemas[schemaName]["x-sap-root-entity"] = type["@ODM.root"]
20252038
odmExtensions(type, schemas[schemaName]);
2039+
erExtensions(type, schemas[schemaName]);
20262040

20272041
if (suffix === SUFFIX.create && required.length > 0)
20282042
schemas[schemaName].required = [...new Set(required)];
@@ -2052,6 +2066,17 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot
20522066
}
20532067
}
20542068

2069+
/**
2070+
* Add entity relationship extensions to OpenAPI schema for a structured type
2071+
* @param {object} type Structured type
2072+
* @param {object} schema OpenAPI schema to augment
2073+
*/
2074+
function erExtensions(type, schema) {
2075+
for (const [annotation, openApiExtension] of Object.entries(ER_ANNOTATIONS)) {
2076+
if (type[annotation]) schema[openApiExtension] = type[annotation];
2077+
}
2078+
}
2079+
20552080
/**
20562081
* Collect all properties of a structured type along the inheritance hierarchy
20572082
* @param {object} type Structured type
@@ -2423,6 +2448,12 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot
24232448
if (element['@ODM.oidReference']?.entityName) {
24242449
s['x-sap-odm-oid-reference-entity-name'] = element['@ODM.oidReference'].entityName
24252450
}
2451+
2452+
for (const key in element) {
2453+
if (key.startsWith(ER_ANNOTATION_PREFIX) && ER_ANNOTATIONS[key]) {
2454+
s[ER_ANNOTATIONS[key]] = element[key];
2455+
}
2456+
}
24262457
return s;
24272458
}
24282459

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,4 @@
3434
"jest": "^29",
3535
"eslint": "^8.56.0"
3636
}
37-
}
37+
}

test/lib/compile/openapi.test.js

Lines changed: 135 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -12,64 +12,64 @@ const SCENARIO = Object.freeze({
1212

1313
function checkAnnotations(csn, annotations, scenario = SCENARIO.positive, property = '') {
1414
const openApi = toOpenApi(csn);
15-
const schemas = Object.entries(openApi.components.schemas).filter(([key]) => key.startsWith('sap.odm.test.A.E1'))
16-
// Test if the openAPI document was generated with some schemas.
17-
expect(openApi.components.schemas).toBeDefined()
18-
expect(openApi).toBeDefined()
19-
expect(schemas.length > 0).toBeTruthy()
20-
21-
// Expect that not-allowed ODM annotations are unavailable in the schema.
22-
if (scenario === SCENARIO.notAllowedAnnotations) {
23-
for (const [, schema] of schemas) {
24-
for (const [annKey] of annotations) {
25-
expect(schema[annKey]).not.toBeDefined()
26-
}
27-
}
28-
return;
29-
}
30-
31-
// Expect that even the ODM annotations with not-matched values will be derived.
32-
if (scenario === SCENARIO.notMatchingValues) {
33-
for (const [, schema] of schemas) {
34-
for (const [annKey, annValue] of annotations) {
35-
expect(schema[annKey]).toBe(annValue)
36-
}
15+
const schemas = Object.entries(openApi.components.schemas).filter(([key]) => key.startsWith('sap.odm.test.A.E1'))
16+
// Test if the openAPI document was generated with some schemas.
17+
expect(openApi.components.schemas).toBeDefined()
18+
expect(openApi).toBeDefined()
19+
expect(schemas.length > 0).toBeTruthy()
20+
21+
// Expect that not-allowed ODM annotations are unavailable in the schema.
22+
if (scenario === SCENARIO.notAllowedAnnotations) {
23+
for (const [, schema] of schemas) {
24+
for (const [annKey] of annotations) {
25+
expect(schema[annKey]).not.toBeDefined()
3726
}
38-
return;
3927
}
28+
return;
29+
}
4030

41-
if (scenario === SCENARIO.checkProperty) {
42-
for (const [, schema] of schemas) {
43-
const propertyObj = schema.properties[property]
44-
for (const [annKey, annValue] of annotations) {
45-
expect(propertyObj[annKey]).toBe(annValue)
46-
}
31+
// Expect that even the ODM annotations with not-matched values will be derived.
32+
if (scenario === SCENARIO.notMatchingValues) {
33+
for (const [, schema] of schemas) {
34+
for (const [annKey, annValue] of annotations) {
35+
expect(schema[annKey]).toBe(annValue)
4736
}
48-
return
4937
}
38+
return;
39+
}
5040

41+
if (scenario === SCENARIO.checkProperty) {
5142
for (const [, schema] of schemas) {
43+
const propertyObj = schema.properties[property]
5244
for (const [annKey, annValue] of annotations) {
53-
expect(schema[annKey]).toBe(annValue)
45+
expect(propertyObj[annKey]).toBe(annValue)
5446
}
5547
}
48+
return
49+
}
5650

57-
// Test that no other places contain the ODM extensions in the OpenAPI document.
58-
59-
// components.schemas where the schemas are not from entity E1.
60-
const notE1 = Object.entries(openApi.components.schemas).filter(([key]) => !key.startsWith('sap.odm.test.A.E1'))
61-
for (const [, schema] of notE1) {
62-
const schemaString = JSON.stringify(schema)
63-
for (const [annKey] of annotations) {
64-
expect(schemaString).not.toContain(annKey)
65-
}
51+
for (const [, schema] of schemas) {
52+
for (const [annKey, annValue] of annotations) {
53+
expect(schema[annKey]).toBe(annValue)
6654
}
55+
}
56+
57+
// Test that no other places contain the ODM extensions in the OpenAPI document.
6758

68-
// all other components of the OpenAPI document except the schemas.
69-
const openApiNoSchemas = JSON.stringify({ ...openApi, components: { parameters: { ...openApi.components.parameters }, responses: { ...openApi.components.responses } } })
59+
// components.schemas where the schemas are not from entity E1.
60+
const notE1 = Object.entries(openApi.components.schemas).filter(([key]) => !key.startsWith('sap.odm.test.A.E1'))
61+
for (const [, schema] of notE1) {
62+
const schemaString = JSON.stringify(schema)
7063
for (const [annKey] of annotations) {
71-
expect(openApiNoSchemas).not.toContain(annKey)
64+
expect(schemaString).not.toContain(annKey)
7265
}
66+
}
67+
68+
// all other components of the OpenAPI document except the schemas.
69+
const openApiNoSchemas = JSON.stringify({ ...openApi, components: { parameters: { ...openApi.components.parameters }, responses: { ...openApi.components.responses } } })
70+
for (const [annKey] of annotations) {
71+
expect(openApiNoSchemas).not.toContain(annKey)
72+
}
7373

7474
}
7575

@@ -416,6 +416,87 @@ service CatalogService {
416416
)
417417
})
418418

419+
describe('ER annotations', () => {
420+
test('er annotations is correct', () => {
421+
const csn = cds.compile.to.csn(`
422+
service A {
423+
@EntityRelationship.entityType: 'sap.vdm.sont:Material'
424+
@EntityRelationship.entityIds : [{propertyTypes: ['sap.vdm.gfn:MaterialId']}]
425+
@ODM.entityName : 'Product'
426+
@ODM.oid : 'oid'
427+
entity Material {
428+
@EntityRelationship.propertyType: 'sap.vdm.gfn:MaterialId'
429+
key ObjectID : String(18);
430+
431+
@EntityRelationship.reference : {
432+
referencedEntityType : 'sap.vdm.sont:BusinessPartner',
433+
referencedPropertyType: 'sap.vdm.gfn::BusinessPartnerNumber'
434+
}
435+
manufacturer : String(40);
436+
437+
@EntityRelationship.reference : {
438+
referencedEntityType : 'sap.sm:PurchaseOrder',
439+
referencedPropertyType: 'sap.sm:PurchaseOrderUUID'
440+
}
441+
@ODM.oidReference.entityName : 'PurchaseOrder'
442+
PurchaseOrder : UUID;
443+
444+
@EntityRelationship.reference : {
445+
referencedEntityType : 'sap.vdm.sont:BillOfMaterial',
446+
referencedPropertyType: 'sap.vdm.gfn:BillOfMaterialId'
447+
}
448+
BOM : String(30);
449+
}
450+
}
451+
`)
452+
const openAPI = toOpenApi(csn);
453+
expect(openAPI).toBeDefined();
454+
const materialSchema = openAPI.components.schemas["A.Material"];
455+
expect(materialSchema).toBeDefined();
456+
expect(materialSchema["x-entity-relationship-entity-type"]).toBe('sap.vdm.sont:Material');
457+
expect(materialSchema["x-entity-relationship-entity-ids"]).toMatchObject([{ "propertyTypes": ["sap.vdm.gfn:MaterialId"] }]);
458+
expect(materialSchema["x-sap-odm-entity-name"]).toBe('Product');
459+
expect(materialSchema["x-sap-odm-oid"]).toBe('oid');
460+
461+
const properties = materialSchema.properties;
462+
expect(properties).toBeDefined();
463+
expect(properties.ObjectID).toMatchObject({
464+
maxLength: 18,
465+
type: 'string',
466+
"x-entity-relationship-property-type": 'sap.vdm.gfn:MaterialId'
467+
});
468+
expect(properties.manufacturer).toMatchObject({
469+
maxLength: 40,
470+
nullable: true,
471+
type: 'string',
472+
"x-entity-relationship-reference": {
473+
referencedEntityType: 'sap.vdm.sont:BusinessPartner',
474+
referencedPropertyType: 'sap.vdm.gfn::BusinessPartnerNumber'
475+
}
476+
});
477+
expect(properties.PurchaseOrder).toMatchObject({
478+
example: '01234567-89ab-cdef-0123-456789abcdef',
479+
format: 'uuid',
480+
nullable: true,
481+
type: 'string',
482+
"x-entity-relationship-reference": {
483+
referencedEntityType: 'sap.sm:PurchaseOrder',
484+
referencedPropertyType: 'sap.sm:PurchaseOrderUUID'
485+
},
486+
"x-sap-odm-oid-reference-entity-name": 'PurchaseOrder'
487+
});
488+
expect(properties.BOM).toMatchObject({
489+
maxLength: 30,
490+
nullable: true,
491+
type: 'string',
492+
"x-entity-relationship-reference": {
493+
referencedEntityType: 'sap.vdm.sont:BillOfMaterial',
494+
referencedPropertyType: 'sap.vdm.gfn:BillOfMaterialId'
495+
}
496+
});
497+
})
498+
});
499+
419500
test('OpenAPI annotations: @OpenAPI.externalDocs annotation is added to the schema', () => {
420501
const csn = cds.compile.to.csn(`
421502
namespace sap.OpenAPI.test;
@@ -475,16 +556,16 @@ service CatalogService {
475556
476557
}`);
477558
const openAPI = toOpenApi(csn);
478-
expect(openAPI).toBeDefined();
479-
expect(openAPI['x-sap-compliance-level']).toBe('sap:base:v1');
480-
expect(openAPI['x-sap-ext-overview'].name).toBe('Communication Scenario');
481-
expect(openAPI['x-sap-ext-overview'].values.text).toBe('Planning Calendar API Integration');
482-
expect(openAPI['x-sap-ext-overview'].values.format).toBe('plain');
483-
expect(openAPI.components.schemas["sap.OpenAPI.test.A.E1"]["x-sap-dpp-is-potentially-sensitive"]).toBe('true');
484-
expect(openAPI.paths["/F1"].get["x-sap-operation-intent"]).toBe('read-collection for function');
485-
expect(openAPI.paths["/A1"].post["x-sap-operation-intent"]).toBe('read-collection for action');
486-
expect(openAPI.paths["/F1"].get["x-sap-deprecated-operation"].deprecationDate).toBe('2022-12-31');
487-
expect(openAPI.paths["/F1"].get["x-sap-deprecated-operation"].successorOperationId).toBe('successorOperation');
488-
expect(openAPI.paths["/F1"].get["x-sap-deprecated-operation"].notValidKey).toBeUndefined();
559+
expect(openAPI).toBeDefined();
560+
expect(openAPI['x-sap-compliance-level']).toBe('sap:base:v1');
561+
expect(openAPI['x-sap-ext-overview'].name).toBe('Communication Scenario');
562+
expect(openAPI['x-sap-ext-overview'].values.text).toBe('Planning Calendar API Integration');
563+
expect(openAPI['x-sap-ext-overview'].values.format).toBe('plain');
564+
expect(openAPI.components.schemas["sap.OpenAPI.test.A.E1"]["x-sap-dpp-is-potentially-sensitive"]).toBe('true');
565+
expect(openAPI.paths["/F1"].get["x-sap-operation-intent"]).toBe('read-collection for function');
566+
expect(openAPI.paths["/A1"].post["x-sap-operation-intent"]).toBe('read-collection for action');
567+
expect(openAPI.paths["/F1"].get["x-sap-deprecated-operation"].deprecationDate).toBe('2022-12-31');
568+
expect(openAPI.paths["/F1"].get["x-sap-deprecated-operation"].successorOperationId).toBe('successorOperation');
569+
expect(openAPI.paths["/F1"].get["x-sap-deprecated-operation"].notValidKey).toBeUndefined();
489570
});
490571
});

0 commit comments

Comments
 (0)