@cap-js-community/common 0.2.4 → 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 +13 -0
- package/README.md +90 -12
- package/package.json +14 -12
- package/src/migration-check/MigrationCheck.js +28 -2
- package/src/replication-cache/ReplicationCache.js +27 -5
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ 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
|
+
|
|
15
|
+
## Version 0.2.5 - 2025-07-07
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Normalize newline character in hash calculation
|
|
20
|
+
|
|
8
21
|
## Version 0.2.4 - 2025-07-03
|
|
9
22
|
|
|
10
23
|
### 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
|
|
105
|
-
- `checkMtx: Boolean`: Includes CDS MTXS persistence
|
|
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
|
-
####
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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.
|
|
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.
|
|
56
|
+
"@sap/cds": "^9.2.0",
|
|
57
57
|
"@sap/cds-common-content": "^3.0.1",
|
|
58
|
-
"@sap/cds-dk": "^9.
|
|
59
|
-
"eslint": "9.
|
|
60
|
-
"eslint-config-prettier": "10.1.
|
|
61
|
-
"eslint-plugin-jest": "29.0.1",
|
|
62
|
-
"eslint-plugin-n": "^17.
|
|
63
|
-
"jest": "30.0.
|
|
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
|
-
|
|
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)) {
|
|
@@ -652,6 +677,7 @@ function normalizeType(csn, type) {
|
|
|
652
677
|
return typeof type === "object" ? JSON.stringify(type) : type;
|
|
653
678
|
}
|
|
654
679
|
|
|
655
|
-
const hash = (buffer, algorithm = "sha256") =>
|
|
680
|
+
const hash = (buffer, algorithm = "sha256") =>
|
|
681
|
+
crypto.createHash(algorithm).update(buffer.toString().replace(/\r\n/g, "\n")).digest("hex");
|
|
656
682
|
|
|
657
683
|
module.exports = MigrationCheck;
|
|
@@ -492,9 +492,13 @@ class ReplicationCache {
|
|
|
492
492
|
return result;
|
|
493
493
|
})(),
|
|
494
494
|
]);
|
|
495
|
-
const
|
|
496
|
-
this.log.info("Replication cache measurement",
|
|
497
|
-
|
|
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.
|
|
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.
|
|
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);
|