@sap/cds-dk 10.1.0 → 10.1.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 +24 -0
- package/bin/bind.js +4 -0
- package/bin/build.js +1 -0
- package/bin/compile/index.js +12 -11
- package/bin/completion/index.js +21 -10
- package/bin/completion/scripts/cds.fish +179 -127
- package/bin/completion/scripts/cds.ps1 +88 -14
- package/bin/completion/scripts/cds.sh +71 -5
- package/bin/completion/scripts/cds.zsh +81 -15
- package/bin/completion/templates/cds-tmpl.ps1 +15 -6
- package/bin/deploy/index.js +7 -3
- package/bin/env.js +4 -1
- package/bin/export.js +9 -1
- package/bin/import.js +4 -1
- package/bin/init.js +1 -0
- package/bin/inspect.js +3 -1
- package/bin/lint.js +5 -3
- package/bin/minify.js +6 -1
- package/bin/repl.js +11 -2
- package/bin/run.js +12 -1
- package/bin/serve.js +13 -2
- package/bin/up.js +4 -1
- package/bin/util/containerize.js +2 -0
- package/bin/watch/index.js +8 -1
- package/lib/build/plugins/internal.js +0 -26
- package/lib/build/plugins/nodejs/index.js +8 -3
- package/lib/build/util.js +2 -4
- package/lib/cds.js +8 -2
- package/lib/init/constants.js +1 -1
- package/lib/init/merge.js +21 -17
- package/lib/init/template/cloud-logging/index.js +3 -3
- package/lib/init/template/data/as-json.js +3 -0
- package/lib/init/template/destination/index.js +3 -3
- package/lib/init/template/enterprise-messaging/index.js +4 -1
- package/lib/init/template/event-mesh/index.js +3 -0
- package/lib/init/template/multitenancy/index.js +2 -2
- package/lib/init/template/workzone-standard/index.js +1 -1
- package/node_modules/@cap-js/db-service/CHANGELOG.md +30 -0
- package/node_modules/@cap-js/db-service/lib/common/generic-pool.js +4 -7
- package/node_modules/@cap-js/db-service/lib/cqn2pql.js +1 -1
- package/node_modules/@cap-js/db-service/lib/cqn2sql.js +15 -11
- package/node_modules/@cap-js/db-service/lib/cqn4sql.js +125 -21
- package/node_modules/@cap-js/db-service/lib/infer/index.js +27 -16
- package/node_modules/@cap-js/db-service/lib/recursion-guard.js +40 -0
- package/node_modules/@cap-js/db-service/lib/search.js +1 -1
- package/node_modules/@cap-js/db-service/package.json +1 -1
- package/node_modules/@cap-js/sqlite/CHANGELOG.md +28 -0
- package/node_modules/@cap-js/sqlite/lib/SQLiteService.js +110 -14
- package/node_modules/@cap-js/sqlite/package.json +3 -3
- package/node_modules/@sap/cds/CHANGELOG.md +19 -0
- package/node_modules/@sap/cds/common.cds +3 -3
- package/node_modules/@sap/cds/lib/compile/minify.js +3 -1
- package/node_modules/@sap/cds/lib/env/cds-env.js +1 -1
- package/node_modules/@sap/cds/lib/env/cds-requires.js +1 -0
- package/node_modules/@sap/cds/libx/_runtime/common/generic/crud.js +7 -1
- package/node_modules/@sap/cds/libx/_runtime/common/utils/cqn.js +1 -1
- package/node_modules/@sap/cds/libx/_runtime/fiori/lean-draft.js +1 -1
- package/node_modules/@sap/cds/libx/_runtime/messaging/common-utils/normalizeIncomingMessage.js +1 -1
- package/node_modules/@sap/cds/libx/_runtime/messaging/kafka.js +1 -1
- package/node_modules/@sap/cds/libx/http/location.js +10 -11
- package/node_modules/@sap/cds/libx/odata/middleware/read.js +1 -1
- package/node_modules/@sap/cds/libx/queue/SchedulingService.js +4 -0
- package/node_modules/@sap/cds/libx/queue/index.js +44 -10
- package/node_modules/@sap/cds/libx/queue/processing.js +47 -16
- package/node_modules/@sap/cds/libx/queue/utils.js +3 -0
- package/node_modules/@sap/cds/package.json +1 -1
- package/node_modules/@sap/cds/tasks/enterprise-messaging-deploy.js +13 -5
- package/node_modules/@sap/cds-compiler/CHANGELOG.md +15 -0
- package/node_modules/@sap/cds-compiler/lib/compiler/resolve.js +7 -3
- package/node_modules/@sap/cds-compiler/lib/model/csnUtils.js +2 -0
- package/node_modules/@sap/cds-compiler/lib/transform/db/applyTransformations.js +12 -0
- package/node_modules/@sap/cds-compiler/lib/transform/db/associations.js +5 -3
- package/node_modules/@sap/cds-compiler/lib/transform/db/flattening.js +4 -3
- package/node_modules/@sap/cds-compiler/lib/transform/effective/flattening.js +17 -1
- package/node_modules/@sap/cds-compiler/lib/transform/effective/main.js +19 -0
- package/node_modules/@sap/cds-compiler/lib/transform/fioriTreeViews.js +12 -1
- package/node_modules/@sap/cds-compiler/lib/transform/forRelationalDB.js +1 -1
- package/node_modules/@sap/cds-compiler/lib/transform/odata/adaptAnnotationRefs.js +11 -5
- package/node_modules/@sap/cds-compiler/lib/transform/translateAssocsToJoins.js +29 -2
- package/node_modules/@sap/cds-compiler/package.json +1 -1
- package/node_modules/@sap/cds-mtxs/CHANGELOG.md +34 -0
- package/node_modules/@sap/cds-mtxs/bin/cds-mtx-migrate.js +26 -5
- package/node_modules/@sap/cds-mtxs/bin/cds-mtx.js +3 -0
- package/node_modules/@sap/cds-mtxs/lib/utils.js +5 -4
- package/node_modules/@sap/cds-mtxs/package.json +1 -1
- package/node_modules/@sap/cds-mtxs/srv/cf/abstract-provisioning-service.js +62 -7
- package/node_modules/@sap/cds-mtxs/srv/cf/saas-provisioning-service.js +1 -1
- package/node_modules/@sap/cds-mtxs/srv/cf/sms-provisioning-service.js +23 -3
- package/node_modules/@sap/cds-mtxs/srv/extensibility/compat/update.js +15 -0
- package/node_modules/@sap/cds-mtxs/srv/extensibility/linter/annotations.js +2 -2
- package/node_modules/@sap/cds-mtxs/srv/extensibility/linter/config.js +2 -0
- package/node_modules/@sap/cds-mtxs/srv/extensibility/linter/index.js +2 -0
- package/node_modules/@sap/cds-mtxs/srv/extensibility/linter/protected-namespace.js +45 -0
- package/node_modules/@sap/cds-mtxs/srv/extensibility-service.js +8 -3
- package/node_modules/@sap/cds-mtxs/srv/jobs-service.js +49 -10
- package/node_modules/@sap/cds-mtxs/srv/model-provider.js +11 -2
- package/node_modules/@sap/cds-mtxs/srv/plugins/common.js +19 -12
- package/node_modules/@sap/cds-mtxs/srv/plugins/hana/hdi-ctnr-mgr.js +6 -3
- package/node_modules/@sap/cds-mtxs/srv/plugins/hana/serverless.js +271 -0
- package/node_modules/@sap/cds-mtxs/srv/plugins/hana.js +3 -2
- package/node_modules/@sap/cds-mtxs/srv/worker/job-worker-pool.js +212 -0
- package/node_modules/@sap/cds-mtxs/srv/worker/job-worker.js +61 -0
- package/node_modules/proxy-addr/HISTORY.md +60 -54
- package/node_modules/proxy-addr/README.md +1 -1
- package/node_modules/proxy-addr/index.js +32 -0
- package/node_modules/proxy-addr/package.json +8 -4
- package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/package.json +1 -1
- package/package.json +2 -2
- /package/node_modules/@sap/cds-mtxs/srv/worker/{pool.js → model-worker-pool.js} +0 -0
- /package/node_modules/@sap/cds-mtxs/srv/worker/{load.js → model-worker.js} +0 -0
|
@@ -11,7 +11,7 @@ const { isBuiltinType, isMagicVariable } = require('../../base/builtins');
|
|
|
11
11
|
const transformUtils = require('../transformUtils');
|
|
12
12
|
const { csnRefs } = require('../../base/csnRefs');
|
|
13
13
|
const { setProp, forEach } = require('../../utils/objectUtils');
|
|
14
|
-
const { transformExpression } = require('./applyTransformations');
|
|
14
|
+
const { transformExpression, isSimpleAnnoExprRef } = require('./applyTransformations');
|
|
15
15
|
const { cloneCsnNonDict } = require('../../base/cloneCsn');
|
|
16
16
|
const { EdmTypeFacetNames } = require('../../edm/EdmPrimitiveTypeDefinitions');
|
|
17
17
|
const { adaptAnnotationsRefs } = require('../odata/adaptAnnotationRefs');
|
|
@@ -246,7 +246,8 @@ function getStructStepsFlattener( csn, options, messageFunctions, resolved, path
|
|
|
246
246
|
adaptRefs.push((...args) => {
|
|
247
247
|
const refChanged = fn(...args);
|
|
248
248
|
if (refChanged && annotation['='])
|
|
249
|
-
|
|
249
|
+
// For simple refs (all-string steps, no param/cast), = contains the string representation of the ref.
|
|
250
|
+
annotation['='] = isSimpleAnnoExprRef(annotation) ? annotation.ref.join('.') : true;
|
|
250
251
|
});
|
|
251
252
|
}
|
|
252
253
|
else {
|
|
@@ -664,7 +665,7 @@ function handleManagedAssociationsAndCreateForeignKeys( csn, options, messageFun
|
|
|
664
665
|
}
|
|
665
666
|
|
|
666
667
|
if (options.transformation === 'effective') {
|
|
667
|
-
adaptAnnotationsRefs(fks, csnUtils, messageFunctions, eltPath);
|
|
668
|
+
adaptAnnotationsRefs(fks, csnUtils, messageFunctions, eltPath, options);
|
|
668
669
|
const validAnnoNames = Object.keys(element).filter(pn => pn[0] === '@' && findAnnotationExpression(element, pn));
|
|
669
670
|
fks.forEach(fk => copyAnnotations(element, fk[1], false, {}, validAnnoNames));
|
|
670
671
|
}
|
|
@@ -53,6 +53,18 @@ function flattenRefs(csn, options, csnUtils, messageFunctions) {
|
|
|
53
53
|
const refFlattener = getStructStepsFlattener(csn, options, messageFunctions, resolved, '_', adaptRefs);
|
|
54
54
|
|
|
55
55
|
forEachDefinition(csn, (def, defName) => {
|
|
56
|
+
// Unbound actions/functions/events: only flatten annotation expressions, not params.
|
|
57
|
+
// Their params may carry $self on-conditions that cannot be resolved without a query context.
|
|
58
|
+
if (def.kind === 'action' || def.kind === 'function' || def.kind === 'event') {
|
|
59
|
+
forEachMemberRecursively(def, (member, memberName, prop, path) => {
|
|
60
|
+
Object.keys(member).filter(pn => pn.startsWith('@') && member[pn]).forEach((pn) => {
|
|
61
|
+
transformAnnotationExpression(member, pn, [ refFlattener ], path);
|
|
62
|
+
adaptRefs.forEach(fn => fn());
|
|
63
|
+
adaptRefs.length = 0;
|
|
64
|
+
});
|
|
65
|
+
}, [ 'definitions', defName ]);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
56
68
|
applyTransformationsOnNonDictionary(csn.definitions, defName, refFlattener, { processAnnotations: true, skipDict: { actions: 1 } }, [ 'definitions' ]);
|
|
57
69
|
|
|
58
70
|
adaptRefs.forEach(fn => fn());
|
|
@@ -97,8 +109,12 @@ function flattenRefs(csn, options, csnUtils, messageFunctions) {
|
|
|
97
109
|
});
|
|
98
110
|
|
|
99
111
|
cleanup.forEach((obj) => {
|
|
100
|
-
|
|
112
|
+
// strip leading $self from ref array and (if it is a string) from '='
|
|
113
|
+
if (obj.ref && obj.ref[0] === '$self') {
|
|
101
114
|
obj.ref.shift();
|
|
115
|
+
if (typeof obj['='] === 'string' && obj['='].startsWith('$self.'))
|
|
116
|
+
obj['='] = obj['='].slice('$self.'.length);
|
|
117
|
+
}
|
|
102
118
|
});
|
|
103
119
|
}
|
|
104
120
|
|
|
@@ -103,6 +103,25 @@ function effectiveCsn( model, options, messageFunctions ) {
|
|
|
103
103
|
|
|
104
104
|
resolveTypesInActionsAfterFlattening(csnUtils);
|
|
105
105
|
|
|
106
|
+
// Rewrite association keys in unbound action/function params to use flattened names.
|
|
107
|
+
// Not needed for bound actions: their params are reached via the parent entity and handled by flattenFKs in managedToUnmanaged.
|
|
108
|
+
forEachDefinition(csn, (def) => {
|
|
109
|
+
if (def.kind !== 'action' && def.kind !== 'function')
|
|
110
|
+
return;
|
|
111
|
+
if (!def.params)
|
|
112
|
+
return;
|
|
113
|
+
for (const param of Object.values(def.params)) {
|
|
114
|
+
if (!param.elements)
|
|
115
|
+
continue;
|
|
116
|
+
for (const element of Object.values(param.elements)) {
|
|
117
|
+
if (element.keys)
|
|
118
|
+
element.keys.forEach((key) => {
|
|
119
|
+
key.ref = [ key.ref.join('_') ];
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
106
125
|
processCalculatedElementsInEntities(csn, options);
|
|
107
126
|
// invalidate csnRef caches after flattening
|
|
108
127
|
transformerUtils.csnUtils = getUtils(csn, 'init-all');
|
|
@@ -16,7 +16,7 @@ const { isBuiltinType } = require('../base/builtins');
|
|
|
16
16
|
* - the foreign key is of scalar type
|
|
17
17
|
* Otherwise, appropriate warnings/errors are raised.
|
|
18
18
|
*/
|
|
19
|
-
function generateFioriTreeViewAnnotationsAndFields(def, defName, messageFunctions, csnUtils, transformers, options) {
|
|
19
|
+
function generateFioriTreeViewAnnotationsAndFields(def, defName, messageFunctions, csnUtils, transformers, options, csn) {
|
|
20
20
|
const { error, warning } = messageFunctions;
|
|
21
21
|
const { isAssociation } = csnUtils;
|
|
22
22
|
const { setAnnotation, addElement, createScalarElement } = transformers;
|
|
@@ -157,6 +157,17 @@ function generateFioriTreeViewAnnotationsAndFields(def, defName, messageFunction
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
function addHierarchyFields(def, defName) {
|
|
160
|
+
addHierarchyFieldsTo(def, defName);
|
|
161
|
+
|
|
162
|
+
// The localized convenience view is a stand-in for the annotated view at
|
|
163
|
+
// runtime, so it must carry the same hierarchy columns. It is generated
|
|
164
|
+
// before this transform runs, hence it is augmented here explicitly.
|
|
165
|
+
const localizedView = csn?.definitions?.[`localized.${ defName }`];
|
|
166
|
+
if (localizedView)
|
|
167
|
+
addHierarchyFieldsTo(localizedView, `localized.${ defName }`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function addHierarchyFieldsTo(def, defName) {
|
|
160
171
|
// add elements in elements dictionary
|
|
161
172
|
const limitedDescendantCount = createScalarElement('LimitedDescendantCount', 'cds.Integer');
|
|
162
173
|
limitedDescendantCount.LimitedDescendantCount['@Core.Computed'] = true;
|
|
@@ -367,7 +367,7 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
|
|
|
367
367
|
findAndMarkSqlServiceArtifacts(artifact, artifactName);
|
|
368
368
|
if (artifact.$dummyService)
|
|
369
369
|
createServiceDummy(artifact, artifactName, csn, messageFunctions);
|
|
370
|
-
generateFioriTreeViews(artifact, artifactName, messageFunctions, csnUtils, { setAnnotation, addElement, createScalarElement }, options);
|
|
370
|
+
generateFioriTreeViews(artifact, artifactName, messageFunctions, csnUtils, { setAnnotation, addElement, createScalarElement }, options, csn);
|
|
371
371
|
}, transformViews ]);
|
|
372
372
|
|
|
373
373
|
// TODO: Could we maybe merge this with the final applyTransformations?
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { transformAnnotationExpression, implicitAs } = require('../../model/csnUtils');
|
|
3
|
+
const { transformAnnotationExpression, implicitAs, isSimpleAnnoExprRef } = require('../../model/csnUtils');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Used during annotating of foreign keys.
|
|
@@ -13,10 +13,10 @@ const { transformAnnotationExpression, implicitAs } = require('../../model/csnUt
|
|
|
13
13
|
* @param {object} messageFunctions
|
|
14
14
|
* @param {CSN.Path} elementPath
|
|
15
15
|
*/
|
|
16
|
-
function adaptAnnotationsRefs(generatedForeignKeys, csnUtils, { error }, elementPath) {
|
|
16
|
+
function adaptAnnotationsRefs(generatedForeignKeys, csnUtils, { error }, elementPath, options) {
|
|
17
17
|
if (Array.isArray(generatedForeignKeys?.[0])) {
|
|
18
18
|
// ensure we are always called with an array of objects. TODO: Cleanup fk creation in for.effective to create array of objects
|
|
19
|
-
adaptAnnotationsRefs(remapToArrayOfObjects(generatedForeignKeys), csnUtils, { error }, elementPath);
|
|
19
|
+
adaptAnnotationsRefs(remapToArrayOfObjects(generatedForeignKeys), csnUtils, { error }, elementPath, options);
|
|
20
20
|
}
|
|
21
21
|
else {
|
|
22
22
|
const reportedErrorsForAnnoPath = {};
|
|
@@ -49,8 +49,14 @@ function adaptAnnotationsRefs(generatedForeignKeys, csnUtils, { error }, element
|
|
|
49
49
|
if (gfkForRef.length === 1) {
|
|
50
50
|
ref[0] = gfkForRef[0].prefix;
|
|
51
51
|
|
|
52
|
-
if (ctx?.annoExpr?.['='])
|
|
53
|
-
|
|
52
|
+
if (ctx?.annoExpr?.['=']) {
|
|
53
|
+
// TODO: Apply the same fix for OData once the OData behavior is aligned;
|
|
54
|
+
// when done, the `options` parameter can be removed from this function.
|
|
55
|
+
if (options?.transformation === 'effective')
|
|
56
|
+
ctx.annoExpr['='] = isSimpleAnnoExprRef(ctx.annoExpr) ? ctx.annoExpr.ref.join('.') : true;
|
|
57
|
+
else
|
|
58
|
+
ctx.annoExpr['='] = true;
|
|
59
|
+
}
|
|
54
60
|
}
|
|
55
61
|
else {
|
|
56
62
|
// check if the annotation reference points to a structure that has been expanded,
|
|
@@ -612,7 +612,7 @@ function translateAssocsToJoins(model, inputOptions = {}) {
|
|
|
612
612
|
args.push({
|
|
613
613
|
op: { val: '=' },
|
|
614
614
|
args: [ constructPathNode( [ srcAlias, prefixFK(assoc.$elementPrefix, assoc.$flatSrcFKs[i]) ] ),
|
|
615
|
-
constructPathNode( [ tgtAlias, assoc.$flatTgtFKs[i] ] ) ],
|
|
615
|
+
constructPathNode( [ tgtAlias, resolveTgtFKToAlias(assoc.$flatTgtFKs[i], tgtAlias) ] ) ],
|
|
616
616
|
});
|
|
617
617
|
}
|
|
618
618
|
return parenthesise((args.length > 1 ? { op: { val: 'and' }, args } : args[0] ));
|
|
@@ -808,7 +808,19 @@ function translateAssocsToJoins(model, inputOptions = {}) {
|
|
|
808
808
|
const assocSource = assoc._main;
|
|
809
809
|
if (assoc.kind === 'mixin' && (i > 0 || fwdTarget !== assocSource) && fwdOrigin.kind !== 'mixin') {
|
|
810
810
|
const tas = Object.values(env.lead.$tableAliases);
|
|
811
|
-
|
|
811
|
+
// The backlink association's target may be reached through a view
|
|
812
|
+
// over it, whose alias is not the target artifact itself; fall back
|
|
813
|
+
// to the alias whose `_origin` chain reaches the target.
|
|
814
|
+
let j = tas.findIndex(ta => ta._artifact === fwdOrigin.target._artifact);
|
|
815
|
+
if (j < 0) {
|
|
816
|
+
j = tas.findIndex((ta) => {
|
|
817
|
+
for (let a = ta._artifact; a; a = a._origin) {
|
|
818
|
+
if (a === fwdOrigin.target._artifact)
|
|
819
|
+
return true;
|
|
820
|
+
}
|
|
821
|
+
return false;
|
|
822
|
+
});
|
|
823
|
+
}
|
|
812
824
|
if (j >= 0 && tas[j].$QA) {
|
|
813
825
|
newTgtAlias.id = tas[j].$QA.name.id;
|
|
814
826
|
newTgtAlias._artifact = tas[j]._effectiveType;
|
|
@@ -1061,6 +1073,21 @@ function translateAssocsToJoins(model, inputOptions = {}) {
|
|
|
1061
1073
|
const backlink = backlinkAssoc.$backlink;
|
|
1062
1074
|
return backlink && (backlink.foreignKeys || backlink.on) ? backlink : fwdAssoc;
|
|
1063
1075
|
}
|
|
1076
|
+
|
|
1077
|
+
// Resolve the target foreign key to the alias of the target table.
|
|
1078
|
+
// If the target foreign key is not found in the scope of the target alias,
|
|
1079
|
+
// try to find it in the origin chain of the foreign key artifact.
|
|
1080
|
+
function resolveTgtFKToAlias( fk, tgtAlias ) {
|
|
1081
|
+
const env = tgtAlias && (tgtAlias._effectiveType || tgtAlias._artifact);
|
|
1082
|
+
const scope = env && env.elements;
|
|
1083
|
+
if (!scope || !fk || scope[fk.id] || !fk._artifact || !fk._artifact._origin)
|
|
1084
|
+
return fk;
|
|
1085
|
+
for (let o = fk._artifact._origin; o; o = o._origin) {
|
|
1086
|
+
if (o.name && scope[o.name.id])
|
|
1087
|
+
return { id: o.name.id, _artifact: fk._artifact, acc: fk.acc };
|
|
1088
|
+
}
|
|
1089
|
+
return fk;
|
|
1090
|
+
}
|
|
1064
1091
|
} // createOnCondition
|
|
1065
1092
|
} // createJoinQA
|
|
1066
1093
|
|
|
@@ -6,6 +6,39 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
8
8
|
|
|
9
|
+
## Version 4.1.0 - 2026-09-04
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Beta: Multitenant provisioning now supports HANA Cloud serverless, resolving one HDI container per tenant through the serverless Tenant API.
|
|
14
|
+
- Beta: The MTX job service can now use native javascript workers to make the MTX sidecar more responsive while jobs (e. g. tenant upgrade) are running.
|
|
15
|
+
Native workers can be enabled by `nativeWorkers` in the jobs configuration, e. g.
|
|
16
|
+
```jsonc
|
|
17
|
+
"cds.xt.SaasProvisioningService": {
|
|
18
|
+
"jobs": {
|
|
19
|
+
"nativeWorkers": true,
|
|
20
|
+
"workerSize": 3,
|
|
21
|
+
"clusterSize": 3
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
```
|
|
25
|
+
- With configuration `cds.requires['cds.xt.SaasProvisioning'].upgrade.skipAlreadyUpgraded: true` or `cds.requires['cds.xt.SmsProvisioning'].upgrade.skipAlreadyUpgraded: true`,
|
|
26
|
+
already upgraded tenants are skipped when running an upgrade of all tenants (`tenants: ['*']`). Tenants are considered as already upgraded if the last upgrade was run after the
|
|
27
|
+
last deployment of the application.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- `SmsProvisioningService` certificate check is now better aligned with the Java implementation.
|
|
32
|
+
- Extension validation checks have been refined.
|
|
33
|
+
- `POST /-/cds/model-provider/getEdmx` now checks the requested service name more exactly.
|
|
34
|
+
- Callback url for Saas Registry and Subscription Manager is now determined in a more robust way.
|
|
35
|
+
|
|
36
|
+
## Version 4.0.4 - 2026-08-21
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- Version bump
|
|
41
|
+
|
|
9
42
|
## Version 4.0.3 - 2026-08-21
|
|
10
43
|
|
|
11
44
|
### Added
|
|
@@ -20,6 +53,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
|
20
53
|
|
|
21
54
|
- HANA credential health-check now detects the available driver via `require.resolve()` instead of reading `package.json`, fixing `MODULE_NOT_FOUND` for apps that follow the "Plug & Play" guidance and don't declare `hdb` or `@sap/hana-client` directly.
|
|
22
55
|
- Tenant upgrade now reports a clear per-tenant `422` error when a stored extension is incompatible with the changed base model, instead of aborting with an uncaught compilation error.
|
|
56
|
+
- `cds-mtx-migrate` now runs more robust with uncommon model configurations.
|
|
23
57
|
|
|
24
58
|
## Version 4.0.2 - 2026-08-07
|
|
25
59
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/* eslint-disable no-console */
|
|
3
3
|
|
|
4
|
+
const cds = require('@sap/cds')
|
|
5
|
+
const { _import, isfile, local, path } = cds.utils
|
|
6
|
+
|
|
4
7
|
async function run(param, options) {
|
|
5
8
|
const { dry, genTenantList, syncTenantList, cleanup, directory, tag, tagRule } = options
|
|
6
9
|
const skipVerification = options['skip-verification'] || options.skipVerification
|
|
@@ -26,7 +29,6 @@ async function run(param, options) {
|
|
|
26
29
|
_handleError(`The migration from @sap/cds-mtx to @sap/cds-mtxs has been removed. Only --init-tenant-list and --sync-tenant-list are still supported.`)
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
const cds = require('@sap/cds')
|
|
30
32
|
const { syncTenantList: synchronizeTenantList, _hasMtEnv } = require('../lib/migration/migration')
|
|
31
33
|
|
|
32
34
|
async function cds_mtx_migrate(tenants, options) {
|
|
@@ -45,11 +47,20 @@ async function run(param, options) {
|
|
|
45
47
|
services.push('@sap/cds-mtxs/srv/extensibility-service')
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
await
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
await _local_server_js()
|
|
51
|
+
cds.emit ('bootstrap')
|
|
52
|
+
const model = await cds.load([...services, '@sap/cds-mtxs/db/extensions'])
|
|
53
|
+
cds.model = cds.compile.for.nodejs(model)
|
|
54
|
+
const d = 'cds.xt.DeploymentService'
|
|
55
|
+
const unserved = !cds.services[d]
|
|
56
|
+
if (unserved) {
|
|
57
|
+
await cds.serve(services)
|
|
58
|
+
}
|
|
59
|
+
await cds.plugins
|
|
60
|
+
|
|
61
|
+
await cds.emit('served', cds.services)
|
|
51
62
|
|
|
52
|
-
|
|
63
|
+
cds.env.requires.multitenancy ??= true // needed for current sidecar config that only enables services
|
|
53
64
|
|
|
54
65
|
if (genTenantList) {
|
|
55
66
|
const tenantListResult = await synchronizeTenantList(tenantList, options)
|
|
@@ -73,6 +84,16 @@ async function run(param, options) {
|
|
|
73
84
|
await cds_mtx_migrate(param, options).catch(console.error)
|
|
74
85
|
}
|
|
75
86
|
|
|
87
|
+
// copied from cds.serve
|
|
88
|
+
async function _local_server_js() {
|
|
89
|
+
const _local = file => isfile(file) || isfile (path.join(cds.env.folders.srv,file))
|
|
90
|
+
const cli_js = process.env.CDS_TYPESCRIPT && _local('cli.ts') || _local('cli.js')
|
|
91
|
+
if (cli_js) {
|
|
92
|
+
console.log ('[cds] - loading server from', { file: local(cli_js) })
|
|
93
|
+
await _import(cli_js)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
76
97
|
function getParameterValue(options, parameter) {
|
|
77
98
|
const index = options.indexOf(parameter)
|
|
78
99
|
const value = (index > -1) ? options[index + 1] : undefined
|
|
@@ -104,14 +104,15 @@ function authUrl(baseUrl, subdomain, pathname) {
|
|
|
104
104
|
return u
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
async function token(credentials, { query = {}, form = {}, subdomain } = {}) {
|
|
107
|
+
async function token(credentials, { query = {}, form = {}, subdomain, path = '/oauth/token' } = {}) {
|
|
108
108
|
const { clientid, clientsecret, certurl, url, certificate, key } = credentials ?? {}
|
|
109
109
|
if (!form.grant_type) throw cds.error('grant_type is required', { status: 400 })
|
|
110
|
-
const u = authUrl(certurl || url, subdomain,
|
|
110
|
+
const u = authUrl(certurl || url, subdomain, path)
|
|
111
111
|
Object.entries(query).forEach(([k, v]) => u.searchParams.append(k, v))
|
|
112
112
|
const body = new URLSearchParams({ client_id: clientid, ...form })
|
|
113
113
|
const headers = { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' }
|
|
114
114
|
let raw
|
|
115
|
+
const _fail = (statusCode, respBody) => new Error(`token request to ${u.origin}${u.pathname} for client ${clientid} failed (${statusCode}): ${respBody}`, { status: statusCode })
|
|
115
116
|
if (certificate) {
|
|
116
117
|
raw = await new Promise((resolve, reject) => {
|
|
117
118
|
const req = require('node:https').request({
|
|
@@ -126,7 +127,7 @@ async function token(credentials, { query = {}, form = {}, subdomain } = {}) {
|
|
|
126
127
|
res.on('data', x => chunks.push(x))
|
|
127
128
|
res.on('end', () => {
|
|
128
129
|
if (res.statusCode >= 200 && res.statusCode < 300) resolve(Buffer.concat(chunks).toString())
|
|
129
|
-
else reject(
|
|
130
|
+
else reject(_fail(res.statusCode, Buffer.concat(chunks).toString()))
|
|
130
131
|
})
|
|
131
132
|
})
|
|
132
133
|
req.on('error', reject)
|
|
@@ -176,7 +177,7 @@ async function fetchResiliently(url, req, { retriesLeft = maxRetries, maxRetryAf
|
|
|
176
177
|
const data = contentType.includes('application/json') ? await res.json() : await res.text()
|
|
177
178
|
|
|
178
179
|
if (!res.ok) {
|
|
179
|
-
let msg = [status, statusText, data?.message, typeof data?.error === 'object' ? JSON.stringify(data.error) : data?.error, data?.description, data?.cause].filter(Boolean).join(' – ')
|
|
180
|
+
let msg = [status, statusText, data?.message, typeof data?.error === 'object' ? JSON.stringify(data.error) : data?.error, data?.description, data?.cause, typeof data?.details === 'object' ? JSON.stringify(data.details) : data?.details].filter(Boolean).join(' – ')
|
|
180
181
|
if (res.status === 429 && res.headers.get('retry-after')) msg += ` – retry after ${res.headers.get('retry-after')}`
|
|
181
182
|
throw Object.assign(new Error(msg), { status, statusText, headers, code: data?.error?.code })
|
|
182
183
|
}
|
|
@@ -5,6 +5,7 @@ const { fetchResiliently, token } = require('../../lib/utils')
|
|
|
5
5
|
const DeploymentService = 'cds.xt.DeploymentService'
|
|
6
6
|
const JobsService = 'cds.xt.JobsService'
|
|
7
7
|
const Tenants = 'cds.xt.Tenants'
|
|
8
|
+
const { SKIPPED_DB } = require('../jobs-service')
|
|
8
9
|
|
|
9
10
|
// we need to run in multitenancy mode for t0 ops
|
|
10
11
|
if (!cds.env.requires.multitenancy) cds.env.requires.multitenancy = true
|
|
@@ -112,9 +113,9 @@ module.exports = class ProvisioningService extends cds.ApplicationService {
|
|
|
112
113
|
)).map(({ ID, metadata, createdAt, modifiedAt }) => ({ subscribedTenantId: ID, ...this._toMetadataJson(metadata, ID), createdAt, modifiedAt }))
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
async _getTenants() {
|
|
116
|
+
async _getTenants(options) {
|
|
116
117
|
const ds = await cds.connect.to(DeploymentService)
|
|
117
|
-
const all = await ds.getTenants()
|
|
118
|
+
const all = await ds.getTenants(options)
|
|
118
119
|
if (cds.env.requires[this.name]?.upgrade?.ignoreNonExistingContainers) {
|
|
119
120
|
const containers = await ds.getContainers()
|
|
120
121
|
return all.filter(t => containers.includes(t))
|
|
@@ -129,9 +130,37 @@ module.exports = class ProvisioningService extends cds.ApplicationService {
|
|
|
129
130
|
const all = tenantIds.includes('*')
|
|
130
131
|
const sharedGenDir = !main.requires.extensibility
|
|
131
132
|
if (sharedGenDir) options.skipResources ??= sharedGenDir
|
|
132
|
-
|
|
133
|
+
|
|
134
|
+
// Merge static upgrade config with dynamic options (dynamic takes precedence)
|
|
135
|
+
const upgradeConfig = cds.env.requires[this.name]?.upgrade
|
|
136
|
+
if (upgradeConfig) {
|
|
137
|
+
options.upgrade = cds.utils.merge({ ...upgradeConfig }, options.upgrade)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Determine skipAfter: from explicit option or derived from package.json mtime
|
|
141
|
+
let skipAfter = options?.upgrade?.skipAfter
|
|
142
|
+
if (!skipAfter && options?.upgrade?.skipAlreadyUpgraded) {
|
|
143
|
+
const { stat } = require('fs/promises')
|
|
144
|
+
const { mtime } = await stat(cds.root + '/package.json')
|
|
145
|
+
skipAfter = mtime.toISOString()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const tenantsOptions = skipAfter ? { skipAfter } : undefined
|
|
149
|
+
let tenants
|
|
150
|
+
let skipped = []
|
|
151
|
+
if (all) {
|
|
152
|
+
tenants = await this._getTenants(tenantsOptions)
|
|
153
|
+
} else if (skipAfter) {
|
|
154
|
+
const upgradeable = await this._getTenants(tenantsOptions)
|
|
155
|
+
tenants = tenantIds.filter(t => upgradeable.includes(t))
|
|
156
|
+
// Requested tenants filtered out by skipAfter are considered already upgraded.
|
|
157
|
+
skipped = tenantIds.filter(t => !upgradeable.includes(t))
|
|
158
|
+
} else {
|
|
159
|
+
tenants = tenantIds
|
|
160
|
+
}
|
|
161
|
+
|
|
133
162
|
const { isSync } = this._parseHeaders(cds.context.http?.req.headers)
|
|
134
|
-
if (!tenants.length && isSync) return
|
|
163
|
+
if (!tenants.length && !skipped.length && isSync) return
|
|
135
164
|
if (sharedGenDir && cds.requires.db.kind === 'hana') { // REVISIT: Ideally part of HANA plugin
|
|
136
165
|
const { resources4, csvs4, directory4 } = require('../plugins/hana')
|
|
137
166
|
const out = await directory4('base', true)
|
|
@@ -141,13 +170,26 @@ module.exports = class ProvisioningService extends cds.ApplicationService {
|
|
|
141
170
|
const { 'cds.xt.DeploymentService': ds } = cds.services
|
|
142
171
|
const js = await cds.connect.to(JobsService)
|
|
143
172
|
const { clusterSize = 3 } = js._config()
|
|
144
|
-
const dbToTenants = (clusterSize > 1 && ds) ? await ds.tenantsByDb(tenants) : [new Set(tenants)]
|
|
145
|
-
|
|
173
|
+
const dbToTenants = (tenants.length && clusterSize > 1 && ds) ? await ds.tenantsByDb(tenants) : [new Set(tenants)]
|
|
174
|
+
// Skipped tenants go into a separate pseudo-db cluster so JobsService records them (and their
|
|
175
|
+
// tasks) as FINISHED without upgrading.
|
|
176
|
+
let clusters = dbToTenants
|
|
177
|
+
if (skipped.length) {
|
|
178
|
+
// Normalize array-form to a db-id-keyed object so skipped tenants get their own cluster.
|
|
179
|
+
clusters = { ...dbToTenants }
|
|
180
|
+
// Drop skipped tenants from regular clusters (some tenantsByDb impls ignore the tenant list).
|
|
181
|
+
const skippedSet = new Set(skipped)
|
|
182
|
+
for (const db of Object.keys(clusters)) {
|
|
183
|
+
clusters[db] = new Set([...clusters[db]].filter(t => tenants.includes(t) && !skippedSet.has(t)))
|
|
184
|
+
}
|
|
185
|
+
clusters[SKIPPED_DB] = skippedSet
|
|
186
|
+
}
|
|
187
|
+
LOG.info('upgrading', { tenants, skipped })
|
|
146
188
|
|
|
147
189
|
|
|
148
190
|
return await new Promise((resolve, reject) => {
|
|
149
191
|
const tx = js.tx({ tenant: cds.context.tenant, user: new cds.User.Privileged() })
|
|
150
|
-
const job = tx.enqueue(DeploymentService, 'upgrade',
|
|
192
|
+
const job = tx.enqueue(DeploymentService, 'upgrade', clusters, { options }, async error => {
|
|
151
193
|
if (error) {
|
|
152
194
|
await this._sendCallback('FAILED', `Tenant upgrade failed with error '${error}'`)
|
|
153
195
|
if (isSync) reject(new Error(error))
|
|
@@ -303,4 +345,17 @@ module.exports = class ProvisioningService extends cds.ApplicationService {
|
|
|
303
345
|
const cred = s?.credentials ?? multitenancy?.credentials ?? {}
|
|
304
346
|
return cred
|
|
305
347
|
}
|
|
348
|
+
|
|
349
|
+
_resolveCallbackUrl(callbackPath, baseUrl) {
|
|
350
|
+
if (!callbackPath || !baseUrl) return undefined
|
|
351
|
+
if (typeof callbackPath !== 'string' || !/^\/(?![/\\])/.test(callbackPath) || callbackPath.includes('\\')) {
|
|
352
|
+
throw cds.error(`Invalid callback path: ${callbackPath}`, { status: 400 })
|
|
353
|
+
}
|
|
354
|
+
const base = new URL(baseUrl)
|
|
355
|
+
const resolved = new URL(callbackPath, base)
|
|
356
|
+
if (resolved.origin !== base.origin) {
|
|
357
|
+
throw cds.error(`Invalid callback path: ${callbackPath}`, { status: 400 })
|
|
358
|
+
}
|
|
359
|
+
return resolved.toString()
|
|
360
|
+
}
|
|
306
361
|
}
|
|
@@ -28,7 +28,7 @@ module.exports = class SaasProvisioningService extends require('./abstract-provi
|
|
|
28
28
|
_parseHeaders(headers) {
|
|
29
29
|
const { prefer, status_callback, mtx_status_callback } = headers ?? {}
|
|
30
30
|
const { saas_registry_url } = this._getCredentials()
|
|
31
|
-
const callbackUrl = mtx_status_callback ?? (status_callback
|
|
31
|
+
const callbackUrl = mtx_status_callback ?? this._resolveCallbackUrl(status_callback, saas_registry_url)
|
|
32
32
|
return {
|
|
33
33
|
callbackUrl,
|
|
34
34
|
isCustomCallback: !!mtx_status_callback,
|
|
@@ -53,7 +53,7 @@ module.exports = class SmsProvisioningService extends require('./abstract-provis
|
|
|
53
53
|
_parseHeaders(headers) {
|
|
54
54
|
const { prefer, status_callback } = headers ?? {}
|
|
55
55
|
const { subscription_manager_url } = this._getCredentials()
|
|
56
|
-
const callbackUrl = (status_callback
|
|
56
|
+
const callbackUrl = this._resolveCallbackUrl(status_callback, subscription_manager_url)
|
|
57
57
|
return {
|
|
58
58
|
callbackUrl,
|
|
59
59
|
isSync: !(prefer?.includes('respond-async') || callbackUrl)
|
|
@@ -102,11 +102,31 @@ module.exports = class SmsProvisioningService extends require('./abstract-provis
|
|
|
102
102
|
const isIssuerValid = isValid(callback_certificate_issuer, cert.issuer)
|
|
103
103
|
const isSubjectValid = isValid(callback_certificate_subject, cert.subject)
|
|
104
104
|
|
|
105
|
+
function parseDN(dn) {
|
|
106
|
+
// Node's X509Certificate.issuer/subject uses newline as RDN separator.
|
|
107
|
+
// Each line is a single "attribute=value" pair. Multiple lines with the
|
|
108
|
+
// same attribute (e.g. multiple OU= entries) are all collected into an array.
|
|
109
|
+
const fields = {}
|
|
110
|
+
for (const line of dn.split('\n')) {
|
|
111
|
+
const eq = line.indexOf('=')
|
|
112
|
+
if (eq === -1) continue
|
|
113
|
+
const attr = line.slice(0, eq).trim().toUpperCase()
|
|
114
|
+
const val = line.slice(eq + 1).trim()
|
|
115
|
+
if (fields[attr]) fields[attr].push(val)
|
|
116
|
+
else fields[attr] = [val]
|
|
117
|
+
}
|
|
118
|
+
return fields
|
|
119
|
+
}
|
|
120
|
+
|
|
105
121
|
function isValid(expected, fromCert) {
|
|
122
|
+
const certFields = parseDN(fromCert)
|
|
106
123
|
return Object.entries(JSON.parse(expected)).every(([k, v]) => {
|
|
107
124
|
if (v === '*') return true
|
|
108
|
-
|
|
109
|
-
|
|
125
|
+
const certVals = certFields[k.toUpperCase()] ?? []
|
|
126
|
+
// For array expected values every element must be present in the cert.
|
|
127
|
+
// For a scalar expected value the cert must contain that exact value.
|
|
128
|
+
if (Array.isArray(v)) return v.every(allowed => certVals.includes(allowed))
|
|
129
|
+
else return certVals.includes(v)
|
|
110
130
|
})
|
|
111
131
|
}
|
|
112
132
|
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
const cds = require('@sap/cds')
|
|
2
|
+
const { path } = cds.utils
|
|
2
3
|
module.exports = (srv) => {
|
|
3
4
|
// mainly normalizing the input
|
|
4
5
|
srv.before(['UPDATE', 'UPSERT'], 'Extensions', req => { // must be synchronous to be handled first
|
|
5
6
|
const { csn: extension, i18n: bundles, csvs: csvData } = req.data
|
|
6
7
|
|
|
8
|
+
// reject file names with path components (path traversal, see DbAdapter.put)
|
|
9
|
+
for (const files of [bundles, csvData]) {
|
|
10
|
+
if (Array.isArray(files)) files.forEach(_assertPlainFileName)
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
// Validation of extension
|
|
8
14
|
if (!extension) throw new cds.error({ message: 'Property "extension" is missing', code: 400 })
|
|
9
15
|
if (Array.isArray(extension)) {
|
|
@@ -41,6 +47,15 @@ module.exports = (srv) => {
|
|
|
41
47
|
req.data.csn = extCsn
|
|
42
48
|
})
|
|
43
49
|
|
|
50
|
+
// rejects a FILE name that is not a plain file name (prevents path traversal)
|
|
51
|
+
const _assertPlainFileName = function (file) {
|
|
52
|
+
const name = file?.name
|
|
53
|
+
if (typeof name !== 'string' || !name.length) return
|
|
54
|
+
if (path.isAbsolute(name) || name.includes('/') || name.includes('\\') || path.basename(name) !== name) {
|
|
55
|
+
throw new cds.error({ message: `Invalid file name: "${name}"`, code: 400 })
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
44
59
|
const _isCSN = str => str.substring(0, 1) === '{'
|
|
45
60
|
|
|
46
61
|
const _mergeCSN = function (extension, merged) {
|
|
@@ -143,8 +143,8 @@ module.exports = class AnnotationsChecker {
|
|
|
143
143
|
// get element permissions from allowlist
|
|
144
144
|
const kind = this._getExtendedKind(fullCsn, extCsn, entityOrService)
|
|
145
145
|
const permissions = allowList.getPermission(kind, entityOrService)
|
|
146
|
-
//
|
|
147
|
-
if (permissions && permissions.annotations && permissions.annotations.includes(annotationName))
|
|
146
|
+
// skip only this annotation; keep checking the remaining ones on the same element
|
|
147
|
+
if (permissions && permissions.annotations && permissions.annotations.includes(annotationName)) continue
|
|
148
148
|
|
|
149
149
|
const fn = checkedExtensionAnnotations.get(annotationName) ?? _createGenericAnnotationWarning
|
|
150
150
|
const warning = fn(annotationName, annotation);
|
|
@@ -2,6 +2,8 @@ const EXTENSION_ALLOWLIST = 'extension-allowlist'
|
|
|
2
2
|
|
|
3
3
|
const PROTECTED_NAMESPACES = ['cds.xt']
|
|
4
4
|
|
|
5
|
+
module.exports.PROTECTED_NAMESPACES = PROTECTED_NAMESPACES
|
|
6
|
+
|
|
5
7
|
const BUILT_IN_NAMESPACES = ['cds.core', 'cds.outbox'] //> cds.outbox for legacy apps
|
|
6
8
|
|
|
7
9
|
module.exports.isBuiltIn = (name) => {
|
|
@@ -4,6 +4,7 @@ const { deduplicateMessages } = require('@sap/cds-compiler')
|
|
|
4
4
|
const NamespaceChecker = require('./namespace')
|
|
5
5
|
const AnnotationsChecker = require('./annotations')
|
|
6
6
|
const AllowlistChecker = require('./allowlist')
|
|
7
|
+
const ProtectedNamespaceChecker = require('./protected-namespace')
|
|
7
8
|
|
|
8
9
|
const customLinters = []
|
|
9
10
|
|
|
@@ -39,6 +40,7 @@ module.exports.lint = (extCsn, fullCsn, env = cds.env, cloneExt = false) => {
|
|
|
39
40
|
const reflectedFullCsn = cds.reflect(fullCsn)
|
|
40
41
|
const compileBaseDir = cds.root
|
|
41
42
|
const messages = [
|
|
43
|
+
...new ProtectedNamespaceChecker().check(reflectedCsn, reflectedFullCsn, compileBaseDir, linter_options),
|
|
42
44
|
...new NamespaceChecker().check(reflectedCsn, reflectedFullCsn, compileBaseDir, linter_options),
|
|
43
45
|
...new AnnotationsChecker().check(reflectedCsn, reflectedFullCsn, compileBaseDir, linter_options, extCsn),
|
|
44
46
|
...new AllowlistChecker().check(reflectedCsn, reflectedFullCsn, compileBaseDir, linter_options)
|