@cap-js/change-tracking 1.1.3 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,127 +1,135 @@
1
- const cds = require("@sap/cds/lib");
2
- const LOG = cds.log("change-log");
3
- const { formatOptions } = require("./format-options");
4
- const { getNameFromPathVal, getDBEntity, splitPath } = require("./entity-helper");
1
+ const cds = require('@sap/cds/lib');
2
+ const LOG = cds.log('change-log');
3
+ const { formatOptions } = require('./format-options');
4
+ const { getNameFromPathVal, getDBEntity, splitPath } = require('./entity-helper');
5
5
 
6
6
  const MODIF_I18N_MAP = {
7
- create: "ChangeLog.modification.create",
8
- update: "ChangeLog.modification.update",
9
- delete: "ChangeLog.modification.delete",
7
+ create: 'ChangeLog.modification.create',
8
+ update: 'ChangeLog.modification.update',
9
+ delete: 'ChangeLog.modification.delete'
10
10
  };
11
11
 
12
12
  const _localizeModification = function (change) {
13
- if (change.modification && MODIF_I18N_MAP[change.modification]) {
14
- change.modification = cds.i18n.labels.for(MODIF_I18N_MAP[change.modification]);
15
- }
13
+ if (change.modification && MODIF_I18N_MAP[change.modification]) {
14
+ change.modification = cds.i18n.labels.for(MODIF_I18N_MAP[change.modification]);
15
+ }
16
16
  };
17
17
 
18
18
  const _localizeDefaultObjectID = function (change) {
19
- if (!change.objectID) {
20
- change.objectID = change.entity ? change.entity : "";
21
- }
22
- if (change.objectID && change.serviceEntityPath && !change.parentObjectID && change.parentKey) {
23
- const path = splitPath(change.serviceEntityPath);
24
- const parentNodePathVal = path[path.length - 2];
25
- const parentEntityName = getNameFromPathVal(parentNodePathVal);
26
- const dbEntity = getDBEntity(parentEntityName);
27
- try {
28
- const labelI18nKey = getTranslationKey(dbEntity['@Common.Label'] || dbEntity['@title']);
29
- change.parentObjectID = cds.i18n.labels.for(labelI18nKey) || labelI18nKey || dbEntity.name;
30
- } catch (e) {
31
- LOG.error("Failed to localize parent object id", e);
32
- throw new Error("Failed to localize parent object id", e);
33
- }
34
- }
19
+ if (!change.objectID) {
20
+ change.objectID = change.entity ? change.entity : '';
21
+ }
22
+ if (change.objectID && change.serviceEntityPath && !change.parentObjectID && change.parentKey) {
23
+ const path = splitPath(change.serviceEntityPath);
24
+ const parentNodePathVal = path[path.length - 2];
25
+ const parentEntityName = getNameFromPathVal(parentNodePathVal);
26
+ const dbEntity = getDBEntity(parentEntityName);
27
+ try {
28
+ const labelI18nKey = getTranslationKey(dbEntity['@Common.Label'] || dbEntity['@title']);
29
+ change.parentObjectID = cds.i18n.labels.for(labelI18nKey) || labelI18nKey || dbEntity.name;
30
+ } catch (e) {
31
+ LOG.error('Failed to localize parent object id', e);
32
+ throw new Error('Failed to localize parent object id', e);
33
+ }
34
+ }
35
35
  };
36
36
 
37
37
  const _localizeEntityType = function (change) {
38
- if (change.entity) {
39
- try {
40
- const labelI18nKey = _getLabelI18nKeyOnEntity(change.serviceEntity);
41
- change.entity = labelI18nKey || change.entity;
42
- } catch (e) {
43
- LOG.error("Failed to localize entity type", e);
44
- throw new Error("Failed to localize entity type", e);
45
- }
46
- }
47
- if (change.serviceEntity) {
48
- try {
49
- const labelI18nKey = _getLabelI18nKeyOnEntity(change.serviceEntity);
50
- change.serviceEntity = labelI18nKey || change.serviceEntity;
51
- } catch (e) {
52
- LOG.error("Failed to localize service entity", e);
53
- throw new Error("Failed to localize service entity", e);
54
- }
55
- }
38
+ if (change.entity) {
39
+ try {
40
+ const labelI18nKey = _getLabelI18nKeyOnEntity(change.serviceEntity);
41
+ change.entity = labelI18nKey || change.entity;
42
+ } catch (e) {
43
+ LOG.error('Failed to localize entity type', e);
44
+ throw new Error('Failed to localize entity type', e);
45
+ }
46
+ }
47
+ if (change.serviceEntity) {
48
+ try {
49
+ const labelI18nKey = _getLabelI18nKeyOnEntity(change.serviceEntity);
50
+ change.serviceEntity = labelI18nKey || change.serviceEntity;
51
+ } catch (e) {
52
+ LOG.error('Failed to localize service entity', e);
53
+ throw new Error('Failed to localize service entity', e);
54
+ }
55
+ }
56
56
  };
57
57
 
58
58
  const getTranslationKey = (value) => {
59
- if (typeof value != 'string') return value;
60
- const result = value.match(/(?<=\{@?(i18n>)).*(?=\})/g)
61
- return result ? result[0] : value
62
- }
59
+ if (typeof value != 'string') return value;
60
+ const result = value.match(/(?<=\{@?(i18n>)).*(?=\})/g);
61
+ return result ? result[0] : value;
62
+ };
63
63
 
64
64
  const _localizeAttribute = function (change) {
65
- if (change.attribute && change.serviceEntity) {
66
- try {
67
- const serviceEntity = cds.model.definitions[change.serviceEntity];
68
- let labelI18nKey = _getLabelI18nKeyOnEntity(change.serviceEntity, change.attribute);
69
- if (!labelI18nKey) {
70
- const element = serviceEntity.elements[change.attribute];
71
- if (element.isAssociation) labelI18nKey = _getLabelI18nKeyOnEntity(element.target);
72
- }
73
- change.attribute = labelI18nKey || change.attribute;
74
- } catch (e) {
75
- LOG.error("Failed to localize change attribute", e);
76
- throw new Error("Failed to localize change attribute", e);
77
- }
78
- }
65
+ if (change.attribute && change.serviceEntity) {
66
+ const model = cds.context?.model ?? cds.model;
67
+ try {
68
+ const serviceEntity = model.definitions[change.serviceEntity];
69
+ if (!serviceEntity) {
70
+ LOG.warn(`Cannot localize the attribute ${change.attribute} of ${change.serviceEntity}, because the service entity is not defined in "cds.model.definitions".`);
71
+ return;
72
+ }
73
+ let labelI18nKey = _getLabelI18nKeyOnEntity(change.serviceEntity, change.attribute);
74
+ if (!labelI18nKey) {
75
+ const element = serviceEntity.elements[change.attribute];
76
+ if (!element) {
77
+ LOG.warn(`Cannot localize the attribute ${change.attribute} of ${change.serviceEntity}, because the attribute does not exist on the entity.`);
78
+ return;
79
+ }
80
+ if (element.isAssociation) labelI18nKey = _getLabelI18nKeyOnEntity(element.target);
81
+ }
82
+ change.attribute = labelI18nKey || change.attribute;
83
+ } catch (e) {
84
+ LOG.error('Failed to localize change attribute', e);
85
+ throw new Error('Failed to localize change attribute', e);
86
+ }
87
+ }
79
88
  };
80
89
 
81
90
  const _getLabelI18nKeyOnEntity = function (entityName, /** optinal */ attribute) {
82
- let def = cds.model.definitions[entityName];
83
- if (attribute) def = def?.elements[attribute]
84
- if (!def) return "";
85
- const i18nKey = getTranslationKey(def['@Common.Label'] || def['@title'] || def['@UI.HeaderInfo.TypeName']);
86
- return cds.i18n.labels.for(i18nKey) || i18nKey;
91
+ const model = cds.context?.model ?? cds.model;
92
+ let def = model.definitions[entityName];
93
+ if (attribute) def = def?.elements[attribute];
94
+ if (!def) return '';
95
+ const i18nKey = getTranslationKey(def['@Common.Label'] || def['@title'] || def['@UI.HeaderInfo.TypeName']);
96
+ return cds.i18n.labels.for(i18nKey) || i18nKey;
87
97
  };
88
98
 
89
99
  const parseTime = (time, locale, options) => {
90
- const timeParts = time.split(':');
91
- const date = new Date();
92
- date.setHours(parseInt(timeParts[0], 10), parseInt(timeParts[1], 10), parseInt(timeParts[2], 10));
93
- return date.toLocaleTimeString(locale, options);
100
+ const timeParts = time.split(':');
101
+ const date = new Date();
102
+ date.setHours(parseInt(timeParts[0], 10), parseInt(timeParts[1], 10), parseInt(timeParts[2], 10));
103
+ return date.toLocaleTimeString(locale, options);
94
104
  };
95
105
 
96
106
  const _localizeValue = (change, locale) => {
97
- if (change.valueDataType !== 'cds.Date' && change.valueDataType !== 'cds.DateTime' && change.valueDataType !== 'cds.Timestamp' && change.valueDataType !== 'cds.Time') {
98
- return;
99
- }
100
- const normalizedLocale = locale.replaceAll('_', '-');
101
- const options = formatOptions[change.valueDataType]?.[normalizedLocale]
102
- ?? formatOptions[change.valueDataType]?.['en']
103
-
104
- if (change.valueDataType === 'cds.Time') {
105
- if (change.valueChangedFrom) change.valueChangedFrom = parseTime(change.valueChangedFrom, normalizedLocale, options);
106
- if (change.valueChangedTo) change.valueChangedTo = parseTime(change.valueChangedTo, normalizedLocale, options);
107
- } else {
108
- const formatter = change.valueDataType === 'cds.Date' ? 'toLocaleDateString' : 'toLocaleString';
109
- if (change.valueChangedFrom) change.valueChangedFrom = new Date(change.valueChangedFrom)[formatter](normalizedLocale, options);
110
- if (change.valueChangedTo) change.valueChangedTo = new Date(change.valueChangedTo)[formatter](normalizedLocale, options);
111
- }
107
+ if (change.valueDataType !== 'cds.Date' && change.valueDataType !== 'cds.DateTime' && change.valueDataType !== 'cds.Timestamp' && change.valueDataType !== 'cds.Time') {
108
+ return;
109
+ }
110
+ const normalizedLocale = locale.replaceAll('_', '-');
111
+ const options = formatOptions[change.valueDataType]?.[normalizedLocale] ?? formatOptions[change.valueDataType]?.['en'];
112
112
 
113
+ if (change.valueDataType === 'cds.Time') {
114
+ if (change.valueChangedFrom) change.valueChangedFrom = parseTime(change.valueChangedFrom, normalizedLocale, options);
115
+ if (change.valueChangedTo) change.valueChangedTo = parseTime(change.valueChangedTo, normalizedLocale, options);
116
+ } else {
117
+ const formatter = change.valueDataType === 'cds.Date' ? 'toLocaleDateString' : 'toLocaleString';
118
+ if (change.valueChangedFrom) change.valueChangedFrom = new Date(change.valueChangedFrom)[formatter](normalizedLocale, options);
119
+ if (change.valueChangedTo) change.valueChangedTo = new Date(change.valueChangedTo)[formatter](normalizedLocale, options);
120
+ }
113
121
  };
114
122
 
115
123
  const localizeLogFields = function (data, locale) {
116
- if (!locale) return
117
- for (const change of data) {
118
- _localizeModification(change);
119
- _localizeAttribute(change);
120
- _localizeEntityType(change);
121
- _localizeDefaultObjectID(change);
122
- _localizeValue(change, locale);
123
- }
124
+ if (!locale) return;
125
+ for (const change of data) {
126
+ _localizeModification(change);
127
+ _localizeAttribute(change);
128
+ _localizeEntityType(change);
129
+ _localizeDefaultObjectID(change);
130
+ _localizeValue(change, locale);
131
+ }
124
132
  };
125
133
  module.exports = {
126
- localizeLogFields,
134
+ localizeLogFields
127
135
  };
@@ -1,115 +1,115 @@
1
1
  // Enhanced class based on cds v5.5.5 @sap/cds/libx/_runtime/common/utils/templateProcessor
2
2
 
3
- const DELIMITER = require("@sap/cds/libx/_runtime/common/utils/templateDelimiter");
3
+ const DELIMITER = require('@sap/cds/libx/_runtime/common/utils/templateDelimiter');
4
4
 
5
5
  const _formatRowContext = (tKey, keyNames, row) => {
6
- const keyValuePairs = keyNames.map((key) => `${key}=${row[key]}`);
7
- const keyValuePairsSerialized = keyValuePairs.join(",");
8
- return `${tKey}(${keyValuePairsSerialized})`;
6
+ const keyValuePairs = keyNames.map((key) => `${key}=${row[key]}`);
7
+ const keyValuePairsSerialized = keyValuePairs.join(',');
8
+ return `${tKey}(${keyValuePairsSerialized})`;
9
9
  };
10
10
 
11
11
  const _processElement = (processFn, row, key, elements, isRoot, pathSegments, picked = {}) => {
12
- const element = elements[key];
13
- const { plain } = picked;
14
-
15
- // do not change-track personal data
16
- const isPersonalData = element && Object.keys(element).some(key => key.startsWith('@PersonalData'));
17
- if (plain && !isPersonalData) {
18
- /**
19
- * @type import('../../types/api').templateProcessorProcessFnArgs
20
- */
21
- const elementInfo = { row, key, element, plain, isRoot, pathSegments };
22
- processFn(elementInfo);
23
- }
12
+ const element = elements[key];
13
+ const { plain } = picked;
14
+
15
+ // do not change-track personal data
16
+ const isPersonalData = element && Object.keys(element).some((key) => key.startsWith('@PersonalData'));
17
+ if (plain && !isPersonalData) {
18
+ /**
19
+ * @type import('../../types/api').templateProcessorProcessFnArgs
20
+ */
21
+ const elementInfo = { row, key, element, plain, isRoot, pathSegments };
22
+ processFn(elementInfo);
23
+ }
24
24
  };
25
25
 
26
26
  function retrieveFirstKey(target) {
27
- const keys = [];
28
- for (const key of Object.keys(target.keys)) {
29
- if (target.keys[key].type !== 'cds.Association' && key !== 'IsActiveEntity') {
30
- keys.push(key);
31
- }
32
- }
33
-
34
- //Revisit: For test 3.5
35
- return keys.filter(k => !k.startsWith('up_'))[0]
27
+ const keys = [];
28
+ for (const key of Object.keys(target.keys)) {
29
+ if (target.keys[key].type !== 'cds.Association' && key !== 'IsActiveEntity') {
30
+ keys.push(key);
31
+ }
32
+ }
33
+
34
+ //Revisit: For test 3.5
35
+ return keys.filter((k) => !k.startsWith('up_'))[0];
36
36
  }
37
37
 
38
38
  const _processRow = (processFn, row, template, tKey, tValue, isRoot, pathOptions) => {
39
- const { template: subTemplate, picked } = tValue;
40
- const key = tKey.split(DELIMITER).pop();
41
- const { segments: pathSegments } = pathOptions;
42
-
43
- if (!subTemplate && pathSegments) {
44
- pathSegments.push(key);
45
- }
46
-
47
- _processElement(processFn, row, key, template.target.elements, isRoot, pathSegments, picked);
48
-
49
- // process deep
50
- if (subTemplate) {
51
- let subRows = row && row[key];
52
-
53
- subRows = Array.isArray(subRows) ? subRows : [subRows];
54
-
55
- // Build entity path
56
- subRows.forEach((subRow) => {
57
- if (subRow && row && row._path) {
58
- /** Enhancement by SME: Support CAP Change Histroy
59
- * Construct path from root entity to current entity.
60
- */
61
- const serviceNodeName = template.target.elements[key].target;
62
- const serviceNode = template.target.elements[key]._target;
63
- subRow._path = `${row._path}/${serviceNodeName}(${subRow[retrieveFirstKey(serviceNode)]})`;
64
- }
65
- });
66
-
67
- _processComplex(processFn, subRows, subTemplate, key, pathOptions);
68
- }
39
+ const { template: subTemplate, picked } = tValue;
40
+ const key = tKey.split(DELIMITER).pop();
41
+ const { segments: pathSegments } = pathOptions;
42
+
43
+ if (!subTemplate && pathSegments) {
44
+ pathSegments.push(key);
45
+ }
46
+
47
+ _processElement(processFn, row, key, template.target.elements, isRoot, pathSegments, picked);
48
+
49
+ // process deep
50
+ if (subTemplate) {
51
+ let subRows = row && row[key];
52
+
53
+ subRows = Array.isArray(subRows) ? subRows : [subRows];
54
+
55
+ // Build entity path
56
+ subRows.forEach((subRow) => {
57
+ if (subRow && row && row._path) {
58
+ /** Enhancement by SME: Support CAP Change Histroy
59
+ * Construct path from root entity to current entity.
60
+ */
61
+ const serviceNodeName = template.target.elements[key].target;
62
+ const serviceNode = template.target.elements[key]._target;
63
+ subRow._path = `${row._path}/${serviceNodeName}(${subRow[retrieveFirstKey(serviceNode)]})`;
64
+ }
65
+ });
66
+
67
+ _processComplex(processFn, subRows, subTemplate, key, pathOptions);
68
+ }
69
69
  };
70
70
 
71
71
  const _processComplex = (processFn, rows, template, tKey, pathOptions) => {
72
- if (rows.length === 0) {
73
- return;
74
- }
72
+ if (rows.length === 0) {
73
+ return;
74
+ }
75
75
 
76
- const segments = pathOptions.segments;
77
- let keyNames;
76
+ const segments = pathOptions.segments;
77
+ let keyNames;
78
78
 
79
- for (const row of rows) {
80
- if (row == null) {
81
- continue;
82
- }
79
+ for (const row of rows) {
80
+ if (row == null) {
81
+ continue;
82
+ }
83
83
 
84
- const args = { processFn, row, template, isRoot: false, pathOptions };
84
+ const args = { processFn, row, template, isRoot: false, pathOptions };
85
85
 
86
- if (pathOptions.includeKeyValues) {
87
- keyNames = keyNames || (template.target.keys && Object.keys(template.target.keys)) || [];
88
- pathOptions.rowKeysGenerator(keyNames, row, template);
89
- const pathSegment = _formatRowContext(tKey, keyNames, { ...row, ...pathOptions.extraKeys });
90
- args.pathOptions.segments = segments ? [...segments, pathSegment] : [pathSegment];
91
- }
86
+ if (pathOptions.includeKeyValues) {
87
+ keyNames = keyNames || (template.target.keys && Object.keys(template.target.keys)) || [];
88
+ pathOptions.rowKeysGenerator(keyNames, row, template);
89
+ const pathSegment = _formatRowContext(tKey, keyNames, { ...row, ...pathOptions.extraKeys });
90
+ args.pathOptions.segments = segments ? [...segments, pathSegment] : [pathSegment];
91
+ }
92
92
 
93
- templateProcessor(args);
94
- }
93
+ templateProcessor(args);
94
+ }
95
95
  };
96
96
 
97
97
  /**
98
98
  * @param {import("../../types/api").TemplateProcessor} args
99
99
  */
100
100
  const templateProcessor = ({ processFn, row, template, isRoot = true, pathOptions = {} }) => {
101
- const segments = pathOptions.segments && [...pathOptions.segments];
102
-
103
- for (const [tKey, tValue] of template.elements) {
104
- // When a managed association is marked with @changelog, both foreign key and assoc are in the template. Skip the association as only the foreign key has a change.
105
- if (template.target.elements[tKey]?.type === 'cds.Association') {
106
- continue;
107
- }
108
- if (segments) {
109
- pathOptions.segments = [...segments];
110
- }
111
- _processRow(processFn, row, template, tKey, tValue, isRoot, pathOptions);
112
- }
101
+ const segments = pathOptions.segments && [...pathOptions.segments];
102
+
103
+ for (const [tKey, tValue] of template.elements) {
104
+ // When a managed association is marked with @changelog, both foreign key and assoc are in the template. Skip the association as only the foreign key has a change.
105
+ if (template.target.elements[tKey]?.type === 'cds.Association') {
106
+ continue;
107
+ }
108
+ if (segments) {
109
+ pathOptions.segments = [...segments];
110
+ }
111
+ _processRow(processFn, row, template, tKey, tValue, isRoot, pathOptions);
112
+ }
113
113
  };
114
114
 
115
- module.exports = {templateProcessor, retrieveFirstKey};
115
+ module.exports = { templateProcessor, retrieveFirstKey };
package/package.json CHANGED
@@ -1,41 +1,49 @@
1
1
  {
2
- "name": "@cap-js/change-tracking",
3
- "version": "1.1.3",
4
- "description": "CDS plugin providing out-of-the box support for automatic capturing, storing, and viewing of the change records of modeled entities.",
5
- "repository": "cap-js/change-tracking",
6
- "author": "SAP SE (https://www.sap.com)",
7
- "license": "Apache-2.0",
8
- "main": "cds-plugin.js",
9
- "files": [
10
- "lib",
11
- "_i18n",
12
- "index.cds",
13
- "CHANGELOG.md",
14
- "README.md"
15
- ],
16
- "scripts": {
17
- "lint": "npx eslint .",
18
- "test": "npx jest --silent"
19
- },
20
- "peerDependencies": {
21
- "@sap/cds": ">=8.5"
22
- },
23
- "engines": {
24
- "node": ">=20.0.0"
25
- },
26
- "devDependencies": {
27
- "@cap-js/change-tracking": "file:.",
28
- "@cap-js/attachments": "^2",
29
- "@cap-js/sqlite": "^1 || ^2",
30
- "@cap-js/cds-test": "*",
31
- "express": "^4"
32
- },
33
- "cds": {
34
- "requires": {
35
- "change-tracking": {
36
- "model": "@cap-js/change-tracking",
37
- "considerLocalizedValues": false
38
- }
39
- }
40
- }
2
+ "name": "@cap-js/change-tracking",
3
+ "version": "1.1.4",
4
+ "description": "CDS plugin providing out-of-the box support for automatic capturing, storing, and viewing of the change records of modeled entities.",
5
+ "repository": "cap-js/change-tracking",
6
+ "author": "SAP SE (https://www.sap.com)",
7
+ "license": "Apache-2.0",
8
+ "main": "cds-plugin.js",
9
+ "files": [
10
+ "lib",
11
+ "_i18n",
12
+ "index.cds",
13
+ "CHANGELOG.md",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "lint": "npx eslint .",
18
+ "test": "npx jest --silent",
19
+ "format": "npx -y prettier@3 . --write",
20
+ "format:check": "npx -y prettier@3 --check ."
21
+ },
22
+ "peerDependencies": {
23
+ "@sap/cds": ">=8.5"
24
+ },
25
+ "engines": {
26
+ "node": ">=20.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "@cap-js/attachments": "^2",
30
+ "@cap-js/cds-test": "*",
31
+ "@cap-js/change-tracking": "file:.",
32
+ "@cap-js/sqlite": "^1 || ^2",
33
+ "express": "^4"
34
+ },
35
+ "cds": {
36
+ "requires": {
37
+ "change-tracking": {
38
+ "model": "@cap-js/change-tracking",
39
+ "considerLocalizedValues": false
40
+ }
41
+ }
42
+ },
43
+ "jest": {
44
+ "testTimeout": 10000
45
+ },
46
+ "workspaces": [
47
+ "tests/*"
48
+ ]
41
49
  }