From 9e9af3ce9575445037900d653c0937774d00587b Mon Sep 17 00:00:00 2001 From: Gregor Wolf Date: Sun, 22 Nov 2020 21:37:38 +0100 Subject: [PATCH] feat: early version of draft support (#50) --- .gitignore | 1 - .vscode/launch.json | 18 ++++++ CHANGELOG.md | 6 +- .../cap-proj/db/data/csw-TypeChecks.csv | 2 + .../cap-proj/default-env-template.json | 18 ++++++ __tests__/__assets__/cap-proj/package.json | 15 +++++ .../cap-proj/rest-client-test/draft.http | 6 ++ .../cap-proj/srv/beershop-service.cds | 9 ++- __tests__/__assets__/test.sql | 64 ++++++++++++++++++- __tests__/lib/pg/service.test.js | 43 +++++++++++++ lib/pg/sql-builder/ReferenceBuilder.js | 23 ------- lib/pg/sql-builder/SelectBuilder.js | 41 ++++++++++++ lib/pg/utils/columns.js | 16 +++-- package.json | 1 + 14 files changed, 224 insertions(+), 39 deletions(-) create mode 100644 __tests__/__assets__/cap-proj/db/data/csw-TypeChecks.csv create mode 100644 __tests__/__assets__/cap-proj/default-env-template.json create mode 100644 __tests__/__assets__/cap-proj/package.json create mode 100644 __tests__/__assets__/cap-proj/rest-client-test/draft.http diff --git a/.gitignore b/.gitignore index 1c054b1..015f476 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,4 @@ .eslintcache node_modules/ .idea/ -__tests__/__assets__/cap-proj/package* *-env.json \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 9306bc8..cbb3d44 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -23,6 +23,24 @@ "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "port": 9229 + }, + { + "type": "node", + "request": "launch", + "name": "cds run pg", + "args": ["run"], + "cwd": "${workspaceFolder}/__tests__/__assets__/cap-proj", + "program": "${workspaceFolder}/node_modules/@sap/cds/bin/cds.js", + "console": "integratedTerminal" + }, + { + "type": "node", + "request": "launch", + "name": "cds run sqlite", + "args": ["run", "--in-memory"], + "cwd": "${workspaceFolder}/__tests__/__assets__/cap-proj", + "program": "${workspaceFolder}/node_modules/@sap/cds/bin/cds.js", + "console": "integratedTerminal" } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b4cfe0..c418ab8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,17 +12,15 @@ All notable changes to this project will be documented in this file. See [standa ### 0.0.24 (2020-11-17) - ### Features -* add multi schema feature ([#44](https://github.com/sapmentors/cds-pg/issues/44)) ([2eb6835](https://github.com/sapmentors/cds-pg/commit/2eb6835bcdaef2f37039eb5be5bad4f4cd5e50f2)) +- add multi schema feature ([#44](https://github.com/sapmentors/cds-pg/issues/44)) ([2eb6835](https://github.com/sapmentors/cds-pg/commit/2eb6835bcdaef2f37039eb5be5bad4f4cd5e50f2)) ### 0.0.23 (2020-11-16) - ### Features -* support SAP CP CF PG Hyperscaler Service ([#43](https://github.com/sapmentors/cds-pg/issues/43)) ([c0ba1dd](https://github.com/sapmentors/cds-pg/commit/c0ba1dde81fe5c30a9e1fec9ba6b7d71c51ac3cb)) +- support SAP CP CF PG Hyperscaler Service ([#43](https://github.com/sapmentors/cds-pg/issues/43)) ([c0ba1dd](https://github.com/sapmentors/cds-pg/commit/c0ba1dde81fe5c30a9e1fec9ba6b7d71c51ac3cb)) ### 0.0.22 (2020-11-13) diff --git a/__tests__/__assets__/cap-proj/db/data/csw-TypeChecks.csv b/__tests__/__assets__/cap-proj/db/data/csw-TypeChecks.csv new file mode 100644 index 0000000..aef86ac --- /dev/null +++ b/__tests__/__assets__/cap-proj/db/data/csw-TypeChecks.csv @@ -0,0 +1,2 @@ +ID,type_String,type_LargeString +5e4ca9ef-7c4c-4b22-8e85-7cadefa02c94,Guía del autoestopista galáctico,"At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat." \ No newline at end of file diff --git a/__tests__/__assets__/cap-proj/default-env-template.json b/__tests__/__assets__/cap-proj/default-env-template.json new file mode 100644 index 0000000..d336641 --- /dev/null +++ b/__tests__/__assets__/cap-proj/default-env-template.json @@ -0,0 +1,18 @@ +{ + "VCAP_SERVICES": { + "postgres-local": [ + { + "name": "postgres", + "label": "postgres", + "tags": ["database"], + "credentials": { + "host": "localhost", + "port": "5432", + "database": "beershop", + "user": "postgres", + "password": "postgres" + } + } + ] + } +} diff --git a/__tests__/__assets__/cap-proj/package.json b/__tests__/__assets__/cap-proj/package.json new file mode 100644 index 0000000..bf2d10d --- /dev/null +++ b/__tests__/__assets__/cap-proj/package.json @@ -0,0 +1,15 @@ +{ + "cds": { + "requires": { + "db": { + "kind": "database" + }, + "database": { + "impl": "../../../", + "model": [ + "srv" + ] + } + } + } +} diff --git a/__tests__/__assets__/cap-proj/rest-client-test/draft.http b/__tests__/__assets__/cap-proj/rest-client-test/draft.http new file mode 100644 index 0000000..06a7e95 --- /dev/null +++ b/__tests__/__assets__/cap-proj/rest-client-test/draft.http @@ -0,0 +1,6 @@ +### Create new TypeChecksWithDraft +POST http://localhost:4004/beershop/TypeChecksWithDraft +Accept:application/json;odata.metadata=minimal;IEEE754Compatible=true +Content-Type:application/json;charset=UTF-8;IEEE754Compatible=true + +{} \ No newline at end of file diff --git a/__tests__/__assets__/cap-proj/srv/beershop-service.cds b/__tests__/__assets__/cap-proj/srv/beershop-service.cds index 34f8ae2..c773cc9 100644 --- a/__tests__/__assets__/cap-proj/srv/beershop-service.cds +++ b/__tests__/__assets__/cap-proj/srv/beershop-service.cds @@ -2,7 +2,10 @@ using {csw} from '../db/schema'; service BeershopService { - entity Beers as projection on csw.Beers; - entity Breweries as projection on csw.Brewery; - entity TypeChecks as projection on csw.TypeChecks; + entity Beers as projection on csw.Beers; + entity Breweries as projection on csw.Brewery; + entity TypeChecks as projection on csw.TypeChecks; + + @odata.draft.enabled + entity TypeChecksWithDraft as projection on csw.TypeChecks; } diff --git a/__tests__/__assets__/test.sql b/__tests__/__assets__/test.sql index 498e334..3dba1f2 100644 --- a/__tests__/__assets__/test.sql +++ b/__tests__/__assets__/test.sql @@ -31,6 +31,40 @@ CREATE TABLE csw_TypeChecks ( PRIMARY KEY(ID) ); +CREATE TABLE DRAFT_DraftAdministrativeData ( + DraftUUID VARCHAR(36) NOT NULL, + CreationDateTime TIMESTAMP, + CreatedByUser VARCHAR(256), + DraftIsCreatedByMe BOOLEAN, + LastChangeDateTime TIMESTAMP, + LastChangedByUser VARCHAR(256), + InProcessByUser VARCHAR(256), + DraftIsProcessedByMe BOOLEAN, + PRIMARY KEY(DraftUUID) +); + +CREATE TABLE BeershopService_TypeChecksWithDraft_drafts ( + ID VARCHAR(36) NOT NULL, + type_Boolean BOOLEAN NULL, + type_Int32 INTEGER NULL, + type_Int64 BIGINT NULL, + type_Decimal DECIMAL(2, 1) NULL, + type_Double NUMERIC(30, 15) NULL, + type_Date DATE NULL, + type_Time TIME NULL, + type_DateTime TIMESTAMP NULL, + type_Timestamp TIMESTAMP NULL, + type_String VARCHAR(5000) NULL, + type_Binary CHAR(100) NULL, + type_LargeBinary BYTEA NULL, + type_LargeString TEXT NULL, + IsActiveEntity BOOLEAN, + HasActiveEntity BOOLEAN, + HasDraftEntity BOOLEAN, + DraftAdministrativeData_DraftUUID VARCHAR(36) NOT NULL, + PRIMARY KEY(ID) +); + CREATE VIEW BeershopService_Beers AS SELECT Beers_0.ID, Beers_0.name, @@ -59,4 +93,32 @@ CREATE VIEW BeershopService_TypeChecks AS SELECT TypeChecks_0.type_Binary, TypeChecks_0.type_LargeBinary, TypeChecks_0.type_LargeString -FROM csw_TypeChecks AS TypeChecks_0 \ No newline at end of file +FROM csw_TypeChecks AS TypeChecks_0; + +CREATE VIEW BeershopService_TypeChecksWithDraft AS SELECT + TypeChecks_0.ID, + TypeChecks_0.type_Boolean, + TypeChecks_0.type_Int32, + TypeChecks_0.type_Int64, + TypeChecks_0.type_Decimal, + TypeChecks_0.type_Double, + TypeChecks_0.type_Date, + TypeChecks_0.type_Time, + TypeChecks_0.type_DateTime, + TypeChecks_0.type_Timestamp, + TypeChecks_0.type_String, + TypeChecks_0.type_Binary, + TypeChecks_0.type_LargeBinary, + TypeChecks_0.type_LargeString +FROM csw_TypeChecks AS TypeChecks_0; + +CREATE VIEW BeershopService_DraftAdministrativeData AS SELECT + DraftAdministrativeData.DraftUUID, + DraftAdministrativeData.CreationDateTime, + DraftAdministrativeData.CreatedByUser, + DraftAdministrativeData.DraftIsCreatedByMe, + DraftAdministrativeData.LastChangeDateTime, + DraftAdministrativeData.LastChangedByUser, + DraftAdministrativeData.InProcessByUser, + DraftAdministrativeData.DraftIsProcessedByMe +FROM DRAFT_DraftAdministrativeData AS DraftAdministrativeData \ No newline at end of file diff --git a/__tests__/lib/pg/service.test.js b/__tests__/lib/pg/service.test.js index 60812fd..1a8e585 100644 --- a/__tests__/lib/pg/service.test.js +++ b/__tests__/lib/pg/service.test.js @@ -80,6 +80,13 @@ describe.each([ expect(response.text.includes(expectedBeersEntitySet)).toBeTruthy() }) + test('List of entities exposed by the service', async () => { + const response = await request.get('/beershop/') + + expect(response.status).toStrictEqual(200) + expect(response.body.value.length).toStrictEqual(4) + }) + describe('odata: GET -> sql: SELECT', () => { beforeEach(async () => { await deploy(this._model, {}).to(this._dbProperties) @@ -180,6 +187,42 @@ describe.each([ }) }) + describe('odata: GET on Draft enabled Entity -> sql: SELECT', () => { + beforeEach(async () => { + await deploy(this._model, {}).to(this._dbProperties) + }) + test('odata: entityset TypeChecksWithDraft -> select all', async () => { + const response = await request.get('/beershop/TypeChecksWithDraft') + expect(response.status).toStrictEqual(200) + }) + test('odata: entityset TypeChecksWithDraft -> select all and count', async () => { + const response = await request.get('/beershop/TypeChecksWithDraft?$count=true') + expect(response.status).toStrictEqual(200) + expect(response.body['@odata.count']).toEqual(1) + }) + test('odata: entityset TypeChecksWithDraft -> select like Fiori Elements UI', async () => { + const response = await request.get( + '/beershop/TypeChecksWithDraft?$count=true&$expand=DraftAdministrativeData&$filter=(IsActiveEntity%20eq%20false%20or%20SiblingEntity/IsActiveEntity%20eq%20null)&$select=HasActiveEntity,ID,IsActiveEntity,type_Boolean,type_Date,type_Int32&$skip=0&$top=30' + ) + expect(response.status).toStrictEqual(200) + expect(response.body['@odata.count']).toEqual(1) + }) + test('odata: create new entityset TypeChecksWithDraft -> create like Fiori Elements UI', async () => { + const response = await request + .post('/beershop/TypeChecksWithDraft') + .send(JSON.stringify({})) + .set('Accept', 'application/json;odata.metadata=minimal;IEEE754Compatible=true') + .set('Content-Type', 'application/json;charset=UTF-8;IEEE754Compatible=true') + // Creates: + // sql > SELECT * FROM BeershopService_TypeChecksWithDraft_drafts ALIAS_1 WHERE ID = $1 + // values > [ 'c436a286-6d1e-44ad-9630-b09e55b9a61e' ] + // But this fails with: + // The key 'ID' does not exist in the given entity + // the column is created with lowercase id + expect(response.status).toStrictEqual(201) + }) + }) + describe('odata: POST -> sql: INSERT', () => { beforeEach(async () => { await deploy(this._model, {}).to(this._dbProperties) diff --git a/lib/pg/sql-builder/ReferenceBuilder.js b/lib/pg/sql-builder/ReferenceBuilder.js index 3e2f6c7..3ed4983 100644 --- a/lib/pg/sql-builder/ReferenceBuilder.js +++ b/lib/pg/sql-builder/ReferenceBuilder.js @@ -54,29 +54,6 @@ class PGReferenceBuilder extends ReferenceBuilder { this._outputObj.sql = this._outputObj.sql.join(' ') return this._outputObj } - - /** - * Override method and add "AS" part do the column name so that it matches the CDS model - * in order to make the mapping simpler we always add the column name as its found in the - * cds model. That way the returning data is directly mapped to the model - * @param {*} refArray - * @override - */ - _parseReference(refArray) { - if (refArray[0].id) { - throw new Error(`${refArray[0].id}: Views with parameters supported only on HANA`) - } - - const entity = this._csn && this._csn.definitions[this._options.entityName] - const element = entity && entity.elements[refArray[0]] - if (element && element.elements) { - // REVISIT we assume that structured elements are already unfolded here - this._outputObj.sql.push(refArray.join('_')) - return - } - - this._outputObj.sql.push(refArray.map((el) => `${this._quoteElement(el)} AS "${el}" `).join('.')) - } } module.exports = PGReferenceBuilder diff --git a/lib/pg/sql-builder/SelectBuilder.js b/lib/pg/sql-builder/SelectBuilder.js index 27241be..e12c253 100644 --- a/lib/pg/sql-builder/SelectBuilder.js +++ b/lib/pg/sql-builder/SelectBuilder.js @@ -49,6 +49,47 @@ class PGSelectBuilder extends SelectBuilder { Object.defineProperty(this, 'ExpressionBuilder', { value: ExpressionBuilder }) } + /** + * Override method and add "AS" part to the column name so that it matches the CDS model + * in order to make the mapping simpler we always add the column name as its found in the + * cds model. That way the returning data is directly mapped to the model + */ + _columns(noQuoting) { + if (this._obj.SELECT.columns) { + for (let index = 0; index < this._obj.SELECT.columns.length; index++) { + const element = this._obj.SELECT.columns[index] + if (!element.as) { + if (element.ref && element.ref.length) { + this._obj.SELECT.columns[index].as = element.ref[element.ref.length - 1] + } + } + } + } else { + // fill columns from entity definition + // to avoid "does not exist in the given entity" error for a SELECT * + this._obj.SELECT.columns = [] + if (this._obj.SELECT.from.ref[0]) { + let name = this._obj.SELECT.from.ref[0].split('_')[0] + if (!name) { + name = this._obj.SELECT.from.ref[0] + } + let entity = this._csn.definitions[name] + if (entity) { + for (var prop in entity.elements) { + if (entity.elements[prop].type !== 'cds.Association') { + let column = { + ref: [prop], + as: prop, + } + this._obj.SELECT.columns.push(column) + } + } + } + } + } + super._columns(noQuoting) + } + /** * overwriting the where builder to eliminate ref field aliases (x as "x") - * introduced rightfully via @see this.ReferenceBuilder._parseReference(), diff --git a/lib/pg/utils/columns.js b/lib/pg/utils/columns.js index 48474aa..46b4096 100644 --- a/lib/pg/utils/columns.js +++ b/lib/pg/utils/columns.js @@ -10,13 +10,15 @@ const getColumns = require('@sap/cds-runtime/lib/db/utils/columns') * @param {Object} result */ const remapColumnNames = (entity, result) => { - const columns = getColumns(entity) - const resultColumns = Object.keys(result) - for (const column of columns) { - if (resultColumns.includes(column.name.toLowerCase())) { - if (column.name.toLowerCase() !== column.name) { - result[column.name] = result[column.name.toLowerCase()] - delete result[column.name.toLowerCase()] + if (typeof entity !== 'undefined') { + const columns = getColumns(entity) + const resultColumns = Object.keys(result) + for (const column of columns) { + if (resultColumns.includes(column.name.toLowerCase())) { + if (column.name.toLowerCase() !== column.name) { + result[column.name] = result[column.name.toLowerCase()] + delete result[column.name.toLowerCase()] + } } } } diff --git a/package.json b/package.json index 433e7f4..1c002c6 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "test:pg:up": "docker-compose -f __tests__/__assets__/cap-proj/stack.yml up -d", "test:pg:down": "docker-compose -f __tests__/__assets__/cap-proj/stack.yml down", "test:as-sqlite": "cd __tests__/__assets__/cap-proj && cds deploy -2 sqlite::memory: --no-save && cds serve all --in-memory", + "test:as-pg": "cd __tests__/__assets__/cap-proj && cp default-env-template.json default-env.json && cds serve all", "lint": "prettier -c . && eslint '*.{js,ts,tsx}'", "release": "standard-version" },