Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support unmanaged associations (to an extend) #112

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions lib/change-log.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,73 @@ const _getEntityIDs = function (txParams) {
return entityIDs
}

/**
*
* @param {*} tx
* @param {*} changes
*
* When consuming app implement '@changelog' on an property element,
* change history can use attribute on associated entity which are specified instead of property value.
*
* eg:
* entity BookStore @(cds.autoexpose): cuid, managed {
* ...
* '@changelog': [bookOfTheMonth.title]
* bookOfTheMonthID: UUID;
* bookOfTheMonth : Association to one Book on bookOfTheMonth.ID = bookOfTheMonthID;
* ...
* }
*/
const _formatPropertyContext = async function (changes, reqData) {
for (const change of changes) {
const p = cds.model.definitions[change.serviceEntity].elements[change.attribute]
if (p?.type === "cds.Association" || typeof change.valueChangedTo === "object" || typeof p["@changelog"] !== "object") continue

const semkeys = getObjIdElementNamesInArray(p["@changelog"], false)
if (!semkeys.length) continue

const associationsUsed = Object.keys(semkeys.reduce((a, semkey) => {
a[semkey.split(".")[0]] = true;
return a;
}, {}));

if(associationsUsed.length > 1) {
throw new Error(`@changelog ${change.entity}.${change.attribute}: only one navigation property can be used in the annotation, found multiple: ${associationsUsed}`)
}

const a = cds.model.definitions[change.serviceEntity].elements[associationsUsed[0]]

const condition = a.on.reduce((conditions, e, i) => {
if (e === "=") {
const targetProperty = [...a.on[i - 1].ref];
targetProperty.shift();
const sourceProperty = a.on[i + 1].ref.join(".");
if(sourceProperty !== change.attribute) {
throw new Error(`@changlog ${change.entity}.${change.attribute}: association ${a.name} is required to only use conditions based on the annotated property, but uses ${sourceProperty}`)
}
conditions.changedFrom[targetProperty.join(".")] = change.valueChangedFrom;
conditions.changedTo[targetProperty.join(".")] = change.valueChangedTo;
} return conditions;
}, {changedFrom: {}, changedTo: {}})

const [from, to] = await cds.db.run([
SELECT.one.from(a.target).where(condition.changedFrom),
SELECT.one.from(a.target).where(condition.changedTo)
])

const semkeysForObjectId = getObjIdElementNamesInArray(p["@changelog"])

const fromObjId = await getObjectId(reqData, a.target, semkeysForObjectId, { curObjFromDbQuery: from || undefined }) // Note: ... || undefined is important for subsequent object destructuring with defaults
if (fromObjId) change.valueChangedFrom = fromObjId

const toObjId = await getObjectId(reqData, a.target, semkeysForObjectId, { curObjFromDbQuery: to || undefined }) // Note: ... || undefined is important for subsequent object destructuring with defaults
if (toObjId) change.valueChangedTo = toObjId

const isVLvA = a["@Common.ValueList.viaAssociation"]
if (!isVLvA) change.valueDataType = getValueEntityType(a.target, semkeysForObjectId)
}
}

/**
*
* @param {*} tx
Expand Down Expand Up @@ -291,6 +358,7 @@ const _isCompositionContextPath = function (aPath, hasComp) {

const _formatChangeLog = async function (changes, req) {
await _formatObjectID(changes, req.data)
await _formatPropertyContext(changes, req.data)
await _formatAssociationContext(changes, req.data)
await _formatCompositionContext(changes, req.data)
}
Expand Down
6 changes: 4 additions & 2 deletions lib/entity-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ const getEntityByContextPath = function (aPath, hasComp = false) {
return entity
}

const getObjIdElementNamesInArray = function (elements) {
const getObjIdElementNamesInArray = function (elements, shift=true) {
if (Array.isArray(elements)) return elements.map(e => {
const splitted = (e["="]||e).split('.')
splitted.shift()
if(shift) {
splitted.shift()
}
return splitted.join('.')
})
else return []
Expand Down
4 changes: 4 additions & 0 deletions tests/bookshop/db/schema.cds
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ entity BookStores @(cds.autoexpose) : managed, cuid {
books : Composition of many Books
on books.bookStore = $self;

@changelog: [bookOfTheMonth.title]
bookOfTheMonthID: UUID;
bookOfTheMonth: Association to one Books on bookOfTheMonth.ID = bookOfTheMonthID;

@title : '{i18n>bookStore.registry}'
registry : Composition of one BookStoreRegistry;

Expand Down
22 changes: 22 additions & 0 deletions tests/integration/fiori-draft-enabled.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,28 @@ describe("change log integration test", () => {
expect(afterChanges.length).to.equal(14);
});

it("unmanaged child entity update without objectID annotation - should log object type for object ID", async () => {


const changeUnmanagedAssociationId = PATCH.bind(
{},
`/odata/v4/admin/BookStores(ID=64625905-c234-4d0d-9bc1-283ee8946770,IsActiveEntity=false)`,
{
bookOfTheMonthID: "676059d4-8851-47f1-b558-3bdc461bf7d5"
}
);
await utils.apiAction("admin", "BookStores", "64625905-c234-4d0d-9bc1-283ee8946770", "AdminService", changeUnmanagedAssociationId);

const bookChanges = await adminService.run(
SELECT.from(ChangeView).where({
entity: "sap.capire.bookshop.BookStores",
attribute: "bookOfTheMonthID",
})
);
expect(bookChanges.length).to.equal(1);
expect(bookChanges[0].valueChangedTo).to.equal("Jane Eyre");
})

it("2.1 Child entity creation - should log basic data type changes (ERP4SMEPREPWORKAPPPLAT-32 ERP4SMEPREPWORKAPPPLAT-613)", async () => {
const action = POST.bind(
{},
Expand Down
Loading