From 81625b56bed91a264f95e6b86f0b7f4d3a818330 Mon Sep 17 00:00:00 2001 From: D050513 Date: Fri, 9 May 2025 11:51:38 +0200 Subject: [PATCH 1/7] add .DS_Store to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 515e907..cef175f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# macOS +.DS_Store + # Logs logs *.log From c488e1ba7014a867e281da82849f21fd90d7c301 Mon Sep 17 00:00:00 2001 From: D050513 Date: Fri, 9 May 2025 11:51:49 +0200 Subject: [PATCH 2/7] rm reuse.yml --- .github/workflows/reuse.yml | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 .github/workflows/reuse.yml diff --git a/.github/workflows/reuse.yml b/.github/workflows/reuse.yml deleted file mode 100644 index 3c1052b..0000000 --- a/.github/workflows/reuse.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: 'Launch Metadata Creation Tool for REUSE' -on: - workflow_dispatch: ~ - -jobs: - create_metadata_proposal: - runs-on: ubuntu-latest - name: 'Metadata Creation Tool' - steps: - - uses: SAP/metadata-creation-tool-for-reuse@main - with: - repository_url: '${{ github.server_url }}/${{ github.repository }}' - access_token: '${{ secrets.REUSE_ACCESS_TOKEN }}' - copyright_owner: 'SAP SE or an SAP affiliate company and contributors' - upstream_contact: 'The CAP team ' From ce8ee2f7138f5400cfdb215a8a662a5a585e4d53 Mon Sep 17 00:00:00 2001 From: D050513 Date: Fri, 9 May 2025 11:52:10 +0200 Subject: [PATCH 3/7] test release with node 20 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef1413e..538d0fe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 18 + node-version: 20 registry-url: https://registry.npmjs.org/ - name: run tests run: | From 65068ebc285de27065a4e6f8c24a3f288a30c52c Mon Sep 17 00:00:00 2001 From: D050513 Date: Fri, 9 May 2025 11:52:38 +0200 Subject: [PATCH 4/7] test with cds versions 9 and 8 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc3c87b..035a720 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: fail-fast: false matrix: node-version: [22.x, 20.x] - cds-version: [8, 7] + cds-version: [9, 8] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} From 0c5b613455ce46604cc7db8b9f4ef1939cd40773 Mon Sep 17 00:00:00 2001 From: D050513 Date: Mon, 12 May 2025 19:47:04 +0200 Subject: [PATCH 5/7] outboxed --- package.json | 4 ++-- test/api/api.test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 310a67b..6716b1a 100644 --- a/package.json +++ b/package.json @@ -30,14 +30,14 @@ "cds": { "requires": { "[test]": { - "outbox": "persistent-outbox" + "outboxed": "persistent-outbox" }, "audit-log": { "handle": [ "READ", "WRITE" ], - "outbox": true, + "outboxed": true, "[development]": { "kind": "audit-log-to-console" }, diff --git a/test/api/api.test.js b/test/api/api.test.js index 3a0f938..121be36 100644 --- a/test/api/api.test.js +++ b/test/api/api.test.js @@ -8,7 +8,7 @@ axios.defaults.validateStatus = () => true cds.env.requires['audit-log'] = { kind: 'audit-log-to-console', impl: '../../srv/log2console', - outbox: true + outboxed: true } const wait = require('node:timers/promises').setTimeout From f8188b4f4bf6fe4f717293ec6045ed9d98d4d9f4 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Tue, 3 Jun 2025 10:11:09 +0200 Subject: [PATCH 6/7] back to outbox --- package.json | 4 ++-- test/api/api.test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 6716b1a..310a67b 100644 --- a/package.json +++ b/package.json @@ -30,14 +30,14 @@ "cds": { "requires": { "[test]": { - "outboxed": "persistent-outbox" + "outbox": "persistent-outbox" }, "audit-log": { "handle": [ "READ", "WRITE" ], - "outboxed": true, + "outbox": true, "[development]": { "kind": "audit-log-to-console" }, diff --git a/test/api/api.test.js b/test/api/api.test.js index 121be36..3a0f938 100644 --- a/test/api/api.test.js +++ b/test/api/api.test.js @@ -8,7 +8,7 @@ axios.defaults.validateStatus = () => true cds.env.requires['audit-log'] = { kind: 'audit-log-to-console', impl: '../../srv/log2console', - outboxed: true + outbox: true } const wait = require('node:timers/promises').setTimeout From 0ea9b73013c133a74b3a3aecbae493ba1d710716 Mon Sep 17 00:00:00 2001 From: D050513 Date: Tue, 3 Jun 2025 10:12:14 +0200 Subject: [PATCH 7/7] [wip] pre calc queries --- cds-plugin.js | 94 +++++++++++++++++++++++++++++++++ lib/utils.js | 25 ++++++++- test/personal-data/crud.test.js | 3 ++ test/personal-data/package.json | 5 ++ 4 files changed, 125 insertions(+), 2 deletions(-) diff --git a/cds-plugin.js b/cds-plugin.js index 84ffcdb..3fc71f7 100644 --- a/cds-plugin.js +++ b/cds-plugin.js @@ -6,6 +6,8 @@ const { hasPersonalData } = require('./lib/utils') const WRITE = ['CREATE', 'UPDATE', 'DELETE'] +const $distance = Symbol('@cap-js/audit-logging:distance') + /* * Add generic audit logging handlers */ @@ -61,6 +63,98 @@ cds.on('served', services => { } }) +cds.on('served', services => { + // prettier-ignore + const { types, classes: { number } } = cds.builtin + + const recurse = (entity, ds, ds_keys, path, definitions) => { + // forwards + for (const assoc in entity.associations) { + const target = definitions[entity.associations[assoc].target] + + if (!target['@PersonalData.EntitySemantics']) continue + if (target['@PersonalData.EntitySemantics'] === 'DataSubject') continue + if (target.own($distance) && target[$distance] <= path.length) continue + + target.set($distance, path.length) + + // the known entity instance as starting point + const kp = Object.keys(target.keys).reduce((acc, cur) => { + if (cur !== 'IsActiveEntity') acc.push(`${cur}=%%%${cur}%%%`) + return acc + }, []) + // path.push({ id: target.name, where: kp }) + path.push({ id: assoc, where: kp }) + + // construct path as string + const p = path.reduce((acc, cur) => { + if (!acc) { + // acc += `${cur.id}${cur.where ? `[${cur.where.join(' and ')}]` : ''}` + acc += `${cur.id}` + } else { + if (cur.id) { + const close = acc.match(/([\]]+)$/)?.[1] + if (close) + acc = + acc.slice(0, close.length * -1) + + `[exists ${cur.id}${cur.where ? `[${cur.where.join(' and ')}]` : ''}]` + + close + else acc += `[exists ${cur.id}${cur.where ? `[${cur.where.join(' and ')}]` : ''}]` + } else if (cur.to) acc += `.${cur.to}` + } + return acc + }, '') + + target._getDataSubjectQuery = row => { + let path = `${p}` + for (const ph of path.match(/%%%(\w+)%%%/g)) { + const ref = ph.slice(3, -3) + const val = row[ref] + path = path.replace(ph, types[target.elements[ref]._type] instanceof number ? val : `'${val}'`) + } + return SELECT.one.from(path).columns(ds_keys) + } + + delete path.at(-1).where + + recurse(target, ds, ds_keys, path, definitions) + + path.pop() + } + + // backwards + const targets = Object.values(definitions).filter( + d => + d['@PersonalData.EntitySemantics'] && + d['@PersonalData.EntitySemantics'] !== 'DataSubject' && + !d.own($distance) && + Object.values(d.associations || {}).some( + a => a.target === entity.name && !Object.values(entity.associations || {}).some(b => b.target === d.name) + ) + ) + + for (const target of targets) { + // debugger + } + } + + for (const service of services) { + if (!(service instanceof cds.ApplicationService)) continue + + const dataSubjects = [] + for (const entity of service.entities) + if (entity['@PersonalData.EntitySemantics'] === 'DataSubject') dataSubjects.push(entity) + if (!dataSubjects.length) continue + + const definitions = service.model.definitions + for (const ds of dataSubjects) { + const ds_keys = Object.keys(ds.keys) + const path = [{ id: ds.name }] + recurse(ds, ds, ds_keys, path, definitions) + } + } +}) + /* * Export base class for extending in custom implementations */ diff --git a/lib/utils.js b/lib/utils.js index 47c00a0..130df31 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -253,7 +253,18 @@ const addDataSubjectForDetailsEntity = (row, log, req, entity, model) => { const map = _getDataSubjectsMap(req) if (map.has(role)) log.data_subject.id = map.get(role) // REVISIT by downward lookups row might already contain ID - some potential to optimize - else map.set(role, _getDataSubjectIdQuery(dataSubjectInfo, row, model)) + else { + // let q = _getDataSubjectIdQuery(dataSubjectInfo, row, model) + let q + if (entity._getDataSubjectQuery) { + q = entity._getDataSubjectQuery(row) + } else { + // debugger + q = _getDataSubjectIdQuery(dataSubjectInfo, row, model) + } + // q = _getDataSubjectIdQuery(dataSubjectInfo, row, model) + map.set(role, q) + } } const resolveDataSubjects = (logs, req) => { @@ -265,7 +276,17 @@ const resolveDataSubjects = (logs, req) => { if (each.data_subject.id instanceof cds.ql.Query) { const q = each.data_subject.id if (!map.has(q)) { - const p = cds.run(q).then(res => map.set(q, res)) + const p = cds + .run(q) + .then(res => { + // debugger + map.set(q, res) + }) + .catch(e => { + q + debugger + throw e + }) map.set(q, p) ps.push(p) } diff --git a/test/personal-data/crud.test.js b/test/personal-data/crud.test.js index 3bade6d..946690a 100644 --- a/test/personal-data/crud.test.js +++ b/test/personal-data/crud.test.js @@ -1462,6 +1462,9 @@ describe('personal data audit logging in CRUD', () => { }) // check only one select used to look up data subject + + const _selects = _logger._logs.debug.filter(l => typeof l === 'string' && l.match(/^SELECT/)) + const selects = _logger._logs.debug.filter( l => typeof l === 'string' && l.match(/^SELECT/) && l.match(/SELECT [Customers.]*ID FROM CRUD_1_Customers/) ) diff --git a/test/personal-data/package.json b/test/personal-data/package.json index 4318efe..43b4c69 100644 --- a/test/personal-data/package.json +++ b/test/personal-data/package.json @@ -1,5 +1,10 @@ { "dependencies": { "@cap-js/audit-logging": "*" + }, + "cds": { + "requires": { + "outbox": false + } } }