From f08f583c9c5797dec4a535efd0dbb959e13e97fe Mon Sep 17 00:00:00 2001 From: Oliver Klemenz Date: Wed, 9 Aug 2023 16:24:44 +0200 Subject: [PATCH] @cov2ap.analytics.skipForKey --- CHANGELOG.md | 3 +- README.md | 1 + package-lock.json | 12 +-- src/index.js | 104 ++++++++++++++-------- test/__snapshots__/analytics-test.js.snap | 26 ++++++ test/_env/srv/analytics.cds | 15 ++++ test/analytics-test.js | 30 +++++++ 7 files changed, 147 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2681c9df..768dffdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## Version 1.11.6 - 2023-09-xx +## Version 1.11.6 - 2023-08-10 ### Changed - Switch to `better-sqlite3` via `@cap-js/sqlite` +- Suppress analytical conversion via entity annotation `@cov2ap.analytics.skipForKey`, if all dimension key elements are requested ## Version 1.11.5 - 2023-08-02 diff --git a/README.md b/README.md index eab93bc8..38fda97a 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,7 @@ The following OData V2 adapter for CDS specific annotations are supported: **Entity Level**: - `@cov2ap.analytics: false`: Suppress analytics conversion for the annotated entity, if set to `false`. +- `@cov2ap.analytics.skipForKey`: Suppress analytical conversion for the annotated entity, if all dimension key elements are requested - `@cov2ap.deltaResponse: 'timestamp'`: Delta response '\_\_delta' is added to response data of annotated entity with current timestamp information. - `@cov2ap.isoTime`: Values of type cds.Time (Edm.Time) are represented in ISO 8601 format for annotated entity. - `@cov2ap.isoDate`: Values of type cds.Date (Edm.DateTime) are represented in ISO 8601 format for annotated entity. diff --git a/package-lock.json b/package-lock.json index f89eb94f..01afe20c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1709,9 +1709,9 @@ "devOptional": true }, "node_modules/@types/node": { - "version": "20.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.8.tgz", - "integrity": "sha512-0mHckf6D2DiIAzh8fM8f3HQCvMKDpK94YQ0DSVkfWTG9BZleYIWudw9cJxX8oCk9bM+vAkDyujDV6dmKHbvQpg==" + "version": "20.4.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.9.tgz", + "integrity": "sha512-8e2HYcg7ohnTUbHk8focoklEQYvemQmu9M/f43DZVx43kHn0tE3BY/6gSDxS7k0SprtS0NHvj+L80cGLnoOUcQ==" }, "node_modules/@types/qs": { "version": "6.9.7", @@ -3013,9 +3013,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.487", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.487.tgz", - "integrity": "sha512-XbCRs/34l31np/p33m+5tdBrdXu9jJkZxSbNxj5I0H1KtV2ZMSB+i/HYqDiRzHaFx2T5EdytjoBRe8QRJE2vQg==", + "version": "1.4.488", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.488.tgz", + "integrity": "sha512-Dv4sTjiW7t/UWGL+H8ZkgIjtUAVZDgb/PwGWvMsCT7jipzUV/u5skbLXPFKb6iV0tiddVi/bcS2/kUrczeWgIQ==", "dev": true }, "node_modules/emittery": { diff --git a/src/index.js b/src/index.js index 5816a1f4..2acc8943 100644 --- a/src/index.js +++ b/src/index.js @@ -2123,24 +2123,17 @@ function cov2ap(options = {}) { function convertAnalytics(url, req) { const definition = req.context && req.context.definition; - if ( - !( - definition && - definition.kind === "entity" && - definition["@cov2ap.analytics"] !== false && - url.query["$select"] && - (definition["@cov2ap.analytics"] === true || - definition["@Analytics"] || - definition["@Analytics.AnalyticalContext"] || - definition["@Analytics.query"] || - definition["@AnalyticalContext"] || - definition["@Aggregation.ApplySupported.PropertyRestrictions"] || - definition["@sap.semantics"] === "aggregate") - ) - ) { + if (!(isAnalyticsEntity(definition) && url.query["$select"])) { return; } const elements = req.context.definitionElements; + const keyDimensions = []; + for (const name of structureKeys(elements)) { + const element = elements[name]; + if (isDimensionElement(element) && element.key) { + keyDimensions.push(element); + } + } const measures = []; const dimensions = []; const selects = url.query["$select"].split(","); @@ -2154,20 +2147,22 @@ function cov2ap(options = {}) { selects.forEach((select) => { const element = elements[select]; if (element) { - if ( - element["@Analytics.AnalyticalContext.Measure"] || - element["@AnalyticalContext.Measure"] || - element["@Analytics.Measure"] || - element["@sap.aggregation.role"] === "measure" - ) { + if (isMeasureElement(element)) { measures.push(element); } else { - // element["@Analytics.AnalyticalContext.Dimension"] || element["@AnalyticalContext.Dimension"] || element["@Analytics.Dimension"] || element["@sap.aggregation.role"] === "dimension" + // isDimensionElement(element) dimensions.push(element); } } }); + if ( + definition["@cov2ap.analytics.skipForKey"] && + keyDimensions.every((keyDimension) => dimensions.includes(keyDimension)) + ) { + return; + } + if (dimensions.length > 0 || measures.length > 0) { url.query["$apply"] = ""; if (dimensions.length) { @@ -2184,8 +2179,7 @@ function cov2ap(options = {}) { } url.query["$apply"] += `aggregate(${measures .map((measure) => { - const aggregation = - measure["@Aggregation.default"] || measure["@Aggregation.Default"] || measure["@DefaultAggregation"]; + const aggregation = aggregationDefault(measure); const aggregationName = aggregation ? aggregation["#"] || aggregation : DefaultAggregation; const aggregationFunction = aggregationName ? AggregationMap[aggregationName.toUpperCase()] : undefined; if (!aggregationFunction) { @@ -2197,12 +2191,7 @@ function cov2ap(options = {}) { if (aggregationFunction.startsWith("$")) { return `${aggregationFunction} as ${AggregationPrefix}${measure.name}`; } else { - const referenceElement = - measure["@Aggregation.referenceElement"] || - measure["@Aggregation.ReferenceElement"] || - measure["@Aggregation.reference"] || - measure["@Aggregation.Reference"]; - return `${referenceElement || measure.name} with ${aggregationFunction} as ${AggregationPrefix}${ + return `${referenceElement(measure) || measure.name} with ${aggregationFunction} as ${AggregationPrefix}${ measure.name }`; } @@ -2229,13 +2218,7 @@ function cov2ap(options = {}) { .map((orderBy) => { let [name, order] = orderBy.split(" "); const element = elements[name]; - if ( - element && - (element["@Analytics.AnalyticalContext.Measure"] || - element["@AnalyticalContext.Measure"] || - element["@Analytics.Measure"] || - element["@sap.aggregation.role"] === "measure") - ) { + if (element && isMeasureElement(element)) { name = `${AggregationPrefix}${element.name}`; } return name + (order ? ` ${order}` : ""); @@ -2257,6 +2240,53 @@ function cov2ap(options = {}) { } } + function isAnalyticsEntity(entity) { + return ( + entity && + entity.kind === "entity" && + entity["@cov2ap.analytics"] !== false && + (entity["@cov2ap.analytics"] || + entity["@cov2ap.analytics.skipForKey"] || + entity["@Analytics"] || + entity["@Analytics.AnalyticalContext"] || + entity["@Analytics.query"] || + entity["@AnalyticalContext"] || + entity["@Aggregation.ApplySupported.PropertyRestrictions"] || + entity["@sap.semantics"] === "aggregate") + ); + } + + function isDimensionElement(element) { + return ( + element["@Analytics.AnalyticalContext.Dimension"] || + element["@AnalyticalContext.Dimension"] || + element["@Analytics.Dimension"] || + element["@sap.aggregation.role"] === "dimension" + ); + } + + function isMeasureElement(element) { + return ( + element["@Analytics.AnalyticalContext.Measure"] || + element["@AnalyticalContext.Measure"] || + element["@Analytics.Measure"] || + element["@sap.aggregation.role"] === "measure" + ); + } + + function aggregationDefault(element) { + return element["@Aggregation.default"] || element["@Aggregation.Default"] || element["@DefaultAggregation"]; + } + + function referenceElement(element) { + return ( + element["@Aggregation.referenceElement"] || + element["@Aggregation.ReferenceElement"] || + element["@Aggregation.reference"] || + element["@Aggregation.Reference"] + ); + } + function convertValue(url, req) { if (url.contextPath.endsWith("/$value")) { url.contextPath = url.contextPath.substr(0, url.contextPath.length - "/$value".length); diff --git a/test/__snapshots__/analytics-test.js.snap b/test/__snapshots__/analytics-test.js.snap index 763a9503..c225f066 100644 --- a/test/__snapshots__/analytics-test.js.snap +++ b/test/__snapshots__/analytics-test.js.snap @@ -35,6 +35,7 @@ exports[`analytics GET $metadata 1`] = ` + @@ -160,6 +161,19 @@ exports[`analytics GET $metadata 1`] = ` + + + + + + + + + + + + + @@ -729,6 +743,18 @@ exports[`analytics GET $metadata 1`] = ` + + + + + + + + + + + + diff --git a/test/_env/srv/analytics.cds b/test/_env/srv/analytics.cds index fdf57911..d3d04455 100644 --- a/test/_env/srv/analytics.cds +++ b/test/_env/srv/analytics.cds @@ -91,4 +91,19 @@ service AnalyticsService { } actions { action order(number: Integer) returns Book; }; + + @cov2ap.analytics.skipForKey + entity HeaderSkipKey as projection on test.Header { + key ID, + description, + @Analytics.Dimension + key country, + @Analytics.Dimension + key currency, + @Analytics.Measure + stock, + @Analytics.Measure + @Aggregation.default : #AVG + price, + }; } diff --git a/test/analytics-test.js b/test/analytics-test.js index bcf0b11a..b0009652 100644 --- a/test/analytics-test.js +++ b/test/analytics-test.js @@ -949,4 +949,34 @@ describe("analytics", () => { }, }); }); + + it("Skip analytics if all dimension key elements are requested", async () => { + let response = await util.callRead(request, "/odata/v2/analytics/HeaderSkipKey?$select=country,currency,stock"); + expect(response.body).toBeDefined(); + expect(response.body.d).toBeDefined(); + expect(response.body.d.results).toBeDefined(); + // no aggregation should have happened + expect(response.body.d.results.length).toEqual(7); + + response = await util.callRead(request, "/odata/v2/analytics/HeaderSkipKey?$select=currency,stock"); + expect(response.body).toBeDefined(); + expect(response.body.d).toBeDefined(); + expect(response.body.d.results).toBeDefined(); + // aggregation should have happened + expect(response.body.d.results.length).toEqual(5); + + response = await util.callRead(request, "/odata/v2/analytics/HeaderSkipKey?$select=ID,country,currency,stock"); + expect(response.body).toBeDefined(); + expect(response.body.d).toBeDefined(); + expect(response.body.d.results).toBeDefined(); + // no aggregation should have happened + expect(response.body.d.results.length).toEqual(7); + + response = await util.callRead(request, "/odata/v2/analytics/HeaderSkipKey?$select=stock"); + expect(response.body).toBeDefined(); + expect(response.body.d).toBeDefined(); + expect(response.body.d.results).toBeDefined(); + // aggregation should have happened + expect(response.body.d.results.length).toEqual(1); + }); });