@cap-js-community/common 0.2.5 → 0.2.6

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 CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## Version 0.2.6 - 2025-08-04
9
+
10
+ ### Fixed
11
+
12
+ - Migration Check `ReleasedElementCompatibleTypeChangeIsNotWhitelisted` to allow compatible type changes
13
+ - Admin tracking writes an admin changes file to keep track of incompatible changes as well
14
+
8
15
  ## Version 0.2.5 - 2025-07-07
9
16
 
10
17
  ### Fixed
package/README.md CHANGED
@@ -65,6 +65,7 @@ Options can be passed to replication cache via CDS environment via `cds.replicat
65
65
  - `check: Number`: Interval to check size and prune. Default is `60000` (1 minute)
66
66
  - `stats: Number`: Interval to log statistics. Default is `300000` (5 minutes)
67
67
  - `size: Number`: Maximal cache size in bytes. Default is `10485760` (10 MB) and in production: `104857600` (100 MB)
68
+ - `pipe: Boolean`: Replication is streamed through pipeline. `chunks` is not used. Default is `true`
68
69
  - `chunks: Number`: Replication chunk size. Default is `1000`
69
70
  - `retries: Number`: Replication retries for failed replications. Default is `3`
70
71
  - `auto: Boolean`: Replication is managed automatically. Default is `true`
@@ -96,31 +97,91 @@ Replication cache is inactive per default for tests (`test` profile). It can be
96
97
 
97
98
  ## Migration Check
98
99
 
100
+ The migration check allows to check for incompatible changes in the CDS model and
101
+ to maintain a whitelist for compatible changes via `cdsmc` command line tool.
102
+
99
103
  ### Options
100
104
 
101
105
  Options can be passed to migration check via CDS environment via `cds.migrationCheck` section:
102
106
 
103
107
  - `baseDir: String`: Specifies the base directory for migration check. Default is `"migration-check"`
104
- - `whitelist: Boolean`: Requires to maintain a whitelist for compatible changes. Default is `true`
105
- - `checkMtx: Boolean`: Includes CDS MTXS persistence into check. Default is `true`
108
+ - `whitelist: Boolean`: Requires maintaining a whitelist for compatible changes. Default is `true`
109
+ - `checkMtx: Boolean`: Includes CDS MTXS persistence in check. Default is `true`
106
110
  - `keep: Boolean`: Keeps whitelist after update, otherwise whitelist is cleared. Default is `false`
107
111
  - `freeze: Boolean`: Freeze the persistence. Event compatible changes are not allowed, Default is `false`
108
112
  - `label: String`: Label to describe the updated hash files in addition to the timestamp. Default is `""`
109
- - `buildPath: String`: Path to the build CSN. If not specified it derived from CAP project type. Default is `null`
113
+ - `buildPath: String`: Path to the build CSN. If not specified, it is derived from the CAP project type. Default is `null`
110
114
  - `adminHash: String`: Specify admin hash to acknowledge incompatible changes. Default is `null`
115
+ - `adminTracking: Boolean`: Track changes acknowledged by admin in an admin changes file. Default is `true`
111
116
 
112
117
  ### Usage
113
118
 
114
- #### Basic Flow
119
+ #### Build Production CSN
120
+
121
+ Production CSN is built for first time when not existing (otherwise it is updated):
122
+
123
+ - Build CSN: `cds build --production`
124
+ - Update Production CSN: `cdsmc -u`
125
+
126
+ > Production CSN MUST be added to version control.
127
+
128
+ #### Migration Check
129
+
130
+ Migration check is used to check for incompatible changes in a repetitive way along development:
131
+
132
+ - Build CSN: `cds build --production`
133
+ - Check Changes: `cdsmc`
134
+
135
+ Incompatible changes are detected and reported as error.
136
+ Compatible changes need to be whitelisted (can be disabled via options).
137
+
138
+ ##### Checks
139
+
140
+ **Incompatible Changes:**
141
+
142
+ - A released entity cannot be removed (`ReleasedEntityCannotBeRemoved`)
143
+ - The draft enablement state of a released entity cannot be changed (`ReleasedEntityDraftEnablementCannotBeChanged`)
144
+ - A released element cannot be removed (`ReleasedElementCannotBeRemoved`)
145
+ - The key of a released element cannot be changed (`ReleasedElementKeyCannotBeChanged`)
146
+ - The managed/unmanaged state of a released element cannot be changed (`ReleasedElementManagedUnmanagedCannotBeChanged`)
147
+ - The virtual state of a released element cannot be changed (`ReleasedElementVirtualCannotBeChanged`)
148
+ - The localization state of a released element cannot be changed (`ReleasedElementLocalizationCannotBeChanged`)
149
+ - A released element cannot be changed to not-nullable (`ReleasedElementNullableCannotBeChanged`)
150
+ - The data type of a released element cannot be changed (`ReleasedElementTypeCannotBeChanged`)
151
+ - The data type of a released element cannot be shortened (`ReleasedElementTypeCannotBeShortened`)
152
+ - The scale or precision of a released element cannot be reduced (`ReleasedElementScalePrecisionCannotBeLower`)
153
+ - The target of a released element cannot be changed (`ReleasedElementTargetCannotBeChanged`)
154
+ - The cardinality of a released element cannot be changed (`ReleasedElementCardinalityCannotBeChanged`)
155
+ - The ON condition of a released element cannot be changed (`ReleasedElementOnConditionCannotBeChanged`)
156
+ - The keys condition of a released element cannot be changed (`ReleasedElementKeysConditionCannotBeChanged`)
157
+ - Enabling journal mode and changing entity in same cycle is not allowed (`ReleasedEntityJournalModeAndEntityChangeIsNotAllowed`)
158
+ - Changes to the index of a released entity are not allowed (`ReleasedEntityIndexChangeIsNotAllowed`)
159
+
160
+ **Compatible Changes:**
161
+
162
+ - Changes to the index of a released entity must be whitelisted (`ReleasedEntityIndexChangeIsNotWhitelisted`)
163
+ - Extending the type of a released element requires whitelisting (`ReleasedElementTypeExtensionIsNotWhitelisted`)
164
+ - Extending the scale or precision of a released element requires whitelisting (`ReleasedElementScalePrecisionExtensionIsNotWhitelisted`)
165
+ - Changing the type of a released element to a compatible type requires whitelisting (`ReleasedElementTypeChangeIsNotWhitelisted`)
166
+ - The new entity is not whitelisted (`NewEntityIsNotWhitelisted`)
167
+ - The new entity element is not whitelisted (`NewEntityElementIsNotWhitelisted`)
168
+ - A new entity element must have a default value if it is not nullable (`NewEntityElementNotNullableDefault`)
169
+ - The new entity index is not whitelisted (`NewEntityIndexIsNotWhitelisted`)
170
+
171
+ #### Update Production CSN
172
+
173
+ The Production CSN can be updated when no migration check errors occur:
115
174
 
116
175
  - Build CSN: `cds build --production`
117
- - Check migrations: `cdsmc`
118
176
  - Update Production CSN: `cdsmc -u`
119
177
 
178
+ > Production CSN MUST be added to version control.
179
+
120
180
  ### Whitelisting
121
181
 
122
- - Maintain the whitelist extension file `migration-extension-whitelist.json` for compatible changes:
123
- - **Whitelist Entity**:
182
+ Maintain the whitelist extension file `migration-extension-whitelist.json` for compatible changes:
183
+
184
+ - **Whitelist Entity**:
124
185
 
125
186
  ```json
126
187
  {
@@ -130,7 +191,7 @@ Options can be passed to migration check via CDS environment via `cds.migrationC
130
191
  }
131
192
  ```
132
193
 
133
- - **Whitelist Entity Element**:
194
+ - **Whitelist Entity Element**:
134
195
 
135
196
  ```json
136
197
  {
@@ -146,18 +207,35 @@ Options can be passed to migration check via CDS environment via `cds.migrationC
146
207
 
147
208
  ### Admin Mode
148
209
 
149
- - Get Admin Hash: `cdsmc -a`
150
- - (Un-)Freeze Persistence (based on options): `cdsmc -u -a`
210
+ #### Incompatible Changes
211
+
212
+ Accepted incompatible changes can be acknowledged and will not be reported as error anymore:
213
+
214
+ - Get current admin hash for incompatible changes: `cdsmc -a`
215
+ - Set admin hash in env: `cds.migrationCheck.adminHash`
216
+
217
+ #### Freeze Persistence
218
+
219
+ CDS persistence can be (temporarily) frozen to prevent any changes (also compatible) to the persistence model:
220
+
221
+ - Activate/Deactivate persistence freeze in env `cds.migrationCheck.freeze`
222
+ - Freeze/Unfreeze Persistence: `cdsmc -u -a`
223
+ - File `./csn-prod.freeze` is created to indicate that persistence is frozen
151
224
 
152
225
  ### Pipeline
153
226
 
227
+ Migration check can be used in a pipeline (e.g. part of Pull Request voter)
228
+ to ensure that incompatible changes are not introduced:
229
+
154
230
  - Build & Check: `cds build --production && cdsmc`
155
- - Update Production CSN: `cdsmc -u`
231
+ - Update Production CSN: `cds build --production && cdsmc -u`
156
232
 
157
- > Production CSN MUST be added to version control
233
+ > Production CSN MUST be added to version control.
158
234
 
159
235
  ## Rate Limiting
160
236
 
237
+ The rate limiting allows to limit the number of requests per service and tenant.
238
+
161
239
  ### Usage
162
240
 
163
241
  ```cds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/common",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "CAP Node.js Community Common",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "engines": {
@@ -53,17 +53,17 @@
53
53
  "devDependencies": {
54
54
  "@cap-js-community/common": "./",
55
55
  "@cap-js/cds-test": "^0.4.0",
56
- "@sap/cds": "^9.1.0",
56
+ "@sap/cds": "^9.2.0",
57
57
  "@sap/cds-common-content": "^3.0.1",
58
- "@sap/cds-dk": "^9.1.0",
59
- "eslint": "9.30.1",
60
- "eslint-config-prettier": "10.1.5",
61
- "eslint-plugin-jest": "29.0.1",
62
- "eslint-plugin-n": "^17.21.0",
63
- "jest": "30.0.4",
64
- "jest-html-reporters": "3.1.7",
65
- "jest-junit": "16.0.0",
66
- "prettier": "3.6.2",
58
+ "@sap/cds-dk": "^9.2.0",
59
+ "eslint": "^9.32.0",
60
+ "eslint-config-prettier": "^10.1.8",
61
+ "eslint-plugin-jest": "^29.0.1",
62
+ "eslint-plugin-n": "^17.21.3",
63
+ "jest": "^30.0.5",
64
+ "jest-html-reporters": "^3.1.7",
65
+ "jest-junit": "^16.0.0",
66
+ "prettier": "^3.6.2",
67
67
  "shelljs": "^0.10.0"
68
68
  },
69
69
  "cds": {
@@ -82,7 +82,8 @@
82
82
  "freeze": false,
83
83
  "label": null,
84
84
  "buildPath": null,
85
- "adminHash": null
85
+ "adminHash": null,
86
+ "adminTracking": true
86
87
  },
87
88
  "rateLimiting": {
88
89
  "plugin": true,
@@ -115,6 +116,7 @@
115
116
  "[production]": {
116
117
  "size": 104857600
117
118
  },
119
+ "pipe": true,
118
120
  "chunks": 1000,
119
121
  "retries": 3,
120
122
  "auto": true,
@@ -32,6 +32,8 @@ const Messages = {
32
32
  ReleasedElementTypeExtensionIsNotWhitelisted: "Extending the type of a released element requires whitelisting",
33
33
  ReleasedElementScalePrecisionExtensionIsNotWhitelisted:
34
34
  "Extending the scale or precision of a released element requires whitelisting",
35
+ ReleasedElementCompatibleTypeChangeIsNotWhitelisted:
36
+ "Changing the type of a released element to a compatible type requires whitelisting",
35
37
 
36
38
  NewEntityIsNotWhitelisted: "The new entity is not whitelisted",
37
39
  NewEntityElementIsNotWhitelisted: "The new entity element is not whitelisted",
@@ -60,6 +62,7 @@ class MigrationCheck {
60
62
  prodHashPath: path.join(basePath, "./csn-prod-hash.json"),
61
63
  prodWhitelistPath: path.join(basePath, "./migration-extension-whitelist.json"),
62
64
  prodWhitelistHashPath: path.join(basePath, "./migration-extension-whitelist-hash.json"),
65
+ prodAdminChangesPath: path.join(basePath, "./migration-admin-changes.json"),
63
66
  prodFreeze: path.join(basePath, "./csn-prod.freeze"),
64
67
  };
65
68
  this.setup();
@@ -207,6 +210,9 @@ class MigrationCheck {
207
210
  for (const message of result.messages) {
208
211
  message.severity = message.severity === "error" ? "warning" : message.severity;
209
212
  }
213
+ if (this.options.adminTracking) {
214
+ fs.writeFileSync(this.paths.prodAdminChangesPath, JSON.stringify(messages, null, 2) + "\n");
215
+ }
210
216
  messages.push({
211
217
  code: "AcceptedByAdmin",
212
218
  text: "Migration check errors accepted by admin",
@@ -214,6 +220,9 @@ class MigrationCheck {
214
220
  });
215
221
  result.success = true;
216
222
  } else {
223
+ if (this.options.adminTracking) {
224
+ fs.writeFileSync(this.paths.prodAdminChangesPath, JSON.stringify(messages, null, 2) + "\n");
225
+ }
217
226
  messages.push({
218
227
  code: "AdminHashInvalid",
219
228
  text: "Admin hash is not valid for current migration check state",
@@ -225,6 +234,9 @@ class MigrationCheck {
225
234
  result.success = false;
226
235
  }
227
236
  }
237
+ if (!this.options.adminHash && fs.existsSync(this.paths.prodAdminChangesPath)) {
238
+ fs.rmSync(this.paths.prodAdminChangesPath);
239
+ }
228
240
  return result;
229
241
  }
230
242
 
@@ -366,7 +378,20 @@ function releasedEntityCheck(csnBuild, csnProd, whitelist, options) {
366
378
  } else if (!elementProd.notNull && elementBuild.notNull) {
367
379
  report(messages, MessagesCodes.ReleasedElementNullableCannotBeChanged, definitionProd.name, elementProdName);
368
380
  } else if (normalizeType(csnProd, elementProd.type) !== normalizeType(csnBuild, elementBuild.type)) {
369
- report(messages, MessagesCodes.ReleasedElementTypeCannotBeChanged, definitionProd.name, elementProdName);
381
+ const prodType = normalizeType(csnProd, elementProd.type);
382
+ const buildType = normalizeType(csnBuild, elementBuild.type);
383
+ if (prodType === "cds.String" && buildType === "cds.LargeString") {
384
+ if (!elementWhitelist && options.whitelist) {
385
+ report(
386
+ messages,
387
+ MessagesCodes.ReleasedElementCompatibleTypeChangeIsNotWhitelisted,
388
+ definitionProd.name,
389
+ elementProdName,
390
+ );
391
+ }
392
+ } else {
393
+ report(messages, MessagesCodes.ReleasedElementTypeCannotBeChanged, definitionProd.name, elementProdName);
394
+ }
370
395
  } else if ((elementProd.length || STRING_DEFAULT_LENGTH) > (elementBuild.length || STRING_DEFAULT_LENGTH)) {
371
396
  report(messages, MessagesCodes.ReleasedElementTypeCannotBeShortened, definitionProd.name, elementProdName);
372
397
  } else if ((elementProd.length || STRING_DEFAULT_LENGTH) < (elementBuild.length || STRING_DEFAULT_LENGTH)) {
@@ -492,9 +492,13 @@ class ReplicationCache {
492
492
  return result;
493
493
  })(),
494
494
  ]);
495
- const percent = ((timeService - timeCache) / timeService) * 100;
496
- this.log.info("Replication cache measurement", Math.round(percent), timeCache, timeService);
497
- this.stats.measureTotal += percent;
495
+ const savedPercent = ((timeService - timeCache) / timeService) * 100;
496
+ this.log.info("Replication cache measurement", {
497
+ timeCache,
498
+ timeService,
499
+ savedPercent: Math.round(savedPercent),
500
+ });
501
+ this.stats.measureTotal += savedPercent;
498
502
  this.stats.measureCount += 1;
499
503
  this.stats.measureRatio = Math.round(this.stats.measureTotal / this.stats.measureCount);
500
504
  return cacheResult;
@@ -696,14 +700,14 @@ class ReplicationCacheEntry {
696
700
  if (thread && cds.context && this.service instanceof SQLiteService) {
697
701
  const srcTx = this.service.tx(cds.context);
698
702
  await this.db.tx({ tenant: this.tenant.id }, async (destTx) => {
699
- await this.loadChunked(srcTx, destTx);
703
+ await this.loadRecords(srcTx, destTx);
700
704
  await this.checkRecords(srcTx, destTx);
701
705
  await this.calcSize(destTx);
702
706
  });
703
707
  } else {
704
708
  await this.service.tx({ tenant: this.tenant.id }, async (srcTx) => {
705
709
  await this.db.tx({ tenant: this.tenant.id }, async (destTx) => {
706
- await this.loadChunked(srcTx, destTx);
710
+ await this.loadRecords(srcTx, destTx);
707
711
  await this.checkRecords(srcTx, destTx);
708
712
  await this.calcSize(destTx);
709
713
  });
@@ -712,6 +716,24 @@ class ReplicationCacheEntry {
712
716
  this.timestamp = Date.now();
713
717
  }
714
718
 
719
+ async loadRecords(srcTx, destTx) {
720
+ if (this.cache.options.pipe) {
721
+ await this.loadPiped(srcTx, destTx);
722
+ } else {
723
+ await this.loadChunked(srcTx, destTx);
724
+ }
725
+ }
726
+
727
+ async loadPiped(srcTx, destTx) {
728
+ const keys = Object.keys(this.definition.keys);
729
+ const selectQuery = SELECT.from(this.definition).orderBy(keys);
730
+ selectQuery.replication = true;
731
+ selectQuery.bind(srcTx);
732
+ const insertQuery = INSERT.into(this.definition);
733
+ insertQuery.bind(destTx);
734
+ await selectQuery.pipeline(insertQuery);
735
+ }
736
+
715
737
  async loadChunked(srcTx, destTx) {
716
738
  const keys = Object.keys(this.definition.keys);
717
739
  const selectQuery = SELECT.from(this.definition).orderBy(keys);