@sap/cds-compiler 2.13.8 → 3.0.0
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 +155 -1594
- package/bin/cdsc.js +144 -66
- package/doc/CHANGELOG_ARCHIVE.md +1592 -0
- package/doc/CHANGELOG_BETA.md +3 -4
- package/doc/CHANGELOG_DEPRECATED.md +35 -1
- package/doc/{DeprecatedOptions.md → DeprecatedOptions_v2.md} +3 -1
- package/doc/Versioning.md +20 -1
- package/lib/api/.eslintrc.json +2 -2
- package/lib/api/main.js +237 -122
- package/lib/api/options.js +17 -88
- package/lib/api/validate.js +12 -16
- package/lib/base/keywords.js +216 -109
- package/lib/base/message-registry.js +152 -37
- package/lib/base/messages.js +145 -83
- package/lib/base/model.js +44 -2
- package/lib/base/optionProcessorHelper.js +19 -0
- package/lib/checks/actionsFunctions.js +7 -5
- package/lib/checks/annotationsOData.js +11 -32
- package/lib/checks/arrayOfs.js +1 -34
- package/lib/checks/cdsPersistence.js +1 -0
- package/lib/checks/elements.js +6 -6
- package/lib/checks/invalidTarget.js +1 -1
- package/lib/checks/nonexpandableStructured.js +1 -1
- package/lib/checks/queryNoDbArtifacts.js +2 -1
- package/lib/checks/selectItems.js +5 -1
- package/lib/checks/types.js +4 -2
- package/lib/checks/utils.js +2 -2
- package/lib/checks/validator.js +4 -5
- package/lib/compiler/assert-consistency.js +16 -10
- package/lib/compiler/base.js +1 -0
- package/lib/compiler/builtins.js +98 -9
- package/lib/compiler/checks.js +22 -70
- package/lib/compiler/define.js +61 -13
- package/lib/compiler/extend.js +79 -14
- package/lib/compiler/finalize-parse-cdl.js +46 -29
- package/lib/compiler/index.js +100 -37
- package/lib/compiler/moduleLayers.js +7 -0
- package/lib/compiler/populate.js +19 -18
- package/lib/compiler/propagator.js +7 -4
- package/lib/compiler/resolve.js +297 -234
- package/lib/compiler/shared.js +107 -102
- package/lib/compiler/tweak-assocs.js +16 -11
- package/lib/compiler/utils.js +5 -0
- package/lib/edm/annotations/genericTranslation.js +93 -21
- package/lib/edm/csn2edm.js +230 -115
- package/lib/edm/edm.js +305 -226
- package/lib/edm/edmPreprocessor.js +509 -438
- package/lib/edm/edmUtils.js +31 -45
- package/lib/gen/Dictionary.json +98 -22
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +10 -30
- package/lib/gen/language.tokens +105 -114
- package/lib/gen/languageLexer.interp +1 -34
- package/lib/gen/languageLexer.js +889 -1007
- package/lib/gen/languageLexer.tokens +95 -106
- package/lib/gen/languageParser.js +20786 -22199
- package/lib/json/csnVersion.js +10 -11
- package/lib/json/from-csn.js +59 -51
- package/lib/json/to-csn.js +10 -10
- package/lib/language/antlrParser.js +2 -2
- package/lib/language/docCommentParser.js +62 -39
- package/lib/language/errorStrategy.js +52 -40
- package/lib/language/genericAntlrParser.js +348 -229
- package/lib/language/language.g4 +629 -653
- package/lib/language/multiLineStringParser.js +14 -42
- package/lib/language/textUtils.js +44 -0
- package/lib/main.d.ts +46 -43
- package/lib/main.js +108 -79
- package/lib/model/csnRefs.js +34 -7
- package/lib/model/csnUtils.js +337 -332
- package/lib/model/enrichCsn.js +1 -0
- package/lib/model/revealInternalProperties.js +30 -10
- package/lib/model/sortViews.js +32 -31
- package/lib/modelCompare/compare.js +6 -6
- package/lib/optionProcessor.js +73 -46
- package/lib/render/.eslintrc.json +1 -1
- package/lib/render/DuplicateChecker.js +4 -7
- package/lib/render/manageConstraints.js +70 -2
- package/lib/render/toCdl.js +1042 -882
- package/lib/render/toHdbcds.js +195 -245
- package/lib/render/toRename.js +44 -22
- package/lib/render/toSql.js +225 -241
- package/lib/render/utils/common.js +145 -15
- package/lib/render/utils/sql.js +20 -19
- package/lib/sql-identifier.js +6 -0
- package/lib/transform/db/.eslintrc.json +4 -3
- package/lib/transform/db/associations.js +2 -2
- package/lib/transform/db/cdsPersistence.js +5 -15
- package/lib/transform/db/constraints.js +4 -2
- package/lib/transform/db/expansion.js +22 -16
- package/lib/transform/db/flattening.js +109 -80
- package/lib/transform/db/transformExists.js +7 -7
- package/lib/transform/db/views.js +9 -6
- package/lib/transform/draft/.eslintrc.json +2 -2
- package/lib/transform/draft/db.js +6 -6
- package/lib/transform/draft/odata.js +6 -7
- package/lib/transform/forHanaNew.js +62 -48
- package/lib/transform/forOdataNew.js +49 -50
- package/lib/transform/localized.js +31 -20
- package/lib/transform/odata/toFinalBaseType.js +16 -14
- package/lib/transform/odata/typesExposure.js +146 -198
- package/lib/transform/odata/utils.js +1 -38
- package/lib/transform/transformUtilsNew.js +67 -84
- package/lib/transform/translateAssocsToJoins.js +7 -3
- package/lib/transform/universalCsn/.eslintrc.json +2 -2
- package/lib/transform/universalCsn/coreComputed.js +16 -9
- package/lib/transform/universalCsn/universalCsnEnricher.js +60 -10
- package/lib/utils/file.js +3 -3
- package/lib/utils/moduleResolve.js +13 -6
- package/lib/utils/timetrace.js +20 -21
- package/package.json +35 -4
- package/share/messages/message-explanations.json +2 -1
- package/share/messages/syntax-expected-integer.md +37 -0
- package/doc/ApiMigration.md +0 -237
- package/doc/CommandLineMigration.md +0 -58
- package/doc/ErrorMessages.md +0 -175
- package/doc/FioriAnnotations.md +0 -94
- package/doc/ODataTransformation.md +0 -273
- package/lib/backends.js +0 -529
- package/lib/fix_antlr4-8_warning.js +0 -56
- package/lib/transform/odata/attachPath.js +0 -96
- package/lib/transform/odata/expandStructKeysInAssociations.js +0 -59
- package/lib/transform/odata/generateForeignKeyElements.js +0 -261
- package/lib/transform/odata/referenceFlattener.js +0 -296
- package/lib/transform/odata/sortByAssociationDependency.js +0 -105
- package/lib/transform/odata/structuralPath.js +0 -72
- package/lib/transform/odata/structureFlattener.js +0 -171
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { setProp, isBetaEnabled } = require('../base/model');
|
|
4
|
-
const { getUtils,
|
|
4
|
+
const { getUtils, cloneCsnNonDict,
|
|
5
5
|
forEachMemberRecursively, forAllQueries, applyTransformationsOnNonDictionary,
|
|
6
6
|
getArtifactDatabaseNameOf, getElementDatabaseNameOf, isBuiltinType, applyTransformations,
|
|
7
7
|
isAspect, walkCsnPath,
|
|
@@ -16,7 +16,7 @@ const { rejectManagedAssociationsAndStructuresForHdbcdsNames } = require('../che
|
|
|
16
16
|
const { addLocalizationViewsWithJoins, addLocalizationViews } = require('../transform/localized');
|
|
17
17
|
const { timetrace } = require('../utils/timetrace');
|
|
18
18
|
const { createReferentialConstraints, assertConstraintIdentifierUniqueness } = require('./db/constraints');
|
|
19
|
-
const { createDict } = require('../utils/objectUtils');
|
|
19
|
+
const { createDict, forEach } = require('../utils/objectUtils');
|
|
20
20
|
const handleExists = require('./db/transformExists');
|
|
21
21
|
const replaceAssociationsInGroupByOrderBy = require('./db/groupByOrderBy');
|
|
22
22
|
const _forEachDefinition = require('../model/csnUtils').forEachDefinition;
|
|
@@ -41,9 +41,7 @@ function forEachDefinition(csn, cb) {
|
|
|
41
41
|
* in HANA CDS style, used by 'toHana', toSql' and 'toRename'.
|
|
42
42
|
* The behavior is controlled by the following options:
|
|
43
43
|
* options = {
|
|
44
|
-
*
|
|
45
|
-
* forHana.alwaysResolveDerivedTypes // Always resolve derived type chains (by default, this is only
|
|
46
|
-
* // done for 'quoted' names). FIXME: Should always be done in general.
|
|
44
|
+
* sqlMapping // See the behavior of 'sqlMapping' in toHana, toSql and toRename
|
|
47
45
|
* }
|
|
48
46
|
* The result model will always have 'options.forHana' set, to indicate that these transformations have happened.
|
|
49
47
|
* The following transformations are made:
|
|
@@ -69,9 +67,9 @@ function forEachDefinition(csn, cb) {
|
|
|
69
67
|
* - (110) Actions and functions (bound or unbound) are ignored.
|
|
70
68
|
* - (120) (a) Services become contexts.
|
|
71
69
|
* - (130) (not for to.hdbcds with hdbcds names): Elements having structured types are flattened into
|
|
72
|
-
* multiple elements (using '_' or '.' as name separator, depending on '
|
|
70
|
+
* multiple elements (using '_' or '.' as name separator, depending on 'sqlMapping').
|
|
73
71
|
* - (140) (not for to.hdbcds with hdbcds names): Managed associations get explicit ON-conditions, with
|
|
74
|
-
* generated foreign key elements (also using '_' or '.' as name separator, depending on '
|
|
72
|
+
* generated foreign key elements (also using '_' or '.' as name separator, depending on 'sqlMapping').
|
|
75
73
|
* - (150) (a) Elements from inherited (included) entities are copied into the receiving entity
|
|
76
74
|
* (b) The 'include' property is removed from entities.
|
|
77
75
|
* - (160) Projections become views, with MIXINs for association elements (adding $projection where
|
|
@@ -94,7 +92,7 @@ function forEachDefinition(csn, cb) {
|
|
|
94
92
|
* (d) Managed association entries in ORDER BY
|
|
95
93
|
* - (240) All artifacts (a), elements, foreign keys, parameters (b) that have a DB representation are annotated
|
|
96
94
|
* with their database name (as '@cds.persistence.name') according to the naming convention chosen
|
|
97
|
-
* in 'options.
|
|
95
|
+
* in 'options.sqlMapping'.
|
|
98
96
|
* - (250) Remove name space definitions again (only in forHanaNew). Maybe we can omit inserting namespace definitions
|
|
99
97
|
* completely (TODO)
|
|
100
98
|
*
|
|
@@ -106,38 +104,35 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
106
104
|
// copy the model as we don't want to change the input model
|
|
107
105
|
timetrace.start('HANA transformation');
|
|
108
106
|
/** @type {CSN.Model} */
|
|
109
|
-
let csn =
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
let csn = cloneCsnNonDict(inputModel, options);
|
|
112
108
|
|
|
113
109
|
checkCSNVersion(csn, options);
|
|
114
110
|
|
|
115
|
-
const pathDelimiter = (options.
|
|
111
|
+
const pathDelimiter = (options.sqlMapping === 'hdbcds') ? '.' : '_';
|
|
116
112
|
|
|
117
113
|
let message, error, warning, info; // message functions
|
|
118
114
|
/** @type {() => void} */
|
|
119
|
-
let
|
|
115
|
+
let throwWithAnyError;
|
|
120
116
|
let artifactRef, inspectRef, effectiveType, get$combined,
|
|
121
|
-
getFinalBaseType, // csnUtils (csnRefs)
|
|
122
117
|
addDefaultTypeFacets, expandStructsInExpression; // transformUtils
|
|
123
118
|
|
|
124
119
|
bindCsnReference();
|
|
125
120
|
|
|
126
|
-
|
|
121
|
+
throwWithAnyError(); // reclassify and throw in case of non-configurable errors
|
|
127
122
|
|
|
128
123
|
if (options.csnFlavor === 'universal' && isBetaEnabled(options, 'enableUniversalCsn')) {
|
|
129
124
|
enrichUniversalCsn(csn, options);
|
|
130
125
|
bindCsnReference();
|
|
131
126
|
}
|
|
132
127
|
|
|
133
|
-
const dialect = options.
|
|
128
|
+
const dialect = options.sqlDialect;
|
|
134
129
|
const doA2J = !(options.transformation === 'hdbcds' && options.sqlMapping === 'hdbcds');
|
|
135
130
|
if (!doA2J)
|
|
136
131
|
forEachDefinition(csn, handleMixinOnConditions);
|
|
137
132
|
|
|
138
133
|
// Run validations on CSN - each validator function has access to the message functions and the inspect ref via this
|
|
139
134
|
const cleanup = validate.forHana(csn, {
|
|
140
|
-
message, error, warning, info, inspectRef, effectiveType, artifactRef, csnUtils: getUtils(csn), csn, options,
|
|
135
|
+
message, error, warning, info, inspectRef, effectiveType, artifactRef, csnUtils: getUtils(csn), csn, options, isAspect
|
|
141
136
|
});
|
|
142
137
|
|
|
143
138
|
// Needs to happen before tuple expansion, so the newly generated WHERE-conditions have it applied
|
|
@@ -146,13 +141,13 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
146
141
|
// Check if structured elements and managed associations are compared in an expression
|
|
147
142
|
// and expand these structured elements. This tuple expansion allows all other
|
|
148
143
|
// subsequent procession steps (especially a2j) to see plain paths in expressions.
|
|
149
|
-
// If errors are detected,
|
|
144
|
+
// If errors are detected, throwWithAnyError() will return from further processing
|
|
150
145
|
|
|
151
146
|
// If this function is ever undefined, we have a bug in our logic.
|
|
152
147
|
// @ts-ignore
|
|
153
148
|
expandStructsInExpression(csn, { drillRef: true });
|
|
154
149
|
|
|
155
|
-
|
|
150
|
+
throwWithAnyError();
|
|
156
151
|
|
|
157
152
|
// FIXME: This does something very similar to cloneWithTransformations -> refactor?
|
|
158
153
|
const transformCsn = transformUtils.transformModel;
|
|
@@ -167,7 +162,7 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
167
162
|
|
|
168
163
|
if(doA2J) {
|
|
169
164
|
// Expand a structured thing in: keys, columns, order by, group by
|
|
170
|
-
expansion.expandStructureReferences(csn, options, pathDelimiter, {error, info,
|
|
165
|
+
expansion.expandStructureReferences(csn, options, pathDelimiter, {error, info, throwWithAnyError});
|
|
171
166
|
bindCsnReference();
|
|
172
167
|
}
|
|
173
168
|
|
|
@@ -222,6 +217,7 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
222
217
|
flattenStructuredElement,
|
|
223
218
|
flattenStructStepsInRef,
|
|
224
219
|
isAssociationOperand, isDollarSelfOrProjectionOperand,
|
|
220
|
+
csnUtils,
|
|
225
221
|
} = transformUtils.getTransformers(csn, options, pathDelimiter);
|
|
226
222
|
|
|
227
223
|
const {
|
|
@@ -229,7 +225,7 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
229
225
|
isAssocOrComposition,
|
|
230
226
|
addStringAnnotationTo,
|
|
231
227
|
cloneWithTransformations,
|
|
232
|
-
} =
|
|
228
|
+
} = csnUtils;
|
|
233
229
|
|
|
234
230
|
// (000) Rename primitive types, make UUID a String
|
|
235
231
|
transformCsn(csn, {
|
|
@@ -353,7 +349,7 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
353
349
|
if(doA2J)
|
|
354
350
|
flattening.removeLeadingSelf(csn);
|
|
355
351
|
|
|
356
|
-
|
|
352
|
+
throwWithAnyError();
|
|
357
353
|
|
|
358
354
|
timetrace.stop();
|
|
359
355
|
|
|
@@ -381,7 +377,22 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
381
377
|
// Set when we turn UUID into String, checked during generateDraftForHana
|
|
382
378
|
'$renamed': killProp,
|
|
383
379
|
// Set when we remove .key from temporal things, used in localized.js
|
|
384
|
-
'$key': killProp
|
|
380
|
+
'$key': killProp,
|
|
381
|
+
// We need .elements easily for rendering - otherwise we have to compute it then
|
|
382
|
+
// Does not fit in the "killers" theme - TODO: Find a better place
|
|
383
|
+
SET: (parent, prop, SET) => {
|
|
384
|
+
if(!SET.elements) {
|
|
385
|
+
const stack = [parent];
|
|
386
|
+
while(stack.length > 0) {
|
|
387
|
+
const query = stack.pop();
|
|
388
|
+
|
|
389
|
+
if(query.SET)
|
|
390
|
+
stack.push(query.SET.args[0]);
|
|
391
|
+
else if(query.SELECT)
|
|
392
|
+
setProp(SET, 'elements', query.SELECT.elements);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
385
396
|
}
|
|
386
397
|
|
|
387
398
|
applyTransformations(csn, killers, [], { skipIgnore: false});
|
|
@@ -393,9 +404,10 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
393
404
|
/* ----------------------------------- Functions start here -----------------------------------------------*/
|
|
394
405
|
|
|
395
406
|
function bindCsnReference(){
|
|
396
|
-
({ error, warning, info, message,
|
|
397
|
-
({ artifactRef, inspectRef, effectiveType, getFinalBaseType, get$combined } = getUtils(csn));
|
|
407
|
+
({ error, warning, info, message, throwWithAnyError } = makeMessageFunction(csn, options, moduleName));
|
|
398
408
|
({ addDefaultTypeFacets, expandStructsInExpression } = transformUtils.getTransformers(csn, options, pathDelimiter));
|
|
409
|
+
// TODO: Can we use csnUtils of the call above (transformUtils.getTransformers)?
|
|
410
|
+
({ artifactRef, inspectRef, effectiveType, get$combined } = getUtils(csn));
|
|
399
411
|
}
|
|
400
412
|
|
|
401
413
|
function bindCsnReferenceOnly(){
|
|
@@ -448,7 +460,7 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
448
460
|
// get$combined also includes elements which are part of "excluding {}"
|
|
449
461
|
// this shouldn't be an issue here, as such references get rejected
|
|
450
462
|
const elementsOfQuerySources = get$combined(query);
|
|
451
|
-
|
|
463
|
+
forEach(elementsOfQuerySources, (id, element) => {
|
|
452
464
|
// if the ref points to an element which is not explicitly exposed in the column list,
|
|
453
465
|
// but through the '*' operator -> replace the $projection / $self with the correct source entity
|
|
454
466
|
if(id === columnToReplace)
|
|
@@ -458,7 +470,7 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
458
470
|
|
|
459
471
|
// No implicit CAST in on-condition
|
|
460
472
|
if(replaceWith && replaceWith.cast) {
|
|
461
|
-
const clone =
|
|
473
|
+
const clone = cloneCsnNonDict(replaceWith, options);
|
|
462
474
|
delete clone.cast;
|
|
463
475
|
return clone;
|
|
464
476
|
}
|
|
@@ -475,10 +487,14 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
475
487
|
if (!artifact._ignore) {
|
|
476
488
|
// Do things specific for entities and views (pass 2)
|
|
477
489
|
if ((artifact.kind === 'entity') && artifact.query) {
|
|
478
|
-
|
|
479
|
-
transformEntityOrViewPass2(
|
|
480
|
-
replaceAssociationsInGroupByOrderBy(
|
|
481
|
-
|
|
490
|
+
const process = (parent, prop, query, path) => {
|
|
491
|
+
transformEntityOrViewPass2(parent, artifact, artifactName, path.concat(prop))
|
|
492
|
+
replaceAssociationsInGroupByOrderBy(parent, options, inspectRef, error, path.concat(prop));
|
|
493
|
+
return query;
|
|
494
|
+
}
|
|
495
|
+
applyTransformationsOnNonDictionary(csn.definitions, artifactName, {
|
|
496
|
+
SELECT: process
|
|
497
|
+
}, {}, [ 'definitions']);
|
|
482
498
|
}
|
|
483
499
|
}
|
|
484
500
|
}
|
|
@@ -490,7 +506,7 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
490
506
|
function recursivelyApplyCommon(artifact, artifactName) {
|
|
491
507
|
if (!artifact._ignore) {
|
|
492
508
|
if (artifact.kind !== 'service' && artifact.kind !== 'context')
|
|
493
|
-
addStringAnnotationTo('@cds.persistence.name', getArtifactDatabaseNameOf(artifactName, options.
|
|
509
|
+
addStringAnnotationTo('@cds.persistence.name', getArtifactDatabaseNameOf(artifactName, options.sqlMapping, csn, options.sqlDialect), artifact);
|
|
494
510
|
|
|
495
511
|
forEachMemberRecursively(artifact, (member, memberName, property, path) => {
|
|
496
512
|
transformCommon(member, memberName, path);
|
|
@@ -498,7 +514,7 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
498
514
|
// Virtual elements in entities and types are not annotated, as they have no DB representation.
|
|
499
515
|
// In views they are, as we generate a null expression for them (null as <colname>)
|
|
500
516
|
if ((!member.virtual || artifact.query))
|
|
501
|
-
addStringAnnotationTo('@cds.persistence.name', getElementDatabaseNameOf(memberName, options.
|
|
517
|
+
addStringAnnotationTo('@cds.persistence.name', getElementDatabaseNameOf(memberName, options.sqlMapping, options.sqlDialect), member);
|
|
502
518
|
}, [ 'definitions', artifactName ]);
|
|
503
519
|
}
|
|
504
520
|
}
|
|
@@ -524,7 +540,7 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
524
540
|
function transformSelfInBacklinks(artifact, artifactName, dummy, path) {
|
|
525
541
|
// Fixme: For toHana mixins must be transformed, for toSql -d hana
|
|
526
542
|
// mixin elements must be transformed, why can't toSql also use mixins?
|
|
527
|
-
if(artifact.kind === 'entity' || artifact.query || (options.
|
|
543
|
+
if(artifact.kind === 'entity' || artifact.query || (options.forHana && options.sqlMapping === 'hdbcds' && artifact.kind === 'type'))
|
|
528
544
|
doit(artifact.elements, path.concat([ 'elements' ]));
|
|
529
545
|
if (artifact.query && artifact.query.SELECT && artifact.query.SELECT.mixin)
|
|
530
546
|
doit(artifact.query.SELECT.mixin, path.concat([ 'query', 'SELECT', 'mixin' ]));
|
|
@@ -555,7 +571,7 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
555
571
|
// For HANA: Report an error on
|
|
556
572
|
// - view with parameters that has an element of type association/composition
|
|
557
573
|
// - association that points to entity with parameters
|
|
558
|
-
if (options.
|
|
574
|
+
if (options.sqlDialect === 'hana' && member.target && isAssocOrComposition(member.type) && !isBetaEnabled(options, 'assocsWithParams')) {
|
|
559
575
|
if (artifact.params) {
|
|
560
576
|
// HANA does not allow 'WITH ASSOCIATIONS' on something with parameters:
|
|
561
577
|
// SAP DBTech JDBC: [7]: feature not supported: parameterized sql view cannot support association: line 1 col 1 (at pos 0)
|
|
@@ -598,7 +614,7 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
598
614
|
sql: 'Table-like entities with parameters are not supported for conversion to SQL',
|
|
599
615
|
});
|
|
600
616
|
}
|
|
601
|
-
else if (options.
|
|
617
|
+
else if (options.sqlDialect === 'sqlite') { // view with params
|
|
602
618
|
// Allow with plain
|
|
603
619
|
error(null, [ 'definitions', artifactName ], `SQLite does not support entities with parameters`);
|
|
604
620
|
}
|
|
@@ -607,8 +623,8 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
607
623
|
if (pname.match(/\W/g) || pname.match(/^\d/) || pname.match(/^_/)) { // parameter name must be regular SQL identifier
|
|
608
624
|
warning(null, [ 'definitions', artifactName, 'params', pname ], `Expecting regular SQL-Identifier`);
|
|
609
625
|
}
|
|
610
|
-
else if (options.
|
|
611
|
-
warning(null, [ 'definitions', artifactName, 'params', pname ], { name: options.
|
|
626
|
+
else if (options.sqlMapping !== 'plain' && pname.toUpperCase() !== pname) { // not plain mode: param name must be all upper
|
|
627
|
+
warning(null, [ 'definitions', artifactName, 'params', pname ], { name: options.sqlMapping },
|
|
612
628
|
'Expecting parameter to be uppercase in naming mode $(NAME)');
|
|
613
629
|
}
|
|
614
630
|
}
|
|
@@ -618,7 +634,7 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
618
634
|
|
|
619
635
|
function handleAssocToJoins() {
|
|
620
636
|
// With flattening errors, it makes little sense to continue.
|
|
621
|
-
|
|
637
|
+
throwWithAnyError();
|
|
622
638
|
// the augmentor isn't able to deal with technical configurations and since assoc2join can ignore it we
|
|
623
639
|
// simply make it invisible and copy it over to the result csn
|
|
624
640
|
forEachDefinition(csn, art => art.technicalConfig && setProp(art, 'technicalConfig', art.technicalConfig));
|
|
@@ -998,24 +1014,22 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
998
1014
|
// (which can currently only be 'positiveInteger') and (optional) the value is in a given range
|
|
999
1015
|
function checkTypeParamValue(node, paramName, range = null, path = null) {
|
|
1000
1016
|
const paramValue = node[paramName];
|
|
1001
|
-
if (paramValue
|
|
1017
|
+
if (paramValue === undefined || paramValue === null) {
|
|
1002
1018
|
if(options.toSql || artifact.query || !['cds.Binary','cds.hana.BINARY', 'cds.hana.NCHAR','cds.hana.CHAR'].includes(node.type)) {
|
|
1003
1019
|
return true;
|
|
1004
1020
|
} else {
|
|
1005
|
-
return error('missing-
|
|
1021
|
+
return error('type-missing-argument', path, { name: paramName, id: node.type, $reviewed: false });
|
|
1006
1022
|
}
|
|
1007
1023
|
}
|
|
1008
1024
|
if (range) {
|
|
1009
1025
|
if (isMaxParameterLengthRestricted(node.type) && range.max && paramValue > range.max) {
|
|
1010
|
-
error(
|
|
1011
|
-
{ prop: paramName, type: node.type, number: range.max, $reviewed: false }
|
|
1012
|
-
'Expecting parameter $(PROP) for type $(TYPE) to not exceed $(NUMBER)');
|
|
1026
|
+
error('type-unexpected-argument', path,
|
|
1027
|
+
{ '#': 'max', prop: paramName, type: node.type, number: range.max, $reviewed: false });
|
|
1013
1028
|
return false;
|
|
1014
1029
|
}
|
|
1015
1030
|
if (range.min && paramValue < range.min) {
|
|
1016
|
-
error(
|
|
1017
|
-
{ prop: paramName, type: node.type, number: range.min, $reviewed: false }
|
|
1018
|
-
'Expecting parameter $(PROP) for type $(TYPE) to be greater than or equal to $(NUMBER)');
|
|
1031
|
+
error('type-unexpected-argument', path,
|
|
1032
|
+
{ '#': 'min', prop: paramName, type: node.type, number: range.min, $reviewed: false });
|
|
1019
1033
|
return false;
|
|
1020
1034
|
}
|
|
1021
1035
|
}
|
|
@@ -1031,7 +1045,7 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
1031
1045
|
* @returns {boolean}
|
|
1032
1046
|
*/
|
|
1033
1047
|
function isMaxParameterLengthRestricted(type) {
|
|
1034
|
-
return !(options.toSql && type === 'cds.String' && (options.
|
|
1048
|
+
return !(options.toSql && type === 'cds.String' && (options.sqlDialect === 'sqlite' || options.sqlDialect === 'plain'));
|
|
1035
1049
|
}
|
|
1036
1050
|
|
|
1037
1051
|
/**
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
const { makeMessageFunction } = require('../base/messages');
|
|
4
4
|
const { isDeprecatedEnabled, isBetaEnabled } = require('../base/model');
|
|
5
5
|
const transformUtils = require('./transformUtilsNew');
|
|
6
|
-
const {
|
|
7
|
-
cloneCsn,
|
|
6
|
+
const { cloneCsnNonDict,
|
|
8
7
|
forEachDefinition,
|
|
9
8
|
forEachMemberRecursively,
|
|
10
9
|
applyTransformationsOnNonDictionary,
|
|
@@ -17,14 +16,12 @@ const { getUtils,
|
|
|
17
16
|
const { checkCSNVersion } = require('../json/csnVersion');
|
|
18
17
|
const validate = require('../checks/validator');
|
|
19
18
|
const { isArtifactInSomeService, isLocalizedArtifactInService } = require('./odata/utils');
|
|
20
|
-
const ReferenceFlattener = require('./odata/referenceFlattener');
|
|
21
|
-
const { flattenCSN } = require('./odata/structureFlattener');
|
|
22
|
-
const generateForeignKeys = require('./odata/generateForeignKeyElements');
|
|
23
|
-
const expandStructKeysInAssociations = require('./odata/expandStructKeysInAssociations');
|
|
24
19
|
const expandToFinalBaseType = require('./odata/toFinalBaseType');
|
|
25
20
|
const { timetrace } = require('../utils/timetrace');
|
|
26
|
-
const { attachPath } = require('./odata/attachPath');
|
|
27
21
|
const enrichUniversalCsn = require('./universalCsn/universalCsnEnricher');
|
|
22
|
+
const flattening = require('./db/flattening');
|
|
23
|
+
const associations = require('./db/associations')
|
|
24
|
+
const expansion = require('./db/expansion');
|
|
28
25
|
const generateDrafts = require('./draft/odata');
|
|
29
26
|
|
|
30
27
|
const { addLocalizationViews } = require('./localized');
|
|
@@ -74,10 +71,10 @@ module.exports = { transform4odataWithCsn };
|
|
|
74
71
|
function transform4odataWithCsn(inputModel, options) {
|
|
75
72
|
timetrace.start('OData transformation');
|
|
76
73
|
// copy the model as we don't want to change the input model
|
|
77
|
-
let csn =
|
|
74
|
+
let csn = cloneCsnNonDict(inputModel, options);
|
|
78
75
|
|
|
79
|
-
const { message, error, warning, info,
|
|
80
|
-
|
|
76
|
+
const { message, error, warning, info, throwWithAnyError } = makeMessageFunction(csn, options, 'for.odata');
|
|
77
|
+
throwWithAnyError();
|
|
81
78
|
|
|
82
79
|
// the new transformer works only with new CSN
|
|
83
80
|
checkCSNVersion(csn, options);
|
|
@@ -88,24 +85,22 @@ function transform4odataWithCsn(inputModel, options) {
|
|
|
88
85
|
extractValidFromToKeyElement,
|
|
89
86
|
checkAssignment, checkMultipleAssignments,
|
|
90
87
|
recurseElements, setAnnotation, renameAnnotation,
|
|
91
|
-
expandStructsInExpression
|
|
88
|
+
expandStructsInExpression,
|
|
89
|
+
csnUtils,
|
|
92
90
|
} = transformers;
|
|
93
91
|
|
|
94
|
-
const csnUtils = getUtils(csn);
|
|
95
92
|
const {
|
|
96
93
|
getCsnDef,
|
|
97
94
|
getServiceName,
|
|
98
95
|
isAssocOrComposition,
|
|
99
96
|
isAssociation,
|
|
100
|
-
isStructured,
|
|
101
97
|
inspectRef,
|
|
102
98
|
artifactRef,
|
|
103
99
|
effectiveType,
|
|
104
|
-
getFinalBaseType,
|
|
105
100
|
} = csnUtils;
|
|
106
101
|
|
|
107
102
|
// are we working with structured OData or not
|
|
108
|
-
const structuredOData = options.
|
|
103
|
+
const structuredOData = options.odataFormat === 'structured' && options.odataVersion === 'v4';
|
|
109
104
|
|
|
110
105
|
// collect all declared non-abstract services from the model
|
|
111
106
|
// use the array when there is a need to identify if an artifact is in a service or not
|
|
@@ -118,7 +113,7 @@ function transform4odataWithCsn(inputModel, options) {
|
|
|
118
113
|
if (options.csnFlavor === 'universal' && isBetaEnabled(options, 'enableUniversalCsn'))
|
|
119
114
|
enrichUniversalCsn(csn, options);
|
|
120
115
|
|
|
121
|
-
const keepLocalizedViews = isDeprecatedEnabled(options, '
|
|
116
|
+
const keepLocalizedViews = isDeprecatedEnabled(options, '_createLocalizedViews');
|
|
122
117
|
|
|
123
118
|
function acceptLocalizedView(_name, parent) {
|
|
124
119
|
csn.definitions[parent].$localized = true;
|
|
@@ -127,13 +122,13 @@ function transform4odataWithCsn(inputModel, options) {
|
|
|
127
122
|
|
|
128
123
|
addLocalizationViews(csn, options, acceptLocalizedView);
|
|
129
124
|
|
|
130
|
-
validate.forOdata(csn, {
|
|
131
|
-
message, error, warning, info, inspectRef, effectiveType, artifactRef, csn, options, csnUtils, services,
|
|
125
|
+
const cleanup = validate.forOdata(csn, {
|
|
126
|
+
message, error, warning, info, inspectRef, effectiveType, artifactRef, csn, options, csnUtils, services, isAspect, isExternalServiceMember
|
|
132
127
|
});
|
|
133
128
|
|
|
134
129
|
|
|
135
130
|
// Throw exception in case of errors
|
|
136
|
-
|
|
131
|
+
throwWithAnyError();
|
|
137
132
|
|
|
138
133
|
// Semantic checks before flattening regarding temporal data
|
|
139
134
|
// TODO: Move in the validator
|
|
@@ -155,35 +150,38 @@ function transform4odataWithCsn(inputModel, options) {
|
|
|
155
150
|
// Check if structured elements and managed associations are compared in an expression
|
|
156
151
|
// and expand these structured elements. This tuple expansion allows all other
|
|
157
152
|
// subsequent procession steps (especially a2j) to see plain paths in expressions.
|
|
158
|
-
// If errors are detected,
|
|
153
|
+
// If errors are detected, throwWithAnyError() will return from further processing
|
|
159
154
|
expandStructsInExpression(csn, { skipArtifact: isExternalServiceMember, drillRef: true });
|
|
160
155
|
|
|
161
|
-
// handles reference flattening
|
|
162
|
-
let referenceFlattener = new ReferenceFlattener();
|
|
163
|
-
referenceFlattener.resolveAllReferences(csn, inspectRef, isStructured);
|
|
164
|
-
attachPath(csn);
|
|
165
|
-
|
|
166
|
-
referenceFlattener.applyAliasesInOnCond(csn, inspectRef);
|
|
167
|
-
|
|
168
156
|
if (!structuredOData) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
157
|
+
expansion.expandStructureReferences(csn, options, '_', { error, info, throwWithAnyError }, { skipArtifact: isExternalServiceMember });
|
|
158
|
+
const resolved = new WeakMap();
|
|
159
|
+
// No refs with struct-steps exist anymore
|
|
160
|
+
flattening.flattenAllStructStepsInRefs(csn, options, resolved, '_', { skipArtifact: isExternalServiceMember });
|
|
161
|
+
// No type references exist anymore
|
|
162
|
+
// Needs to happen exactly between flattenAllStructStepsInRefs and flattenElements to keep model resolvable.
|
|
163
|
+
// OData doesn't resolve type chains after the first 'items'
|
|
164
|
+
flattening.resolveTypeReferences(csn, options, resolved, '_',
|
|
165
|
+
{ skip: [ 'action', 'aspect', 'event', 'function', 'type'], skipArtifact: isExternalServiceMember, skipStandard: { items: true } });
|
|
166
|
+
// No structured elements exists anymore
|
|
167
|
+
flattening.flattenElements(csn, options, '_', error,
|
|
168
|
+
{ skip: ['action', 'aspect', 'event', 'function', 'type'], skipArtifact: isExternalServiceMember,
|
|
169
|
+
skipStandard: { items: true }, // don't drill further into .items
|
|
170
|
+
skipDict: { actions: true } }); // don't drill further into .actions -> bound actions, action-artifacts are handled by skip
|
|
174
171
|
}
|
|
175
172
|
|
|
176
|
-
//
|
|
177
|
-
|
|
173
|
+
// TODO: add the generated foreign keys to the columns when we are in a view
|
|
174
|
+
// see db/views.js::addForeignKeysToColumns
|
|
175
|
+
flattening.handleManagedAssociationsAndCreateForeignKeys(csn, options, error, '_', !structuredOData, { skipArtifact: isExternalServiceMember });
|
|
178
176
|
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
//
|
|
186
|
-
|
|
177
|
+
// Allow using managed associations as steps in on-conditions to access their fks
|
|
178
|
+
// To be done after handleManagedAssociationsAndCreateForeignKeys,
|
|
179
|
+
// since then the foreign keys of the managed assocs are part of the elements
|
|
180
|
+
if(!structuredOData)
|
|
181
|
+
forEachDefinition(csn, associations.getManagedAssocStepsInOnConditionFinalizer(csn, '_'));
|
|
182
|
+
|
|
183
|
+
// structure flattener reports errors, further processing is not safe -> throw exception in case of errors
|
|
184
|
+
throwWithAnyError();
|
|
187
185
|
|
|
188
186
|
// Apply default type facets as set by options
|
|
189
187
|
// Flatten on-conditions in unmanaged associations
|
|
@@ -207,22 +205,23 @@ function transform4odataWithCsn(inputModel, options) {
|
|
|
207
205
|
generateDrafts(csn, options, services)
|
|
208
206
|
|
|
209
207
|
// Deal with all kind of annotations manipulations here
|
|
208
|
+
const skipPersNameKinds = {'service':1, 'context':1, 'namespace':1, 'annotation':1, 'action':1, 'function':1};
|
|
210
209
|
forEachDefinition(csn, (def, defName) => {
|
|
211
210
|
// Resolve annotation shorthands for entities, types, annotations, ...
|
|
212
211
|
renameShorthandAnnotations(def);
|
|
213
212
|
|
|
214
213
|
// Annotate artifacts with their DB names if requested.
|
|
215
214
|
// Skip artifacts that have no DB equivalent anyway
|
|
216
|
-
if (options.
|
|
217
|
-
def['@cds.persistence.name'] = getArtifactDatabaseNameOf(defName, options.
|
|
215
|
+
if (options.sqlMapping && !(def.kind in skipPersNameKinds))
|
|
216
|
+
def['@cds.persistence.name'] = getArtifactDatabaseNameOf(defName, options.sqlMapping, csn, 'hana'); // hana to allow naming mode "hdbcds"
|
|
218
217
|
|
|
219
|
-
forEachMemberRecursively(def, (member, memberName, propertyName
|
|
218
|
+
forEachMemberRecursively(def, (member, memberName, propertyName) => {
|
|
220
219
|
// Annotate elements, foreign keys, parameters, etc. with their DB names if requested
|
|
221
220
|
// Only these are actually required and don't annotate virtual elements in entities or types
|
|
222
221
|
// as they have no DB representation (although in views)
|
|
223
|
-
if (options.
|
|
222
|
+
if (options.sqlMapping && typeof member === 'object' && !(member.kind === 'action' || member.kind === 'function') && propertyName !== 'enum' && (!member.virtual || def.query)) {
|
|
224
223
|
// If we have a 'preserved dotted name' (i.e. we are a result of flattening), use that for the @cds.persistence.name annotation
|
|
225
|
-
member['@cds.persistence.name'] = getElementDatabaseNameOf(
|
|
224
|
+
member['@cds.persistence.name'] = getElementDatabaseNameOf(member._flatElementNameWithDots || memberName, options.sqlMapping, 'hana'); // hana to allow "hdbcds"
|
|
226
225
|
}
|
|
227
226
|
|
|
228
227
|
// Mark fields with @odata.on.insert/update as @Core.Computed
|
|
@@ -252,9 +251,9 @@ function transform4odataWithCsn(inputModel, options) {
|
|
|
252
251
|
}, { skipArtifact: isExternalServiceMember })
|
|
253
252
|
|
|
254
253
|
// Throw exception in case of errors
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (options.testMode) csn =
|
|
254
|
+
throwWithAnyError();
|
|
255
|
+
cleanup();
|
|
256
|
+
if (options.testMode) csn = cloneCsnNonDict(csn, options); // sort, keep hidden properties
|
|
258
257
|
timetrace.stop();
|
|
259
258
|
return csn;
|
|
260
259
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { makeMessageFunction } = require('../base/messages');
|
|
4
|
-
const { setProp } = require('../base/model');
|
|
4
|
+
const { setProp, isDeprecatedEnabled} = require('../base/model');
|
|
5
5
|
const { hasErrors } = require('../base/messages');
|
|
6
|
-
const {
|
|
6
|
+
const { forEachKey } = require('../utils/objectUtils');
|
|
7
7
|
const { cleanSymbols } = require('../base/cleanSymbols.js');
|
|
8
8
|
const {
|
|
9
|
-
|
|
9
|
+
cloneCsnDictionary,
|
|
10
|
+
cloneCsnNonDict,
|
|
11
|
+
applyAnnotationsFromExtensions,
|
|
10
12
|
forEachDefinition,
|
|
11
13
|
forEachGeneric,
|
|
12
14
|
forAllQueries,
|
|
@@ -67,7 +69,8 @@ const _targetFor = Symbol('_targetFor');
|
|
|
67
69
|
* @param {CSN.Options} options
|
|
68
70
|
* @param {boolean} useJoins If true, rewrite the "localized" association to a
|
|
69
71
|
* join in direct convenience views.
|
|
70
|
-
* @param {acceptLocalizedView} [acceptLocalizedView] optional callback function returning true if the localized view
|
|
72
|
+
* @param {acceptLocalizedView} [acceptLocalizedView] optional callback function returning true if the localized view
|
|
73
|
+
* name and its parent name provided as parameter should be created
|
|
71
74
|
*/
|
|
72
75
|
function _addLocalizationViews(csn, options, useJoins, acceptLocalizedView = null) {
|
|
73
76
|
// Don't try to create convenience views with errors.
|
|
@@ -86,6 +89,12 @@ function _addLocalizationViews(csn, options, useJoins, acceptLocalizedView = nul
|
|
|
86
89
|
|
|
87
90
|
forEachDefinition(csn, definition => cleanSymbols(definition, _hasLocalizedView, _isViewForEntity, _isViewForView, _targetFor));
|
|
88
91
|
|
|
92
|
+
// In case that the user tried to annotate `localized.*` artifacts, apply them.
|
|
93
|
+
applyAnnotationsFromExtensions(csn, {
|
|
94
|
+
overwrite: true,
|
|
95
|
+
filter: (name) => name.startsWith('localized.')
|
|
96
|
+
});
|
|
97
|
+
|
|
89
98
|
sortCsnDefinitionsForTests(csn, options);
|
|
90
99
|
return csn;
|
|
91
100
|
|
|
@@ -141,9 +150,8 @@ function _addLocalizationViews(csn, options, useJoins, acceptLocalizedView = nul
|
|
|
141
150
|
else
|
|
142
151
|
view = createLocalizedViewForEntity(art, artName, textElements);
|
|
143
152
|
|
|
153
|
+
copyPersistenceAnnotations(view, art, options);
|
|
144
154
|
csn.definitions[viewName] = view;
|
|
145
|
-
|
|
146
|
-
copyPersistenceAnnotations(csn.definitions[viewName], art);
|
|
147
155
|
}
|
|
148
156
|
|
|
149
157
|
/**
|
|
@@ -245,9 +253,9 @@ function _addLocalizationViews(csn, options, useJoins, acceptLocalizedView = nul
|
|
|
245
253
|
};
|
|
246
254
|
|
|
247
255
|
if (view.query)
|
|
248
|
-
convenienceView.query =
|
|
256
|
+
convenienceView.query = cloneCsnNonDict(view.query, options);
|
|
249
257
|
else if (view.projection)
|
|
250
|
-
convenienceView.projection =
|
|
258
|
+
convenienceView.projection = cloneCsnNonDict(view.projection, options);
|
|
251
259
|
|
|
252
260
|
convenienceView.elements = cloneCsnDictionary(view.elements, options);
|
|
253
261
|
convenienceView[_isViewForView] = true;
|
|
@@ -549,7 +557,7 @@ function _addLocalizationViews(csn, options, useJoins, acceptLocalizedView = nul
|
|
|
549
557
|
if (!obj || typeof obj !== 'object' || Array.isArray(obj))
|
|
550
558
|
return;
|
|
551
559
|
|
|
552
|
-
for (const prop of [ 'ref', 'target'
|
|
560
|
+
for (const prop of [ 'ref', 'target' ]) {
|
|
553
561
|
const val = obj[prop];
|
|
554
562
|
if (prop === 'ref') {
|
|
555
563
|
rewriteRefToLocalized(obj);
|
|
@@ -662,21 +670,24 @@ function copyLocation(target, source) {
|
|
|
662
670
|
}
|
|
663
671
|
|
|
664
672
|
/**
|
|
665
|
-
* Copy
|
|
666
|
-
* the target. Ignores existing annotations on the
|
|
673
|
+
* Copy @cds.persistence.exists/skip annotations from the source to
|
|
674
|
+
* the target. Ignores existing annotations on the _target_.
|
|
667
675
|
*
|
|
668
676
|
* @param {CSN.Artifact} target
|
|
669
677
|
* @param {CSN.Artifact} source
|
|
678
|
+
* @param {CSN.Options} options
|
|
670
679
|
*/
|
|
671
|
-
function copyPersistenceAnnotations(target, source) {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
+
function copyPersistenceAnnotations(target, source, options) {
|
|
681
|
+
const doNotCopyExists = isDeprecatedEnabled( options, 'eagerPersistenceForGeneratedEntities' );
|
|
682
|
+
forEachKey(source, anno => {
|
|
683
|
+
// Note:
|
|
684
|
+
// Because `.exists` is copied to the convenience view, it could
|
|
685
|
+
// lead to some localization views referencing non-existing ones.
|
|
686
|
+
// But that is the contract: User says that it already exists!
|
|
687
|
+
// In v2, `.exists` was never copied.
|
|
688
|
+
if (anno === '@cds.persistence.skip' || (!doNotCopyExists && anno === '@cds.persistence.exists'))
|
|
689
|
+
target[anno] = source[anno];
|
|
690
|
+
});
|
|
680
691
|
}
|
|
681
692
|
|
|
682
693
|
/**
|