@cap-js/change-tracking 2.0.0-beta.1 → 2.0.0-beta.3
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 +27 -2
- package/README.md +87 -244
- package/_i18n/i18n.properties +0 -4
- package/cds-plugin.js +4 -2
- package/index.cds +114 -90
- package/lib/h2/java-codegen.js +34 -44
- package/lib/h2/triggers.js +5 -6
- package/lib/hana/composition.js +13 -15
- package/lib/hana/register.js +26 -7
- package/lib/hana/sql-expressions.js +9 -20
- 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 +24 -34
- 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 +22 -32
- package/lib/sqlite/triggers.js +24 -25
- package/lib/utils/change-tracking.js +7 -3
- package/lib/utils/composition-helpers.js +4 -6
- package/lib/utils/entity-collector.js +67 -42
- package/lib/utils/session-variables.js +10 -12
- package/package.json +1 -2
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,72 @@ 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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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 attributeI18n.locale = $user.locale
|
|
38
|
+
left outer join i18nKeys as entityI18n
|
|
39
|
+
on entityI18n.ID = change.entity
|
|
40
|
+
and entityI18n.locale = $user.locale
|
|
41
|
+
left outer join i18nKeys as modificationI18n
|
|
42
|
+
on modificationI18n.ID = change.modification
|
|
43
|
+
and modificationI18n.locale = $user.locale
|
|
44
|
+
{
|
|
45
|
+
key change.ID @UI.Hidden,
|
|
46
|
+
change.parent : redirected to ChangeView,
|
|
47
|
+
change.children : redirected to ChangeView,
|
|
48
|
+
change.attribute,
|
|
49
|
+
change.valueChangedFrom,
|
|
50
|
+
change.valueChangedTo,
|
|
51
|
+
change.entity,
|
|
52
|
+
change.entityKey,
|
|
53
|
+
change.objectID,
|
|
54
|
+
change.modification,
|
|
55
|
+
change.valueDataType,
|
|
56
|
+
change.createdAt,
|
|
57
|
+
change.createdBy,
|
|
58
|
+
change.transactionID,
|
|
59
|
+
COALESCE(
|
|
60
|
+
attributeI18n.text, (
|
|
61
|
+
select text from i18nKeys
|
|
62
|
+
where
|
|
63
|
+
ID = change.attribute
|
|
64
|
+
and locale = 'en'
|
|
65
|
+
), change.attribute
|
|
66
|
+
) as attributeLabel : String(15) @title: '{i18n>Changes.attribute}',
|
|
67
|
+
COALESCE(
|
|
68
|
+
entityI18n.text, (
|
|
69
|
+
select text from i18nKeys
|
|
70
|
+
where
|
|
71
|
+
ID = change.entity
|
|
72
|
+
and locale = 'en'
|
|
73
|
+
), change.entity
|
|
74
|
+
) as entityLabel : String(24) @title: '{i18n>Changes.entity}',
|
|
75
|
+
COALESCE(
|
|
76
|
+
modificationI18n.text, (
|
|
77
|
+
select text from i18nKeys
|
|
78
|
+
where
|
|
79
|
+
ID = change.modification
|
|
80
|
+
and locale = 'en'
|
|
81
|
+
), change.modification
|
|
82
|
+
) as modificationLabel : String(16) @title: '{i18n>Changes.modification}',
|
|
83
|
+
COALESCE(
|
|
84
|
+
change.valueChangedFromLabel, change.valueChangedFrom
|
|
85
|
+
) as valueChangedFromLabel : String(5000) @(
|
|
86
|
+
title: '{i18n>Changes.valueChangedFrom}',
|
|
87
|
+
UI.MultiLineText
|
|
88
|
+
),
|
|
89
|
+
COALESCE(
|
|
90
|
+
change.valueChangedToLabel, change.valueChangedTo
|
|
91
|
+
) as valueChangedToLabel : String(5000) @(
|
|
92
|
+
title: '{i18n>Changes.valueChangedTo}',
|
|
93
|
+
UI.MultiLineText
|
|
94
|
+
),
|
|
95
|
+
// For the hierarchy
|
|
96
|
+
null as LimitedDescendantCount : Int16 @UI.Hidden,
|
|
97
|
+
null as DistanceFromRoot : Int16 @UI.Hidden,
|
|
98
|
+
null as DrillState : String @UI.Hidden,
|
|
99
|
+
null as LimitedRank : Int16 @UI.Hidden,
|
|
101
100
|
};
|
|
102
101
|
|
|
103
102
|
entity i18nKeys {
|
|
@@ -159,46 +158,46 @@ annotate ChangeView with @(UI: {
|
|
|
159
158
|
},
|
|
160
159
|
LineItem : [
|
|
161
160
|
{
|
|
162
|
-
Value
|
|
163
|
-
@UI.Importance
|
|
161
|
+
Value : modificationLabel,
|
|
162
|
+
@UI.Importance: #Low
|
|
164
163
|
},
|
|
165
164
|
{
|
|
166
|
-
Value
|
|
167
|
-
@UI.Importance
|
|
165
|
+
Value : entityLabel,
|
|
166
|
+
@UI.Importance: #Medium
|
|
168
167
|
},
|
|
169
168
|
{
|
|
170
|
-
Value
|
|
171
|
-
@UI.Importance
|
|
169
|
+
Value : objectID,
|
|
170
|
+
@UI.Importance: #Medium
|
|
172
171
|
},
|
|
173
172
|
{
|
|
174
|
-
Value
|
|
175
|
-
@UI.Importance
|
|
173
|
+
Value : attributeLabel,
|
|
174
|
+
@UI.Importance: #Medium
|
|
176
175
|
},
|
|
177
176
|
{
|
|
178
|
-
Value
|
|
179
|
-
@UI.Importance
|
|
177
|
+
Value : valueChangedToLabel,
|
|
178
|
+
@UI.Importance: #High
|
|
180
179
|
},
|
|
181
180
|
{
|
|
182
|
-
Value
|
|
183
|
-
@UI.Importance
|
|
181
|
+
Value : valueChangedFromLabel,
|
|
182
|
+
@UI.Importance: #High
|
|
184
183
|
},
|
|
185
184
|
{
|
|
186
|
-
Value
|
|
187
|
-
@UI.Importance
|
|
185
|
+
Value : createdAt,
|
|
186
|
+
@UI.Importance: #Low
|
|
188
187
|
},
|
|
189
188
|
{
|
|
190
|
-
Value
|
|
191
|
-
@UI.Importance
|
|
189
|
+
Value : createdBy,
|
|
190
|
+
@UI.Importance: #High
|
|
192
191
|
},
|
|
193
192
|
],
|
|
194
193
|
DeleteHidden : true,
|
|
195
194
|
}) {
|
|
196
195
|
valueChangedFrom @UI.Hidden;
|
|
197
|
-
valueChangedTo
|
|
198
|
-
parent
|
|
199
|
-
entityKey
|
|
200
|
-
entity
|
|
201
|
-
attribute
|
|
196
|
+
valueChangedTo @UI.Hidden;
|
|
197
|
+
parent @UI.Hidden;
|
|
198
|
+
entityKey @UI.Hidden;
|
|
199
|
+
entity @UI.Hidden;
|
|
200
|
+
attribute @UI.Hidden;
|
|
202
201
|
};
|
|
203
202
|
|
|
204
203
|
annotate ChangeView with @(
|
|
@@ -229,3 +228,28 @@ annotate ChangeView with @(
|
|
|
229
228
|
'LimitedRank'
|
|
230
229
|
],
|
|
231
230
|
);
|
|
231
|
+
|
|
232
|
+
// Annotations for searching
|
|
233
|
+
annotate ChangeView with @(cds.search: {
|
|
234
|
+
valueChangedFrom: false,
|
|
235
|
+
valueChangedTo : false,
|
|
236
|
+
entity : false,
|
|
237
|
+
attribute : false,
|
|
238
|
+
modification : false,
|
|
239
|
+
valueDataType : false,
|
|
240
|
+
modificationLabel,
|
|
241
|
+
entityLabel,
|
|
242
|
+
entityKey,
|
|
243
|
+
objectID,
|
|
244
|
+
attributeLabel,
|
|
245
|
+
valueChangedFromLabel,
|
|
246
|
+
valueChangedToLabel,
|
|
247
|
+
createdBy,
|
|
248
|
+
}) {
|
|
249
|
+
entityLabel @Search.ranking: HIGH;
|
|
250
|
+
attributeLabel @Search.ranking: HIGH;
|
|
251
|
+
objectID @Search.ranking: HIGH;
|
|
252
|
+
|
|
253
|
+
entityKey @Search.ranking: LOW;
|
|
254
|
+
modificationLabel @Search.ranking: LOW;
|
|
255
|
+
};
|
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}";`;
|
|
@@ -717,7 +709,7 @@ function _generateObjectIDCalculation(objectIDs, entity, refRow) {
|
|
|
717
709
|
|
|
718
710
|
for (const oid of objectIDs) {
|
|
719
711
|
if (oid.included) {
|
|
720
|
-
parts.push(`SELECT CAST(? AS VARCHAR) AS val`);
|
|
712
|
+
parts.push(`SELECT COALESCE(CAST(? AS VARCHAR), '<empty>') AS val`);
|
|
721
713
|
bindings.push(`${refRow}.getString("${oid.name}")`);
|
|
722
714
|
} else {
|
|
723
715
|
// Sub-select needed (Lookup)
|
|
@@ -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);';
|
package/lib/hana/register.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const cds = require('@sap/cds');
|
|
2
|
-
const { fs } = cds.utils;
|
|
3
2
|
|
|
4
|
-
const { prepareCSNForTriggers, generateTriggersForEntities,
|
|
3
|
+
const { prepareCSNForTriggers, generateTriggersForEntities, ensureUndeployJsonHasTriggerPattern } = require('../utils/trigger-utils.js');
|
|
4
|
+
const { getLabelTranslations } = require('../localization.js');
|
|
5
5
|
|
|
6
6
|
function registerHDICompilerHook() {
|
|
7
7
|
const _hdi_migration = cds.compiler.to.hdi.migration;
|
|
@@ -10,17 +10,36 @@ function registerHDICompilerHook() {
|
|
|
10
10
|
const { runtimeCSN, hierarchy, entities } = prepareCSNForTriggers(csn, true);
|
|
11
11
|
|
|
12
12
|
const triggers = generateTriggersForEntities(runtimeCSN, hierarchy, entities, generateHANATriggers);
|
|
13
|
-
|
|
13
|
+
const data = [];
|
|
14
14
|
if (triggers.length > 0) {
|
|
15
15
|
delete csn.definitions['sap.changelog.CHANGE_TRACKING_DUMMY']['@cds.persistence.skip'];
|
|
16
|
-
writeLabelsCSV(entities, runtimeCSN);
|
|
17
|
-
const dir = 'db/src/gen/data/';
|
|
18
|
-
fs.writeFileSync(`${dir}/sap.changelog-CHANGE_TRACKING_DUMMY.csv`, `X\n1`);
|
|
19
16
|
ensureUndeployJsonHasTriggerPattern();
|
|
17
|
+
|
|
18
|
+
const labels = getLabelTranslations(entities, runtimeCSN);
|
|
19
|
+
const header = 'ID;locale;text';
|
|
20
|
+
const escape = (v) => {
|
|
21
|
+
const s = String(v ?? '');
|
|
22
|
+
return s.includes(';') || s.includes('\n') || s.includes('"') ? `"${s.replace(/"/g, '""')}"` : s;
|
|
23
|
+
};
|
|
24
|
+
const rows = labels.map((row) => `${escape(row.ID)};${escape(row.locale)};${escape(row.text)}`);
|
|
25
|
+
const i18nContent = [header, ...rows].join('\n') + '\n';
|
|
26
|
+
|
|
27
|
+
data.push(
|
|
28
|
+
{
|
|
29
|
+
name: 'sap.changelog-CHANGE_TRACKING_DUMMY',
|
|
30
|
+
sql: 'X\n1',
|
|
31
|
+
suffix: '.csv'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'sap.changelog-i18nKeys',
|
|
35
|
+
sql: i18nContent,
|
|
36
|
+
suffix: '.csv'
|
|
37
|
+
}
|
|
38
|
+
);
|
|
20
39
|
}
|
|
21
40
|
|
|
22
41
|
const ret = _hdi_migration(csn, options, beforeImage);
|
|
23
|
-
ret.definitions =
|
|
42
|
+
ret.definitions = ret.definitions.concat(triggers).concat(data);
|
|
24
43
|
return ret;
|
|
25
44
|
};
|
|
26
45
|
}
|