@cap-js/change-tracking 2.0.0-beta.1 → 2.0.0-beta.2
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.
- package/CHANGELOG.md +13 -2
- package/_i18n/i18n.properties +0 -4
- package/cds-plugin.js +4 -2
- package/index.cds +77 -90
- package/lib/h2/java-codegen.js +33 -43
- package/lib/h2/triggers.js +5 -6
- package/lib/hana/composition.js +13 -15
- package/lib/hana/sql-expressions.js +8 -19
- package/lib/hana/triggers.js +26 -27
- package/lib/model-enhancer.js +1 -1
- package/lib/postgres/composition.js +11 -12
- package/lib/postgres/sql-expressions.js +23 -33
- package/lib/postgres/triggers.js +10 -12
- package/lib/skipHandlers.js +1 -1
- package/lib/sqlite/composition.js +12 -13
- package/lib/sqlite/sql-expressions.js +21 -31
- package/lib/sqlite/triggers.js +24 -25
- package/lib/utils/change-tracking.js +1 -1
- package/lib/utils/session-variables.js +10 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
-
## Version 2.0.0 -
|
|
7
|
+
## Version 2.0.0-beta.3 - tbd
|
|
8
8
|
|
|
9
9
|
### Added
|
|
10
10
|
|
|
@@ -12,7 +12,18 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
|
12
12
|
|
|
13
13
|
### Changed
|
|
14
14
|
|
|
15
|
-
## Version 2.0.0-beta -
|
|
15
|
+
## Version 2.0.0-beta.2 - 11.03.26
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Fixed a server crash when resolving table names
|
|
19
|
+
- Support entity level `@changelog` annotation where no explicit elements for the object ID are defined
|
|
20
|
+
- Trigger generation works again for MTX scenarios
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- Improved performance when quering changes
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## Version 2.0.0-beta.1 - 06.03.26
|
|
16
27
|
|
|
17
28
|
### Added
|
|
18
29
|
- Trigger generation for SQLite, HANA, Postgres and H2 to perform change tracking on a database level
|
package/_i18n/i18n.properties
CHANGED
|
@@ -37,10 +37,6 @@ Changes.modification.create=Create
|
|
|
37
37
|
Changes.modification.update=Update
|
|
38
38
|
#XFLD: Field label
|
|
39
39
|
Changes.modification.delete=Delete
|
|
40
|
-
#XFLD: Field label
|
|
41
|
-
Changes.createdAt=Changed On
|
|
42
|
-
#XFLD: Field label
|
|
43
|
-
Changes.createdBy=Changed By
|
|
44
40
|
|
|
45
41
|
## Change History Table##
|
|
46
42
|
########################################
|
package/cds-plugin.js
CHANGED
|
@@ -9,8 +9,10 @@ const { registerHDICompilerHook } = require('./lib/hana/register.js');
|
|
|
9
9
|
|
|
10
10
|
cds.on('loaded', enhanceModel);
|
|
11
11
|
cds.on('listening', registerSessionVariableHandlers);
|
|
12
|
-
cds.once('served',
|
|
13
|
-
|
|
12
|
+
cds.once('served', async () => {
|
|
13
|
+
await deploySQLiteTriggers();
|
|
14
|
+
await deployPostgresLabels();
|
|
15
|
+
});
|
|
14
16
|
|
|
15
17
|
registerH2CompilerHook();
|
|
16
18
|
registerPostgresCompilerHook();
|
package/index.cds
CHANGED
|
@@ -17,8 +17,8 @@ entity aspect @(UI.Facets: [{
|
|
|
17
17
|
@UI.PartOfPreview: false
|
|
18
18
|
}]) {
|
|
19
19
|
changes : Association to many ChangeView
|
|
20
|
-
on changes.entityKey
|
|
21
|
-
and changes.entity
|
|
20
|
+
on changes.entityKey = ID
|
|
21
|
+
and changes.entity = 'ENTITY';
|
|
22
22
|
key ID : String;
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -31,73 +31,60 @@ entity aspect @(UI.Facets: [{
|
|
|
31
31
|
@readonly
|
|
32
32
|
@cds.autoexpose
|
|
33
33
|
view ChangeView as
|
|
34
|
-
select from Changes as change
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
) as objectID : String(24) @title: '{i18n>Changes.objectID}',
|
|
89
|
-
COALESCE(
|
|
90
|
-
change.valueChangedFromLabel, change.valueChangedFrom
|
|
91
|
-
) as valueChangedFromLabel : String(5000) @title: '{i18n>Changes.valueChangedFrom}',
|
|
92
|
-
COALESCE(
|
|
93
|
-
change.valueChangedToLabel, change.valueChangedTo
|
|
94
|
-
) as valueChangedToLabel : String(5000) @title: '{i18n>Changes.valueChangedTo}',
|
|
95
|
-
|
|
96
|
-
// For the hierarchy
|
|
97
|
-
null as LimitedDescendantCount : Int16 @UI.Hidden,
|
|
98
|
-
null as DistanceFromRoot : Int16 @UI.Hidden,
|
|
99
|
-
null as DrillState : String @UI.Hidden,
|
|
100
|
-
null as LimitedRank : Int16 @UI.Hidden,
|
|
34
|
+
select from Changes as change
|
|
35
|
+
left outer join i18nKeys as attributeI18n
|
|
36
|
+
on attributeI18n.ID = change.attribute
|
|
37
|
+
and (
|
|
38
|
+
attributeI18n.locale = $user.locale
|
|
39
|
+
or attributeI18n.locale = 'en'
|
|
40
|
+
)
|
|
41
|
+
left outer join i18nKeys as entityI18n
|
|
42
|
+
on entityI18n.ID = change.entity
|
|
43
|
+
and (
|
|
44
|
+
entityI18n.locale = $user.locale
|
|
45
|
+
or entityI18n.locale = 'en'
|
|
46
|
+
)
|
|
47
|
+
left outer join i18nKeys as modificationI18n
|
|
48
|
+
on modificationI18n.ID = change.modification
|
|
49
|
+
and (
|
|
50
|
+
modificationI18n.locale = $user.locale
|
|
51
|
+
or modificationI18n.locale = 'en'
|
|
52
|
+
)
|
|
53
|
+
{
|
|
54
|
+
key change.ID @UI.Hidden,
|
|
55
|
+
change.parent: redirected to ChangeView,
|
|
56
|
+
change.children: redirected to ChangeView,
|
|
57
|
+
change.attribute,
|
|
58
|
+
change.valueChangedFrom,
|
|
59
|
+
change.valueChangedTo,
|
|
60
|
+
change.entity,
|
|
61
|
+
change.entityKey,
|
|
62
|
+
change.objectID,
|
|
63
|
+
change.modification,
|
|
64
|
+
change.valueDataType,
|
|
65
|
+
change.createdAt,
|
|
66
|
+
change.createdBy,
|
|
67
|
+
change.transactionID,
|
|
68
|
+
COALESCE(
|
|
69
|
+
attributeI18n.text, change.attribute
|
|
70
|
+
) as attributeLabel : String(15) @title: '{i18n>Changes.attribute}',
|
|
71
|
+
COALESCE(
|
|
72
|
+
entityI18n.text, change.entity
|
|
73
|
+
) as entityLabel : String(24) @title: '{i18n>Changes.entity}',
|
|
74
|
+
COALESCE(
|
|
75
|
+
modificationI18n.text, change.modification
|
|
76
|
+
) as modificationLabel : String(16) @title: '{i18n>Changes.modification}',
|
|
77
|
+
COALESCE(
|
|
78
|
+
change.valueChangedFromLabel, change.valueChangedFrom
|
|
79
|
+
) as valueChangedFromLabel : String(5000) @title: '{i18n>Changes.valueChangedFrom}',
|
|
80
|
+
COALESCE(
|
|
81
|
+
change.valueChangedToLabel, change.valueChangedTo
|
|
82
|
+
) as valueChangedToLabel : String(5000) @title: '{i18n>Changes.valueChangedTo}',
|
|
83
|
+
// For the hierarchy
|
|
84
|
+
null as LimitedDescendantCount : Int16 @UI.Hidden,
|
|
85
|
+
null as DistanceFromRoot : Int16 @UI.Hidden,
|
|
86
|
+
null as DrillState : String @UI.Hidden,
|
|
87
|
+
null as LimitedRank : Int16 @UI.Hidden,
|
|
101
88
|
};
|
|
102
89
|
|
|
103
90
|
entity i18nKeys {
|
|
@@ -159,46 +146,46 @@ annotate ChangeView with @(UI: {
|
|
|
159
146
|
},
|
|
160
147
|
LineItem : [
|
|
161
148
|
{
|
|
162
|
-
Value
|
|
163
|
-
@UI.Importance
|
|
149
|
+
Value : modificationLabel,
|
|
150
|
+
@UI.Importance: #Low
|
|
164
151
|
},
|
|
165
152
|
{
|
|
166
|
-
Value
|
|
167
|
-
@UI.Importance
|
|
153
|
+
Value : entityLabel,
|
|
154
|
+
@UI.Importance: #Medium
|
|
168
155
|
},
|
|
169
156
|
{
|
|
170
|
-
Value
|
|
171
|
-
@UI.Importance
|
|
157
|
+
Value : objectID,
|
|
158
|
+
@UI.Importance: #Medium
|
|
172
159
|
},
|
|
173
160
|
{
|
|
174
|
-
Value
|
|
175
|
-
@UI.Importance
|
|
161
|
+
Value : attributeLabel,
|
|
162
|
+
@UI.Importance: #Medium
|
|
176
163
|
},
|
|
177
164
|
{
|
|
178
|
-
Value
|
|
179
|
-
@UI.Importance
|
|
165
|
+
Value : valueChangedToLabel,
|
|
166
|
+
@UI.Importance: #High
|
|
180
167
|
},
|
|
181
168
|
{
|
|
182
|
-
Value
|
|
183
|
-
@UI.Importance
|
|
169
|
+
Value : valueChangedFromLabel,
|
|
170
|
+
@UI.Importance: #High
|
|
184
171
|
},
|
|
185
172
|
{
|
|
186
|
-
Value
|
|
187
|
-
@UI.Importance
|
|
173
|
+
Value : createdAt,
|
|
174
|
+
@UI.Importance: #Low
|
|
188
175
|
},
|
|
189
176
|
{
|
|
190
|
-
Value
|
|
191
|
-
@UI.Importance
|
|
177
|
+
Value : createdBy,
|
|
178
|
+
@UI.Importance: #High
|
|
192
179
|
},
|
|
193
180
|
],
|
|
194
181
|
DeleteHidden : true,
|
|
195
182
|
}) {
|
|
196
183
|
valueChangedFrom @UI.Hidden;
|
|
197
|
-
valueChangedTo
|
|
198
|
-
parent
|
|
199
|
-
entityKey
|
|
200
|
-
entity
|
|
201
|
-
attribute
|
|
184
|
+
valueChangedTo @UI.Hidden;
|
|
185
|
+
parent @UI.Hidden;
|
|
186
|
+
entityKey @UI.Hidden;
|
|
187
|
+
entity @UI.Hidden;
|
|
188
|
+
attribute @UI.Hidden;
|
|
202
189
|
};
|
|
203
190
|
|
|
204
191
|
annotate ChangeView with @(
|
package/lib/h2/java-codegen.js
CHANGED
|
@@ -4,29 +4,21 @@ const { createTriggerCQN2SQL } = require('../TriggerCQN2SQL.js');
|
|
|
4
4
|
|
|
5
5
|
const cqn4sql = require('@cap-js/db-service/lib/cqn4sql');
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
let model;
|
|
7
|
+
const _cqn2sqlCache = new WeakMap();
|
|
9
8
|
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function getModel() {
|
|
16
|
-
return model;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function _toSQL(query) {
|
|
20
|
-
if (!SQLiteCQN2SQL) {
|
|
9
|
+
function _toSQL(query, model) {
|
|
10
|
+
let cqn2sql = _cqn2sqlCache.get(model);
|
|
11
|
+
if (!cqn2sql) {
|
|
21
12
|
const SQLiteService = require('@cap-js/sqlite');
|
|
22
13
|
const TriggerCQN2SQL = createTriggerCQN2SQL(SQLiteService.CQN2SQL);
|
|
23
|
-
|
|
14
|
+
cqn2sql = new TriggerCQN2SQL({ model: model });
|
|
15
|
+
_cqn2sqlCache.set(model, cqn2sql);
|
|
24
16
|
}
|
|
25
17
|
const sqlCQN = cqn4sql(query, model);
|
|
26
|
-
return
|
|
18
|
+
return cqn2sql.SELECT(sqlCQN);
|
|
27
19
|
}
|
|
28
20
|
|
|
29
|
-
function handleAssocLookup(column, refRow) {
|
|
21
|
+
function handleAssocLookup(column, refRow, model) {
|
|
30
22
|
let bindings = [];
|
|
31
23
|
let where = {};
|
|
32
24
|
|
|
@@ -56,8 +48,8 @@ function handleAssocLookup(column, refRow) {
|
|
|
56
48
|
const textsQuery = SELECT.one.from(localizedInfo.textsEntity).columns(columns).where(textsWhere);
|
|
57
49
|
const baseQuery = SELECT.one.from(column.target).columns(columns).where(where);
|
|
58
50
|
|
|
59
|
-
const textsSQL = _toSQL(textsQuery);
|
|
60
|
-
const baseSQL = _toSQL(baseQuery);
|
|
51
|
+
const textsSQL = _toSQL(textsQuery, model);
|
|
52
|
+
const baseSQL = _toSQL(baseQuery, model);
|
|
61
53
|
|
|
62
54
|
// Add locale binding (fetched from session variable @$user.locale)
|
|
63
55
|
const textsBindings = [...bindings, 'locale'];
|
|
@@ -73,7 +65,7 @@ function handleAssocLookup(column, refRow) {
|
|
|
73
65
|
const query = SELECT.one.from(column.target).columns(columns).where(where);
|
|
74
66
|
|
|
75
67
|
return {
|
|
76
|
-
sql: `(${_toSQL(query)})`,
|
|
68
|
+
sql: `(${_toSQL(query, model)})`,
|
|
77
69
|
bindings: bindings
|
|
78
70
|
};
|
|
79
71
|
}
|
|
@@ -288,12 +280,12 @@ ${grandParentHelper}${parentIdHelper}
|
|
|
288
280
|
}`;
|
|
289
281
|
}
|
|
290
282
|
|
|
291
|
-
function _generateCreateBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
|
|
283
|
+
function _generateCreateBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
|
|
292
284
|
const reference = 'newRow';
|
|
293
285
|
|
|
294
286
|
// Set modification type for grandparent entry creation (must be before keysCalc which uses it)
|
|
295
287
|
const modificationTypeSetup = grandParentCompositionInfo ? 'String modificationType = "create";' : '';
|
|
296
|
-
const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo);
|
|
288
|
+
const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, model, compositionParentInfo, grandParentCompositionInfo);
|
|
297
289
|
|
|
298
290
|
// Composition parent handling - with deep linking support when grandParentCompositionInfo exists
|
|
299
291
|
let parentIdSetup = '';
|
|
@@ -314,7 +306,7 @@ function _generateCreateBody(entity, columns, objectIDs, rootEntity, rootObjectI
|
|
|
314
306
|
.map((col) => {
|
|
315
307
|
// Prepare Value Expression
|
|
316
308
|
const { sqlExpr, bindings } = _prepareValueExpression(col, reference);
|
|
317
|
-
const labelRes = _prepareLabelExpression(col, reference); // label expression
|
|
309
|
+
const labelRes = _prepareLabelExpression(col, reference, model); // label expression
|
|
318
310
|
|
|
319
311
|
// SQL Statement - include PARENT_ID if composition parent info exists
|
|
320
312
|
const insertSQL = compositionParentInfo
|
|
@@ -346,12 +338,12 @@ function _generateCreateBody(entity, columns, objectIDs, rootEntity, rootObjectI
|
|
|
346
338
|
return `${modificationTypeSetup}\n${keysCalc}\n${parentIdSetup}\n${columnBlocks}`;
|
|
347
339
|
}
|
|
348
340
|
|
|
349
|
-
function _generateUpdateBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
|
|
341
|
+
function _generateUpdateBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
|
|
350
342
|
const reference = 'newRow';
|
|
351
343
|
|
|
352
344
|
// Set modification type for grandparent entry creation (must be before keysCalc which uses it)
|
|
353
345
|
const modificationTypeSetup = grandParentCompositionInfo ? 'String modificationType = "update";' : '';
|
|
354
|
-
const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo);
|
|
346
|
+
const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, model, compositionParentInfo, grandParentCompositionInfo);
|
|
355
347
|
|
|
356
348
|
// Composition parent handling - with deep linking support when grandParentCompositionInfo exists
|
|
357
349
|
let parentIdSetup = '';
|
|
@@ -374,8 +366,8 @@ function _generateUpdateBody(entity, columns, objectIDs, rootEntity, rootObjectI
|
|
|
374
366
|
const newRes = _prepareValueExpression(col, 'newRow');
|
|
375
367
|
const oldRes = _prepareValueExpression(col, 'oldRow');
|
|
376
368
|
// Prepare new and old Label (lookup values if col.alt exists)
|
|
377
|
-
const newLabelRes = _prepareLabelExpression(col, 'newRow');
|
|
378
|
-
const oldLabelRes = _prepareLabelExpression(col, 'oldRow');
|
|
369
|
+
const newLabelRes = _prepareLabelExpression(col, 'newRow', model);
|
|
370
|
+
const oldLabelRes = _prepareLabelExpression(col, 'oldRow', model);
|
|
379
371
|
|
|
380
372
|
// Check column values from ResultSet for Change Logic
|
|
381
373
|
let checkCols = [col.name];
|
|
@@ -420,12 +412,12 @@ function _generateUpdateBody(entity, columns, objectIDs, rootEntity, rootObjectI
|
|
|
420
412
|
return `${modificationTypeSetup}\n${keysCalc}\n${parentIdSetup}\n${columnBlocks}`;
|
|
421
413
|
}
|
|
422
414
|
|
|
423
|
-
function _generateDeleteBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
|
|
415
|
+
function _generateDeleteBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
|
|
424
416
|
const reference = 'oldRow';
|
|
425
417
|
|
|
426
418
|
// Set modification type for grandparent entry creation (must be before keysCalc which uses it)
|
|
427
419
|
const modificationTypeSetup = grandParentCompositionInfo ? 'String modificationType = "delete";' : '';
|
|
428
|
-
const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo);
|
|
420
|
+
const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, model, compositionParentInfo, grandParentCompositionInfo);
|
|
429
421
|
|
|
430
422
|
// First delete existing changelogs for this entity
|
|
431
423
|
const deleteSQL = `DELETE FROM sap_changelog_Changes WHERE ENTITY = ? AND ENTITYKEY = ?`;
|
|
@@ -452,7 +444,7 @@ function _generateDeleteBody(entity, columns, objectIDs, rootEntity, rootObjectI
|
|
|
452
444
|
// Prepare Old Value (raw FK value)
|
|
453
445
|
const { sqlExpr, bindings } = _prepareValueExpression(col, reference);
|
|
454
446
|
// Prepare Old Label (lookup value if col.alt exists)
|
|
455
|
-
const labelRes = _prepareLabelExpression(col, reference);
|
|
447
|
+
const labelRes = _prepareLabelExpression(col, reference, model);
|
|
456
448
|
|
|
457
449
|
// SQL Statement - include PARENT_ID if composition parent info exists
|
|
458
450
|
const insertSQL = compositionParentInfo
|
|
@@ -487,12 +479,12 @@ function _generateDeleteBody(entity, columns, objectIDs, rootEntity, rootObjectI
|
|
|
487
479
|
${columnBlocks}`;
|
|
488
480
|
}
|
|
489
481
|
|
|
490
|
-
function _generateDeleteBodyPreserve(entity, columns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
|
|
482
|
+
function _generateDeleteBodyPreserve(entity, columns, objectIDs, rootEntity, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
|
|
491
483
|
const reference = 'oldRow';
|
|
492
484
|
|
|
493
485
|
// Set modification type for grandparent entry creation (must be before keysCalc which uses it)
|
|
494
486
|
const modificationTypeSetup = grandParentCompositionInfo ? 'String modificationType = "delete";' : '';
|
|
495
|
-
const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo);
|
|
487
|
+
const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, model, compositionParentInfo, grandParentCompositionInfo);
|
|
496
488
|
|
|
497
489
|
// Composition parent handling - with deep linking support when grandParentCompositionInfo exists
|
|
498
490
|
let parentIdSetup = '';
|
|
@@ -514,7 +506,7 @@ function _generateDeleteBodyPreserve(entity, columns, objectIDs, rootEntity, roo
|
|
|
514
506
|
// Prepare Old Value (raw FK value)
|
|
515
507
|
const { sqlExpr, bindings } = _prepareValueExpression(col, reference);
|
|
516
508
|
// Prepare Old Label (lookup value if col.alt exists)
|
|
517
|
-
const labelRes = _prepareLabelExpression(col, reference);
|
|
509
|
+
const labelRes = _prepareLabelExpression(col, reference, model);
|
|
518
510
|
|
|
519
511
|
// SQL Statement - include PARENT_ID if composition parent info exists
|
|
520
512
|
const insertSQL = compositionParentInfo
|
|
@@ -545,13 +537,13 @@ function _generateDeleteBodyPreserve(entity, columns, objectIDs, rootEntity, roo
|
|
|
545
537
|
return `${modificationTypeSetup}\n${keysCalc}\n${parentIdSetup}\n${columnBlocks}`;
|
|
546
538
|
}
|
|
547
539
|
|
|
548
|
-
function _generateKeyCalculationJava(entity, rootEntity, ref, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
|
|
540
|
+
function _generateKeyCalculationJava(entity, rootEntity, ref, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
|
|
549
541
|
// extract keys for entity (entity.keys is undefined)
|
|
550
542
|
let keys = utils.extractKeys(entity.keys);
|
|
551
543
|
const entityKeyExp = keys.map((k) => `${ref}.getString("${k}")`).join(' + "||" + ');
|
|
552
544
|
|
|
553
545
|
const objectIDs = utils.getObjectIDs(entity, model);
|
|
554
|
-
const objectIDBlock = _generateObjectIDCalculation(objectIDs, entity, ref);
|
|
546
|
+
const objectIDBlock = _generateObjectIDCalculation(objectIDs, entity, ref, model);
|
|
555
547
|
|
|
556
548
|
// Add parent key calculation for composition parent linking
|
|
557
549
|
let parentKeyBlock = '';
|
|
@@ -589,7 +581,7 @@ function _generateKeyCalculationJava(entity, rootEntity, ref, rootObjectIDs, com
|
|
|
589
581
|
}
|
|
590
582
|
|
|
591
583
|
// Compute parent objectID (the display name of the composition parent entity)
|
|
592
|
-
const parentObjectIDCalcBlock = _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, ref, entity);
|
|
584
|
+
const parentObjectIDCalcBlock = _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, ref, entity, model);
|
|
593
585
|
parentObjectIDBlock = parentObjectIDCalcBlock;
|
|
594
586
|
|
|
595
587
|
if (grandParentCompositionInfo && !parentKeyBinding.type) {
|
|
@@ -683,9 +675,9 @@ function _prepareValueExpression(col, rowVar) {
|
|
|
683
675
|
}
|
|
684
676
|
|
|
685
677
|
// Returns label expression for a column
|
|
686
|
-
function _prepareLabelExpression(col, rowVar) {
|
|
678
|
+
function _prepareLabelExpression(col, rowVar, model) {
|
|
687
679
|
if (col.target && col.alt) {
|
|
688
|
-
const { sql, bindings } = handleAssocLookup(col, rowVar);
|
|
680
|
+
const { sql, bindings } = handleAssocLookup(col, rowVar, model);
|
|
689
681
|
return { sqlExpr: sql, bindings: bindings };
|
|
690
682
|
}
|
|
691
683
|
// No label for scalars or associations without @changelog override
|
|
@@ -704,7 +696,7 @@ function _wrapInTryCatch(sql, bindings) {
|
|
|
704
696
|
}`;
|
|
705
697
|
}
|
|
706
698
|
|
|
707
|
-
function _generateObjectIDCalculation(objectIDs, entity, refRow) {
|
|
699
|
+
function _generateObjectIDCalculation(objectIDs, entity, refRow, model) {
|
|
708
700
|
// fallback to entity name (will be translated via i18nKeys in ChangeView)
|
|
709
701
|
if (!objectIDs || objectIDs.length === 0) {
|
|
710
702
|
return `String objectID = "${entity.name}";`;
|
|
@@ -727,7 +719,7 @@ function _generateObjectIDCalculation(objectIDs, entity, refRow) {
|
|
|
727
719
|
}, {});
|
|
728
720
|
|
|
729
721
|
const query = SELECT.one.from(entity.name).columns(oid.name).where(where);
|
|
730
|
-
const sql = `(${_toSQL(query)})`;
|
|
722
|
+
const sql = `(${_toSQL(query, model)})`;
|
|
731
723
|
|
|
732
724
|
parts.push(`SELECT CAST(${sql} AS VARCHAR) AS val`);
|
|
733
725
|
|
|
@@ -761,7 +753,7 @@ function _generateObjectIDCalculation(objectIDs, entity, refRow) {
|
|
|
761
753
|
* This is used when a child entity has a tracked composition parent — the parent's
|
|
762
754
|
* changelog entry needs a meaningful objectID rather than just the key.
|
|
763
755
|
*/
|
|
764
|
-
function _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, refRow, childEntity) {
|
|
756
|
+
function _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, refRow, childEntity, model) {
|
|
765
757
|
if (!rootObjectIDs || rootObjectIDs.length === 0) {
|
|
766
758
|
const rootEntityName = rootEntity ? rootEntity.name : '';
|
|
767
759
|
return `String parentObjectID = "${rootEntityName}";`;
|
|
@@ -798,7 +790,7 @@ function _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, refRow, c
|
|
|
798
790
|
|
|
799
791
|
const targetEntity = isCompositionOfOne ? binding.rootEntityName : rootEntity.name;
|
|
800
792
|
const query = SELECT.one.from(targetEntity).columns(oid.name).where(where);
|
|
801
|
-
const sql = `(${_toSQL(query)})`;
|
|
793
|
+
const sql = `(${_toSQL(query, model)})`;
|
|
802
794
|
|
|
803
795
|
parts.push(`SELECT CAST(${sql} AS VARCHAR) AS val`);
|
|
804
796
|
|
|
@@ -833,8 +825,6 @@ function _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, refRow, c
|
|
|
833
825
|
}
|
|
834
826
|
|
|
835
827
|
module.exports = {
|
|
836
|
-
setModel,
|
|
837
|
-
getModel,
|
|
838
828
|
_generateJavaMethod,
|
|
839
829
|
_generateCreateBody,
|
|
840
830
|
_generateUpdateBody,
|
package/lib/h2/triggers.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
const utils = require('../utils/change-tracking.js');
|
|
2
2
|
const config = require('@sap/cds').env.requires['change-tracking'];
|
|
3
3
|
const { getCompositionParentInfo, getGrandParentCompositionInfo } = require('../utils/composition-helpers.js');
|
|
4
|
-
const {
|
|
4
|
+
const { _generateJavaMethod, _generateCreateBody, _generateUpdateBody, _generateDeleteBody, _generateDeleteBodyPreserve } = require('./java-codegen.js');
|
|
5
5
|
|
|
6
6
|
function generateH2Trigger(csn, entity, rootEntity, mergedAnnotations = null, rootMergedAnnotations = null, grandParentContext = {}) {
|
|
7
|
-
setModel(csn);
|
|
8
7
|
const { columns: trackedColumns } = utils.extractTrackedColumns(entity, csn, mergedAnnotations);
|
|
9
8
|
const objectIDs = utils.getObjectIDs(entity, csn, mergedAnnotations?.entityAnnotation);
|
|
10
9
|
const rootObjectIDs = utils.getObjectIDs(rootEntity, csn, rootMergedAnnotations?.entityAnnotation);
|
|
@@ -21,13 +20,13 @@ function generateH2Trigger(csn, entity, rootEntity, mergedAnnotations = null, ro
|
|
|
21
20
|
if (!shouldGenerateTriggers) return null;
|
|
22
21
|
|
|
23
22
|
// Generate the Java code for each section
|
|
24
|
-
const createBody = !config?.disableCreateTracking ? _generateCreateBody(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo) : '';
|
|
25
|
-
const updateBody = !config?.disableUpdateTracking ? _generateUpdateBody(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo) : '';
|
|
23
|
+
const createBody = !config?.disableCreateTracking ? _generateCreateBody(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo) : '';
|
|
24
|
+
const updateBody = !config?.disableUpdateTracking ? _generateUpdateBody(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo) : '';
|
|
26
25
|
let deleteBody = '';
|
|
27
26
|
if (!config?.disableDeleteTracking) {
|
|
28
27
|
deleteBody = config?.preserveDeletes
|
|
29
|
-
? _generateDeleteBodyPreserve(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo)
|
|
30
|
-
: _generateDeleteBody(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo);
|
|
28
|
+
? _generateDeleteBodyPreserve(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo)
|
|
29
|
+
: _generateDeleteBody(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo);
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
// Define the full Create Trigger SQL
|
package/lib/hana/composition.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
const utils = require('../utils/change-tracking.js');
|
|
2
|
-
const {
|
|
2
|
+
const { toSQL, compositeKeyExpr, buildGrandParentObjectIDExpr } = require('./sql-expressions.js');
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Builds rootObjectID select for composition of many
|
|
6
6
|
*/
|
|
7
|
-
function buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, binding, refRow) {
|
|
8
|
-
|
|
7
|
+
function buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, binding, refRow, model) {
|
|
8
|
+
const rootEntityKeyExpr = compositeKeyExpr(binding.map((k) => `:${refRow}.${k}`));
|
|
9
|
+
|
|
10
|
+
if (!rootObjectIDs || rootObjectIDs.length === 0) return rootEntityKeyExpr;
|
|
9
11
|
|
|
10
12
|
const rootKeys = utils.extractKeys(rootEntity.keys);
|
|
11
|
-
if (rootKeys.length !== binding.length) return
|
|
13
|
+
if (rootKeys.length !== binding.length) return rootEntityKeyExpr;
|
|
12
14
|
|
|
13
15
|
const where = {};
|
|
14
16
|
for (let i = 0; i < rootKeys.length; i++) {
|
|
@@ -18,11 +20,10 @@ function buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, binding, r
|
|
|
18
20
|
const parts = [];
|
|
19
21
|
for (const oid of rootObjectIDs) {
|
|
20
22
|
const query = SELECT.one.from(rootEntity.name).columns(oid.name).where(where);
|
|
21
|
-
parts.push(`COALESCE(TO_NVARCHAR((${toSQL(query)})), '')`);
|
|
23
|
+
parts.push(`COALESCE(TO_NVARCHAR((${toSQL(query, model)})), '')`);
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
const concatLogic = parts.join(" || ', ' || ");
|
|
25
|
-
const rootEntityKeyExpr = compositeKeyExpr(binding.map((k) => `:${refRow}.${k}`));
|
|
26
27
|
|
|
27
28
|
return `COALESCE(NULLIF(${concatLogic}, ''), ${rootEntityKeyExpr})`;
|
|
28
29
|
}
|
|
@@ -32,8 +33,7 @@ function buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, binding, r
|
|
|
32
33
|
* In composition of one, the parent entity has FK to the child (e.g., BookStores.registry_ID -> BookStoreRegistry.ID)
|
|
33
34
|
* So we need to do a reverse lookup: find the parent record that has FK pointing to this child.
|
|
34
35
|
*/
|
|
35
|
-
function buildCompositionOfOneParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef) {
|
|
36
|
-
const model = getModel();
|
|
36
|
+
function buildCompositionOfOneParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef, model) {
|
|
37
37
|
const { parentEntityName, compositionFieldName, parentKeyBinding } = compositionParentInfo;
|
|
38
38
|
const { compositionName, childKeys } = parentKeyBinding;
|
|
39
39
|
|
|
@@ -101,23 +101,21 @@ function buildCompositionOfOneParentContext(compositionParentInfo, rootObjectIDs
|
|
|
101
101
|
return { declares, insertSQL, parentEntityName, compositionFieldName, parentKeyExpr };
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
function buildCompositionParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef, grandParentCompositionInfo = null) {
|
|
105
|
-
const model = getModel();
|
|
104
|
+
function buildCompositionParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef, model, grandParentCompositionInfo = null) {
|
|
106
105
|
const { parentEntityName, compositionFieldName, parentKeyBinding } = compositionParentInfo;
|
|
107
106
|
|
|
108
107
|
// Handle composition of one (parent has FK to child - need reverse lookup)
|
|
109
108
|
if (parentKeyBinding.type === 'compositionOfOne') {
|
|
110
|
-
return buildCompositionOfOneParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef);
|
|
109
|
+
return buildCompositionOfOneParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef, model);
|
|
111
110
|
}
|
|
112
111
|
|
|
113
112
|
const parentKeyExpr = compositeKeyExpr(parentKeyBinding.map((k) => `:${rowRef}.${k}`));
|
|
114
113
|
|
|
115
114
|
// Build rootObjectID expression for the parent entity
|
|
116
115
|
const rootEntity = model.definitions[parentEntityName];
|
|
117
|
-
const rootObjectIDExpr = buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, parentKeyBinding, rowRef);
|
|
116
|
+
const rootObjectIDExpr = buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, parentKeyBinding, rowRef, model);
|
|
118
117
|
|
|
119
|
-
let declares
|
|
120
|
-
let insertSQL;
|
|
118
|
+
let declares, insertSQL;
|
|
121
119
|
|
|
122
120
|
if (grandParentCompositionInfo) {
|
|
123
121
|
// When we have grandparent info, we need to:
|
|
@@ -134,7 +132,7 @@ function buildCompositionParentContext(compositionParentInfo, rootObjectIDs, mod
|
|
|
134
132
|
// Build grandparent objectID expression
|
|
135
133
|
const grandParentEntity = model.definitions[grandParentEntityName];
|
|
136
134
|
const grandParentObjectIDs = utils.getObjectIDs(grandParentEntity, model);
|
|
137
|
-
const grandParentObjectIDExpr = grandParentObjectIDs?.length > 0 ? buildGrandParentObjectIDExpr(grandParentObjectIDs, grandParentEntity, parentEntityName, parentKeyBinding, grandParentKeyBinding, rowRef) : grandParentKeyExpr;
|
|
135
|
+
const grandParentObjectIDExpr = grandParentObjectIDs?.length > 0 ? buildGrandParentObjectIDExpr(grandParentObjectIDs, grandParentEntity, parentEntityName, parentKeyBinding, grandParentKeyBinding, rowRef, model) : grandParentKeyExpr;
|
|
138
136
|
|
|
139
137
|
// Add grandparent_id to declares
|
|
140
138
|
declares = 'DECLARE parent_id NVARCHAR(36);\n\tDECLARE grandparent_id NVARCHAR(36);';
|