@cap-js/change-tracking 1.1.4 → 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 +40 -1
- package/README.md +45 -71
- package/_i18n/i18n.properties +10 -22
- package/_i18n/i18n_ar.properties +3 -3
- package/_i18n/i18n_bg.properties +3 -3
- package/_i18n/i18n_cs.properties +3 -3
- package/_i18n/i18n_da.properties +3 -3
- package/_i18n/i18n_de.properties +3 -3
- package/_i18n/i18n_el.properties +3 -3
- package/_i18n/i18n_en.properties +3 -3
- package/_i18n/i18n_en_US_saptrc.properties +3 -32
- package/_i18n/i18n_es.properties +3 -3
- package/_i18n/i18n_es_MX.properties +3 -3
- package/_i18n/i18n_fi.properties +3 -3
- package/_i18n/i18n_fr.properties +3 -3
- package/_i18n/i18n_he.properties +3 -3
- package/_i18n/i18n_hr.properties +3 -3
- package/_i18n/i18n_hu.properties +3 -3
- package/_i18n/i18n_it.properties +3 -3
- package/_i18n/i18n_ja.properties +3 -3
- package/_i18n/i18n_kk.properties +3 -3
- package/_i18n/i18n_ko.properties +3 -3
- package/_i18n/i18n_ms.properties +3 -3
- package/_i18n/i18n_nl.properties +3 -3
- package/_i18n/i18n_no.properties +3 -3
- package/_i18n/i18n_pl.properties +3 -3
- package/_i18n/i18n_pt.properties +3 -3
- package/_i18n/i18n_ro.properties +3 -3
- package/_i18n/i18n_ru.properties +3 -3
- package/_i18n/i18n_sh.properties +3 -3
- package/_i18n/i18n_sk.properties +3 -3
- package/_i18n/i18n_sl.properties +3 -3
- package/_i18n/i18n_sv.properties +3 -3
- package/_i18n/i18n_th.properties +3 -3
- package/_i18n/i18n_tr.properties +3 -3
- package/_i18n/i18n_uk.properties +3 -3
- package/_i18n/i18n_vi.properties +3 -3
- package/_i18n/i18n_zh_CN.properties +3 -3
- package/_i18n/i18n_zh_TW.properties +3 -3
- package/cds-plugin.js +16 -263
- package/index.cds +187 -76
- package/lib/TriggerCQN2SQL.js +42 -0
- package/lib/h2/java-codegen.js +833 -0
- package/lib/h2/register.js +27 -0
- package/lib/h2/triggers.js +41 -0
- package/lib/hana/composition.js +248 -0
- package/lib/hana/register.js +28 -0
- package/lib/hana/sql-expressions.js +213 -0
- package/lib/hana/triggers.js +253 -0
- package/lib/localization.js +53 -117
- package/lib/model-enhancer.js +266 -0
- package/lib/postgres/composition.js +190 -0
- package/lib/postgres/register.js +44 -0
- package/lib/postgres/sql-expressions.js +261 -0
- package/lib/postgres/triggers.js +113 -0
- package/lib/skipHandlers.js +34 -0
- package/lib/sqlite/composition.js +234 -0
- package/lib/sqlite/register.js +28 -0
- package/lib/sqlite/sql-expressions.js +228 -0
- package/lib/sqlite/triggers.js +163 -0
- package/lib/utils/change-tracking.js +394 -0
- package/lib/utils/composition-helpers.js +67 -0
- package/lib/utils/entity-collector.js +297 -0
- package/lib/utils/session-variables.js +276 -0
- package/lib/utils/trigger-utils.js +94 -0
- package/package.json +17 -7
- package/lib/change-log.js +0 -538
- package/lib/entity-helper.js +0 -217
- package/lib/format-options.js +0 -66
- package/lib/template-processor.js +0 -115
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
const utils = require('../utils/change-tracking.js');
|
|
2
|
+
const { CT_SKIP_VAR, getEntitySkipVarName, getElementSkipVarName } = require('../utils/session-variables.js');
|
|
3
|
+
const { createTriggerCQN2SQL } = require('../TriggerCQN2SQL.js');
|
|
4
|
+
|
|
5
|
+
const cqn4sql = require('@cap-js/db-service/lib/cqn4sql');
|
|
6
|
+
|
|
7
|
+
const _cqn2sqlCache = new WeakMap();
|
|
8
|
+
|
|
9
|
+
function _toSQL(query, model) {
|
|
10
|
+
let cqn2sql = _cqn2sqlCache.get(model);
|
|
11
|
+
if (!cqn2sql) {
|
|
12
|
+
const SQLiteService = require('@cap-js/sqlite');
|
|
13
|
+
const TriggerCQN2SQL = createTriggerCQN2SQL(SQLiteService.CQN2SQL);
|
|
14
|
+
cqn2sql = new TriggerCQN2SQL({ model: model });
|
|
15
|
+
_cqn2sqlCache.set(model, cqn2sql);
|
|
16
|
+
}
|
|
17
|
+
const sqlCQN = cqn4sql(query, model);
|
|
18
|
+
return cqn2sql.SELECT(sqlCQN);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function handleAssocLookup(column, refRow, model) {
|
|
22
|
+
let bindings = [];
|
|
23
|
+
let where = {};
|
|
24
|
+
|
|
25
|
+
if (column.foreignKeys) {
|
|
26
|
+
where = column.foreignKeys.reduce((acc, k) => {
|
|
27
|
+
acc[k] = { ref: ['?'], param: true };
|
|
28
|
+
return acc;
|
|
29
|
+
}, {});
|
|
30
|
+
bindings = column.foreignKeys.map((fk) => `${refRow}.getString("${column.name}_${fk}")`);
|
|
31
|
+
} else if (column.on) {
|
|
32
|
+
where = column.on.reduce((acc, k) => {
|
|
33
|
+
acc[k] = { ref: ['?'], param: true };
|
|
34
|
+
return acc;
|
|
35
|
+
}, {});
|
|
36
|
+
bindings = column.on.map((assoc) => `${refRow}.getString("${assoc}")`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const alt = column.alt.map((s) => s.split('.').slice(1).join('.'));
|
|
40
|
+
const columns = alt.length === 1 ? alt[0] : utils.buildConcatXpr(alt);
|
|
41
|
+
|
|
42
|
+
// Check if target entity has localized data
|
|
43
|
+
const localizedInfo = utils.getLocalizedLookupInfo(column.target, column.alt, model);
|
|
44
|
+
|
|
45
|
+
if (localizedInfo) {
|
|
46
|
+
// Build locale-aware lookup: try .texts table first, fall back to base entity
|
|
47
|
+
const textsWhere = { ...where, locale: { ref: ['?'], param: true } };
|
|
48
|
+
const textsQuery = SELECT.one.from(localizedInfo.textsEntity).columns(columns).where(textsWhere);
|
|
49
|
+
const baseQuery = SELECT.one.from(column.target).columns(columns).where(where);
|
|
50
|
+
|
|
51
|
+
const textsSQL = _toSQL(textsQuery, model);
|
|
52
|
+
const baseSQL = _toSQL(baseQuery, model);
|
|
53
|
+
|
|
54
|
+
// Add locale binding (fetched from session variable @$user.locale)
|
|
55
|
+
const textsBindings = [...bindings, 'locale'];
|
|
56
|
+
const baseBindings = [...bindings];
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
sql: `(SELECT COALESCE((${textsSQL}), (${baseSQL})))`,
|
|
60
|
+
bindings: [...textsBindings, ...baseBindings],
|
|
61
|
+
needsLocale: true
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const query = SELECT.one.from(column.target).columns(columns).where(where);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
sql: `(${_toSQL(query, model)})`,
|
|
69
|
+
bindings: bindings
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function _generateJavaMethod(createBody, updateBody, deleteBody, entityName, compositionParentInfo = null, grandParentCompositionInfo = null) {
|
|
74
|
+
const entitySkipVar = getEntitySkipVarName(entityName);
|
|
75
|
+
|
|
76
|
+
// Extract values for use in template strings
|
|
77
|
+
const parentEntityName = compositionParentInfo?.parentEntityName ?? '';
|
|
78
|
+
const compositionFieldName = compositionParentInfo?.compositionFieldName ?? '';
|
|
79
|
+
const grandParentEntityName = grandParentCompositionInfo?.grandParentEntityName ?? '';
|
|
80
|
+
const grandParentCompositionFieldName = grandParentCompositionInfo?.grandParentCompositionFieldName ?? '';
|
|
81
|
+
|
|
82
|
+
// Add grandparent entry helper method when grandParentCompositionInfo exists
|
|
83
|
+
// Note: grandparent entries always use 'update' modification since they represent changes to an existing parent's composition
|
|
84
|
+
const grandParentHelper = grandParentCompositionInfo
|
|
85
|
+
? `
|
|
86
|
+
private String ensureGrandParentCompositionEntry(Connection conn, String grandParentKey, String grandParentObjectID) throws SQLException {
|
|
87
|
+
String grandParentId = null;
|
|
88
|
+
long transactionId = getTransactionId(conn);
|
|
89
|
+
|
|
90
|
+
String checkSQL = "SELECT ID FROM sap_changelog_Changes WHERE ENTITY = ? AND ENTITYKEY = ? AND ATTRIBUTE = ? AND VALUEDATATYPE = 'cds.Composition' AND TRANSACTIONID = ?";
|
|
91
|
+
try (PreparedStatement stmt = conn.prepareStatement(checkSQL)) {
|
|
92
|
+
stmt.setString(1, "${grandParentEntityName}");
|
|
93
|
+
stmt.setString(2, grandParentKey);
|
|
94
|
+
stmt.setString(3, "${grandParentCompositionFieldName}");
|
|
95
|
+
stmt.setLong(4, transactionId);
|
|
96
|
+
try (ResultSet rs = stmt.executeQuery()) {
|
|
97
|
+
if (rs.next()) {
|
|
98
|
+
grandParentId = rs.getString(1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (grandParentId == null) {
|
|
104
|
+
grandParentId = java.util.UUID.randomUUID().toString();
|
|
105
|
+
String insertSQL = "INSERT INTO sap_changelog_Changes (ID, PARENT_ID, ATTRIBUTE, ENTITY, ENTITYKEY, OBJECTID, CREATEDAT, CREATEDBY, VALUEDATATYPE, MODIFICATION, TRANSACTIONID) VALUES (?, NULL, ?, ?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_USER(), 'cds.Composition', 'update', ?)";
|
|
106
|
+
try (PreparedStatement stmt = conn.prepareStatement(insertSQL)) {
|
|
107
|
+
stmt.setString(1, grandParentId);
|
|
108
|
+
stmt.setString(2, "${grandParentCompositionFieldName}");
|
|
109
|
+
stmt.setString(3, "${grandParentEntityName}");
|
|
110
|
+
stmt.setString(4, grandParentKey);
|
|
111
|
+
stmt.setString(5, grandParentObjectID);
|
|
112
|
+
stmt.setLong(6, transactionId);
|
|
113
|
+
stmt.executeUpdate();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return grandParentId;
|
|
118
|
+
}
|
|
119
|
+
`
|
|
120
|
+
: '';
|
|
121
|
+
|
|
122
|
+
// Add parent ID helper method if needed
|
|
123
|
+
const parentIdHelper = compositionParentInfo
|
|
124
|
+
? `
|
|
125
|
+
private String ensureCompositionParentEntry(Connection conn, String parentEntityKey, String objectID, String modification${grandParentCompositionInfo ? ', String parentChangelogId' : ''}) throws SQLException {
|
|
126
|
+
String parentId = null;
|
|
127
|
+
long transactionId = getTransactionId(conn);
|
|
128
|
+
|
|
129
|
+
String checkSQL = "SELECT ID FROM sap_changelog_Changes WHERE ENTITY = ? AND ENTITYKEY = ? AND ATTRIBUTE = ? AND VALUEDATATYPE = 'cds.Composition' AND TRANSACTIONID = ?";
|
|
130
|
+
try (PreparedStatement stmt = conn.prepareStatement(checkSQL)) {
|
|
131
|
+
stmt.setString(1, "${parentEntityName}");
|
|
132
|
+
stmt.setString(2, parentEntityKey);
|
|
133
|
+
stmt.setString(3, "${compositionFieldName}");
|
|
134
|
+
stmt.setLong(4, transactionId);
|
|
135
|
+
try (ResultSet rs = stmt.executeQuery()) {
|
|
136
|
+
if (rs.next()) {
|
|
137
|
+
parentId = rs.getString(1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (parentId == null) {
|
|
143
|
+
${
|
|
144
|
+
grandParentCompositionInfo
|
|
145
|
+
? ''
|
|
146
|
+
: `
|
|
147
|
+
String effectiveModification = modification;
|
|
148
|
+
if ("create".equals(modification)) {
|
|
149
|
+
String parentCreatedCheckSQL = "SELECT COUNT(*) FROM sap_changelog_Changes WHERE ENTITY = ? AND ENTITYKEY = ? AND MODIFICATION = 'create' AND TRANSACTIONID = ?";
|
|
150
|
+
try (PreparedStatement checkStmt = conn.prepareStatement(parentCreatedCheckSQL)) {
|
|
151
|
+
checkStmt.setString(1, "${parentEntityName}");
|
|
152
|
+
checkStmt.setString(2, parentEntityKey);
|
|
153
|
+
checkStmt.setLong(3, transactionId);
|
|
154
|
+
try (ResultSet rs = checkStmt.executeQuery()) {
|
|
155
|
+
if (rs.next() && rs.getInt(1) == 0) {
|
|
156
|
+
effectiveModification = "update";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
modification = effectiveModification;`
|
|
162
|
+
}
|
|
163
|
+
parentId = java.util.UUID.randomUUID().toString();
|
|
164
|
+
String insertSQL = "INSERT INTO sap_changelog_Changes (ID, PARENT_ID, ATTRIBUTE, ENTITY, ENTITYKEY, OBJECTID, CREATEDAT, CREATEDBY, VALUEDATATYPE, MODIFICATION, TRANSACTIONID) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_USER(), 'cds.Composition', ?, ?)";
|
|
165
|
+
try (PreparedStatement stmt = conn.prepareStatement(insertSQL)) {
|
|
166
|
+
stmt.setString(1, parentId);
|
|
167
|
+
stmt.setString(2, ${grandParentCompositionInfo ? 'parentChangelogId' : 'null'});
|
|
168
|
+
stmt.setString(3, "${compositionFieldName}");
|
|
169
|
+
stmt.setString(4, "${parentEntityName}");
|
|
170
|
+
stmt.setString(5, parentEntityKey);
|
|
171
|
+
stmt.setString(6, objectID);
|
|
172
|
+
stmt.setString(7, modification);
|
|
173
|
+
stmt.setLong(8, transactionId);
|
|
174
|
+
stmt.executeUpdate();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return parentId;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private long getTransactionId(Connection conn) throws SQLException {
|
|
182
|
+
try (PreparedStatement stmt = conn.prepareStatement("SELECT TRANSACTION_ID()")) {
|
|
183
|
+
try (ResultSet rs = stmt.executeQuery()) {
|
|
184
|
+
if (rs.next()) {
|
|
185
|
+
return rs.getLong(1);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
`
|
|
192
|
+
: '';
|
|
193
|
+
|
|
194
|
+
return `
|
|
195
|
+
import org.h2.tools.TriggerAdapter;
|
|
196
|
+
import java.sql.Connection;
|
|
197
|
+
import java.sql.ResultSet;
|
|
198
|
+
import java.sql.PreparedStatement;
|
|
199
|
+
import java.sql.SQLException;
|
|
200
|
+
import java.util.Objects;
|
|
201
|
+
|
|
202
|
+
@CODE
|
|
203
|
+
TriggerAdapter create() {
|
|
204
|
+
return new TriggerAdapter() {
|
|
205
|
+
private boolean shouldSkipChangeTracking(Connection conn) throws SQLException {
|
|
206
|
+
try (PreparedStatement stmt = conn.prepareStatement("SELECT @${CT_SKIP_VAR}")) {
|
|
207
|
+
try (ResultSet rs = stmt.executeQuery()) {
|
|
208
|
+
if (rs.next()) {
|
|
209
|
+
String value = rs.getString(1);
|
|
210
|
+
if ("true".equals(value)) return true;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
try (PreparedStatement stmt = conn.prepareStatement("SELECT @${entitySkipVar}")) {
|
|
215
|
+
try (ResultSet rs = stmt.executeQuery()) {
|
|
216
|
+
if (rs.next()) {
|
|
217
|
+
String value = rs.getString(1);
|
|
218
|
+
if ("true".equals(value)) return true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private boolean shouldSkipElement(Connection conn, String varName) throws SQLException {
|
|
226
|
+
try (PreparedStatement stmt = conn.prepareStatement("SELECT @" + varName)) {
|
|
227
|
+
try (ResultSet rs = stmt.executeQuery()) {
|
|
228
|
+
if (rs.next()) {
|
|
229
|
+
String value = rs.getString(1);
|
|
230
|
+
if ("true".equals(value)) return true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private String getLocale(Connection conn) throws SQLException {
|
|
238
|
+
try (PreparedStatement stmt = conn.prepareStatement("SELECT @\\$user.locale")) {
|
|
239
|
+
try (ResultSet rs = stmt.executeQuery()) {
|
|
240
|
+
if (rs.next()) {
|
|
241
|
+
return rs.getString(1);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private boolean hasExistingCompositionEntry(Connection conn, String entityName, String entityKey, String attribute) throws SQLException {
|
|
249
|
+
String sql = "SELECT 1 FROM sap_changelog_Changes WHERE ENTITY = ? AND ENTITYKEY = ? AND ATTRIBUTE = ? AND VALUEDATATYPE = 'cds.Composition' AND TRANSACTIONID = TRANSACTION_ID()";
|
|
250
|
+
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
|
|
251
|
+
stmt.setString(1, entityName);
|
|
252
|
+
stmt.setString(2, entityKey);
|
|
253
|
+
stmt.setString(3, attribute);
|
|
254
|
+
try (ResultSet rs = stmt.executeQuery()) {
|
|
255
|
+
return rs.next();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
${grandParentHelper}${parentIdHelper}
|
|
260
|
+
@Override
|
|
261
|
+
public void fire(Connection conn, ResultSet oldRow, ResultSet newRow) throws SQLException {
|
|
262
|
+
if (shouldSkipChangeTracking(conn)) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
String locale = getLocale(conn);
|
|
266
|
+
|
|
267
|
+
boolean isInsert = oldRow == null;
|
|
268
|
+
boolean isDelete = newRow == null;
|
|
269
|
+
boolean isUpdate = !isInsert && !isDelete;
|
|
270
|
+
|
|
271
|
+
if (isInsert) {
|
|
272
|
+
${createBody}
|
|
273
|
+
} else if (isUpdate) {
|
|
274
|
+
${updateBody}
|
|
275
|
+
} else if (isDelete) {
|
|
276
|
+
${deleteBody}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function _generateCreateBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
|
|
284
|
+
const reference = 'newRow';
|
|
285
|
+
|
|
286
|
+
// Set modification type for grandparent entry creation (must be before keysCalc which uses it)
|
|
287
|
+
const modificationTypeSetup = grandParentCompositionInfo ? 'String modificationType = "create";' : '';
|
|
288
|
+
const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, model, compositionParentInfo, grandParentCompositionInfo);
|
|
289
|
+
|
|
290
|
+
// Composition parent handling - with deep linking support when grandParentCompositionInfo exists
|
|
291
|
+
let parentIdSetup = '';
|
|
292
|
+
if (compositionParentInfo) {
|
|
293
|
+
if (grandParentCompositionInfo) {
|
|
294
|
+
parentIdSetup = `String parentId = ensureCompositionParentEntry(conn, parentEntityKey, parentObjectID, "create", parentChangelogId);`;
|
|
295
|
+
} else {
|
|
296
|
+
parentIdSetup = `String parentId = ensureCompositionParentEntry(conn, parentEntityKey, parentObjectID, "create");`;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Handle composition-only triggers (no tracked columns)
|
|
301
|
+
if (columns.length === 0 && compositionParentInfo) {
|
|
302
|
+
return `${modificationTypeSetup}\n${keysCalc}\n${parentIdSetup}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const columnBlocks = columns
|
|
306
|
+
.map((col) => {
|
|
307
|
+
// Prepare Value Expression
|
|
308
|
+
const { sqlExpr, bindings } = _prepareValueExpression(col, reference);
|
|
309
|
+
const labelRes = _prepareLabelExpression(col, reference, model); // label expression
|
|
310
|
+
|
|
311
|
+
// SQL Statement - include PARENT_ID if composition parent info exists
|
|
312
|
+
const insertSQL = compositionParentInfo
|
|
313
|
+
? `INSERT INTO sap_changelog_Changes
|
|
314
|
+
(ID, PARENT_ID, ATTRIBUTE, VALUECHANGEDFROM, VALUECHANGEDTO, VALUECHANGEDFROMLABEL, VALUECHANGEDTOLABEL, ENTITY, ENTITYKEY, OBJECTID, CREATEDAT, CREATEDBY, VALUEDATATYPE, MODIFICATION, TRANSACTIONID)
|
|
315
|
+
VALUES
|
|
316
|
+
(RANDOM_UUID(), ?, '${col.name}', NULL, ${sqlExpr}, NULL, ${labelRes.sqlExpr}, ?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_USER(), '${col.type}', 'create', TRANSACTION_ID())`
|
|
317
|
+
: `INSERT INTO sap_changelog_Changes
|
|
318
|
+
(ID, ATTRIBUTE, VALUECHANGEDFROM, VALUECHANGEDTO, VALUECHANGEDFROMLABEL, VALUECHANGEDTOLABEL, ENTITY, ENTITYKEY, OBJECTID, CREATEDAT, CREATEDBY, VALUEDATATYPE, MODIFICATION, TRANSACTIONID)
|
|
319
|
+
VALUES
|
|
320
|
+
(RANDOM_UUID(), '${col.name}', NULL, ${sqlExpr}, NULL, ${labelRes.sqlExpr}, ?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_USER(), '${col.type}', 'create', TRANSACTION_ID())`;
|
|
321
|
+
|
|
322
|
+
// Bindings: ParentId (if applicable) + NewVal Bindings + Standard Metadata Bindings
|
|
323
|
+
const allBindings = compositionParentInfo ? ['parentId', ...bindings, ...labelRes.bindings, 'entityName', 'entityKey', 'objectID'] : [...bindings, ...labelRes.bindings, 'entityName', 'entityKey', 'objectID'];
|
|
324
|
+
|
|
325
|
+
const tryBlock = _wrapInTryCatch(insertSQL, allBindings);
|
|
326
|
+
|
|
327
|
+
// Element skip check variable name
|
|
328
|
+
const elementSkipVar = getElementSkipVarName(entity.name, col.name);
|
|
329
|
+
|
|
330
|
+
// Null Check Wrapper + Element Skip Check
|
|
331
|
+
const valExpression = bindings.map((b) => b).join(' != null && ') + ' != null';
|
|
332
|
+
return `if ((${valExpression}) && !shouldSkipElement(conn, "${elementSkipVar}")) {
|
|
333
|
+
${tryBlock}
|
|
334
|
+
}`;
|
|
335
|
+
})
|
|
336
|
+
.join('\n');
|
|
337
|
+
|
|
338
|
+
return `${modificationTypeSetup}\n${keysCalc}\n${parentIdSetup}\n${columnBlocks}`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function _generateUpdateBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
|
|
342
|
+
const reference = 'newRow';
|
|
343
|
+
|
|
344
|
+
// Set modification type for grandparent entry creation (must be before keysCalc which uses it)
|
|
345
|
+
const modificationTypeSetup = grandParentCompositionInfo ? 'String modificationType = "update";' : '';
|
|
346
|
+
const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, model, compositionParentInfo, grandParentCompositionInfo);
|
|
347
|
+
|
|
348
|
+
// Composition parent handling - with deep linking support when grandParentCompositionInfo exists
|
|
349
|
+
let parentIdSetup = '';
|
|
350
|
+
if (compositionParentInfo) {
|
|
351
|
+
if (grandParentCompositionInfo) {
|
|
352
|
+
parentIdSetup = `String parentId = ensureCompositionParentEntry(conn, parentEntityKey, parentObjectID, "update", parentChangelogId);`;
|
|
353
|
+
} else {
|
|
354
|
+
parentIdSetup = `String parentId = ensureCompositionParentEntry(conn, parentEntityKey, parentObjectID, "update");`;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Handle composition-only triggers (no tracked columns)
|
|
359
|
+
if (columns.length === 0 && compositionParentInfo) {
|
|
360
|
+
return `${modificationTypeSetup}\n${keysCalc}\n${parentIdSetup}`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const columnBlocks = columns
|
|
364
|
+
.map((col) => {
|
|
365
|
+
// Prepare new and old Value
|
|
366
|
+
const newRes = _prepareValueExpression(col, 'newRow');
|
|
367
|
+
const oldRes = _prepareValueExpression(col, 'oldRow');
|
|
368
|
+
// Prepare new and old Label (lookup values if col.alt exists)
|
|
369
|
+
const newLabelRes = _prepareLabelExpression(col, 'newRow', model);
|
|
370
|
+
const oldLabelRes = _prepareLabelExpression(col, 'oldRow', model);
|
|
371
|
+
|
|
372
|
+
// Check column values from ResultSet for Change Logic
|
|
373
|
+
let checkCols = [col.name];
|
|
374
|
+
if (col.foreignKeys && col.foreignKeys.length > 0) {
|
|
375
|
+
checkCols = col.foreignKeys.map((fk) => `${col.name}_${fk}`);
|
|
376
|
+
} else if (col.on && col.on.length > 0) {
|
|
377
|
+
checkCols = col.on.map((m) => m.foreignKeyField);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Generate the Java condition: (col1_new != col1_old || col2_new != col2_old)
|
|
381
|
+
const changeCheck = checkCols.map((dbCol) => `!Objects.equals(newRow.getObject("${dbCol}"), oldRow.getObject("${dbCol}"))`).join(' || ');
|
|
382
|
+
|
|
383
|
+
// SQL Statement - include PARENT_ID if composition parent info exists
|
|
384
|
+
const insertSQL = compositionParentInfo
|
|
385
|
+
? `INSERT INTO sap_changelog_Changes
|
|
386
|
+
(ID, PARENT_ID, ATTRIBUTE, VALUECHANGEDFROM, VALUECHANGEDTO, VALUECHANGEDFROMLABEL, VALUECHANGEDTOLABEL, ENTITY, ENTITYKEY, OBJECTID, CREATEDAT, CREATEDBY, VALUEDATATYPE, MODIFICATION, TRANSACTIONID)
|
|
387
|
+
VALUES
|
|
388
|
+
(RANDOM_UUID(), ?, '${col.name}', ${oldRes.sqlExpr}, ${newRes.sqlExpr}, ${oldLabelRes.sqlExpr}, ${newLabelRes.sqlExpr}, ?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_USER(), '${col.type}', 'update', TRANSACTION_ID())`
|
|
389
|
+
: `INSERT INTO sap_changelog_Changes
|
|
390
|
+
(ID, ATTRIBUTE, VALUECHANGEDFROM, VALUECHANGEDTO, VALUECHANGEDFROMLABEL, VALUECHANGEDTOLABEL, ENTITY, ENTITYKEY, OBJECTID, CREATEDAT, CREATEDBY, VALUEDATATYPE, MODIFICATION, TRANSACTIONID)
|
|
391
|
+
VALUES
|
|
392
|
+
(RANDOM_UUID(), '${col.name}', ${oldRes.sqlExpr}, ${newRes.sqlExpr}, ${oldLabelRes.sqlExpr}, ${newLabelRes.sqlExpr}, ?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_USER(), '${col.type}', 'update', TRANSACTION_ID())`;
|
|
393
|
+
|
|
394
|
+
// Bindings: ParentId (if applicable) + OldVal + NewVal + Metadata
|
|
395
|
+
const allBindings = compositionParentInfo
|
|
396
|
+
? ['parentId', ...oldRes.bindings, ...newRes.bindings, ...oldLabelRes.bindings, ...newLabelRes.bindings, 'entityName', 'entityKey', 'objectID']
|
|
397
|
+
: [...oldRes.bindings, ...newRes.bindings, ...oldLabelRes.bindings, ...newLabelRes.bindings, 'entityName', 'entityKey', 'objectID'];
|
|
398
|
+
|
|
399
|
+
// Element skip check variable name
|
|
400
|
+
const elementSkipVar = getElementSkipVarName(entity.name, col.name);
|
|
401
|
+
|
|
402
|
+
// For composition-of-one columns, add deduplication check to prevent duplicate entries
|
|
403
|
+
// when child trigger has already created a composition entry for this transaction
|
|
404
|
+
const compositionCheck = col.type === 'cds.Composition' ? ` && !hasExistingCompositionEntry(conn, entityName, entityKey, "${col.name}")` : '';
|
|
405
|
+
|
|
406
|
+
return `if ((${changeCheck}) && !shouldSkipElement(conn, "${elementSkipVar}")${compositionCheck}) {
|
|
407
|
+
${_wrapInTryCatch(insertSQL, allBindings)}
|
|
408
|
+
}`;
|
|
409
|
+
})
|
|
410
|
+
.join('\n');
|
|
411
|
+
|
|
412
|
+
return `${modificationTypeSetup}\n${keysCalc}\n${parentIdSetup}\n${columnBlocks}`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function _generateDeleteBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
|
|
416
|
+
const reference = 'oldRow';
|
|
417
|
+
|
|
418
|
+
// Set modification type for grandparent entry creation (must be before keysCalc which uses it)
|
|
419
|
+
const modificationTypeSetup = grandParentCompositionInfo ? 'String modificationType = "delete";' : '';
|
|
420
|
+
const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, model, compositionParentInfo, grandParentCompositionInfo);
|
|
421
|
+
|
|
422
|
+
// First delete existing changelogs for this entity
|
|
423
|
+
const deleteSQL = `DELETE FROM sap_changelog_Changes WHERE ENTITY = ? AND ENTITYKEY = ?`;
|
|
424
|
+
const deleteBlock = _wrapInTryCatch(deleteSQL, ['entityName', 'entityKey']);
|
|
425
|
+
|
|
426
|
+
// Composition parent handling - with deep linking support when grandParentCompositionInfo exists
|
|
427
|
+
let parentIdSetup = '';
|
|
428
|
+
if (compositionParentInfo) {
|
|
429
|
+
if (grandParentCompositionInfo) {
|
|
430
|
+
parentIdSetup = `String parentId = ensureCompositionParentEntry(conn, parentEntityKey, parentObjectID, "delete", parentChangelogId);`;
|
|
431
|
+
} else {
|
|
432
|
+
parentIdSetup = `String parentId = ensureCompositionParentEntry(conn, parentEntityKey, parentObjectID, "delete");`;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Handle composition-only triggers (no tracked columns)
|
|
437
|
+
if (columns.length === 0 && compositionParentInfo) {
|
|
438
|
+
return `${modificationTypeSetup}\n${keysCalc}\n${deleteBlock}\n${parentIdSetup}`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Then insert delete changelog entries for each tracked column
|
|
442
|
+
const columnBlocks = columns
|
|
443
|
+
.map((col) => {
|
|
444
|
+
// Prepare Old Value (raw FK value)
|
|
445
|
+
const { sqlExpr, bindings } = _prepareValueExpression(col, reference);
|
|
446
|
+
// Prepare Old Label (lookup value if col.alt exists)
|
|
447
|
+
const labelRes = _prepareLabelExpression(col, reference, model);
|
|
448
|
+
|
|
449
|
+
// SQL Statement - include PARENT_ID if composition parent info exists
|
|
450
|
+
const insertSQL = compositionParentInfo
|
|
451
|
+
? `INSERT INTO sap_changelog_Changes
|
|
452
|
+
(ID, PARENT_ID, ATTRIBUTE, VALUECHANGEDFROM, VALUECHANGEDTO, VALUECHANGEDFROMLABEL, VALUECHANGEDTOLABEL, ENTITY, ENTITYKEY, OBJECTID, CREATEDAT, CREATEDBY, VALUEDATATYPE, MODIFICATION, TRANSACTIONID)
|
|
453
|
+
VALUES
|
|
454
|
+
(RANDOM_UUID(), ?, '${col.name}', ${sqlExpr}, NULL, ${labelRes.sqlExpr}, NULL, ?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_USER(), '${col.type}', 'delete', TRANSACTION_ID())`
|
|
455
|
+
: `INSERT INTO sap_changelog_Changes
|
|
456
|
+
(ID, ATTRIBUTE, VALUECHANGEDFROM, VALUECHANGEDTO, VALUECHANGEDFROMLABEL, VALUECHANGEDTOLABEL, ENTITY, ENTITYKEY, OBJECTID, CREATEDAT, CREATEDBY, VALUEDATATYPE, MODIFICATION, TRANSACTIONID)
|
|
457
|
+
VALUES
|
|
458
|
+
(RANDOM_UUID(), '${col.name}', ${sqlExpr}, NULL, ${labelRes.sqlExpr}, NULL, ?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_USER(), '${col.type}', 'delete', TRANSACTION_ID())`;
|
|
459
|
+
|
|
460
|
+
const allBindings = compositionParentInfo ? ['parentId', ...bindings, ...labelRes.bindings, 'entityName', 'entityKey', 'objectID'] : [...bindings, ...labelRes.bindings, 'entityName', 'entityKey', 'objectID'];
|
|
461
|
+
|
|
462
|
+
const tryBlock = _wrapInTryCatch(insertSQL, allBindings);
|
|
463
|
+
|
|
464
|
+
// Element skip check variable name
|
|
465
|
+
const elementSkipVar = getElementSkipVarName(entity.name, col.name);
|
|
466
|
+
|
|
467
|
+
// Null Check Wrapper + Element Skip Check
|
|
468
|
+
const valExpression = bindings.map((b) => b).join(' != null && ') + ' != null';
|
|
469
|
+
return `if ((${valExpression}) && !shouldSkipElement(conn, "${elementSkipVar}")) {
|
|
470
|
+
${tryBlock}
|
|
471
|
+
}`;
|
|
472
|
+
})
|
|
473
|
+
.join('\n');
|
|
474
|
+
|
|
475
|
+
return `${modificationTypeSetup}
|
|
476
|
+
${keysCalc}
|
|
477
|
+
${deleteBlock}
|
|
478
|
+
${parentIdSetup}
|
|
479
|
+
${columnBlocks}`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function _generateDeleteBodyPreserve(entity, columns, objectIDs, rootEntity, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
|
|
483
|
+
const reference = 'oldRow';
|
|
484
|
+
|
|
485
|
+
// Set modification type for grandparent entry creation (must be before keysCalc which uses it)
|
|
486
|
+
const modificationTypeSetup = grandParentCompositionInfo ? 'String modificationType = "delete";' : '';
|
|
487
|
+
const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, model, compositionParentInfo, grandParentCompositionInfo);
|
|
488
|
+
|
|
489
|
+
// Composition parent handling - with deep linking support when grandParentCompositionInfo exists
|
|
490
|
+
let parentIdSetup = '';
|
|
491
|
+
if (compositionParentInfo) {
|
|
492
|
+
if (grandParentCompositionInfo) {
|
|
493
|
+
parentIdSetup = `String parentId = ensureCompositionParentEntry(conn, parentEntityKey, parentObjectID, "delete", parentChangelogId);`;
|
|
494
|
+
} else {
|
|
495
|
+
parentIdSetup = `String parentId = ensureCompositionParentEntry(conn, parentEntityKey, parentObjectID, "delete");`;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Handle composition-only triggers (no tracked columns)
|
|
500
|
+
if (columns.length === 0 && compositionParentInfo) {
|
|
501
|
+
return `${modificationTypeSetup}\n${keysCalc}\n${parentIdSetup}`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const columnBlocks = columns
|
|
505
|
+
.map((col) => {
|
|
506
|
+
// Prepare Old Value (raw FK value)
|
|
507
|
+
const { sqlExpr, bindings } = _prepareValueExpression(col, reference);
|
|
508
|
+
// Prepare Old Label (lookup value if col.alt exists)
|
|
509
|
+
const labelRes = _prepareLabelExpression(col, reference, model);
|
|
510
|
+
|
|
511
|
+
// SQL Statement - include PARENT_ID if composition parent info exists
|
|
512
|
+
const insertSQL = compositionParentInfo
|
|
513
|
+
? `INSERT INTO sap_changelog_Changes
|
|
514
|
+
(ID, PARENT_ID, ATTRIBUTE, VALUECHANGEDFROM, VALUECHANGEDTO, VALUECHANGEDFROMLABEL, VALUECHANGEDTOLABEL, ENTITY, ENTITYKEY, OBJECTID, CREATEDAT, CREATEDBY, VALUEDATATYPE, MODIFICATION, TRANSACTIONID)
|
|
515
|
+
VALUES
|
|
516
|
+
(RANDOM_UUID(), ?, '${col.name}', ${sqlExpr}, NULL, ${labelRes.sqlExpr}, NULL, ?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_USER(), '${col.type}', 'delete', TRANSACTION_ID())`
|
|
517
|
+
: `INSERT INTO sap_changelog_Changes
|
|
518
|
+
(ID, ATTRIBUTE, VALUECHANGEDFROM, VALUECHANGEDTO, VALUECHANGEDFROMLABEL, VALUECHANGEDTOLABEL, ENTITY, ENTITYKEY, OBJECTID, CREATEDAT, CREATEDBY, VALUEDATATYPE, MODIFICATION, TRANSACTIONID)
|
|
519
|
+
VALUES
|
|
520
|
+
(RANDOM_UUID(), '${col.name}', ${sqlExpr}, NULL, ${labelRes.sqlExpr}, NULL, ?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_USER(), '${col.type}', 'delete', TRANSACTION_ID())`;
|
|
521
|
+
|
|
522
|
+
const allBindings = compositionParentInfo ? ['parentId', ...bindings, ...labelRes.bindings, 'entityName', 'entityKey', 'objectID'] : [...bindings, ...labelRes.bindings, 'entityName', 'entityKey', 'objectID'];
|
|
523
|
+
|
|
524
|
+
const tryBlock = _wrapInTryCatch(insertSQL, allBindings);
|
|
525
|
+
|
|
526
|
+
// Element skip check variable name
|
|
527
|
+
const elementSkipVar = getElementSkipVarName(entity.name, col.name);
|
|
528
|
+
|
|
529
|
+
// Null Check Wrapper + Element Skip Check
|
|
530
|
+
const valExpression = bindings.map((b) => b).join(' != null && ') + ' != null';
|
|
531
|
+
return `if ((${valExpression}) && !shouldSkipElement(conn, "${elementSkipVar}")) {
|
|
532
|
+
${tryBlock}
|
|
533
|
+
}`;
|
|
534
|
+
})
|
|
535
|
+
.join('\n');
|
|
536
|
+
|
|
537
|
+
return `${modificationTypeSetup}\n${keysCalc}\n${parentIdSetup}\n${columnBlocks}`;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function _generateKeyCalculationJava(entity, rootEntity, ref, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
|
|
541
|
+
// extract keys for entity (entity.keys is undefined)
|
|
542
|
+
let keys = utils.extractKeys(entity.keys);
|
|
543
|
+
const entityKeyExp = keys.map((k) => `${ref}.getString("${k}")`).join(' + "||" + ');
|
|
544
|
+
|
|
545
|
+
const objectIDs = utils.getObjectIDs(entity, model);
|
|
546
|
+
const objectIDBlock = _generateObjectIDCalculation(objectIDs, entity, ref, model);
|
|
547
|
+
|
|
548
|
+
// Add parent key calculation for composition parent linking
|
|
549
|
+
let parentKeyBlock = '';
|
|
550
|
+
let parentObjectIDBlock = '';
|
|
551
|
+
let parentChangelogLookupBlock = '';
|
|
552
|
+
if (compositionParentInfo) {
|
|
553
|
+
const { parentKeyBinding } = compositionParentInfo;
|
|
554
|
+
|
|
555
|
+
// Handle composition of one (parent has FK to child - need reverse lookup)
|
|
556
|
+
if (parentKeyBinding.type === 'compositionOfOne') {
|
|
557
|
+
const { compositionName, childKeys } = parentKeyBinding;
|
|
558
|
+
const parentEntity = model.definitions[compositionParentInfo.parentEntityName];
|
|
559
|
+
const parentKeys = utils.extractKeys(parentEntity.keys);
|
|
560
|
+
|
|
561
|
+
// Build FK field names and WHERE clause for reverse lookup
|
|
562
|
+
const parentFKFields = childKeys.map((k) => `${compositionName}_${k}`);
|
|
563
|
+
const whereClause = parentFKFields.map((fk) => `${fk} = ?`).join(' AND ');
|
|
564
|
+
const selectColumns = parentKeys.join(" || '||' || ");
|
|
565
|
+
const selectSQL = `SELECT ${selectColumns} FROM ${utils.transformName(compositionParentInfo.parentEntityName)} WHERE ${whereClause}`;
|
|
566
|
+
const bindings = childKeys.map((ck) => `${ref}.getString("${ck}")`);
|
|
567
|
+
|
|
568
|
+
parentKeyBlock = `String parentEntityKey = null;
|
|
569
|
+
try (PreparedStatement stmtPK = conn.prepareStatement("${selectSQL.replace(/"/g, '\\"')}")) {
|
|
570
|
+
${bindings.map((b, i) => `stmtPK.setString(${i + 1}, ${b});`).join('\n ')}
|
|
571
|
+
try (ResultSet rsPK = stmtPK.executeQuery()) {
|
|
572
|
+
if (rsPK.next()) {
|
|
573
|
+
parentEntityKey = rsPK.getString(1);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}`;
|
|
577
|
+
} else {
|
|
578
|
+
// Standard composition of many: child has FK to parent
|
|
579
|
+
const parentKeyExp = parentKeyBinding.map((k) => `${ref}.getString("${k}")`).join(' + "||" + ');
|
|
580
|
+
parentKeyBlock = `String parentEntityKey = ${parentKeyExp};`;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Compute parent objectID (the display name of the composition parent entity)
|
|
584
|
+
const parentObjectIDCalcBlock = _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, ref, entity, model);
|
|
585
|
+
parentObjectIDBlock = parentObjectIDCalcBlock;
|
|
586
|
+
|
|
587
|
+
if (grandParentCompositionInfo && !parentKeyBinding.type) {
|
|
588
|
+
const { grandParentKeyBinding } = grandParentCompositionInfo;
|
|
589
|
+
const parentEntity = model.definitions[compositionParentInfo.parentEntityName];
|
|
590
|
+
const parentKeys = utils.extractKeys(parentEntity.keys);
|
|
591
|
+
|
|
592
|
+
// Build SQL to look up grandparent key from parent entity
|
|
593
|
+
const grandParentKeyLookupSQL = grandParentKeyBinding.map((k) => k).join(" || '||' || ");
|
|
594
|
+
const parentTableName = utils.transformName(compositionParentInfo.parentEntityName);
|
|
595
|
+
const parentWhereClause = parentKeys.map((pk) => `${pk} = ?`).join(' AND ');
|
|
596
|
+
|
|
597
|
+
parentChangelogLookupBlock = `
|
|
598
|
+
String parentChangelogId = null;
|
|
599
|
+
String grandParentKeySQL = "SELECT ${grandParentKeyLookupSQL} FROM ${parentTableName} WHERE ${parentWhereClause}";
|
|
600
|
+
String grandParentKey = null;
|
|
601
|
+
try (PreparedStatement gpKeyStmt = conn.prepareStatement(grandParentKeySQL)) {
|
|
602
|
+
${parentKeyBinding.map((k, i) => `gpKeyStmt.setString(${i + 1}, ${ref}.getString("${k}"));`).join('\n')}
|
|
603
|
+
try (ResultSet gpKeyRs = gpKeyStmt.executeQuery()) {
|
|
604
|
+
if (gpKeyRs.next()) {
|
|
605
|
+
grandParentKey = gpKeyRs.getString(1);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (grandParentKey != null) {
|
|
610
|
+
parentChangelogId = ensureGrandParentCompositionEntry(conn, grandParentKey, parentObjectID);
|
|
611
|
+
}`;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return `
|
|
616
|
+
String entityName = "${entity.name}";
|
|
617
|
+
String entityKey = ${entityKeyExp};
|
|
618
|
+
${objectIDBlock}
|
|
619
|
+
${parentKeyBlock}
|
|
620
|
+
${parentObjectIDBlock}
|
|
621
|
+
${parentChangelogLookupBlock}
|
|
622
|
+
`;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function _prepareValueExpression(col, rowVar) {
|
|
626
|
+
// REVISIT
|
|
627
|
+
if (col.type === 'cds.Boolean') {
|
|
628
|
+
const val = `${rowVar}.getString("${col.name}")`;
|
|
629
|
+
return {
|
|
630
|
+
sqlExpr: `CASE WHEN ? IN ('1', 'TRUE', 'true') THEN 'true' WHEN ? IN ('0', 'FALSE', 'false') THEN 'false' ELSE NULL END`,
|
|
631
|
+
bindings: [val, val]
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (col.target && col.foreignKeys) {
|
|
636
|
+
if (col.foreignKeys.length === 1) {
|
|
637
|
+
// Single foreign key
|
|
638
|
+
return {
|
|
639
|
+
sqlExpr: '?',
|
|
640
|
+
bindings: [`${rowVar}.getString("${col.name}_${col.foreignKeys[0]}")`]
|
|
641
|
+
};
|
|
642
|
+
} else {
|
|
643
|
+
// Composite key handling (concatenation)
|
|
644
|
+
const expr = col.foreignKeys.map(() => '?').join(" || ' ' || ");
|
|
645
|
+
const binds = col.foreignKeys.map((fk) => `${rowVar}.getString("${col.name}_${fk}")`);
|
|
646
|
+
return { sqlExpr: expr, bindings: binds };
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (col.target && col.on) {
|
|
651
|
+
if (col.on.length === 1) {
|
|
652
|
+
return {
|
|
653
|
+
sqlExpr: '?',
|
|
654
|
+
bindings: [`${rowVar}.getString("${col.on[0].foreignKeyField}")`]
|
|
655
|
+
};
|
|
656
|
+
} else {
|
|
657
|
+
const expr = col.on.map(() => '?').join(" || ' ' || ");
|
|
658
|
+
const binds = col.on.map((m) => `${rowVar}.getString("${m.foreignKeyField}")`);
|
|
659
|
+
return { sqlExpr: expr, bindings: binds };
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Scalar value - apply truncation for String and LargeString types
|
|
664
|
+
if (col.type === 'cds.String' || col.type === 'cds.LargeString') {
|
|
665
|
+
return {
|
|
666
|
+
sqlExpr: "CASE WHEN LENGTH(?) > 5000 THEN LEFT(?, 4997) || '...' ELSE ? END",
|
|
667
|
+
bindings: [`${rowVar}.getString("${col.name}")`, `${rowVar}.getString("${col.name}")`, `${rowVar}.getString("${col.name}")`]
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
sqlExpr: '?',
|
|
673
|
+
bindings: [`${rowVar}.getString("${col.name}")`]
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Returns label expression for a column
|
|
678
|
+
function _prepareLabelExpression(col, rowVar, model) {
|
|
679
|
+
if (col.target && col.alt) {
|
|
680
|
+
const { sql, bindings } = handleAssocLookup(col, rowVar, model);
|
|
681
|
+
return { sqlExpr: sql, bindings: bindings };
|
|
682
|
+
}
|
|
683
|
+
// No label for scalars or associations without @changelog override
|
|
684
|
+
return { sqlExpr: 'NULL', bindings: [] };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function _wrapInTryCatch(sql, bindings) {
|
|
688
|
+
// Escapes quotes for Java String
|
|
689
|
+
const cleanSql = sql.replace(/"/g, '\\"').replace(/\n/g, ' ');
|
|
690
|
+
|
|
691
|
+
const setParams = bindings.map((b, i) => `stmt.setString(${i + 1}, ${b});`).join('\n ');
|
|
692
|
+
|
|
693
|
+
return `try (PreparedStatement stmt = conn.prepareStatement("${cleanSql}")) {
|
|
694
|
+
${setParams}
|
|
695
|
+
stmt.executeUpdate();
|
|
696
|
+
}`;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function _generateObjectIDCalculation(objectIDs, entity, refRow, model) {
|
|
700
|
+
// fallback to entity name (will be translated via i18nKeys in ChangeView)
|
|
701
|
+
if (!objectIDs || objectIDs.length === 0) {
|
|
702
|
+
return `String objectID = "${entity.name}";`;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Build SQL Query for the ObjectID string
|
|
706
|
+
const parts = [];
|
|
707
|
+
const bindings = [];
|
|
708
|
+
const keys = utils.extractKeys(entity.keys);
|
|
709
|
+
|
|
710
|
+
for (const oid of objectIDs) {
|
|
711
|
+
if (oid.included) {
|
|
712
|
+
parts.push(`SELECT CAST(? AS VARCHAR) AS val`);
|
|
713
|
+
bindings.push(`${refRow}.getString("${oid.name}")`);
|
|
714
|
+
} else {
|
|
715
|
+
// Sub-select needed (Lookup)
|
|
716
|
+
const where = keys.reduce((acc, k) => {
|
|
717
|
+
acc[k] = { ref: ['?'], param: true };
|
|
718
|
+
return acc;
|
|
719
|
+
}, {});
|
|
720
|
+
|
|
721
|
+
const query = SELECT.one.from(entity.name).columns(oid.name).where(where);
|
|
722
|
+
const sql = `(${_toSQL(query, model)})`;
|
|
723
|
+
|
|
724
|
+
parts.push(`SELECT CAST(${sql} AS VARCHAR) AS val`);
|
|
725
|
+
|
|
726
|
+
// Add bindings for the WHERE clause of this sub-select
|
|
727
|
+
keys.forEach((k) => bindings.push(`${refRow}.getString("${k}")`));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Combine parts into one query that returns a single string
|
|
732
|
+
// H2 Syntax: SELECT GROUP_CONCAT(val SEPARATOR ', ') FROM (...)
|
|
733
|
+
const unionSql = parts.join(' UNION ALL ');
|
|
734
|
+
const finalSql = `SELECT GROUP_CONCAT(val SEPARATOR ', ') FROM (${unionSql}) AS tmp`;
|
|
735
|
+
|
|
736
|
+
// Return Java Code Block
|
|
737
|
+
return `
|
|
738
|
+
String objectID = entityKey;
|
|
739
|
+
try (PreparedStatement stmtOID = conn.prepareStatement("${finalSql.replace(/"/g, '\\"')}")) {
|
|
740
|
+
${bindings.map((b, i) => `stmtOID.setString(${i + 1}, ${b});`).join('\n ')}
|
|
741
|
+
|
|
742
|
+
try (ResultSet rsOID = stmtOID.executeQuery()) {
|
|
743
|
+
if (rsOID.next()) {
|
|
744
|
+
String res = rsOID.getString(1);
|
|
745
|
+
if (res != null) objectID = res;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}`;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Generates Java code to compute the composition parent's objectID (display name).
|
|
753
|
+
* This is used when a child entity has a tracked composition parent — the parent's
|
|
754
|
+
* changelog entry needs a meaningful objectID rather than just the key.
|
|
755
|
+
*/
|
|
756
|
+
function _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, refRow, childEntity, model) {
|
|
757
|
+
if (!rootObjectIDs || rootObjectIDs.length === 0) {
|
|
758
|
+
const rootEntityName = rootEntity ? rootEntity.name : '';
|
|
759
|
+
return `String parentObjectID = "${rootEntityName}";`;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Build SQL Query for the parent's ObjectID string
|
|
763
|
+
const parts = [];
|
|
764
|
+
const bindings = [];
|
|
765
|
+
|
|
766
|
+
// Check for composition of one scenario
|
|
767
|
+
const binding = childEntity ? utils.getRootBinding(childEntity, rootEntity) : null;
|
|
768
|
+
const isCompositionOfOne = binding && binding.type === 'compositionOfOne';
|
|
769
|
+
|
|
770
|
+
for (const oid of rootObjectIDs) {
|
|
771
|
+
if (oid.included && !isCompositionOfOne) {
|
|
772
|
+
parts.push(`SELECT CAST(? AS VARCHAR) AS val`);
|
|
773
|
+
bindings.push(`${refRow}.getString("${oid.name}")`);
|
|
774
|
+
} else {
|
|
775
|
+
// Sub-select needed (Lookup)
|
|
776
|
+
let where;
|
|
777
|
+
if (isCompositionOfOne) {
|
|
778
|
+
// For composition of one, use the backlink pattern
|
|
779
|
+
where = binding.childKeys.reduce((acc, ck) => {
|
|
780
|
+
acc[`${binding.compositionName}_${ck}`] = { ref: ['?'], param: true };
|
|
781
|
+
return acc;
|
|
782
|
+
}, {});
|
|
783
|
+
} else {
|
|
784
|
+
const rootKeys = utils.extractKeys(rootEntity.keys);
|
|
785
|
+
where = rootKeys.reduce((acc, k) => {
|
|
786
|
+
acc[k] = { ref: ['?'], param: true };
|
|
787
|
+
return acc;
|
|
788
|
+
}, {});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const targetEntity = isCompositionOfOne ? binding.rootEntityName : rootEntity.name;
|
|
792
|
+
const query = SELECT.one.from(targetEntity).columns(oid.name).where(where);
|
|
793
|
+
const sql = `(${_toSQL(query, model)})`;
|
|
794
|
+
|
|
795
|
+
parts.push(`SELECT CAST(${sql} AS VARCHAR) AS val`);
|
|
796
|
+
|
|
797
|
+
// Add bindings for the WHERE clause of this sub-select
|
|
798
|
+
if (isCompositionOfOne) {
|
|
799
|
+
binding.childKeys.forEach((ck) => bindings.push(`${refRow}.getString("${ck}")`));
|
|
800
|
+
} else {
|
|
801
|
+
const rootKeys = utils.extractKeys(rootEntity.keys);
|
|
802
|
+
rootKeys.forEach((k) => bindings.push(`${refRow}.getString("${k}")`));
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Combine parts into one query that returns a single string
|
|
808
|
+
// H2 Syntax: SELECT GROUP_CONCAT(val SEPARATOR ', ') FROM (...)
|
|
809
|
+
const unionSql = parts.join(' UNION ALL ');
|
|
810
|
+
const finalSql = `SELECT GROUP_CONCAT(val SEPARATOR ', ') FROM (${unionSql}) AS tmp`;
|
|
811
|
+
|
|
812
|
+
// Return Java Code Block
|
|
813
|
+
return `
|
|
814
|
+
String parentObjectID = entityKey;
|
|
815
|
+
try (PreparedStatement stmtPOID = conn.prepareStatement("${finalSql.replace(/"/g, '\\"')}")) {
|
|
816
|
+
${bindings.map((b, i) => `stmtPOID.setString(${i + 1}, ${b});`).join('\n ')}
|
|
817
|
+
|
|
818
|
+
try (ResultSet rsPOID = stmtPOID.executeQuery()) {
|
|
819
|
+
if (rsPOID.next()) {
|
|
820
|
+
String res = rsPOID.getString(1);
|
|
821
|
+
if (res != null) parentObjectID = res;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}`;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
module.exports = {
|
|
828
|
+
_generateJavaMethod,
|
|
829
|
+
_generateCreateBody,
|
|
830
|
+
_generateUpdateBody,
|
|
831
|
+
_generateDeleteBody,
|
|
832
|
+
_generateDeleteBodyPreserve
|
|
833
|
+
};
|