From 9661d6f13daef41f134ab3169ec7ee38e1d121b4 Mon Sep 17 00:00:00 2001 From: Wenjun Zheng <86405404+Sv7enNowitzki@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:51:23 +0800 Subject: [PATCH] Adapt to CDS 8 and optimize the logic for finding the root entity. (#105) Fix: 1. The `serviceEntity` is not captured in the `ChangeLog` table in some cases. 2. When modeling an `inline entity`, the keys attribute in the `Changes` table recorded the unexpected association and parent ID. 3. When reqData is undefined, special handling is required. 4. When running test cases in CDS 8, the request failed with a status code of 404. 5. In CDS 8, for auto-exposed Compositions, all direct CRUD requests are rejected in non-draft cases. When executing test cases, an error with the message `ENTITY_IS_AUTOEXPOSED` occurs because the ChangeView falls under the aforementioned circumstances. 6. In CDS 8, CAP does not support queries for draft-enabled entities on the application service. When running test cases, the related test cases failed and contained the error message: `SqliteError: NOT NULL constraint failed: AdminService_Books_drafts.DraftAdministrativeData_DraftUUID`. 7. In CDS 8, the `cds.transaction` will be deprecated and needs to be replaced with `req.event`. 8. `req._params` and `req.context` are not official APIs and need to be replaced with official properties to ensure code stability. Optimize: 1. Implement a method to analyze entities, determine their structure, add a flag to indicate whether it is a root entity, and when the entity is a child entity, record information about its parent entity. --- cds-plugin.js | 148 ++++++++++++++--- lib/change-log.js | 153 ++++++++++++++++-- lib/entity-helper.js | 7 +- ...pire.bookshop-BookStores.bookInventory.csv | 3 + .../data/sap.capire.bookshop-Order.Items.csv | 3 + .../db/data/sap.capire.bookshop-Order.csv | 3 +- tests/bookshop/db/schema.cds | 12 ++ tests/bookshop/srv/admin-service.cds | 2 +- tests/bookshop/srv/admin-service.js | 18 ++- .../integration/fiori-draft-disabled.test.js | 115 ++++++++++--- tests/integration/fiori-draft-enabled.test.js | 115 +++++++++---- tests/integration/service-api.test.js | 94 ++++++++--- tests/utils/api.js | 6 +- 13 files changed, 562 insertions(+), 117 deletions(-) create mode 100644 tests/bookshop/db/data/sap.capire.bookshop-BookStores.bookInventory.csv create mode 100644 tests/bookshop/db/data/sap.capire.bookshop-Order.Items.csv diff --git a/cds-plugin.js b/cds-plugin.js index 5590077..6a7c66f 100644 --- a/cds-plugin.js +++ b/cds-plugin.js @@ -1,5 +1,8 @@ const cds = require('@sap/cds') +const isRoot = 'change-tracking-isRootEntity' +const hasParent = 'change-tracking-parentEntity' + const isChangeTracked = (entity) => ( (entity['@changelog'] || entity.elements && Object.values(entity.elements).some(e => e['@changelog'])) && entity.query?.SET?.op !== 'union' @@ -7,6 +10,10 @@ const isChangeTracked = (entity) => ( // Add the appropriate Side Effects attribute to the custom action const addSideEffects = (actions, flag, element) => { + if (!flag && (element === undefined || element === null)) { + return + } + for (const se of Object.values(actions)) { const target = flag ? 'TargetProperties' : 'TargetEntities' const sideEffectAttr = se[`@Common.SideEffects.${target}`] @@ -23,6 +30,114 @@ const addSideEffects = (actions, flag, element) => { } } +function setChangeTrackingIsRootEntity(entity, csn, val = true) { + if (csn.definitions?.[entity.name]) { + csn.definitions[entity.name][isRoot] = val; + } +} + +function checkAndSetRootEntity(parentEntity, entity, csn) { + if (entity[isRoot] === false) { + return entity; + } + if (parentEntity) { + return compositionRoot(parentEntity, csn); + } else { + setChangeTrackingIsRootEntity(entity, csn); + return { ...csn.definitions?.[entity.name], name: entity.name }; + } +} + +function processEntities(m) { + for (let name in m.definitions) { + compositionRoot({...m.definitions[name], name}, m) + } +} + +function compositionRoot(entity, csn) { + if (!entity || entity.kind !== 'entity') { + return; + } + const parentEntity = compositionParent(entity, csn); + return checkAndSetRootEntity(parentEntity, entity, csn); +} + +function compositionParent(entity, csn) { + if (!entity || entity.kind !== 'entity') { + return; + } + const parentAssociation = compositionParentAssociation(entity, csn); + return parentAssociation ?? null; +} + +function compositionParentAssociation(entity, csn) { + if (!entity || entity.kind !== 'entity') { + return; + } + const elements = entity.elements ?? {}; + + // Add the change-tracking-isRootEntity attribute of the child entity + processCompositionElements(entity, csn, elements); + + const hasChildFlag = entity[isRoot] !== false; + const hasParentEntity = entity[hasParent]; + + if (hasChildFlag || !hasParentEntity) { + // Find parent association of the entity + const parentAssociation = findParentAssociation(entity, csn, elements); + if (parentAssociation) { + const parentAssociationTarget = elements[parentAssociation]?.target; + if (hasChildFlag) setChangeTrackingIsRootEntity(entity, csn, false); + return { + ...csn.definitions?.[parentAssociationTarget], + name: parentAssociationTarget + }; + } else return; + } + return { ...csn.definitions?.[entity.name], name: entity.name }; +} + +function processCompositionElements(entity, csn, elements) { + for (const name in elements) { + const element = elements[name]; + const target = element?.target; + const definition = csn.definitions?.[target]; + if ( + element.type !== 'cds.Composition' || + target === entity.name || + !definition || + definition[isRoot] === false + ) { + continue; + } + setChangeTrackingIsRootEntity({ ...definition, name: target }, csn, false); + } +} + +function findParentAssociation(entity, csn, elements) { + return Object.keys(elements).find((name) => { + const element = elements[name]; + const target = element?.target; + if (element.type === 'cds.Association' && target !== entity.name) { + const parentDefinition = csn.definitions?.[target] ?? {}; + const parentElements = parentDefinition?.elements ?? {}; + return !!Object.keys(parentElements).find((parentEntityName) => { + const parentElement = parentElements?.[parentEntityName] ?? {}; + if (parentElement.type === 'cds.Composition') { + const isCompositionEntity = parentElement.target === entity.name; + // add parent information in the current entity + if (isCompositionEntity) { + csn.definitions[entity.name][hasParent] = { + associationName: name, + entityName: target + }; + } + return isCompositionEntity; + } + }); + } + }); +} // Unfold @changelog annotations in loaded model cds.on('loaded', m => { @@ -31,6 +146,9 @@ cds.on('loaded', m => { const { 'sap.changelog.aspect': aspect } = m.definitions; if (!aspect) return // some other model const { '@UI.Facets': [facet], elements: { changes } } = aspect changes.on.pop() // remove ID -> filled in below + + // Process entities to define the relation + processEntities(m) for (let name in m.definitions) { const entity = m.definitions[name] @@ -63,28 +181,20 @@ cds.on('loaded', m => { if(!entity['@changelog.disable_facet']) entity['@UI.Facets']?.push(facet) } - // The changehistory list should be refreshed after the custom action is triggered + if (entity.actions) { + const hasParentInfo = entity[hasParent]; + const entityName = hasParentInfo?.entityName; + const parentEntity = entityName ? m.definitions[entityName] : null; - // Update the changehistory list on the current entity when the custom action of the entity is triggered - if (entity['@UI.Facets']) { - addSideEffects(entity.actions, true) - } + const isParentRootAndHasFacets = parentEntity?.[isRoot] && parentEntity?.['@UI.Facets']; - // When the custom action of the child entity is performed, the change history list of the parent entity is updated - if (entity.elements) { - //ToDo: Revisit Breaklook with node.js Expert - breakLoop: for (const [ele, eleValue] of Object.entries(entity.elements)) { - const parentEntity = m.definitions[eleValue.target] - if (parentEntity && parentEntity['@UI.Facets'] && eleValue.type === 'cds.Association') { - for (const value of Object.values(parentEntity.elements)) { - if (value.target === name) { - addSideEffects(entity.actions, false, ele) - break breakLoop - } - } - } - } + if (entity[isRoot] && entity['@UI.Facets']) { + // Add side effects for root entity + addSideEffects(entity.actions, true); + } else if (isParentRootAndHasFacets) { + // Add side effects for child entity + addSideEffects(entity.actions, false, hasParentInfo?.associationName); } } } diff --git a/lib/change-log.js b/lib/change-log.js index ccaebde..4342ab5 100644 --- a/lib/change-log.js +++ b/lib/change-log.js @@ -15,6 +15,7 @@ const { getValueEntityType, } = require("./entity-helper") const { localizeLogFields } = require("./localization") +const isRoot = "change-tracking-isRootEntity" const _getRootEntityPathVals = function (txContext, entity, entityKey) { @@ -26,21 +27,22 @@ const _getRootEntityPathVals = function (txContext, entity, entityKey) { if (txContext.event === "CREATE") { const curEntityPathVal = `${entity.name}(${entityKey})` serviceEntityPathVals.push(curEntityPathVal) + txContext.hasComp && entityIDs.pop(); } else { // When deleting Composition of one node via REST API in draft-disabled mode, // the child node ID would be missing in URI if (txContext.event === "DELETE" && !entityIDs.find((x) => x === entityKey)) { entityIDs.push(entityKey) } - const curEntity = getEntityByContextPath(path) + const curEntity = getEntityByContextPath(path, txContext.hasComp) const curEntityID = entityIDs.pop() const curEntityPathVal = `${curEntity.name}(${curEntityID})` serviceEntityPathVals.push(curEntityPathVal) } - while (_isCompositionContextPath(path)) { - const hostEntity = getEntityByContextPath(path = path.slice(0, -1)) + while (_isCompositionContextPath(path, txContext.hasComp)) { + const hostEntity = getEntityByContextPath(path = path.slice(0, -1), txContext.hasComp) const hostEntityID = entityIDs.pop() const hostEntityPathVal = `${hostEntity.name}(${hostEntityID})` serviceEntityPathVals.unshift(hostEntityPathVal) @@ -55,7 +57,7 @@ const _getAllPathVals = function (txContext) { const entityIDs = _getEntityIDs(txContext.params) for (let idx = 0; idx < paths.length; idx++) { - const entity = getEntityByContextPath(paths.slice(0, idx + 1)) + const entity = getEntityByContextPath(paths.slice(0, idx + 1), txContext.hasComp) const entityID = entityIDs[idx] const entityPathVal = `${entity.name}(${entityID})` pathVals.push(entityPathVal) @@ -64,6 +66,28 @@ const _getAllPathVals = function (txContext) { return pathVals } +function convertSubjectToParams(subject) { + let params = []; + let subjectRef = []; + subject?.ref?.forEach((item)=>{ + if (typeof item === 'string') { + subjectRef.push(item) + return + } + + const keys = {} + let id = item.id + if (!id) return + for (let j = 0; j < item?.where?.length; j = j + 4) { + const key = item.where[j].ref[0] + const value = item.where[j + 2].val + if (key !== 'IsActiveEntity') keys[key] = value + } + params.push(keys); + }) + return params.length > 0 ? params : subjectRef; +} + const _getEntityIDs = function (txParams) { const entityIDs = [] for (const param of txParams) { @@ -255,12 +279,12 @@ const _formatObjectID = async function (changes, reqData) { } } -const _isCompositionContextPath = function (aPath) { +const _isCompositionContextPath = function (aPath, hasComp) { if (!aPath) return if (typeof aPath === 'string') aPath = aPath.split('/') if (aPath.length < 2) return false - const target = getEntityByContextPath(aPath) - const parent = getEntityByContextPath(aPath.slice(0, -1)) + const target = getEntityByContextPath(aPath, hasComp) + const parent = getEntityByContextPath(aPath.slice(0, -1), hasComp) if (!parent.compositions) return false return Object.values(parent.compositions).some(c => c._target === target) } @@ -289,10 +313,26 @@ function _trackedChanges4 (srv, target, diff) { template, row: diff, processFn: ({ row, key, element }) => { const from = row._old?.[key] const to = row[key] + const eleParentKeys = element.parent.keys if (from === to) return - const keys = Object.keys(element.parent.keys) + /** + * + * For the Inline entity such as Items, + * further filtering is required on the keys + * within the 'association' and 'foreign key' to ultimately retain the keys of the entity itself. + * entity Order : cuid { + * title : String; + * Items : Composition of many { + * key ID : UUID; + * quantity : Integer; + * } + * } + */ + const keys = Object.keys(eleParentKeys) .filter(k => k !== "IsActiveEntity") + .filter(k => eleParentKeys[k]?.type !== "cds.Association") // Skip association + .filter(k => !eleParentKeys[k]?.["@odata.foreignKey4"]) // Skip foreign key .map(k => `${k}=${row[k]}`) .join(', ') @@ -339,20 +379,90 @@ const _prepareChangeLogForComposition = async function (entity, entityKey, chang return [ rootEntity, rootEntityID ] } +async function generatePathAndParams (req, entityKey) { + const { target, data } = req; + const { ID, foreignKey, parentEntity } = getAssociationDetails(target); + const hasParentAndForeignKey = parentEntity && data[foreignKey]; + const targetEntity = hasParentAndForeignKey ? parentEntity : target; + const targetKey = hasParentAndForeignKey ? data[foreignKey] : entityKey; + + let compContext = { + path: hasParentAndForeignKey + ? `${parentEntity.name}/${target.name}` + : `${target.name}`, + params: hasParentAndForeignKey + ? [{ [ID]: data[foreignKey] }, { [ID]: entityKey }] + : [{ [ID]: entityKey }], + hasComp: true + }; + + if (hasParentAndForeignKey && parentEntity[isRoot]) { + return compContext; + } + + let parentAssoc = await processEntity(targetEntity, targetKey, compContext); + while (parentAssoc && !parentAssoc.entity[isRoot]) { + parentAssoc = await processEntity( + parentAssoc.entity, + parentAssoc.ID, + compContext + ); + } + return compContext; +} + +async function processEntity (entity, entityKey, compContext) { + const { ID, foreignKey, parentEntity } = getAssociationDetails(entity); + + if (foreignKey && parentEntity) { + const parentResult = + (await SELECT.one + .from(entity.name) + .where({ [ID]: entityKey }) + .columns(foreignKey)) || {}; + const hasForeignKey = parentResult[foreignKey]; + if (!hasForeignKey) return; + compContext.path = `${parentEntity.name}/${compContext.path}`; + compContext.params.unshift({ [ID]: parentResult[foreignKey] }); + return { + entity: parentEntity, + [ID]: hasForeignKey ? parentResult[foreignKey] : undefined + }; + } +} + +function getAssociationDetails (entity) { + if (!entity) return {}; + const assocName = entity['change-tracking-parentEntity']?.associationName; + const assoc = entity.elements[assocName]; + const parentEntity = assoc?._target; + const foreignKey = assoc?.keys?.[0]?.$generatedFieldName; + const ID = assoc?.keys?.[0]?.ref[0] || 'ID'; + return { ID, foreignKey, parentEntity }; +} + async function track_changes (req) { let diff = await req.diff() if (!diff) return let target = req.target - let isDraftEnabled = !!target.drafts - let isComposition = _isCompositionContextPath(req.context.path) + let compContext = null; let entityKey = diff.ID - - if (cds.transaction(req).context.event === "DELETE" && !cds.env.requires["change-tracking"]?.preserveDeletes) { - if (isDraftEnabled || !isComposition) { - return await DELETE.from(`sap.changelog.ChangeLog`).where({ entityKey }) - } + const params = convertSubjectToParams(req.subject); + if (req.subject.ref.length === 1 && params.length === 1 && !target[isRoot]) { + compContext = await generatePathAndParams(req, entityKey); + } + let isComposition = _isCompositionContextPath( + compContext?.path || req.path, + compContext?.hasComp + ); + if ( + req.event === "DELETE" && + target[isRoot] && + !cds.env.requires["change-tracking"]?.preserveDeletes + ) { + return await DELETE.from(`sap.changelog.ChangeLog`).where({ entityKey }); } let changes = _trackedChanges4(this, target, diff) @@ -360,13 +470,22 @@ async function track_changes (req) { await _formatChangeLog(changes, req) if (isComposition) { - [ target, entityKey ] = await _prepareChangeLogForComposition(target, entityKey, changes, this) + let reqInfo = { + data: req.data, + context: { + path: compContext?.path || req.path, + params: compContext?.params || params, + event: req.event, + hasComp: compContext?.hasComp + } + }; + [ target, entityKey ] = await _prepareChangeLogForComposition(target, entityKey, changes, reqInfo) } const dbEntity = getDBEntity(target) await INSERT.into("sap.changelog.ChangeLog").entries({ entity: dbEntity.name, entityKey: entityKey, - serviceEntity: target.name, + serviceEntity: target.name || target, changes: changes.filter(c => c.valueChangedFrom || c.valueChangedTo).map((c) => ({ ...c, valueChangedFrom: `${c.valueChangedFrom ?? ''}`, diff --git a/lib/entity-helper.js b/lib/entity-helper.js index fc0b050..be564ce 100644 --- a/lib/entity-helper.js +++ b/lib/entity-helper.js @@ -11,7 +11,8 @@ const getUUIDFromPathVal = function (pathVal) { return regRes ? regRes[1] : "" } -const getEntityByContextPath = function (aPath) { +const getEntityByContextPath = function (aPath, hasComp = false) { + if (hasComp) return cds.model.definitions[aPath[aPath.length - 1]] let entity = cds.model.definitions[aPath[0]] for (let each of aPath.slice(1)) { entity = entity.elements[each]?._target @@ -96,7 +97,7 @@ async function getObjectId (reqData, entityName, fields, curObj) { // This function can recursively retrieve the desired information from reqData without having to read it from db. _db_data = _getCompositionObjFromReq(reqData, IDval) // When multiple layers of child nodes are deleted at the same time, the deep layer of child nodes will lose the information of the upper nodes, so data needs to be extracted from the db. - const entityKeys = Object.keys(reqData).filter(item => !Object.keys(assoc._target.keys).some(ele => item === ele)); + const entityKeys = reqData ? Object.keys(reqData).filter(item => !Object.keys(assoc._target.keys).some(ele => item === ele)) : []; if (!_db_data || JSON.stringify(_db_data) === '{}' || entityKeys.length === 0) { _db_data = await getCurObjFromDbQuery(assoc._target, IDval, ID); } @@ -166,7 +167,7 @@ const hasComposition = function (parentEntity, subEntity) { } const _getCompositionObjFromReq = function (obj, targetID) { - if (obj.ID === targetID) { + if (obj?.ID === targetID) { return obj; } diff --git a/tests/bookshop/db/data/sap.capire.bookshop-BookStores.bookInventory.csv b/tests/bookshop/db/data/sap.capire.bookshop-BookStores.bookInventory.csv new file mode 100644 index 0000000..dcdddb2 --- /dev/null +++ b/tests/bookshop/db/data/sap.capire.bookshop-BookStores.bookInventory.csv @@ -0,0 +1,3 @@ +ID;up__ID;title +3ccf474c-3881-44b7-99fb-59a2a4668418;64625905-c234-4d0d-9bc1-283ee8946770;Eleonora +3583f982-d7df-4aad-ab26-301d4a157cd7;64625905-c234-4d0d-9bc1-283ee8946770;Catweazle diff --git a/tests/bookshop/db/data/sap.capire.bookshop-Order.Items.csv b/tests/bookshop/db/data/sap.capire.bookshop-Order.Items.csv new file mode 100644 index 0000000..d6bbb6c --- /dev/null +++ b/tests/bookshop/db/data/sap.capire.bookshop-Order.Items.csv @@ -0,0 +1,3 @@ +ID;up__ID;quantity +2b23bb4b-4ac7-4a24-ac02-aa10cabd842c;3b23bb4b-4ac7-4a24-ac02-aa10cabd842c;10.0 +2b23bb4b-4ac7-4a24-ac02-aa10cabd843c;3b23bb4b-4ac7-4a24-ac02-aa10cabd842c;11.0 diff --git a/tests/bookshop/db/data/sap.capire.bookshop-Order.csv b/tests/bookshop/db/data/sap.capire.bookshop-Order.csv index b3a62a6..5862881 100644 --- a/tests/bookshop/db/data/sap.capire.bookshop-Order.csv +++ b/tests/bookshop/db/data/sap.capire.bookshop-Order.csv @@ -1,3 +1,4 @@ ID;netAmount;status;report_ID;header_ID 0a41a187-a2ff-4df6-bd12-fae8996e6e31;1.0;Post;0a41a666-a2ff-4df6-bd12-fae8996e6666;8567d0de-d44f-11ed-afa1-0242ac120002 -6ac4afbf-deda-45ae-88e6-2883157cc010;2.0;Post;b1a92b71-8ed9-4862-8151-ad951898002f;6b75449a-d44f-11ed-afa1-0242ac120002 \ No newline at end of file +6ac4afbf-deda-45ae-88e6-2883157cc010;2.0;Post;b1a92b71-8ed9-4862-8151-ad951898002f;6b75449a-d44f-11ed-afa1-0242ac120002 +3b23bb4b-4ac7-4a24-ac02-aa10cabd842c;3.0;Post diff --git a/tests/bookshop/db/schema.cds b/tests/bookshop/db/schema.cds index 40d9cb9..30daa98 100644 --- a/tests/bookshop/db/schema.cds +++ b/tests/bookshop/db/schema.cds @@ -105,6 +105,13 @@ entity BookStores @(cds.autoexpose) : managed, cuid { @title : '{i18n>bookStore.registry}' registry : Composition of one BookStoreRegistry; + + @title : '{i18n>bookStore.bookInventory}' + bookInventory : Composition of many { + key ID : UUID; + @changelog + title : String; + } } @fiori.draft.enabled @@ -195,6 +202,11 @@ entity Order : cuid { on orderItems.order = $self; netAmount : Decimal(19, 2); status : String; + Items : Composition of many { + key ID : UUID; + @changelog + quantity : Integer; + } } entity OrderType : cuid { diff --git a/tests/bookshop/srv/admin-service.cds b/tests/bookshop/srv/admin-service.cds index df5bdcf..785296a 100644 --- a/tests/bookshop/srv/admin-service.cds +++ b/tests/bookshop/srv/admin-service.cds @@ -1,5 +1,4 @@ using {sap.capire.bookshop as my} from '../db/schema'; -using {sap.capire.bookshop.PaymentAgreementStatusCodes as PaymentAgreementStatusCodes} from '../db/codelists'; service AdminService { @odata.draft.enabled @@ -21,6 +20,7 @@ service AdminService { entity Authors as projection on my.Authors; entity Report as projection on my.Report; entity Order as projection on my.Order; + entity Order.Items as projection on my.Order.Items; entity OrderItem as projection on my.OrderItem; entity OrderItemNote as projection on my.OrderItemNote actions { diff --git a/tests/bookshop/srv/admin-service.js b/tests/bookshop/srv/admin-service.js index ce20d3e..2532f11 100644 --- a/tests/bookshop/srv/admin-service.js +++ b/tests/bookshop/srv/admin-service.js @@ -12,20 +12,30 @@ module.exports = cds.service.impl(async (srv) => { const onActivateVolumns = async (req) => { const entity = req.entity; - const entityID = req._params[req._params.length - 1].ID; + const entityID = "dd1fdd7d-da2a-4600-940b-0baf2946c9bf"; await UPDATE.entity(entity) .where({ ID: entityID }) .set({ ActivationStatus_code: "VALID" }); + + const booksEntity = "AdminService.Books"; + const booksID = "676059d4-8851-47f1-b558-3bdc461bf7d5"; + await UPDATE.entity(booksEntity, { ID: booksID }) + .set({ title: "Black Myth wukong" }); }; const onActivateOrderItemNote = async (req) => { const entity = req.entity; - const entityID = req._params[req._params.length - 1]; + const entityID = "a40a9fd8-573d-4f41-1111-fa8ea0d8b1bc"; await UPDATE.entity(entity) .where({ ID: entityID }) .set({ ActivationStatus_code: "VALID" }); + + const Level2Object = "AdminService.Level2Object"; + const Level2ObjectID = "55bb60e4-ed86-46e6-9378-346153eba8d4"; + await UPDATE.entity(Level2Object, { ID: Level2ObjectID }) + .set({ title: "Game Science" }); }; - srv.on("activate", "Volumns", onActivateVolumns); - srv.on("activate", "OrderItemNote", onActivateOrderItemNote); + srv.on("activate", "AdminService.Volumns", onActivateVolumns); + srv.on("activate", "AdminService.OrderItemNote", onActivateOrderItemNote); }); diff --git a/tests/integration/fiori-draft-disabled.test.js b/tests/integration/fiori-draft-disabled.test.js index 8fbc3ad..af89c0d 100644 --- a/tests/integration/fiori-draft-disabled.test.js +++ b/tests/integration/fiori-draft-disabled.test.js @@ -8,13 +8,16 @@ let adminService = null; let ChangeView = null; let db = null; let ChangeEntity = null; +let ChangeLog = null; describe("change log draft disabled test", () => { beforeAll(async () => { adminService = await cds.connect.to("AdminService"); ChangeView = adminService.entities.ChangeView; + ChangeView["@cds.autoexposed"] = false; db = await cds.connect.to("sql:my.db"); ChangeEntity = db.model.definitions["sap.changelog.Changes"]; + ChangeLog = db.model.definitions["sap.changelog.ChangeLog"]; }); beforeEach(async () => { @@ -22,7 +25,7 @@ describe("change log draft disabled test", () => { }); it("1.1 Root entity creation - should log basic data type changes (ERP4SMEPREPWORKAPPPLAT-32 ERP4SMEPREPWORKAPPPLAT-613)", async () => { - const author = await POST(`/admin/Authors`, { + const author = await POST(`/odata/v4/admin/Authors`, { name_firstName: "Sam", name_lastName: "Smiths", placeOfBirth: "test place", @@ -54,7 +57,7 @@ describe("change log draft disabled test", () => { }); it("1.2 Root entity update - should log basic data type changes (ERP4SMEPREPWORKAPPPLAT-32 ERP4SMEPREPWORKAPPPLAT-613)", async () => { - await PATCH(`/admin/Authors(ID=d4d4a1b3-5b83-4814-8a20-f039af6f0387)`, { + await PATCH(`/odata/v4/admin/Authors(ID=d4d4a1b3-5b83-4814-8a20-f039af6f0387)`, { placeOfBirth: "new placeOfBirth", }); @@ -74,7 +77,7 @@ describe("change log draft disabled test", () => { }); it("1.3 Root entity delete - should delete related changes (ERP4SMEPREPWORKAPPPLAT-32 ERP4SMEPREPWORKAPPPLAT-613)", async () => { - const author = await POST(`/admin/Authors`, { + const author = await POST(`/odata/v4/admin/Authors`, { name_firstName: "Sam", name_lastName: "Smiths", placeOfBirth: "test place", @@ -83,7 +86,7 @@ describe("change log draft disabled test", () => { const beforeChanges = await adminService.run(SELECT.from(ChangeView)); expect(beforeChanges.length > 0).to.be.true; - await DELETE(`/admin/Authors(ID=${author.data.ID})`); + await DELETE(`/odata/v4/admin/Authors(ID=${author.data.ID})`); const afterChanges = await adminService.run(SELECT.from(ChangeView)); expect(afterChanges.length).to.equal(0); @@ -152,7 +155,7 @@ describe("change log draft disabled test", () => { it("3.1 Composition creatition by odata request on draft disabled entity - should log changes for root entity (ERP4SMEPREPWORKAPPPLAT-670)", async () => { await POST( - `/admin/Order(ID=0a41a187-a2ff-4df6-bd12-fae8996e6e31)/orderItems(ID=9a61178f-bfb3-4c17-8d17-c6b4a63e0097)/notes`, + `/odata/v4/admin/Order(ID=0a41a187-a2ff-4df6-bd12-fae8996e6e31)/orderItems(ID=9a61178f-bfb3-4c17-8d17-c6b4a63e0097)/notes`, { content: "new content", } @@ -170,11 +173,19 @@ describe("change log draft disabled test", () => { expect(orderChange.valueChangedTo).to.equal("new content"); expect(orderChange.parentKey).to.equal("9a61178f-bfb3-4c17-8d17-c6b4a63e0097"); expect(orderChange.parentObjectID).to.equal("sap.capire.bookshop.OrderItem"); + + // Check the changeLog to make sure the entity information is root + let changeLogs = await adminService.run(SELECT.from(ChangeLog)); + + expect(changeLogs.length).to.equal(1); + expect(changeLogs[0].entity).to.equal("sap.capire.bookshop.Order"); + expect(changeLogs[0].entityKey).to.equal("0a41a187-a2ff-4df6-bd12-fae8996e6e31"); + expect(changeLogs[0].serviceEntity).to.equal("AdminService.Order"); }); it("3.2 Composition update by odata request on draft disabled entity - should log changes for root entity (ERP4SMEPREPWORKAPPPLAT-670)", async () => { await PATCH( - `/admin/Order(ID=0a41a187-a2ff-4df6-bd12-fae8996e6e31)/orderItems(ID=9a61178f-bfb3-4c17-8d17-c6b4a63e0097)/notes(ID=a40a9fd8-573d-4f41-1111-fa8ea0d8b1bc)`, + `/odata/v4/admin/Order(ID=0a41a187-a2ff-4df6-bd12-fae8996e6e31)/orderItems(ID=9a61178f-bfb3-4c17-8d17-c6b4a63e0097)/notes(ID=a40a9fd8-573d-4f41-1111-fa8ea0d8b1bc)`, { content: "new content", } @@ -193,11 +204,19 @@ describe("change log draft disabled test", () => { expect(orderChange.valueChangedTo).to.equal("new content"); expect(orderChange.parentKey).to.equal("9a61178f-bfb3-4c17-8d17-c6b4a63e0097"); expect(orderChange.parentObjectID).to.equal("sap.capire.bookshop.OrderItem"); + + // Check the changeLog to make sure the entity information is root + let changeLogs = await adminService.run(SELECT.from(ChangeLog)); + + expect(changeLogs.length).to.equal(1); + expect(changeLogs[0].entity).to.equal("sap.capire.bookshop.Order"); + expect(changeLogs[0].entityKey).to.equal("0a41a187-a2ff-4df6-bd12-fae8996e6e31"); + expect(changeLogs[0].serviceEntity).to.equal("AdminService.Order"); }); it("3.3 Composition delete by odata request on draft disabled entity - should log changes for root entity (ERP4SMEPREPWORKAPPPLAT-670)", async () => { await DELETE( - `/admin/Order(ID=0a41a187-a2ff-4df6-bd12-fae8996e6e31)/orderItems(ID=9a61178f-bfb3-4c17-8d17-c6b4a63e0097)/notes(ID=a40a9fd8-573d-4f41-1111-fa8ea0d8b1bc)` + `/odata/v4/admin/Order(ID=0a41a187-a2ff-4df6-bd12-fae8996e6e31)/orderItems(ID=9a61178f-bfb3-4c17-8d17-c6b4a63e0097)/notes(ID=a40a9fd8-573d-4f41-1111-fa8ea0d8b1bc)` ); let changes = await adminService.run(SELECT.from(ChangeView)); @@ -218,7 +237,7 @@ describe("change log draft disabled test", () => { it("3.4 Composition create by odata request on draft disabled entity - should log changes for root entity if url path contains association entity (ERP4SMEPREPWORKAPPPLAT-670)", async () => { // Report has association to many Orders, changes on OrderItem shall be logged on Order await POST( - `admin/Report(ID=0a41a666-a2ff-4df6-bd12-fae8996e6666)/orders(ID=0a41a187-a2ff-4df6-bd12-fae8996e6e31)/orderItems`, + `/odata/v4/admin/Report(ID=0a41a666-a2ff-4df6-bd12-fae8996e6666)/orders(ID=0a41a187-a2ff-4df6-bd12-fae8996e6e31)/orderItems`, { order_ID: "0a41a187-a2ff-4df6-bd12-fae8996e6e31", quantity: 10, @@ -233,6 +252,23 @@ describe("change log draft disabled test", () => { expect(orderChanges.length).to.equal(2); }); + it("3.5 Composition of inline entity for draft disabled entity", async () => { + await PATCH(`/odata/v4/admin/Order_Items(up__ID=3b23bb4b-4ac7-4a24-ac02-aa10cabd842c,ID=2b23bb4b-4ac7-4a24-ac02-aa10cabd842c)`, { + quantity: 12.0 + }); + + const changes = await adminService.run(SELECT.from(ChangeView)); + + expect(changes.length).to.equal(1); + const change = changes[0]; + expect(change.attribute).to.equal("quantity"); + expect(change.modification).to.equal("Update"); + expect(change.valueChangedFrom).to.equal("10"); + expect(change.valueChangedTo).to.equal("12"); + expect(change.parentKey).to.equal("3b23bb4b-4ac7-4a24-ac02-aa10cabd842c"); + expect(change.keys).to.equal("ID=2b23bb4b-4ac7-4a24-ac02-aa10cabd842c"); + }); + it("4.1 Annotate multiple native and attributes comming from one or more associated table as the object ID (ERP4SMEPREPWORKAPPPLAT-913)", async () => { cds.services.AdminService.entities.OrderItem["@changelog"] = [ { "=": "customer.city" }, @@ -240,7 +276,7 @@ describe("change log draft disabled test", () => { { "=": "price" }, { "=": "quantity" }, ]; - await PATCH(`/admin/OrderItem(ID=9a61178f-bfb3-4c17-8d17-c6b4a63e0097)`, { + await PATCH(`/odata/v4/admin/OrderItem(ID=9a61178f-bfb3-4c17-8d17-c6b4a63e0097)`, { quantity: 14, }); @@ -261,7 +297,7 @@ describe("change log draft disabled test", () => { { "=": "dateOfDeath" }, { "=": "dateOfBirth" }, ]; - await PATCH(`/admin/Authors(ID=d4d4a1b3-5b83-4814-8a20-f039af6f0387)`, { + await PATCH(`/odata/v4/admin/Authors(ID=d4d4a1b3-5b83-4814-8a20-f039af6f0387)`, { placeOfBirth: "new placeOfBirth", }); @@ -284,7 +320,7 @@ describe("change log draft disabled test", () => { { "=": "customer.country" }, { "=": "customer.name" }, ]; - await PATCH(`/admin/OrderItem(ID=9a61178f-bfb3-4c17-8d17-c6b4a63e0097)`, { + await PATCH(`/odata/v4/admin/OrderItem(ID=9a61178f-bfb3-4c17-8d17-c6b4a63e0097)`, { quantity: 14, }); @@ -297,8 +333,9 @@ describe("change log draft disabled test", () => { }); it("5.1 value data type records data type of native attributes of the entity or attributes from association table which are annotated as the displayed value(ERP4SMEPREPWORKAPPPLAT-873)", async () => { - await POST(`/admin/OrderItem`, { + await POST(`/odata/v4/admin/OrderItem`, { ID: "9a61178f-bfb3-4c17-8d17-c6b4a63e0422", + order_ID: "6ac4afbf-deda-45ae-88e6-2883157cc010", customer_ID: "47f97f40-4f41-488a-b10b-a5725e762d57", quantity: 27, }); @@ -317,7 +354,7 @@ describe("change log draft disabled test", () => { expect(customerChangeInDb.valueChangedTo).to.equal("Japan, Honda, Ōsaka"); expect(customerChangeInDb.valueDataType).to.equal("cds.String, cds.String, cds.String"); - await PATCH(`/admin/OrderItem(ID=9a61178f-bfb3-4c17-8d17-c6b4a63e0097)`, { + await PATCH(`/odata/v4/admin/OrderItem(ID=9a61178f-bfb3-4c17-8d17-c6b4a63e0097)`, { customer_ID: "5c30d395-db0a-4095-bd7e-d4de3464660a", }); @@ -328,6 +365,7 @@ describe("change log draft disabled test", () => { attribute: "customer", modification: "update", }); + expect(customerUpdateChangesInDb.length).to.equal(1); const customerUpdateChangeInDb = customerUpdateChangesInDb[0]; @@ -419,10 +457,12 @@ describe("change log draft disabled test", () => { { ID: "48268451-8552-42a6-a3d7-67564be97733", title: "new Level1Object title", + parent_ID: "a670e8e1-ee06-4cad-9cbd-a2354dc37c9d", child: [ { ID: "12ed5dd8-d45b-11ed-afa1-1942bd228115", title: "new Level2Object title", + parent_ID: "48268451-8552-42a6-a3d7-67564be97733" } ] } @@ -447,9 +487,11 @@ describe("change log draft disabled test", () => { child: [ { ID: "48268451-8552-42a6-a3d7-67564be97733", + parent_ID: "a670e8e1-ee06-4cad-9cbd-a2354dc37c9d", child:[{ ID: "12ed5dd8-d45b-11ed-afa1-1942bd228115", - title: "Level2Object title changed" + title: "Level2Object title changed", + parent_ID:"48268451-8552-42a6-a3d7-67564be97733" }] } ] @@ -556,7 +598,7 @@ describe("change log draft disabled test", () => { }); it("10.1 Composition of one creatition by odata request on draft disabled entity - should log changes for root entity (ERP4SMEPREPWORKAPPPLAT-2913 ERP4SMEPREPWORKAPPPLAT-3063)", async () => { - await POST(`/admin/Order`, { + await POST(`/odata/v4/admin/Order`, { ID: "11234567-89ab-cdef-0123-456789abcdef", header: { status: "Ordered", @@ -578,7 +620,7 @@ describe("change log draft disabled test", () => { it("10.2 Composition of one update by odata request on draft disabled entity - should log changes for root entity (ERP4SMEPREPWORKAPPPLAT-2913 ERP4SMEPREPWORKAPPPLAT-3063)", async () => { cds.services.AdminService.entities.Order["@changelog"] = [{ "=": "status" }]; - await PATCH(`/admin/Order(ID=0a41a187-a2ff-4df6-bd12-fae8996e6e31)`, { + await PATCH(`/odata/v4/admin/Order(ID=0a41a187-a2ff-4df6-bd12-fae8996e6e31)`, { header: { ID: "8567d0de-d44f-11ed-afa1-0242ac120002", status: "Ordered", @@ -603,7 +645,7 @@ describe("change log draft disabled test", () => { it("10.3 Composition of one delete by odata request on draft disabled entity - should log changes for root entity (ERP4SMEPREPWORKAPPPLAT-2913 ERP4SMEPREPWORKAPPPLAT-3063)", async () => { // Check if the object ID obtaining failed due to lacking parentKey would lead to dump cds.services.AdminService.entities.Order["@changelog"] = [{ "=": "status" }]; - await DELETE(`/admin/Order(ID=0a41a187-a2ff-4df6-bd12-fae8996e6e31)/header`); + await DELETE(`/odata/v4/admin/Order(ID=0a41a187-a2ff-4df6-bd12-fae8996e6e31)/header`); const changes = await adminService.run(SELECT.from(ChangeView)); const headerChanges = changes.filter((change) => { @@ -622,10 +664,7 @@ describe("change log draft disabled test", () => { it("11.2 The change log should be captured when a child entity in draft-disabled mode triggers a custom action (ERP4SMEPREPWORKAPPPLAT-6211)", async () => { await POST( - `/odata/v4/admin/Order(ID=0a41a187-a2ff-4df6-bd12-fae8996e6e31)/orderItems(ID=9a61178f-bfb3-4c17-8d17-c6b4a63e0097)/notes(ID=a40a9fd8-573d-4f41-1111-fa8ea0d8b1bc)/AdminService.activate`, - { - ActivationStatus_code: "VALID", - }, + `/odata/v4/admin/Order(ID=0a41a187-a2ff-4df6-bd12-fae8996e6e31)/orderItems(ID=9a61178f-bfb3-4c17-8d17-c6b4a63e0097)/notes(ID=a40a9fd8-573d-4f41-1111-fa8ea0d8b1bc)/AdminService.activate` ); let changes = await SELECT.from(ChangeView).where({ entity: "sap.capire.bookshop.OrderItemNote", @@ -636,5 +675,39 @@ describe("change log draft disabled test", () => { expect(changes[0].valueChangedTo).to.equal("VALID"); expect(changes[0].entityKey).to.equal("0a41a187-a2ff-4df6-bd12-fae8996e6e31"); expect(changes[0].parentKey).to.equal("9a61178f-bfb3-4c17-8d17-c6b4a63e0097"); + + // Check the changeLog to make sure the entity information is root + let changeLogs = await SELECT.from(ChangeLog).where({ + entity: "sap.capire.bookshop.Order", + entityKey: "0a41a187-a2ff-4df6-bd12-fae8996e6e31", + serviceEntity: "AdminService.Order", + }); + + expect(changeLogs.length).to.equal(1); + expect(changeLogs[0].entity).to.equal("sap.capire.bookshop.Order"); + expect(changeLogs[0].entityKey).to.equal("0a41a187-a2ff-4df6-bd12-fae8996e6e31"); + expect(changeLogs[0].serviceEntity).to.equal("AdminService.Order"); + + changes = await SELECT.from(ChangeView).where({ + entity: "sap.capire.bookshop.Level2Object", + attribute: "title", + }); + expect(changes.length).to.equal(1); + expect(changes[0].valueChangedFrom).to.equal("Level2Object title2"); + expect(changes[0].valueChangedTo).to.equal("Game Science"); + expect(changes[0].entityKey).to.equal("6ac4afbf-deda-45ae-88e6-2883157cd576"); + expect(changes[0].parentKey).to.equal("ae0d8b10-84cf-4777-a489-a198d1717c75"); + + // Check the changeLog to make sure the entity information is root + changeLogs = await SELECT.from(ChangeLog).where({ + entity: "sap.capire.bookshop.RootObject", + entityKey: "6ac4afbf-deda-45ae-88e6-2883157cd576", + serviceEntity: "AdminService.RootObject", + }); + + expect(changeLogs.length).to.equal(1); + expect(changeLogs[0].entity).to.equal("sap.capire.bookshop.RootObject"); + expect(changeLogs[0].entityKey).to.equal("6ac4afbf-deda-45ae-88e6-2883157cd576"); + expect(changeLogs[0].serviceEntity).to.equal("AdminService.RootObject"); }); }); diff --git a/tests/integration/fiori-draft-enabled.test.js b/tests/integration/fiori-draft-enabled.test.js index b3c52ba..17d5dfe 100644 --- a/tests/integration/fiori-draft-enabled.test.js +++ b/tests/integration/fiori-draft-enabled.test.js @@ -10,13 +10,16 @@ let ChangeView = null; let db = null; let ChangeEntity = null; let utils = null; +let ChangeLog = null; describe("change log integration test", () => { beforeAll(async () => { adminService = await cds.connect.to("AdminService"); ChangeView = adminService.entities.ChangeView; + ChangeView["@cds.autoexposed"] = false; db = await cds.connect.to("sql:my.db"); ChangeEntity = db.model.definitions["sap.changelog.Changes"]; + ChangeLog = db.model.definitions["sap.changelog.ChangeLog"]; utils = new RequestSend(POST); }); @@ -62,7 +65,7 @@ describe("change log integration test", () => { const beforeChanges = await adminService.run(SELECT.from(ChangeView)); expect(beforeChanges.length > 0).to.be.true; - await DELETE(`/admin/RootEntity(ID=01234567-89ab-cdef-0123-987654fedcba,IsActiveEntity=true)`); + await DELETE(`/odata/v4/admin/RootEntity(ID=01234567-89ab-cdef-0123-987654fedcba,IsActiveEntity=true)`); const afterChanges = await adminService.run(SELECT.from(ChangeView)); @@ -85,7 +88,7 @@ describe("change log integration test", () => { it("2.1 Child entity creation - should log basic data type changes (ERP4SMEPREPWORKAPPPLAT-32 ERP4SMEPREPWORKAPPPLAT-613)", async () => { const action = POST.bind( {}, - `/admin/BookStores(ID=64625905-c234-4d0d-9bc1-283ee8946770,IsActiveEntity=false)/books`, + `/odata/v4/admin/BookStores(ID=64625905-c234-4d0d-9bc1-283ee8946770,IsActiveEntity=false)/books`, { ID: "9d703c23-54a8-4eff-81c1-cdce6b8376b2", title: "test title", @@ -166,7 +169,7 @@ describe("change log integration test", () => { }); it("2.2 Child entity update - should log basic data type changes (ERP4SMEPREPWORKAPPPLAT-32 ERP4SMEPREPWORKAPPPLAT-613)", async () => { - const action = PATCH.bind({}, `/admin/Books(ID=9d703c23-54a8-4eff-81c1-cdce6b8376b1,IsActiveEntity=false)`, { + const action = PATCH.bind({}, `/odata/v4/admin/Books(ID=9d703c23-54a8-4eff-81c1-cdce6b8376b1,IsActiveEntity=false)`, { title: "new title", author_ID: "47f97f40-4f41-488a-b10b-a5725e762d5e", genre_ID: 16, @@ -262,7 +265,7 @@ describe("change log integration test", () => { }); it("2.3 Child entity delete - should log basic data type changes (ERP4SMEPREPWORKAPPPLAT-32 ERP4SMEPREPWORKAPPPLAT-613)", async () => { - const action = DELETE.bind({}, `/admin/Books(ID=9d703c23-54a8-4eff-81c1-cdce6b8376b1,IsActiveEntity=false)`); + const action = DELETE.bind({}, `/odata/v4/admin/Books(ID=9d703c23-54a8-4eff-81c1-cdce6b8376b1,IsActiveEntity=false)`); await utils.apiAction("admin", "BookStores", "64625905-c234-4d0d-9bc1-283ee8946770", "AdminService", action); const bookChanges = await adminService.run( @@ -340,7 +343,7 @@ describe("change log integration test", () => { delete cds.db.entities.Books["@changelog"]; delete cds.db.entities.BookStores["@changelog"]; - const action = PATCH.bind({}, `/admin/Books(ID=9d703c23-54a8-4eff-81c1-cdce6b8376b1,IsActiveEntity=false)`, { + const action = PATCH.bind({}, `/odata/v4/admin/Books(ID=9d703c23-54a8-4eff-81c1-cdce6b8376b1,IsActiveEntity=false)`, { title: "new title", }); await utils.apiAction("admin", "BookStores", "64625905-c234-4d0d-9bc1-283ee8946770", "AdminService", action); @@ -370,6 +373,25 @@ describe("change log integration test", () => { ]; }); + it("2.7 Composition of inline entity for draft enabled entity", async () => { + const action = PATCH.bind({}, `/odata/v4/admin/BookStores_bookInventory(up__ID=64625905-c234-4d0d-9bc1-283ee8946770,ID=3ccf474c-3881-44b7-99fb-59a2a4668418,IsActiveEntity=false)`, { + title: "update title" + }); + + await utils.apiAction("admin", "BookStores", "64625905-c234-4d0d-9bc1-283ee8946770", "AdminService", action); + + const changes = await adminService.run(SELECT.from(ChangeView)); + + expect(changes.length).to.equal(1); + const change = changes[0]; + expect(change.attribute).to.equal("title"); + expect(change.modification).to.equal("Update"); + expect(change.valueChangedFrom).to.equal("Eleonora"); + expect(change.valueChangedTo).to.equal("update title"); + expect(change.parentKey).to.equal("64625905-c234-4d0d-9bc1-283ee8946770"); + expect(change.keys).to.equal("ID=3ccf474c-3881-44b7-99fb-59a2a4668418"); + }); + it("4.1 Annotate multiple native and attributes comming from one or more associated table as the object ID (ERP4SMEPREPWORKAPPPLAT-913)", async () => { // After appending object id as below, the object ID sequence should be: // title, author.name.firstName, author.name.lastName, stock, bookStore.name, bookStore.location @@ -381,7 +403,7 @@ describe("change log integration test", () => { const action = POST.bind( {}, - `/admin/BookStores(ID=64625905-c234-4d0d-9bc1-283ee8946770,IsActiveEntity=false)/books`, + `/odata/v4/admin/BookStores(ID=64625905-c234-4d0d-9bc1-283ee8946770,IsActiveEntity=false)/books`, { ID: "9d703c23-54a8-4eff-81c1-cdce6b8376b2", title: "test title", @@ -426,7 +448,7 @@ describe("change log integration test", () => { { "=": "author.name.lastName" }, ]; - const actionPH = PATCH.bind({}, `/admin/Books(ID=9d703c23-54a8-4eff-81c1-cdce6b8376b2,IsActiveEntity=false)`, { + const actionPH = PATCH.bind({}, `/odata/v4/admin/Books(ID=9d703c23-54a8-4eff-81c1-cdce6b8376b2,IsActiveEntity=false)`, { title: "test title 1", }); await utils.apiAction("admin", "BookStores", "64625905-c234-4d0d-9bc1-283ee8946770", "AdminService", actionPH); @@ -454,7 +476,7 @@ describe("change log integration test", () => { { "=": "author.name.lastName" }, ]; - const actionDE = DELETE.bind({}, `/admin/Books(ID=9d703c23-54a8-4eff-81c1-cdce6b8376b2,IsActiveEntity=false)`); + const actionDE = DELETE.bind({}, `/odata/v4/admin/Books(ID=9d703c23-54a8-4eff-81c1-cdce6b8376b2,IsActiveEntity=false)`); await utils.apiAction("admin", "BookStores", "64625905-c234-4d0d-9bc1-283ee8946770", "AdminService", actionDE); const deleteTitleChanges = await adminService.run( @@ -484,7 +506,7 @@ describe("change log integration test", () => { { "=": "stock" }, ]; - const action = PATCH.bind({}, `/admin/Books(ID=9d703c23-54a8-4eff-81c1-cdce6b8376b1,IsActiveEntity=false)`, { + const action = PATCH.bind({}, `/odata/v4/admin/Books(ID=9d703c23-54a8-4eff-81c1-cdce6b8376b1,IsActiveEntity=false)`, { title: "new title", author_ID: "47f97f40-4f41-488a-b10b-a5725e762d5e", genre_ID: 16, @@ -518,7 +540,7 @@ describe("change log integration test", () => { { "=": "genre.ID" }, ]; - const action = PATCH.bind({}, `/admin/Books(ID=9d703c23-54a8-4eff-81c1-cdce6b8376b1,IsActiveEntity=false)`, { + const action = PATCH.bind({}, `/odata/v4/admin/Books(ID=9d703c23-54a8-4eff-81c1-cdce6b8376b1,IsActiveEntity=false)`, { title: "new title", author_ID: "47f97f40-4f41-488a-b10b-a5725e762d5e", genre_ID: 16, @@ -558,7 +580,7 @@ describe("change log integration test", () => { const action = POST.bind( {}, - `/admin/BookStores(ID=64625905-c234-4d0d-9bc1-283ee8946770,IsActiveEntity=false)/books`, + `/odata/v4/admin/BookStores(ID=64625905-c234-4d0d-9bc1-283ee8946770,IsActiveEntity=false)/books`, { ID: "9d703c23-54a8-4eff-81c1-cdce6b8376b2", author_ID: "d4d4a1b3-5b83-4814-8a20-f039af6f0387", @@ -592,7 +614,7 @@ describe("change log integration test", () => { { "=": "author.name.lastName" }, ]; - const actionPH = PATCH.bind({}, `/admin/Books(ID=9d703c23-54a8-4eff-81c1-cdce6b8376b2,IsActiveEntity=false)`, { + const actionPH = PATCH.bind({}, `/odata/v4/admin/Books(ID=9d703c23-54a8-4eff-81c1-cdce6b8376b2,IsActiveEntity=false)`, { author_ID: "47f97f40-4f41-488a-b10b-a5725e762d5e", }); await utils.apiAction("admin", "BookStores", "64625905-c234-4d0d-9bc1-283ee8946770", "AdminService", actionPH); @@ -621,7 +643,7 @@ describe("change log integration test", () => { const action = POST.bind( {}, - `/admin/BookStores(ID=64625905-c234-4d0d-9bc1-283ee8946770,IsActiveEntity=false)/books`, + `/odata/v4/admin/BookStores(ID=64625905-c234-4d0d-9bc1-283ee8946770,IsActiveEntity=false)/books`, { ID: "9d703c23-54a8-4eff-81c1-cdce6b8376b2", title: "test title", @@ -652,7 +674,7 @@ describe("change log integration test", () => { { "=": "books.price" }, ]; - const actionPH = PATCH.bind({}, `/admin/Books(ID=9d703c23-54a8-4eff-81c1-cdce6b8376b2,IsActiveEntity=false)`, { + const actionPH = PATCH.bind({}, `/odata/v4/admin/Books(ID=9d703c23-54a8-4eff-81c1-cdce6b8376b2,IsActiveEntity=false)`, { stock: 3, }); await utils.apiAction("admin", "BookStores", "64625905-c234-4d0d-9bc1-283ee8946770", "AdminService", actionPH); @@ -677,7 +699,7 @@ describe("change log integration test", () => { it("6.1 Single attribute from the code list could be annotated as value (ERP4SMEPREPWORKAPPPLAT-1055)", async () => { // When BookStore is created, the lifecycle status will be set to "in preparation" by default - const action = POST.bind({}, `/admin/BookStores`, { + const action = POST.bind({}, `/odata/v4/admin/BookStores`, { ID: "01234567-89ab-cdef-0123-456789abcdef", name: "test name", }); @@ -706,7 +728,7 @@ describe("change log integration test", () => { const actionPH = PATCH.bind( {}, - `/admin/BookStores(ID=01234567-89ab-cdef-0123-456789abcdef,IsActiveEntity=false)`, + `/odata/v4/admin/BookStores(ID=01234567-89ab-cdef-0123-456789abcdef,IsActiveEntity=false)`, { lifecycleStatus: { code: "CL", @@ -734,7 +756,7 @@ describe("change log integration test", () => { it("6.2 Multiple attributes from the code list could be annotated as value (ERP4SMEPREPWORKAPPPLAT-1055)", async () => { const action = POST.bind( {}, - `/admin/BookStores(ID=64625905-c234-4d0d-9bc1-283ee8946770,IsActiveEntity=false)/books`, + `/odata/v4/admin/BookStores(ID=64625905-c234-4d0d-9bc1-283ee8946770,IsActiveEntity=false)/books`, { ID: "7e9d4199-4602-47f1-8767-85dae82ce639", bookType: { @@ -757,7 +779,7 @@ describe("change log integration test", () => { expect(bookTypeChange.valueChangedFrom).to.equal(""); expect(bookTypeChange.valueChangedTo).to.equal("Management, Management Books"); - const actionPH = PATCH.bind({}, `/admin/Books(ID=7e9d4199-4602-47f1-8767-85dae82ce639,IsActiveEntity=false)`, { + const actionPH = PATCH.bind({}, `/odata/v4/admin/Books(ID=7e9d4199-4602-47f1-8767-85dae82ce639,IsActiveEntity=false)`, { bookType: { code: "SCI", }, @@ -786,7 +808,7 @@ describe("change log integration test", () => { { "=": "lifecycleStatus.name" }, ]; - const action = POST.bind({}, `/admin/BookStores`, { + const action = POST.bind({}, `/odata/v4/admin/BookStores`, { ID: "01234567-89ab-cdef-0123-456789abcdef", name: "test name", }); @@ -819,7 +841,7 @@ describe("change log integration test", () => { ]; const actionPH = PATCH.bind( {}, - `/admin/BookStores(ID=01234567-89ab-cdef-0123-456789abcdef,IsActiveEntity=false)`, + `/odata/v4/admin/BookStores(ID=01234567-89ab-cdef-0123-456789abcdef,IsActiveEntity=false)`, { lifecycleStatus: { code: "CL", @@ -872,7 +894,7 @@ describe("change log integration test", () => { const BookStoresChange = BookStoresChanges[0]; expect(BookStoresChange.objectID).to.equal("new name"); - const updateBookStoresAction = PATCH.bind({}, `/admin/BookStores(ID=9d703c23-54a8-4eff-81c1-cdce6b6587c4,IsActiveEntity=false)`, { + const updateBookStoresAction = PATCH.bind({}, `/odata/v4/admin/BookStores(ID=9d703c23-54a8-4eff-81c1-cdce6b6587c4,IsActiveEntity=false)`, { name: "name update", }); await utils.apiAction( @@ -1229,7 +1251,7 @@ describe("change log integration test", () => { delete cds.services.AdminService.entities.BookStores["@changelog"]; const action = POST.bind( {}, - `/admin/BookStores(ID=64625905-c234-4d0d-9bc1-283ee8946770,IsActiveEntity=false)/books`, + `/odata/v4/admin/BookStores(ID=64625905-c234-4d0d-9bc1-283ee8946770,IsActiveEntity=false)/books`, { ID: "9d703c23-54a8-4eff-81c1-cdce6b8376b2", title: "test title", @@ -1280,7 +1302,7 @@ describe("change log integration test", () => { }); it("10.4 Composition of one node creation - should log changes for root entity (ERP4SMEPREPWORKAPPPLAT-2913)", async () => { - const action = POST.bind({}, `/admin/BookStores`, { + const action = POST.bind({}, `/odata/v4/admin/BookStores`, { ID: "01234567-89ab-cdef-0123-456789abcdef", name: "Murder on the Orient Express", registry: { @@ -1324,7 +1346,7 @@ describe("change log integration test", () => { it("10.5.1 Composition of one node updated on root node - should log changes for root entity (ERP4SMEPREPWORKAPPPLAT-2913)", async () => { const action = PATCH.bind( {}, - `/admin/BookStores(ID=5ab2a87b-3a56-4d97-a697-7af72334a384,IsActiveEntity=false)`, + `/odata/v4/admin/BookStores(ID=5ab2a87b-3a56-4d97-a697-7af72334a384,IsActiveEntity=false)`, { registry: { ID: "12ed5dd8-d45b-11ed-afa1-0242ac120001", @@ -1357,7 +1379,7 @@ describe("change log integration test", () => { // Update by calling API on child node const action = PATCH.bind( {}, - `/admin/BookStoreRegistry(ID=12ed5dd8-d45b-11ed-afa1-0242ac120002,IsActiveEntity=false)`, + `/odata/v4/admin/BookStoreRegistry(ID=12ed5dd8-d45b-11ed-afa1-0242ac120002,IsActiveEntity=false)`, { validOn: "2022-01-01", } @@ -1382,7 +1404,7 @@ describe("change log integration test", () => { it("10.6 Composition of one node deleted - should log changes for root entity (ERP4SMEPREPWORKAPPPLAT-2913)", async () => { const action = DELETE.bind( {}, - `/admin/BookStoreRegistry(ID=12ed5dd8-d45b-11ed-afa1-0242ac120002,IsActiveEntity=false)` + `/odata/v4/admin/BookStoreRegistry(ID=12ed5dd8-d45b-11ed-afa1-0242ac120002,IsActiveEntity=false)` ); await utils.apiAction("admin", "BookStores", "8aaed432-8336-4b0d-be7e-3ef1ce7f13ea", "AdminService", action); const registryChanges = await adminService.run( @@ -1403,19 +1425,52 @@ describe("change log integration test", () => { it("11.1 The change log should be captured when a child entity in draft-enabled mode triggers a custom action (ERP4SMEPREPWORKAPPPLAT-6211)", async () => { await POST( - `/odata/v4/admin/BookStores(ID=64625905-c234-4d0d-9bc1-283ee8946770,IsActiveEntity=true)/books(ID=9d703c23-54a8-4eff-81c1-cdce6b8376b1,IsActiveEntity=true)/volumns(ID=dd1fdd7d-da2a-4600-940b-0baf2946c9bf,IsActiveEntity=true)/AdminService.activate`, - { - ActivationStatus_code: "VALID", - }, + `/odata/v4/admin/BookStores(ID=64625905-c234-4d0d-9bc1-283ee8946770,IsActiveEntity=true)/books(ID=9d703c23-54a8-4eff-81c1-cdce6b8376b1,IsActiveEntity=true)/volumns(ID=dd1fdd7d-da2a-4600-940b-0baf2946c9bf,IsActiveEntity=true)/AdminService.activate` ); let changes = await SELECT.from(ChangeView).where({ entity: "sap.capire.bookshop.Volumns", attribute: "ActivationStatus", }); + expect(changes.length).to.equal(1); expect(changes[0].valueChangedFrom).to.equal(""); expect(changes[0].valueChangedTo).to.equal("VALID"); expect(changes[0].entityKey).to.equal("64625905-c234-4d0d-9bc1-283ee8946770"); expect(changes[0].parentKey).to.equal("9d703c23-54a8-4eff-81c1-cdce6b8376b1"); + + // Check the changeLog to make sure the entity information is root + let changeLogs = await SELECT.from(ChangeLog).where({ + entity: "sap.capire.bookshop.BookStores", + entityKey: "64625905-c234-4d0d-9bc1-283ee8946770", + serviceEntity: "AdminService.BookStores", + }); + + expect(changeLogs.length).to.equal(1); + expect(changeLogs[0].entity).to.equal("sap.capire.bookshop.BookStores"); + expect(changeLogs[0].entityKey).to.equal("64625905-c234-4d0d-9bc1-283ee8946770"); + expect(changeLogs[0].serviceEntity).to.equal("AdminService.BookStores"); + + changes = await SELECT.from(ChangeView).where({ + entity: "sap.capire.bookshop.Books", + attribute: "title", + }); + + expect(changes.length).to.equal(1); + expect(changes[0].valueChangedFrom).to.equal("Jane Eyre"); + expect(changes[0].valueChangedTo).to.equal("Black Myth wukong"); + expect(changes[0].entityKey).to.equal("5ab2a87b-3a56-4d97-a697-7af72334a384"); + expect(changes[0].parentKey).to.equal("5ab2a87b-3a56-4d97-a697-7af72334a384"); + + // Check the changeLog to make sure the entity information is root + changeLogs = await SELECT.from(ChangeLog).where({ + entity: "sap.capire.bookshop.BookStores", + entityKey: "5ab2a87b-3a56-4d97-a697-7af72334a384", + serviceEntity: "AdminService.BookStores", + }); + + expect(changeLogs.length).to.equal(1); + expect(changeLogs[0].entity).to.equal("sap.capire.bookshop.BookStores"); + expect(changeLogs[0].entityKey).to.equal("5ab2a87b-3a56-4d97-a697-7af72334a384"); + expect(changeLogs[0].serviceEntity).to.equal("AdminService.BookStores"); }); }); diff --git a/tests/integration/service-api.test.js b/tests/integration/service-api.test.js index 499036b..93131d7 100644 --- a/tests/integration/service-api.test.js +++ b/tests/integration/service-api.test.js @@ -6,11 +6,16 @@ jest.setTimeout(5 * 60 * 1000); let adminService = null; let ChangeView = null; +let ChangeLog = null; +let db = null; describe("change log integration test", () => { beforeAll(async () => { adminService = await cds.connect.to("AdminService"); + db = await cds.connect.to("sql:my.db"); ChangeView = adminService.entities.ChangeView; + ChangeView["@cds.autoexposed"] = false; + ChangeLog = db.model.definitions["sap.changelog.ChangeLog"]; }); beforeEach(async () => { @@ -19,20 +24,24 @@ describe("change log integration test", () => { it("1.6 When the global switch is on, all changelogs should be retained after the root entity is deleted, and a changelog for the deletion operation should be generated", async () => { cds.env.requires["change-tracking"].preserveDeletes = true; - const level3EntityData = [ + + const authorData = [ { - ID: "12ed5dd8-d45b-11ed-afa1-0242ac654321", - title: "Service api Level3 title", - parent_ID: "dd1fdd7d-da2a-4600-940b-0baf2946c4ff", - }, - ]; - await adminService.run(INSERT.into(adminService.entities.Level3Entity).entries(level3EntityData)); - let beforeChanges = await SELECT.from(ChangeView); + ID: "64625905-c234-4d0d-9bc1-283ee8940812", + name_firstName: "Sam", + name_lastName: "Smiths", + placeOfBirth: "test place", + } + ] + + await INSERT.into(adminService.entities.Authors).entries(authorData); + const beforeChanges = await adminService.run(SELECT.from(ChangeView)); expect(beforeChanges.length > 0).to.be.true; - await adminService.run(DELETE.from(adminService.entities.RootEntity).where({ ID: "64625905-c234-4d0d-9bc1-283ee8940812" })); - let afterChanges = await SELECT.from(ChangeView); - expect(afterChanges.length).to.equal(11); + await DELETE.from(adminService.entities.Authors).where({ ID: "64625905-c234-4d0d-9bc1-283ee8940812" }); + + const afterChanges = await adminService.run(SELECT.from(ChangeView)); + expect(afterChanges.length).to.equal(6); }); it("2.5 Root entity deep creation by service API - should log changes on root entity (ERP4SMEPREPWORKAPPPLAT-32 ERP4SMEPREPWORKAPPPLAT-613)", async () => { @@ -42,6 +51,7 @@ describe("change log integration test", () => { location: "test location", books: [ { + ID: "f35b2d4c-9b21-4b9a-9b3c-ca1ad32a0d1a", title: "test title", descr: "test", stock: 333, @@ -50,7 +60,10 @@ describe("change log integration test", () => { }, ], }; - await adminService.run(INSERT.into(adminService.entities.BookStores).entries(bookStoreData)); + await INSERT.into(adminService.entities.BookStores).entries(bookStoreData); + + // REVISIT: CAP currently does not support run queries on the draft-enabled entity on application service (details in CAP/Issue#16292) + // await adminService.run(INSERT.into(adminService.entities.BookStores).entries(bookStoreData)); let changes = await SELECT.from(ChangeView).where({ entity: "sap.capire.bookshop.BookStores", @@ -87,6 +100,28 @@ describe("change log integration test", () => { expect(changes[0].parentObjectID).to.equal("Shakespeare and Company"); }); + it("3.6 Composition operation of inline entity operation by QL API", async () => { + await UPDATE(adminService.entities["Order.Items"]) + .where({ + up__ID: "3b23bb4b-4ac7-4a24-ac02-aa10cabd842c", + ID: "2b23bb4b-4ac7-4a24-ac02-aa10cabd842c" + }) + .with({ + quantity: 12 + }); + + const changes = await adminService.run(SELECT.from(ChangeView)); + + expect(changes.length).to.equal(1); + const change = changes[0]; + expect(change.attribute).to.equal("quantity"); + expect(change.modification).to.equal("Update"); + expect(change.valueChangedFrom).to.equal("10"); + expect(change.valueChangedTo).to.equal("12"); + expect(change.parentKey).to.equal("3b23bb4b-4ac7-4a24-ac02-aa10cabd842c"); + expect(change.keys).to.equal("ID=2b23bb4b-4ac7-4a24-ac02-aa10cabd842c"); + }); + it("7.3 Annotate fields from chained associated entities as objectID (ERP4SMEPREPWORKAPPPLAT-4542)", async () => { cds.services.AdminService.entities.BookStores["@changelog"].push({ "=": "city.name" }) @@ -94,7 +129,7 @@ describe("change log integration test", () => { ID: "9d703c23-54a8-4eff-81c1-cdce6b6587c4", name: "new name", }; - await adminService.run(INSERT.into(adminService.entities.BookStores).entries(bookStoreData)); + await INSERT.into(adminService.entities.BookStores).entries(bookStoreData); let createBookStoresChanges = await SELECT.from(ChangeView).where({ entity: "sap.capire.bookshop.BookStores", attribute: "name", @@ -131,7 +166,7 @@ describe("change log integration test", () => { parent_ID: "dd1fdd7d-da2a-4600-940b-0baf2946c4ff", }, ]; - await adminService.run(INSERT.into(adminService.entities.Level3Entity).entries(level3EntityData)); + await INSERT.into(adminService.entities.Level3Entity).entries(level3EntityData); let createChanges = await SELECT.from(ChangeView).where({ entity: "sap.capire.bookshop.Level3Entity", attribute: "title", @@ -140,6 +175,20 @@ describe("change log integration test", () => { expect(createChanges.length).to.equal(1); const createChange = createChanges[0]; expect(createChange.objectID).to.equal("In Preparation"); + expect(createChange.parentKey).to.equal("dd1fdd7d-da2a-4600-940b-0baf2946c4ff"); + expect(createChange.parentObjectID).to.equal("In Preparation"); + + // Check the changeLog to make sure the entity information is root + const changeLogs = await SELECT.from(ChangeLog).where({ + entity: "sap.capire.bookshop.RootEntity", + entityKey: "64625905-c234-4d0d-9bc1-283ee8940812", + serviceEntity: "AdminService.RootEntity", + }) + + expect(changeLogs.length).to.equal(1); + expect(changeLogs[0].entity).to.equal("sap.capire.bookshop.RootEntity"); + expect(changeLogs[0].entityKey).to.equal("64625905-c234-4d0d-9bc1-283ee8940812"); + expect(changeLogs[0].serviceEntity).to.equal("AdminService.RootEntity"); await UPDATE(adminService.entities.Level3Entity, "12ed5dd8-d45b-11ed-afa1-0242ac654321").with({ title: "L3 title changed by QL API", @@ -152,6 +201,8 @@ describe("change log integration test", () => { expect(createChanges.length).to.equal(1); const updateChange = updateChanges[0]; expect(updateChange.objectID).to.equal("In Preparation"); + expect(createChange.parentKey).to.equal("dd1fdd7d-da2a-4600-940b-0baf2946c4ff"); + expect(createChange.parentObjectID).to.equal("In Preparation"); await DELETE.from(adminService.entities.Level3Entity).where({ ID: "12ed5dd8-d45b-11ed-afa1-0242ac654321" }); let deleteChanges = await SELECT.from(ChangeView).where({ @@ -162,6 +213,8 @@ describe("change log integration test", () => { expect(deleteChanges.length).to.equal(1); const deleteChange = deleteChanges[0]; expect(deleteChange.objectID).to.equal("In Preparation"); + expect(createChange.parentKey).to.equal("dd1fdd7d-da2a-4600-940b-0baf2946c4ff"); + expect(createChange.parentObjectID).to.equal("In Preparation"); // Test object id when parent and child nodes are created at the same time const RootEntityData = { @@ -172,16 +225,18 @@ describe("change log integration test", () => { { ID: "12ed5dd8-d45b-11ed-afa1-0242ac120003", title: "New name for Level1Entity", + parent_ID: "01234567-89ab-cdef-0123-987654fedcba", child: [ { ID: "12ed5dd8-d45b-11ed-afa1-0242ac124446", - title: "New name for Level2Entity" + title: "New name for Level2Entity", + parent_ID: "12ed5dd8-d45b-11ed-afa1-0242ac120003" }, ], }, ], }; - await adminService.run(INSERT.into(adminService.entities.RootEntity).entries(RootEntityData)); + await INSERT.into(adminService.entities.RootEntity).entries(RootEntityData); const createEntityChanges = await adminService.run( SELECT.from(ChangeView).where({ @@ -258,7 +313,7 @@ describe("change log integration test", () => { info_ID: "bc21e0d9-a313-4f52-8336-c1be5f88c346", }, ]; - await adminService.run(INSERT.into(adminService.entities.RootEntity).entries(rootEntityData)); + await INSERT.into(adminService.entities.RootEntity).entries(rootEntityData); let createChanges = await SELECT.from(ChangeView).where({ entity: "sap.capire.bookshop.RootEntity", attribute: "info", @@ -294,7 +349,10 @@ describe("change log integration test", () => { validOn: "2022-01-01", }, }; - await adminService.run(INSERT.into(adminService.entities.BookStores).entries(bookStoreData)); + await INSERT.into(adminService.entities.BookStores).entries(bookStoreData); + + // REVISIT: CAP currently does not support run queries on the draft-enabled entity on application service (details in CAP/Issue#16292) + // await adminService.run(INSERT.into(adminService.entities.BookStores).entries(bookStoreData)); let changes = await SELECT.from(ChangeView).where({ entity: "sap.capire.bookshop.BookStoreRegistry", diff --git a/tests/utils/api.js b/tests/utils/api.js index 98362a4..d3b77ce 100644 --- a/tests/utils/api.js +++ b/tests/utils/api.js @@ -4,15 +4,15 @@ class RequestSend { } async apiAction(serviceName, entityName, id, path, action, isRootCreated = false) { if (!isRootCreated) { - await this.post(`/${serviceName}/${entityName}(ID=${id},IsActiveEntity=true)/${path}.draftEdit`, { + await this.post(`/odata/v4/${serviceName}/${entityName}(ID=${id},IsActiveEntity=true)/${path}.draftEdit`, { PreserveChanges: true, }); } await action(); - await this.post(`/${serviceName}/${entityName}(ID=${id},IsActiveEntity=false)/${path}.draftPrepare`, { + await this.post(`/odata/v4/${serviceName}/${entityName}(ID=${id},IsActiveEntity=false)/${path}.draftPrepare`, { SideEffectsQualifier: "", }); - await this.post(`/${serviceName}/${entityName}(ID=${id},IsActiveEntity=false)/${path}.draftActivate`, {}); + await this.post(`/odata/v4/${serviceName}/${entityName}(ID=${id},IsActiveEntity=false)/${path}.draftActivate`, {}); } }